/*
 * 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.shiro.web.filter.mgt

import org.apache.shiro.config.ConfigurationException
import org.apache.shiro.web.filter.authz.SslFilter
import org.apache.shiro.web.servlet.ShiroFilter
import org.hamcrest.Matchers
import org.junit.jupiter.api.BeforeEach

import javax.servlet.Filter
import javax.servlet.FilterChain
import javax.servlet.FilterConfig
import javax.servlet.ServletContext
import org.junit.jupiter.api.Test

import static org.easymock.EasyMock.*
import static org.junit.jupiter.api.Assertions.*
import static org.hamcrest.MatcherAssert.assertThat

/**
 * Unit tests for the {@link DefaultFilterChainManager} implementation.
 */
class DefaultFilterChainManagerTest {

    DefaultFilterChainManager manager;

    @BeforeEach
    void setUp() {
        this.manager = new DefaultFilterChainManager();
    }

    //SHIRO-205
    @Test
    void testToNameConfigPairNoBrackets() {
        def token = "foo"

        String[] pair = manager.toNameConfigPair(token);

        assertNotNull pair
        assertEquals 2, pair.length
        assertEquals "foo", pair[0]
        assertNull pair[1]
    }

    //SHIRO-205
    @Test
    void testToNameConfigPairWithEmptyBrackets() {
        def token = "foo[]"

        String[] pair = manager.toNameConfigPair(token);

        assertNotNull pair
        assertEquals 2, pair.length
        assertEquals "foo", pair[0]
        assertNull pair[1]
    }

    //SHIRO-205
    @Test
    void testToNameConfigPairWithPopulatedBrackets() {
        def token = "foo[bar, baz]"

        String[] pair = manager.toNameConfigPair(token);

        assertNotNull pair
        assertEquals 2, pair.length
        assertEquals "foo", pair[0]
        assertEquals "bar, baz", pair[1]
    }

    //SHIRO-205 - asserts backwards compatibility before SHIRO-205 was implemented:
    @Test
    void testToNameConfigPairWithNestedQuotesInBrackets() {
        def token = 'roles["guest, admin"]'

        String[] pair = manager.toNameConfigPair(token);

        assertNotNull pair
        assertEquals 2, pair.length
        assertEquals "roles", pair[0]
        assertEquals "guest, admin", pair[1]
    }

    //SHIRO-205 - asserts backwards compatibility before SHIRO-205 was implemented:
    //@since 1.2.2
    @Test
    void testToNameConfigPairWithIndividualNestedQuotesInBrackets() {
        def token = 'roles["guest", "admin"]'

        String[] pair = manager.toNameConfigPair(token);

        assertNotNull pair
        assertEquals 2, pair.length
        assertEquals "roles", pair[0]
        assertEquals '"guest", "admin"', pair[1]
    }

    //SHIRO-205
    @Test
    void testFilterChainConfigWithNestedCommas() {
        def chain = "a, b[c], d[e, f], g[h, i, j], k"

        String[] tokens = manager.splitChainDefinition(chain);

        assertNotNull tokens
        assertEquals 5, tokens.length
        assertEquals "a", tokens[0]
        assertEquals "b[c]", tokens[1]
        assertEquals "d[e, f]", tokens[2]
        assertEquals "g[h, i, j]", tokens[3]
        assertEquals "k", tokens[4]
    }

    //SHIRO-205
    @Test
    void testFilterChainConfigWithNestedQuotedCommas() {
        def chain = "a, b[c], d[e, f], g[h, i, j], k"

        String[] tokens = manager.splitChainDefinition(chain);

        assertNotNull tokens
        assertEquals 5, tokens.length
        assertEquals "a", tokens[0]
        assertEquals "b[c]", tokens[1]
        assertEquals "d[e, f]", tokens[2]
        assertEquals "g[h, i, j]", tokens[3]
        assertEquals "k", tokens[4]
    }

    @Test
    void testNewInstanceDefaultFilters() {
        for (DefaultFilter defaultFilter : DefaultFilter.values()) {
            assertNotNull(manager.getFilter(defaultFilter.name()));
        }
        assertFalse(manager.hasChains());
    }

