/*
 * 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.web.servlet;

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

import java.lang.reflect.Array;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.servlet.ServletContext;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.WebMock;
import org.apache.commons.chain2.ChainUtil;
import org.apache.commons.chain2.Context;
import org.apache.commons.chain2.impl.ContextBaseTestCase;
import org.apache.commons.chain2.web.WebContext;
import org.apache.commons.logging.LogFactory;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;


/**
 * Extension of <code>ContextBaseTestCase</code> to validate the
 * extra functionality of this implementation.
 */

public final class ServletWebContextBaseTestCase extends ContextBaseTestCase {


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


    // Servlet API Objects
    /** ServletContext */
    private ServletContext scontext = null;
    /** HttpServletRequest */
    private HttpServletRequest request = null;
    /** HttpServletResponse */
    private HttpServletResponse response = null;
    /** HttpSession */
    private HttpSession session = null;


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


    /**
     * Set up instance variables required by this test case.
     */
    @Before
    @Override
    public void setUp() {

        this.scontext = WebMock.createServletContext();
        this.scontext.setAttribute("akey1", "avalue1");
        this.scontext.setAttribute("akey2", "avalue2");
        this.scontext.setAttribute("akey3", "avalue3");
        this.scontext.setAttribute("akey4", "avalue4");
        this.scontext.setInitParameter("ikey1", "ivalue1");
        this.scontext.setInitParameter("ikey2", "ivalue2");
        this.scontext.setInitParameter("ikey3", "ivalue3");

        this.session = WebMock.createHttpSession();
        this.session.setAttribute("skey1", "svalue1");
        this.session.setAttribute("skey2", "svalue2");
        this.session.setAttribute("skey3", "svalue3");
        Mockito.when(this.session.getServletContext()).thenReturn(this.scontext);

        final Map<String, String[]> hMap = new HashMap<>();
        hMap.put("hkey1", new String[]{"hvalue1"});
        hMap.put("hkey2", new String[]{"hvalue2a", "hvalue2b"});
        final Map<String, String[]> pMap = new HashMap<>();
        pMap.put("pkey1", new String[]{"pvalue1"});
        pMap.put("pkey2", new String[]{"pvalue2a", "pvalue2b"});

        this.request = WebMock.createHttpServletRequest(this.session, hMap, pMap);
        Mockito.when(this.request.getContextPath()).thenReturn("/context");
        Mockito.when(this.request.getServletPath()).thenReturn("/servlet");
        Mockito.when(this.request.getPathInfo()).thenReturn("/path/info");
        Mockito.when(this.request.getQueryString()).thenReturn("a=b&c=d");
        Assert.assertNotNull(this.request.getSession());

        this.request.setAttribute("rkey1", "rvalue1");
        this.request.setAttribute("rkey2", "rvalue2");

        Mockito.when(this.request.getCookies()).thenReturn(new Cookie[]{
            new Cookie("ckey1", "cvalue1"), new Cookie("ckey2", "cvalue2")
        });

        this.response = Mockito.mock(HttpServletResponse.class);

        super.setContext(createContext());
    }


    /**
     * Tear down instance variables required by this test case.
     */
    @After
    @Override
    public void tearDown() {
        this.scontext = null;
        this.session = null;
        this.request = null;
        this.response = null;
        super.setContext(null);
    }


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


    /**
     * Test getApplicationScope()
     * @throws Exception Exception
     */
    @Test
    public void testApplicationScope() throws Exception {

        Map<String, Object> map =
                ((WebContext<String, Object>) super.getContext()).getApplicationScope();
        assertNotNull(map);

        testApplicationScope1(map);

        testApplicationScope2(map);

    }

