/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.commons.chain2.impl;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import org.apache.commons.chain2.ChainUtil;
import org.apache.commons.chain2.Context;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;



/**
 * <p>Test case for the <code>ContextBase</code> class.</p>
 *
 * @version $Id$
 */

public class ContextBaseTestCase {


    // ---------------------------------------------------- Instance Variables


    /**
     * The {@link Context} instance under test.
     */
    private Context<String, Object> context = null;


    /**
     * @return the context
     */
    protected final Context<String, Object> getContext() {
        return this.context;
    }

    /**
     * @param ctx the context to set
     */
    protected final void setContext(final Context<String, Object> ctx) {
        this.context = ctx;
    }

    // -------------------------------------------------- Overall Test Methods


    /**
     * Set up instance variables required by this test case.
     */
    @Before
    public void setUp() {
        this.context = createContext();
    }

    /**
     * Tear down instance variables required by this test case.
     */
    @After
    public void tearDown() {
        this.context = null;
    }


    // ------------------------------------------------ Individual Test Methods


    /**
     * Test ability to get, put, and remove attributes
     */
    @Test
    public void testAttributes() {

        testAttributes1();

        testAttributes2();

    }

    /**
     * testAttributes1
     */
    private void testAttributes1() {

        checkAttributeCount(0);

        this.context.put("foo", "This is foo");
        checkAttributeCount(1);
        Object value = this.context.get("foo");
        assertNotNull("Returned foo", value);
        assertTrue("Returned foo type", value instanceof String);
        assertEquals("Returned foo value", "This is foo", value);

        this.context.put("bar", "This is bar");
        checkAttributeCount(2);
        value = this.context.get("bar");
        assertNotNull("Returned bar", value);
        assertTrue("Returned bar type", value instanceof String);
        assertEquals("Returned bar value", "This is bar", value);

        this.context.put("baz", "This is baz");
        checkAttributeCount(3);
        value = this.context.get("baz");
        assertNotNull("Returned baz", value);
        assertTrue("Returned baz type", value instanceof String);
        assertEquals("Returned baz value", "This is baz", value);

    }

    /**
     * testAttributes2
     */
    private void testAttributes2() {

        this.context.put("baz", "This is new baz");
        // Replaced, not added
        checkAttributeCount(3);
        Object value = this.context.get("baz");
        assertNotNull("Returned baz", value);
        assertTrue("Returned baz type", value instanceof String);
        assertEquals("Returned baz value", "This is new baz", value);

        this.context.remove("bar");
        checkAttributeCount(2);
        assertNull("Did not return bar", this.context.get("bar"));
        assertNotNull("Still returned foo", this.context.get("foo"));
        assertNotNull("Still returned baz", this.context.get("baz"));

        this.context.clear();
        checkAttributeCount(0);
        assertNull("Did not return foo", this.context.get("foo"));
        assertNull("Did not return bar", this.context.get("bar"));
        assertNull("Did not return baz", this.context.get("baz"));

    }


    /**
     * Test containsKey() and containsValue()
     */
    @Test
    public void testContains() {

        assertFalse(this.context.containsKey("bop"));
        assertFalse(this.context.containsValue("bop value"));
        this.context.put("bop", "bop value");
        assertTrue(this.context.containsKey("bop"));
        assertTrue(this.context.containsValue("bop value"));
        this.context.remove("bop");
        assertFalse(this.context.containsKey("bop"));
        assertFalse(this.context.containsValue("bop value"));

    }


    /**
     * Test equals() and hashCode()
     */
    @Test
    public void testEquals() {

        // Compare to self
        assertEquals(this.context, this.context);
        assertEquals(this.context.hashCode(), this.context.hashCode());

        // Compare to equivalent instance
        Context<String, Object> other = createContext();
        assertEquals(this.context, other);
        assertEquals(this.context.hashCode(), other.hashCode());

        // Compare to non-equivalent instance - other modified
        other.put("bop", "bop value");
        assertTrue(!this.context.equals(other));
        assertTrue(this.context.hashCode() != other.hashCode());

        // Compare to non-equivalent instance - self modified
        // reset to equivalence
        other = createContext();
        this.context.put("bop", "bop value");
        assertTrue(!this.context.equals(other));
        assertTrue(this.context.hashCode() != other.hashCode());

    }