    protected FilterConfig createNiceMockFilterConfig() {
        FilterConfig mock = createNiceMock(FilterConfig.class);
        ServletContext mockServletContext = createNiceMock(ServletContext.class);
        expect(mock.getServletContext()).andReturn(mockServletContext);
        return mock;
    }

    @Test
    void testNewInstanceWithFilterConfig() {
        FilterConfig mock = createNiceMockFilterConfig();
        replay(mock);
        this.manager = new DefaultFilterChainManager(mock);
        for (DefaultFilter defaultFilter : DefaultFilter.values()) {
            assertNotNull(manager.getFilter(defaultFilter.name()));
        }
        assertFalse(manager.hasChains());
        verify(mock);
    }

    @Test
    void testCreateChain() {
        try {
            manager.createChain(null, null);
        } catch (NullPointerException expected) {
        }
        try {
            manager.createChain("test", null);
        } catch (NullPointerException expected) {
        }

        manager.createChain("test", "authc, roles[manager], perms[\"user:read,write:12345\"");

        assertTrue(manager.hasChains());

        Set<String> chainNames = manager.getChainNames();
        assertNotNull(chainNames);
        assertEquals(1, chainNames.size());
        assertTrue(chainNames.contains("test"));

        Map<String, NamedFilterList> chains = manager.getFilterChains();
        assertEquals(1, chains.size());
        assertTrue(chains.containsKey("test"));
        manager.setFilterChains(chains);

        NamedFilterList chain = manager.getChain("test");
        assertNotNull(chain);

        Filter filter = chain.get(0);
        assertNotNull(filter);
        assertEquals(DefaultFilter.authc.getFilterClass(), filter.getClass());

        filter = chain.get(1);
        assertNotNull(filter);
        assertEquals(DefaultFilter.roles.getFilterClass(), filter.getClass());

        filter = chain.get(2);
        assertNotNull(filter);
        assertEquals(DefaultFilter.perms.getFilterClass(), filter.getClass());
    }

    @Test
    void testWithGlobalFilters() {
        DefaultFilterChainManager manager = new DefaultFilterChainManager()
        manager.setGlobalFilters(["invalidRequest", "port"])
        assertThat(manager.filterChains, Matchers.anEmptyMap())

        // add a chain
        manager.createChain("test", "authc, roles[manager], perms[\"user:read,write:12345\"")

        assertThat(manager.getChain("test"), Matchers.contains(
                Matchers.instanceOf(DefaultFilter.invalidRequest.getFilterClass()),
                Matchers.instanceOf(DefaultFilter.port.getFilterClass()),
                Matchers.instanceOf(DefaultFilter.authc.getFilterClass()),
                Matchers.instanceOf(DefaultFilter.roles.getFilterClass()),
                Matchers.instanceOf(DefaultFilter.perms.getFilterClass())
        ))

        // the  "default" chain doesn't exist until it is created
        assertThat(manager.getChain("/**"), Matchers.nullValue())
        // create it
        manager.createDefaultChain("/**")
        // verify it
        assertThat(manager.getChain("/**"), Matchers.contains(
                Matchers.instanceOf(DefaultFilter.invalidRequest.getFilterClass()),
                Matchers.instanceOf(DefaultFilter.port.getFilterClass())
        ))
    }

    @Test
    void addDefaultChainWithSameName() {

        DefaultFilterChainManager manager = new DefaultFilterChainManager()
        manager.setGlobalFilters(["invalidRequest", "port"])

        // create a chain
        manager.createChain("test", "authc")

        // create the default chain with the same name
        manager.createDefaultChain("test")

        // since the "default" chain was created with the same name as an existing chain, we could end up adding the
        // global filters to the chain twice, test to verify it is only once
        assertThat(manager.getChain("test"), Matchers.contains(
                Matchers.instanceOf(DefaultFilter.invalidRequest.getFilterClass()),
                Matchers.instanceOf(DefaultFilter.port.getFilterClass()),
                Matchers.instanceOf(DefaultFilter.authc.getFilterClass())
        ))

    }