    /**
     * Test getApplicationScope()
     * @param map Map
     * @throws Exception Exception
     */
    private void testApplicationScope1(final Map<String, Object> map) throws Exception {

        // Initial contents
        checkMapSize(map, 4);
        assertEquals("avalue1", map.get("akey1"));
        assertEquals("avalue2", map.get("akey2"));
        assertEquals("avalue3", map.get("akey3"));
        assertEquals("avalue4", map.get("akey4"));

        // Transparency - entrySet()
        checkEntrySet(map, true, Object.class);

        // Transparency - removal via web object
        this.scontext.removeAttribute("akey1");
        checkMapSize(map, 3);
        assertNull(map.get("akey1"));

        // Transparency - removal via map
        map.remove("akey2");
        checkMapSize(map, 2);
        assertNull(this.scontext.getAttribute("akey2"));

        // Transparency - addition via web object
        this.scontext.setAttribute("akeyA", "avalueA");
        checkMapSize(map, 3);
        assertEquals("avalueA", map.get("akeyA"));

    }

    /**
     * Test getApplicationScope()
     * @param map Map
     */
    private void testApplicationScope2(final Map<String, Object> map) {
        // Transparency - addition via map
        map.put("akeyB", "avalueB");
        checkMapSize(map, 4);
        assertEquals("avalueB", this.scontext.getAttribute("akeyB"));

        // Transparency - replacement via web object
        this.scontext.setAttribute("akeyA", "newvalueA");
        checkMapSize(map, 4);
        assertEquals("newvalueA", map.get("akeyA"));

        // Transparency - replacement via map
        map.put("akeyB", "newvalueB");
        assertEquals(4, map.size());
        assertEquals("newvalueB", this.scontext.getAttribute("akeyB"));

        // Clearing the map
        map.clear();
        checkMapSize(map, 0);

        // Test putAll()
        Map<String, Object> values = new HashMap<>();
        values.put("1", "One");
        values.put("2", "Two");
        map.putAll(values);
        assertEquals("putAll(1)", "One", map.get("1"));
        assertEquals("putAll(2)", "Two", map.get("2"));
        checkMapSize(map, 2);
    }


    /**
     * Test equals() and hashCode()
     * Copied from ContextBaseTestCase with customized creation of "other"
     */
    @Test
    @Override
    public void testEquals() {

        // FIXME - ServletWebContextBase needs a better equals()

        // Compare to self
        assertEquals(super.getContext(), super.getContext());
        assertEquals(super.getContext().hashCode(), super.getContext().hashCode());

        // Compare to equivalent instance
        Context<String, Object> other =
                new ServletWebContextBase(this.scontext, this.request, this.response);
        // assertTrue(context.equals(other));
        assertEquals(super.getContext().hashCode(), other.hashCode());

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

        // Compare to non-equivalent instance - self modified
        other = new ServletWebContextBase(this.scontext, this.request, this.response);
        super.getContext().put("bop", "bop value");
        // assertTrue(!context.equals(other));
        assertTrue(super.getContext().hashCode() != other.hashCode());

    }


    /**
     * Test getHeader()
     * @throws Exception Exception
     */
    @Test
    public void testHeader() throws Exception {

        Map<String, String> map = ((WebContext<String, Object>) super.getContext()).getHeader();
        assertNotNull(map);

        // Initial contents
        checkMapSize(map, 2);
        assertEquals("hvalue1", map.get("hkey1"));
        assertEquals("hvalue2a", map.get("hkey2"));
        assertTrue(map.containsKey("hkey1"));
        assertTrue(map.containsKey("hkey2"));
        assertTrue(map.containsValue("hvalue1"));
        assertTrue(map.containsValue("hvalue2a"));

        // Transparency - entrySet()
        checkEntrySet(map, false, String.class);

        // Unsupported operations on read-only map
        try {
            map.clear();
            fail("Should have thrown UnsupportedOperationException");
        } catch (final UnsupportedOperationException e) {
            // expected result
            LogFactory.getLog(ServletWebContextBaseTestCase.class).debug(e.getMessage());
        }
        try {
            map.put("hkey3", "hvalue3");
            fail("Should have thrown UnsupportedOperationException");
        } catch (final UnsupportedOperationException e) {
            // expected result
            LogFactory.getLog(ServletWebContextBaseTestCase.class).debug(e.getMessage());
        }
        try {
            map.putAll(new HashMap<>());
            fail("Should have thrown UnsupportedOperationException");
        } catch (final UnsupportedOperationException e) {
            // expected result
            LogFactory.getLog(ServletWebContextBaseTestCase.class).debug(e.getMessage());
        }
        try {
            map.remove("hkey1");
            fail("Should have thrown UnsupportedOperationException");
        } catch (final UnsupportedOperationException e) {
            // expected result
            LogFactory.getLog(ServletWebContextBaseTestCase.class).debug(e.getMessage());
        }
    }