    /**
     * contextKeySetDoesNotSupportAdd
     */
    @Test(expected = UnsupportedOperationException.class)
    public void contextKeySetDoesNotSupportAdd() {
        Set<String> keySet = this.context.keySet();
        keySet.add("bop");
    }

    /**
     * contextKeySetDoesNotSupportAddAll
     */
    @Test(expected = UnsupportedOperationException.class)
    public void contextKeySetDoesNotSupportAddAll() {
        Collection<String> adds = new ArrayList<>();
        adds.add("bop");

        Set<String> keySet = this.context.keySet();
        keySet.addAll(adds);
    }

    /**
     * Test keySet()
     */
    @Test
    public void testKeySet() {

        testKeySet1();

        // Add the new elements
        this.context.put("foo", "foo value");
        this.context.put("bar", "bar value");
        this.context.put("baz", "baz value");

        Collection<String> all = new ArrayList<>();
        all.add("foo");
        all.add("bar");
        all.add("baz");

        testKeySet2(all);
        testKeySet3(all);

        // Add the new elements #2
        this.context.put("foo", "foo value");
        this.context.put("bar", "bar value");
        this.context.put("baz", "baz value");

        all.add("foo");
        all.add("bar");
        all.add("baz");

        testKeySet4(all);

    }

    /**
     * testKeySet1
     */
    private void testKeySet1() {

        // Before-modification checks
        Set<String> keySet = this.context.keySet();
        assertEquals(createContext().size(), keySet.size());
        assertFalse(keySet.contains("foo"));
        assertFalse(keySet.contains("bar"));
        assertFalse(keySet.contains("baz"));
        assertFalse(keySet.contains("bop"));

    }

    /**
     * testKeySet2
     * @param all Collection
     */
    private void testKeySet2(final Collection<String> all) {

        // After-modification checks
        Set<String> keySet = this.context.keySet();
        assertEquals(expectedAttributeCount() + 3, keySet.size());
        assertTrue(keySet.contains("foo"));
        assertTrue(keySet.contains("bar"));
        assertTrue(keySet.contains("baz"));
        assertFalse(keySet.contains("bop"));
        assertTrue(keySet.containsAll(all));

        // Remove a single element via remove()
        this.context.remove("bar");
        all.remove("bar");
        keySet = this.context.keySet();
        assertEquals(expectedAttributeCount() + 2, keySet.size());
        assertTrue(keySet.contains("foo"));
        assertFalse(keySet.contains("bar"));
        assertTrue(keySet.contains("baz"));
        assertFalse(keySet.contains("bop"));
        assertTrue(keySet.containsAll(all));

    }

    /**
     * testKeySet3
     * @param all Collection
     */
    private void testKeySet3(final Collection<String> all) {

        // Remove a single element via keySet.remove()
        Set<String> keySet = this.context.keySet();
        keySet.remove("baz");
        all.remove("baz");
        keySet = this.context.keySet();
        assertEquals(expectedAttributeCount() + 1, keySet.size());
        assertTrue(keySet.contains("foo"));
        assertFalse(keySet.contains("bar"));
        assertFalse(keySet.contains("baz"));
        assertFalse(keySet.contains("bop"));
        assertTrue(keySet.containsAll(all));

        // Remove all elements via keySet.clear()
        keySet.clear();
        all.clear();
        assertEquals(expectedAttributeCount(), keySet.size());
        assertFalse(keySet.contains("foo"));
        assertFalse(keySet.contains("bar"));
        assertFalse(keySet.contains("baz"));
        assertFalse(keySet.contains("bop"));
        assertTrue(keySet.containsAll(all));

    }