    /**
     * Helps assert <a href="https://issues.apache.org/jira/browse/SHIRO-429">SHIRO-429</a>
     * @since 1.2.2
     */
    @Test
    void testCreateChainWithQuotedInstanceConfig() {

        manager.createChain("test", 'authc, perms["perm1", "perm2"]');

        assertTrue(manager.hasChains());

        Set<String> chainNames = manager.getChainNames();
        assertNotNull(chainNames);
        assertEquals(1, chainNames.size());
        assertTrue(chainNames.contains("test"));

        Map<String, NamedFilterList> chains = manager.getFilterChains();
        assertEquals(1, chains.size());
        assertTrue(chains.containsKey("test"));
        manager.setFilterChains(chains);

        NamedFilterList chain = manager.getChain("test");
        assertNotNull(chain);

        Filter filter = chain.get(0);
        assertNotNull(filter);
        assertEquals(DefaultFilter.authc.getFilterClass(), filter.getClass());

        filter = chain.get(1);
        assertNotNull(filter);
        assertEquals(DefaultFilter.perms.getFilterClass(), filter.getClass());
    }

    @Test
    void testBeanMethods() {
        Map<String, Filter> filters = manager.getFilters();
        assertEquals(filters.size(), DefaultFilter.values().length);
        manager.setFilters(filters);
    }

    @Test
    void testAddFilter() {
        FilterConfig mockFilterConfig = createNiceMockFilterConfig();
        replay(mockFilterConfig);
        this.manager = new DefaultFilterChainManager(mockFilterConfig);
        manager.addFilter("test", new SslFilter());
        Filter filter = manager.getFilter("test");
        assertNotNull(filter);
        assertEquals(SslFilter.class, filter.getClass());
        verify(mockFilterConfig);
    }

    @Test
    void testAddFilterNoInit() {
        FilterConfig mockFilterConfig = createNiceMockFilterConfig();
        Filter mockFilter = createNiceMock(Filter.class);

        replay mockFilterConfig, mockFilter

        this.manager = new DefaultFilterChainManager(mockFilterConfig);

        this.manager.addFilter("blah", mockFilter);

        assertNotNull this.manager.filters['blah']
        assertSame this.manager.filters['blah'], mockFilter

        verify mockFilterConfig, mockFilter
    }

    @Test
    void testAddFilterNoFilterConfig() {
        SslFilter filter = new SslFilter();
        manager.addFilter("test", filter);
        assertNotNull manager.filters['test']
        assertSame manager.filters['test'], filter
    }

    @Test
    void testAddToChain() {
        FilterConfig mockFilterConfig = createNiceMockFilterConfig();
        replay(mockFilterConfig);
        this.manager = new DefaultFilterChainManager(mockFilterConfig);

        manager.addFilter("testSsl", new SslFilter());
        manager.createChain("test", "anon");

        try {
            manager.addToChain("test", null);
            fail "Should have thrown an IllegalArgumentException"
        } catch (IllegalArgumentException expected) {
        }
        try {
            manager.addToChain(null, "testSsl");
            fail "Should have thrown an IllegalArgumentException"
        } catch (IllegalArgumentException expected) {
        }
    }

    @Test
    void testAddToChainNotPathProcessor() {
        FilterConfig mockFilterConfig = createNiceMockFilterConfig();
        replay(mockFilterConfig);
        this.manager = new DefaultFilterChainManager(mockFilterConfig);

        manager.addFilter("nonPathProcessor", new ShiroFilter());
        manager.createChain("test", "nonPathProcessor");

        try {
            manager.addToChain("test", "nonPathProcessor", "dummyConfig");
            fail "Should have thrown a ConfigurationException"
        } catch (ConfigurationException expected) {
        }
    }

    @Test
    void testProxy() {
        FilterChain mock = createNiceMock(FilterChain.class);
        replay(mock);
        manager.createChain("test", "anon");
        this.manager.proxy(mock, "test");
        verify(mock);
    }

    @Test
    void testProxyNoChain() {
        FilterChain mock = createNiceMock(FilterChain.class);
        replay(mock);
        try {
            this.manager.proxy(mock, "blah");
            fail "Should have thrown an IllegalArgumentException"
        } catch (IllegalArgumentException expected) {
        }
        verify(mock);
    }

}