    /**
     * Test getHeaderValues()
     * @throws Exception Exception
     */
    @Test
    public void testHeaderValues() throws Exception {

        Map<String, String[]> map =
                ((WebContext<String, Object>) super.getContext()).getHeaderValues();
        assertNotNull(map);

        testHeaderValues1(map);

    }

    /**
     * Test getHeaderValues()
     * @param map Map
     * @throws Exception Exception
     */
    private void testHeaderValues1(final Map<String, String[]> map) throws Exception {

        // Initial contents
        checkMapSize(map, 2);
        Object value1 = map.get("hkey1");
        assertNotNull(value1);
        String[] values1 = (String[]) value1;
        assertEquals(1, values1.length);
        assertEquals("hvalue1", values1[0]);
        Object value2 = map.get("hkey2");
        assertNotNull(value2);
        String[] values2 = (String[]) value2;
        assertEquals(2, values2.length);
        assertEquals("hvalue2a", values2[0]);
        assertEquals("hvalue2b", values2[1]);
        assertTrue(map.containsKey("hkey1"));
        assertTrue(map.containsKey("hkey2"));
        assertTrue(map.containsValue(values1));
        assertTrue(map.containsValue(values2));

        // Transparency - entrySet()
        checkEntrySet(map, false, String[].class);

        testHeaderValues2(map, values2);

    }

    /**
     * Test getHeaderValues()
     * @param map Map
     * @param values2 String[]
     */
    private void testHeaderValues2(final Map<String, String[]> map, final String[] values2) {

        // Unsupported operations on read-only map
        try {
            map.clear();
            fail("Should have thrown UnsupportedOperationException");
        } catch (final UnsupportedOperationException e) {
            // expected result
            LogFactory.getLog(ServletWebContextBaseTestCase.class).debug(e.getMessage());
        }
        try {
            map.put("hkey3", values2);
            fail("Should have thrown UnsupportedOperationException");
        } catch (final UnsupportedOperationException e) {
            // expected result
            LogFactory.getLog(ServletWebContextBaseTestCase.class).debug(e.getMessage());
        }
        try {
            map.putAll(new HashMap<>());
            fail("Should have thrown UnsupportedOperationException");
        } catch (final UnsupportedOperationException e) {
            // expected result
            LogFactory.getLog(ServletWebContextBaseTestCase.class).debug(e.getMessage());
        }
        try {
            map.remove("hkey1");
            fail("Should have thrown UnsupportedOperationException");
        } catch (final UnsupportedOperationException e) {
            // expected result
            LogFactory.getLog(ServletWebContextBaseTestCase.class).debug(e.getMessage());
        }

    }