    /**
     * testKeySet4
     * @param all Collection
     */
    private void testKeySet4(final Collection<String> all) {

        // After-modification checks #2
        Set<String> keySet = this.context.keySet();
        assertEquals(expectedAttributeCount() + 3, keySet.size());
        assertTrue(keySet.contains("foo"));
        assertTrue(keySet.contains("bar"));
        assertTrue(keySet.contains("baz"));
        assertFalse(keySet.contains("bop"));
        assertTrue(keySet.containsAll(all));

    }


    /**
     * Test state of newly created instance
     */
    @Test
    public void testPristine() {

        checkAttributeCount(0);
        assertNull("No 'foo' attribute", this.context.get("foo"));

    }


    /**
     * Test putAll()
     */
    @Test
    public void testPutAll() {

        // Check preconditions
        checkAttributeCount(0);
        assertNull(this.context.get("foo"));
        assertNull(this.context.get("bar"));
        assertNull(this.context.get("baz"));
        assertFalse(this.context.containsKey("foo"));
        assertFalse(this.context.containsKey("bar"));
        assertFalse(this.context.containsKey("baz"));
        assertFalse(this.context.containsValue("foo value"));
        assertFalse(this.context.containsValue("bar value"));
        assertFalse(this.context.containsValue("baz value"));

        // Call putAll()
        Map<String, String> adds = new HashMap<>();
        adds.put("foo", "foo value");
        adds.put("bar", "bar value");
        adds.put("baz", "baz value");
        this.context.putAll(adds);

        // Check postconditions
        checkAttributeCount(3);
        assertEquals("foo value", this.context.get("foo"));
        assertEquals("bar value", this.context.get("bar"));
        assertEquals("baz value", this.context.get("baz"));
        assertTrue(this.context.containsKey("foo"));
        assertTrue(this.context.containsKey("bar"));
        assertTrue(this.context.containsKey("baz"));
        assertTrue(this.context.containsValue("foo value"));
        assertTrue(this.context.containsValue("bar value"));
        assertTrue(this.context.containsValue("baz value"));

    }


    /**
     * Test serialization
     */
    @Test
    public void testSerialization() {

        // ContextBase is implicitly declared Serializable because it
        // extends HashMap.  However, it is not possible to make
        // the concrete subclasses of WebContext Serializable, because
        // the underlying container objects that they wrap will not be.
        // Therefore, skip testing serializability of these implementations
        if (ContextBase.class != this.context.getClass()) {
            return;
        }

        // Set up the context with some parameters
        this.context.put("foo", "foo value");
        this.context.put("bar", "bar value");
        this.context.put("baz", "baz value");
        checkAttributeCount(3);

        // Serialize to a byte array
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
            oos.writeObject(this.context);
        } catch (final IOException e) {
            throw new RuntimeException(e);
        }

        // Deserialize back to a new object
        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        try (ObjectInputStream ois = new ObjectInputStream(bais)) {
            Context<String, Object> newContext = ChainUtil.cast(ois.readObject());

            // Do some rudimentary checks to make sure we have the same contents
            assertTrue(newContext.containsKey("foo"));
            assertTrue(newContext.containsKey("bar"));
            assertTrue(newContext.containsKey("baz"));
            checkAttributeCount(3);
        } catch (final IOException | ClassNotFoundException e) {
            throw new RuntimeException(e);
        }

    }



    // -------------------------------------------------------- Support Methods

    /**
     * Verify the number of defined attributes
     * @param expected int
     */
    protected void checkAttributeCount(final int expected) {
        int actual = this.context.keySet().size();
        assertEquals("Correct attribute count", expectedAttributeCount() + expected, actual);
        if (expected == 0) {
            assertTrue("Context should be empty", this.context.isEmpty());
        } else {
            assertFalse("Context should not be empty", this.context.isEmpty());
        }
    }

    /**
     * Create a new instance of the appropriate Context type for this test case
     * @return Context
     */
    protected Context<String, Object> createContext() {
        return new ContextBase();
    }

    /**
     * Return the expected size() for a Context for this test case
     * @return int
     */
    protected int expectedAttributeCount() {
        return createContext().size();
    }

}