    /**
     * Test getInitParam()
     * @throws Exception Exception
     */
    @Test
    public void testInitParam() throws Exception {

        Map<String, String> map = ((WebContext<String, Object>) super.getContext()).getInitParam();
        assertNotNull(map);

        // Initial contents
        checkMapSize(map, 3);
        assertEquals("ivalue1", map.get("ikey1"));
        assertEquals("ivalue2", map.get("ikey2"));
        assertEquals("ivalue3", map.get("ikey3"));
        assertTrue(map.containsKey("ikey1"));
        assertTrue(map.containsKey("ikey2"));
        assertTrue(map.containsKey("ikey3"));
        assertTrue(map.containsValue("ivalue1"));
        assertTrue(map.containsValue("ivalue2"));
        assertTrue(map.containsValue("ivalue3"));

        // Transparency - entrySet()
        checkEntrySet(map, false, String.class);

        // Unsupported operations on read-only map
        try {
            map.clear();
            fail("Should have thrown UnsupportedOperationException");
        } catch (final UnsupportedOperationException e) {
            // expected result
            LogFactory.getLog(ServletWebContextBaseTestCase.class).debug(e.getMessage());
        }
        try {
            map.put("ikey4", "ivalue4");
            fail("Should have thrown UnsupportedOperationException");
        } catch (final UnsupportedOperationException e) {
            // expected result
            LogFactory.getLog(ServletWebContextBaseTestCase.class).debug(e.getMessage());
        }
        try {
            map.putAll(new HashMap<>());
            fail("Should have thrown UnsupportedOperationException");
        } catch (final UnsupportedOperationException e) {
            // expected result
            LogFactory.getLog(ServletWebContextBaseTestCase.class).debug(e.getMessage());
        }
        try {
            map.remove("ikey1");
            fail("Should have thrown UnsupportedOperationException");
        } catch (final UnsupportedOperationException e) {
            // expected result
            LogFactory.getLog(ServletWebContextBaseTestCase.class).debug(e.getMessage());
        }

    }


    /**
     * Test getParam()
     * @throws Exception Exception
     */
    @Test
    public void testParam() throws Exception {

        Map<String, String> map = ((WebContext<String, Object>) super.getContext()).getParam();
        assertNotNull(map);

        // Initial contents
        checkMapSize(map, 2);
        assertEquals("pvalue1", map.get("pkey1"));
        assertEquals("pvalue2a", map.get("pkey2"));
        assertTrue(map.containsKey("pkey1"));
        assertTrue(map.containsKey("pkey2"));
        assertTrue(map.containsValue("pvalue1"));
        assertTrue(map.containsValue("pvalue2a"));

        checkEntrySet(map, false, String.class);

        // Unsupported operations on read-only map
        try {
            map.clear();
            fail("Should have thrown UnsupportedOperationException");
        } catch (final UnsupportedOperationException e) {
            // expected result
            LogFactory.getLog(ServletWebContextBaseTestCase.class).debug(e.getMessage());
        }
        try {
            map.put("pkey3", "pvalue3");
            fail("Should have thrown UnsupportedOperationException");
        } catch (final UnsupportedOperationException e) {
            // expected result
            LogFactory.getLog(ServletWebContextBaseTestCase.class).debug(e.getMessage());
        }
        try {
            map.putAll(new HashMap<>());
            fail("Should have thrown UnsupportedOperationException");
        } catch (final UnsupportedOperationException e) {
            // expected result
            LogFactory.getLog(ServletWebContextBaseTestCase.class).debug(e.getMessage());
        }
        try {
            map.remove("pkey1");
            fail("Should have thrown UnsupportedOperationException");
        } catch (final UnsupportedOperationException e) {
            // expected result
            LogFactory.getLog(ServletWebContextBaseTestCase.class).debug(e.getMessage());
        }

    }


    /**
     * Test getParamValues()
     */
    @Test
    public void testParamValues() {

        Map<String, String[]> map =
                ((WebContext<String, Object>) super.getContext()).getParamValues();
        assertNotNull(map);

        testParamValues1(map);

    }

    /**
     * Test getParamValues()
     * @param map Map
     */
    private void testParamValues1(final Map<String, String[]> map) {
        // Initial contents
        checkMapSize(map, 2);
        Object value1 = map.get("pkey1");
        assertNotNull(value1);
        String[] values1 = (String[]) value1;
        assertEquals(1, values1.length);
        assertEquals("pvalue1", values1[0]);
        Object value2 = map.get("pkey2");
        assertNotNull(value2);
        String[] values2 = (String[]) value2;
        assertEquals(2, values2.length);
        assertEquals("pvalue2a", values2[0]);
        assertEquals("pvalue2b", values2[1]);
        assertTrue(map.containsKey("pkey1"));
        assertTrue(map.containsKey("pkey2"));
        assertTrue(map.containsValue(values1));
        assertTrue(map.containsValue(values2));

        testParamValues2(map, values2);
    }

    /**
     * Test getParamValues()
     * @param map Map
     * @param values2 String[]
     */
    private void testParamValues2(final Map<String, String[]> map, final String[] values2) {

        // Unsupported operations on read-only map
        try {
            map.clear();
            fail("Should have thrown UnsupportedOperationException");
        } catch (final UnsupportedOperationException e) {
            // expected result
            LogFactory.getLog(ServletWebContextBaseTestCase.class).debug(e.getMessage());
        }
        try {
            map.put("pkey3", values2);
            fail("Should have thrown UnsupportedOperationException");
        } catch (final UnsupportedOperationException e) {
            // expected result
            LogFactory.getLog(ServletWebContextBaseTestCase.class).debug(e.getMessage());
        }
        try {
            map.putAll(new HashMap<>());
            fail("Should have thrown UnsupportedOperationException");
        } catch (final UnsupportedOperationException e) {
            // expected result
            LogFactory.getLog(ServletWebContextBaseTestCase.class).debug(e.getMessage());
        }
        try {
            map.remove("pkey1");
            fail("Should have thrown UnsupportedOperationException");
        } catch (final UnsupportedOperationException e) {
            // expected result
            LogFactory.getLog(ServletWebContextBaseTestCase.class).debug(e.getMessage());
        }

    }


    /**
     * Test getCookies()
     */
    @Test
    public void testCookies() {

        Map<String, Cookie> map = ((WebContext<String, Object>) super.getContext()).getCookies();
        assertNotNull(map);

        // Initial contents
        checkMapSize(map, 2);
        Cookie cookie1 = map.get("ckey1");
        assertNotNull(cookie1);
        assertEquals("cvalue1", cookie1.getValue());
        Cookie cookie2 = map.get("ckey2");
        assertNotNull(cookie2);
        assertEquals("cvalue2", cookie2.getValue());
        assertTrue(map.containsKey("ckey1"));
        assertTrue(map.containsKey("ckey2"));
        assertTrue(map.containsValue(cookie1));
        assertTrue(map.containsValue(cookie2));

        // Unsupported operations on read-only map
        try {
            map.clear();
            fail("Should have thrown UnsupportedOperationException");
        } catch (final UnsupportedOperationException e) {
            // expected result
            LogFactory.getLog(ServletWebContextBaseTestCase.class).debug(e.getMessage());
        }
        /* remove
           map is typed, that assignment is not possible by definition
        try {
            map.put("ckey3", "ZZZ");
            fail("Should have thrown UnsupportedOperationException");
        } catch (ClassCastException e) {
            // expected result
        }
        */
        try {
            map.putAll(new HashMap<>());
            fail("Should have thrown UnsupportedOperationException");
        } catch (final UnsupportedOperationException e) {
            // expected result
            LogFactory.getLog(ServletWebContextBaseTestCase.class).debug(e.getMessage());
        }
        try {
            map.remove("ckey1");
            fail("Should have thrown UnsupportedOperationException");
        } catch (final UnsupportedOperationException e) {
            // expected result
            LogFactory.getLog(ServletWebContextBaseTestCase.class).debug(e.getMessage());
        }

    }

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

        super.testPristine();
        ServletWebContext<String, Object> swcontext =
                (ServletWebContext<String, Object>) super.getContext();

        // Properties should all be non-null
        assertNotNull(swcontext.getApplicationScope());
        assertNotNull(swcontext.getHeader());
        assertNotNull(swcontext.getHeaderValues());
        assertNotNull(swcontext.getInitParam());
        assertNotNull(swcontext.getParam());
        assertNotNull(swcontext.getParamValues());
        assertNotNull(swcontext.getCookies());
        assertNotNull(swcontext.getRequestScope());
        assertNotNull(swcontext.getSessionScope());

        // Attribute-property transparency
        assertSame(swcontext.getApplicationScope(), swcontext.get("applicationScope"));
        assertSame(swcontext.getHeader(), swcontext.get("header"));
        assertSame(swcontext.getHeaderValues(), swcontext.get("headerValues"));
        assertSame(swcontext.getInitParam(), swcontext.get("initParam"));
        assertSame(swcontext.getParam(), swcontext.get("param"));
        assertSame(swcontext.getParamValues(), swcontext.get("paramValues"));
        assertSame(swcontext.getCookies(), swcontext.get("cookies"));
        assertSame(swcontext.getRequestScope(), swcontext.get("requestScope"));
        assertSame(swcontext.getSessionScope(), swcontext.get("sessionScope"));

    }


    /**
     * Test release()
     */
    @Test
    public void testRelease() {

        ServletWebContext<String, Object> swcontext =
                (ServletWebContext<String, Object>) super.getContext();
        swcontext.release();

        // Properties should all be null
        assertNull(swcontext.getApplicationScope());
        assertNull(swcontext.getHeader());
        assertNull(swcontext.getHeaderValues());
        assertNull(swcontext.getInitParam());
        assertNull(swcontext.getParam());
        assertNull(swcontext.getParamValues());
        assertNull(swcontext.getCookies());
        assertNull(swcontext.getRequestScope());
        assertNull(swcontext.getSessionScope());

        // Attributes should all be null
        assertNull(swcontext.get("applicationScope"));
        assertNull(swcontext.get("header"));
        assertNull(swcontext.get("headerValues"));
        assertNull(swcontext.get("initParam"));
        assertNull(swcontext.get("param"));
        assertNull(swcontext.get("paramValues"));
        assertNull(swcontext.get("cookies"));
        assertNull(swcontext.get("requestScope"));
        assertNull(swcontext.get("sessionScope"));

    }


    /**
     * Test getRequestScope()
     * @throws Exception Exception
     */
    @Test
    public void testRequestScope() throws Exception {

        Map<String, Object> map =
                ((WebContext<String, Object>) super.getContext()).getRequestScope();
        assertNotNull(map);

        testRequestScope1(map);

        testRequestScope2(map);

    }

    /**
     * Test getRequestScope()
     * @param map Map
     * @throws Exception Exception
     */
    private void testRequestScope1(final Map<String, Object> map) throws Exception {

        // Initial contents
        checkMapSize(map, 2);
        assertEquals("rvalue1", map.get("rkey1"));
        assertEquals("rvalue2", map.get("rkey2"));

        // Transparency - entrySet()
        checkEntrySet(map, true, Object.class);

        // Transparency - removal via web object
        this.request.removeAttribute("rkey1");
        checkMapSize(map, 1);
        assertNull(map.get("rkey1"));

        // Transparency - removal via map
        map.remove("rkey2");
        checkMapSize(map, 0);
        assertNull(this.request.getAttribute("rkey2"));

        // Transparency - addition via web object
        this.request.setAttribute("rkeyA", "rvalueA");
        checkMapSize(map, 1);
        assertEquals("rvalueA", map.get("rkeyA"));

    }

    /**
     * Test getRequestScope()
     * @param map Map
     */
    private void testRequestScope2(final Map<String, Object> map) {

        // Transparency - addition via map
        map.put("rkeyB", "rvalueB");
        checkMapSize(map, 2);
        assertEquals("rvalueB", this.request.getAttribute("rkeyB"));

        // Transparency - replacement via web object
        this.request.setAttribute("rkeyA", "newvalueA");
        checkMapSize(map, 2);
        assertEquals("newvalueA", map.get("rkeyA"));

        // Transparency - replacement via map
        map.put("rkeyB", "newvalueB");
        checkMapSize(map, 2);
        assertEquals("newvalueB", this.request.getAttribute("rkeyB"));

        // Clearing the map
        map.clear();
        checkMapSize(map, 0);

        // Test putAll()
        Map<String, Object> values = new HashMap<>();
        values.put("1", "One");
        values.put("2", "Two");
        map.putAll(values);
        assertEquals("putAll(1)", "One", map.get("1"));
        assertEquals("putAll(2)", "Two", map.get("2"));
        checkMapSize(map, 2);

    }


    /**
     * Test getSessionScope()
     * @throws Exception Exception
     */
    @Test
    public void testSessionScope() throws Exception {

        Map<String, Object> map =
                ((WebContext<String, Object>) super.getContext()).getSessionScope();
        assertNotNull(map);

        testSessionScope1(map);

        testSessionScope2(map);

    }

    /**
     * Test getSessionScope()
     * @param map Map
     * @throws Exception Exception
     */
    private void testSessionScope1(final Map<String, Object> map) throws Exception {
        // Initial contents
        checkMapSize(map, 3);
        assertEquals("svalue1", map.get("skey1"));
        assertEquals("svalue2", map.get("skey2"));
        assertEquals("svalue3", map.get("skey3"));

        // Transparency - entrySet()
        checkEntrySet(map, true, Object.class);

        // Transparency - removal via web object
        this.session.removeAttribute("skey1");
        checkMapSize(map, 2);
        assertNull(map.get("skey1"));

        // Transparency - removal via map
        map.remove("skey2");
        checkMapSize(map, 1);
        assertNull(this.session.getAttribute("skey2"));

        // Transparency - addition via web object
        this.session.setAttribute("skeyA", "svalueA");
        checkMapSize(map, 2);
        assertEquals("svalueA", map.get("skeyA"));

        // Transparency - addition via map
        map.put("skeyB", "svalueB");
        checkMapSize(map, 3);
        assertEquals("svalueB", this.session.getAttribute("skeyB"));

    }

    /**
     * Test getSessionScope()
     * @param map Map
     */
    private void testSessionScope2(final Map<String, Object> map) {

        // Transparency - replacement via web object
        this.session.setAttribute("skeyA", "newvalueA");
        checkMapSize(map, 3);
        assertEquals("newvalueA", map.get("skeyA"));

        // Transparency - replacement via map
        map.put("skeyB", "newvalueB");
        checkMapSize(map, 3);
        assertEquals("newvalueB", this.session.getAttribute("skeyB"));

        // Clearing the map
        map.clear();
        checkMapSize(map, 0);

        // Test putAll()
        Map<String, Object> values = new HashMap<>();
        values.put("1", "One");
        values.put("2", "Two");
        map.putAll(values);
        assertEquals("putAll(1)", "One", map.get("1"));
        assertEquals("putAll(2)", "Two", map.get("2"));
        checkMapSize(map, 2);

    }


    /**
     * Test getSessionScope() without Session
     */
    @Test
    public void testSessionScopeWithoutSession() {

        // Create a Context without a session
        final HttpServletRequest req = WebMock.createHttpServletRequest(
                WebMock.createHttpSession(), new HashMap<>(), new HashMap<>());

        ServletWebContext<String, Object> ctx = new ServletWebContextBase(this.scontext,
                req, this.response);
        assertNull("Session(A)", ctx.getRequest().getSession(false));

        // Get the session Map & check session doesn't exist
        Map<String, Object> sessionMap = ctx.getSessionScope();

        testSessionScopeWithoutSession1(ctx, sessionMap);

        testSessionScopeWithoutSession2(ctx, sessionMap);

    }

    /**
     * Test getSessionScope() without Session
     * @param ctx ServletWebContext
     * @param sessionMap Map
     */
    private void testSessionScopeWithoutSession1(final ServletWebContext<String, Object> ctx,
            final Map<String, Object> sessionMap) {

        assertNull("Session(B)", ctx.getRequest().getSession(false));
        assertNotNull("Session Map(A)", sessionMap);

        // test clear()
        sessionMap.clear();
        assertNull("Session(C)", ctx.getRequest().getSession(false));

        // test containsKey()
        assertFalse("containsKey()", sessionMap.containsKey("ABC"));
        assertNull("Session(D)", ctx.getRequest().getSession(false));

        // test containsValue()
        assertFalse("containsValue()", sessionMap.containsValue("ABC"));
        assertNull("Session(E)", ctx.getRequest().getSession(false));

        // test entrySet()
        Set<Entry<String, Object>> entrySet = sessionMap.entrySet();
        assertNotNull("entrySet", entrySet);
        assertEquals("entrySet Size", 0, entrySet.size());
        assertNull("Session(F)", ctx.getRequest().getSession(false));

        // test equals()
        final Object value = "ABC";
        assertNotEquals("equals()", sessionMap, value);
        assertNull("Session(G)", ctx.getRequest().getSession(false));

        // test get()
        assertNull("get()", sessionMap.get("ABC"));
        assertNull("Session(H)", ctx.getRequest().getSession(false));

        // test hashCode()
        // sessionMap.hashCode();
        assertNull("Session(I)", ctx.getRequest().getSession(false));

    }

    /**
     * Test getSessionScope() without Session
     * @param ctx ServletWebContext
     * @param sessionMap Map
     */
    private void testSessionScopeWithoutSession2(final ServletWebContext<String, Object> ctx,
            final Map<String, Object> sessionMap) {

        // test isEmpty()
        assertTrue("isEmpty()", sessionMap.isEmpty());
        assertNull("Session(J)", ctx.getRequest().getSession(false));

        // test keySet()
        Set<String> keySet = sessionMap.keySet();
        assertNotNull("keySet", keySet);
        assertEquals("keySet Size", 0, keySet.size());
        assertNull("Session(K)", ctx.getRequest().getSession(false));

        // test putAll() with an empty Map
        sessionMap.putAll(new HashMap<>());
        assertNull("Session(L)", ctx.getRequest().getSession(false));

        // test remove()
        assertNull("remove()", sessionMap.remove("ABC"));
        assertNull("Session(M)", ctx.getRequest().getSession(false));

        // test size()
        assertEquals("size() Size", 0, sessionMap.size());
        assertNull("Session(N)", ctx.getRequest().getSession(false));

        // test values()
        Collection<Object> values = sessionMap.values();
        assertNotNull("values", values);
        assertEquals("values Size", 0, values.size());
        assertNull("Session(O)", ctx.getRequest().getSession(false));

        // test put()
        try {
            assertNull("put()", sessionMap.put("ABC", "XYZ"));
            assertNotNull("Session(P)", ctx.getRequest().getSession(false));
        } catch (final UnsupportedOperationException e) {
            // expected: currently MockHttpServletRequest throws this
            //           when trying to create a HttpSession
            LogFactory.getLog(ServletWebContextBaseTestCase.class).debug(e.getMessage());
        }

    }


    // ------------------------------------------------------- Protected Methods


    /**
     * @param map Map
     * @param size int
     */
    protected void checkMapSize(final Map<?, ?> map, final int size) {
        // Check reported size of the map
        assertEquals(size, map.size());
        assertEquals(size, map.keySet().size());
        // Iterate over entry set
        assertEquals(size, map.entrySet().size());
        // Count the values
        assertEquals(size, map.values().size());
    }

    /**
     *  Test to ensure proper entrySet() and are modifiable optionally
     * @param <T> Generic
     * @param map Map
     * @param modifiable boolean
     * @param cls Class
     * @throws Exception Exception
     */
    protected <T> void checkEntrySet(final Map<String, T> map,
            final boolean modifiable, final Class<T> cls) throws Exception {
        assertTrue(map.size() > 1);
        Set<Entry<String, T>> entries = map.entrySet();
        assertEquals(map.size(), entries.size());
        Entry<String, T> o = entries.iterator().next();

        try {
            if (!cls.isArray()) {
                o.setValue(cls.getConstructor().newInstance());
            } else {
                o.setValue(ChainUtil.cast(Array.newInstance(cls.getComponentType(), 0)));
            }
            if (!modifiable) {
                fail("Should have thrown UnsupportedOperationException");
            }
        } catch (final UnsupportedOperationException e) {
            // expected result
            LogFactory.getLog(ServletWebContextBaseTestCase.class).debug(e.getMessage());
        }
    }

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

}
