/*
 * 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.tomcat.util.descriptor.web;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

import jakarta.servlet.HttpConstraintElement;
import jakarta.servlet.HttpMethodConstraintElement;
import jakarta.servlet.ServletSecurityElement;
import jakarta.servlet.annotation.ServletSecurity;
import jakarta.servlet.annotation.ServletSecurity.EmptyRoleSemantic;

import org.junit.Assert;
import org.junit.Test;

import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.util.http.Method;

public class TestSecurityConstraint {

    private static final String URL_PATTERN = "/test";
    private static final String ROLE1 = "R1";

    private static final Log DUMMY_LOG = LogFactory.getLog("DUMMY");

    private static final SecurityConstraint GET_ONLY;
    private static final SecurityConstraint POST_ONLY;

    private static final SecurityConstraint GET_OMIT;
    private static final SecurityConstraint POST_OMIT;

    static {
        // Configure the constraints to use in the tests
        GET_ONLY = new SecurityConstraint();
        GET_ONLY.addAuthRole(ROLE1);
        SecurityCollection scGetOnly = new SecurityCollection();
        scGetOnly.addMethod(Method.GET);
        scGetOnly.addPattern(URL_PATTERN);
        scGetOnly.setName("GET-ONLY");
        GET_ONLY.addCollection(scGetOnly);

        POST_ONLY = new SecurityConstraint();
        POST_ONLY.addAuthRole(ROLE1);
        SecurityCollection scPostOnly = new SecurityCollection();
        scPostOnly.addMethod(Method.POST);
        scPostOnly.addPattern(URL_PATTERN);
        scPostOnly.setName("POST_ONLY");
        POST_ONLY.addCollection(scPostOnly);

        GET_OMIT = new SecurityConstraint();
        GET_OMIT.addAuthRole(ROLE1);
        SecurityCollection scGetOmit = new SecurityCollection();
        scGetOmit.addOmittedMethod(Method.GET);
        scGetOmit.addPattern(URL_PATTERN);
        scGetOmit.setName("GET_OMIT");
        GET_OMIT.addCollection(scGetOmit);

        POST_OMIT = new SecurityConstraint();
        POST_OMIT.addAuthRole(ROLE1);
        SecurityCollection scPostOmit = new SecurityCollection();
        scPostOmit.addOmittedMethod(Method.POST);
        scPostOmit.addPattern(URL_PATTERN);
        scPostOmit.setName("POST_OMIT");
        POST_OMIT.addCollection(scPostOmit);
    }

    /**
     * Uses the examples in SRV.13.4 as the basis for these tests
     */
    @Test
    public void testCreateConstraints() {

        ServletSecurityElement element;
        SecurityConstraint[] result;
        Set<HttpMethodConstraintElement> hmces = new HashSet<>();

        // Example 13-1
        // @ServletSecurity
        element = new ServletSecurityElement();
        result = SecurityConstraint.createConstraints(element, URL_PATTERN);

        Assert.assertEquals(0, result.length);

        // Example 13-2
        // @ServletSecurity(
        //     @HttpConstraint(
        //         transportGuarantee = TransportGuarantee.CONFIDENTIAL))
        element = new ServletSecurityElement(
                new HttpConstraintElement(
                        ServletSecurity.TransportGuarantee.CONFIDENTIAL));
        result = SecurityConstraint.createConstraints(element, URL_PATTERN);

        Assert.assertEquals(1, result.length);
        Assert.assertFalse(result[0].getAuthConstraint());
        Assert.assertTrue(result[0].findCollections()[0].findPattern(URL_PATTERN));
        Assert.assertEquals(0, result[0].findCollections()[0].findMethods().length);
        Assert.assertEquals(ServletSecurity.TransportGuarantee.CONFIDENTIAL.name(),
                result[0].getUserConstraint());

        // Example 13-3
        // @ServletSecurity(@HttpConstraint(EmptyRoleSemantic.DENY))
        element = new ServletSecurityElement(
                new HttpConstraintElement(EmptyRoleSemantic.DENY));
        result = SecurityConstraint.createConstraints(element, URL_PATTERN);

        Assert.assertEquals(1, result.length);
        Assert.assertTrue(result[0].getAuthConstraint());
        Assert.assertTrue(result[0].findCollections()[0].findPattern(URL_PATTERN));
        Assert.assertEquals(0, result[0].findCollections()[0].findMethods().length);
        Assert.assertEquals(ServletSecurity.TransportGuarantee.NONE.name(),
                result[0].getUserConstraint());

        // Example 13-4
        // @ServletSecurity(@HttpConstraint(rolesAllowed = "R1"))
        element = new ServletSecurityElement(new HttpConstraintElement(
                ServletSecurity.TransportGuarantee.NONE, ROLE1));
        result = SecurityConstraint.createConstraints(element, URL_PATTERN);

        Assert.assertEquals(1, result.length);
        Assert.assertTrue(result[0].getAuthConstraint());
        Assert.assertEquals(1, result[0].findAuthRoles().length);
        Assert.assertTrue(result[0].findAuthRole(ROLE1));
        Assert.assertTrue(result[0].findCollections()[0].findPattern(URL_PATTERN));
        Assert.assertEquals(0, result[0].findCollections()[0].findMethods().length);
        Assert.assertEquals(ServletSecurity.TransportGuarantee.NONE.name(),
                result[0].getUserConstraint());

        // Example 13-5
        // @ServletSecurity((httpMethodConstraints = {
        //     @HttpMethodConstraint(value = Method.GET, rolesAllowed = "R1"),
        //     @HttpMethodConstraint(value = Method.POST, rolesAllowed = "R1",
        //     transportGuarantee = TransportGuarantee.CONFIDENTIAL)
        // })
        hmces.clear();
        hmces.add(new HttpMethodConstraintElement(Method.GET,
                new HttpConstraintElement(
                        ServletSecurity.TransportGuarantee.NONE, ROLE1)));
        hmces.add(new HttpMethodConstraintElement(Method.POST,
                new HttpConstraintElement(
                        ServletSecurity.TransportGuarantee.CONFIDENTIAL,
                        ROLE1)));
        element = new ServletSecurityElement(hmces);
        result = SecurityConstraint.createConstraints(element, URL_PATTERN);

        Assert.assertEquals(2, result.length);
        for (int i = 0; i < 2; i++) {
            Assert.assertTrue(result[i].getAuthConstraint());
            Assert.assertEquals(1, result[i].findAuthRoles().length);
            Assert.assertTrue(result[i].findAuthRole(ROLE1));
            Assert.assertTrue(result[i].findCollections()[0].findPattern(URL_PATTERN));
            Assert.assertEquals(1, result[i].findCollections()[0].findMethods().length);
            String method = result[i].findCollections()[0].findMethods()[0];
            if (Method.GET.equals(method)) {
                Assert.assertEquals(ServletSecurity.TransportGuarantee.NONE.name(),
                        result[i].getUserConstraint());
            } else if (Method.POST.equals(method)) {
                Assert.assertEquals(ServletSecurity.TransportGuarantee.CONFIDENTIAL.name(),
                        result[i].getUserConstraint());
            } else {
                Assert.fail("Unexpected method :[" + method + "]");
            }
        }

        // Example 13-6
        // @ServletSecurity(value = @HttpConstraint(rolesAllowed = "R1"),
        //     httpMethodConstraints = @HttpMethodConstraint(Method.GET))
        hmces.clear();
        hmces.add(new HttpMethodConstraintElement(Method.GET));
        element = new ServletSecurityElement(
                new HttpConstraintElement(
                        ServletSecurity.TransportGuarantee.NONE,
                        ROLE1),
                hmces);
        result = SecurityConstraint.createConstraints(element, URL_PATTERN);

        Assert.assertEquals(2, result.length);
        for (int i = 0; i < 2; i++) {
            Assert.assertTrue(result[i].findCollections()[0].findPattern(URL_PATTERN));
            if (result[i].findCollections()[0].findMethods().length == 1) {
                Assert.assertEquals(Method.GET,
                        result[i].findCollections()[0].findMethods()[0]);
                Assert.assertFalse(result[i].getAuthConstraint());
            } else if (result[i].findCollections()[0].findOmittedMethods().length == 1) {
                Assert.assertEquals(Method.GET,
                        result[i].findCollections()[0].findOmittedMethods()[0]);
                Assert.assertTrue(result[i].getAuthConstraint());
                Assert.assertEquals(1, result[i].findAuthRoles().length);
                Assert.assertEquals(ROLE1, result[i].findAuthRoles()[0]);
            } else {
                Assert.fail("Unexpected number of methods defined");
            }
            Assert.assertEquals(ServletSecurity.TransportGuarantee.NONE.name(),
                    result[i].getUserConstraint());
        }

        // Example 13-7
        // @ServletSecurity(value = @HttpConstraint(rolesAllowed = "R1"),
        //     httpMethodConstraints = @HttpMethodConstraint(value=Method.TRACE,
        //         emptyRoleSemantic = EmptyRoleSemantic.DENY))
        hmces.clear();
        hmces.add(new HttpMethodConstraintElement(Method.TRACE,
                new HttpConstraintElement(EmptyRoleSemantic.DENY)));
        element = new ServletSecurityElement(
                new HttpConstraintElement(
                        ServletSecurity.TransportGuarantee.NONE,
                        ROLE1),
                hmces);
        result = SecurityConstraint.createConstraints(element, URL_PATTERN);

        Assert.assertEquals(2, result.length);
        for (int i = 0; i < 2; i++) {
            Assert.assertTrue(result[i].findCollections()[0].findPattern(URL_PATTERN));
            if (result[i].findCollections()[0].findMethods().length == 1) {
                Assert.assertEquals(Method.TRACE,
                        result[i].findCollections()[0].findMethods()[0]);
                Assert.assertTrue(result[i].getAuthConstraint());
                Assert.assertEquals(0, result[i].findAuthRoles().length);
            } else if (result[i].findCollections()[0].findOmittedMethods().length == 1) {
                Assert.assertEquals(Method.TRACE,
                        result[i].findCollections()[0].findOmittedMethods()[0]);
                Assert.assertTrue(result[i].getAuthConstraint());
                Assert.assertEquals(1, result[i].findAuthRoles().length);
                Assert.assertEquals(ROLE1, result[i].findAuthRoles()[0]);
            } else {
                Assert.fail("Unexpected number of methods defined");
            }
            Assert.assertEquals(ServletSecurity.TransportGuarantee.NONE.name(),
                    result[i].getUserConstraint());
        }

        // Example 13-8 is the same as 13-4
        // Example 13-9 is the same as 13-7
    }


    @Test
    public void testFindUncoveredHttpMethods01() {
        // No new constraints if denyUncoveredHttpMethods is false
        SecurityConstraint[] result =
                SecurityConstraint.findUncoveredHttpMethods(
                        new SecurityConstraint[] {GET_ONLY}, false, DUMMY_LOG);
        Assert.assertEquals(0, result.length);
    }


    @Test
    public void testFindUncoveredHttpMethods02() {
        // No new constraints if denyUncoveredHttpMethods is false
        SecurityConstraint[] result =
                SecurityConstraint.findUncoveredHttpMethods(
                        new SecurityConstraint[] {GET_OMIT}, false, DUMMY_LOG);
        Assert.assertEquals(0, result.length);
    }


    @Test
    public void testFindUncoveredHttpMethods03() {
        // No new constraints if denyUncoveredHttpMethods is false
        SecurityConstraint[] result =
                SecurityConstraint.findUncoveredHttpMethods(
                        new SecurityConstraint[] {POST_ONLY}, false, DUMMY_LOG);
        Assert.assertEquals(0, result.length);
    }


    @Test
    public void testFindUncoveredHttpMethods04() {
        // No new constraints if denyUncoveredHttpMethods is false
        SecurityConstraint[] result =
                SecurityConstraint.findUncoveredHttpMethods(
                        new SecurityConstraint[] {POST_OMIT}, false, DUMMY_LOG);
        Assert.assertEquals(0, result.length);
    }


    @Test
    public void testFindUncoveredHttpMethods05() {
        SecurityConstraint[] result =
                SecurityConstraint.findUncoveredHttpMethods(
                        new SecurityConstraint[] {GET_ONLY}, true, DUMMY_LOG);
        Assert.assertEquals(1, result.length);
        // Should be a deny constraint
        Assert.assertTrue(result[0].getAuthConstraint());
        // Should have a single collection
        Assert.assertEquals(1, result[0].findCollections().length);
        SecurityCollection sc = result[0].findCollections()[0];
        // Should list GET as an omitted method
        Assert.assertEquals(0, sc.findMethods().length);
        Assert.assertEquals(1, sc.findOmittedMethods().length);
        Assert.assertEquals(Method.GET, sc.findOmittedMethods()[0]);
    }


    @Test
    public void testFindUncoveredHttpMethods06() {
        SecurityConstraint[] result =
                SecurityConstraint.findUncoveredHttpMethods(
                        new SecurityConstraint[] {POST_ONLY}, true, DUMMY_LOG);
        Assert.assertEquals(1, result.length);
        // Should be a deny constraint
        Assert.assertTrue(result[0].getAuthConstraint());
        // Should have a single collection
        Assert.assertEquals(1, result[0].findCollections().length);
        SecurityCollection sc = result[0].findCollections()[0];
        // Should list POST as an omitted method
        Assert.assertEquals(0, sc.findMethods().length);
        Assert.assertEquals(1, sc.findOmittedMethods().length);
        Assert.assertEquals(Method.POST, sc.findOmittedMethods()[0]);
    }


    @Test
    public void testFindUncoveredHttpMethods07() {
        SecurityConstraint[] result =
                SecurityConstraint.findUncoveredHttpMethods(
                        new SecurityConstraint[] {GET_OMIT}, true, DUMMY_LOG);
        Assert.assertEquals(1, result.length);
        // Should be a deny constraint
        Assert.assertTrue(result[0].getAuthConstraint());
        // Should have a single collection
        Assert.assertEquals(1, result[0].findCollections().length);
        SecurityCollection sc = result[0].findCollections()[0];
        // Should list GET as an method
        Assert.assertEquals(0, sc.findOmittedMethods().length);
        Assert.assertEquals(1, sc.findMethods().length);
        Assert.assertEquals(Method.GET, sc.findMethods()[0]);
    }


    @Test
    public void testFindUncoveredHttpMethods08() {
        SecurityConstraint[] result =
                SecurityConstraint.findUncoveredHttpMethods(
                        new SecurityConstraint[] {POST_OMIT}, true, DUMMY_LOG);
        Assert.assertEquals(1, result.length);
        // Should be a deny constraint
        Assert.assertTrue(result[0].getAuthConstraint());
        // Should have a single collection
        Assert.assertEquals(1, result[0].findCollections().length);
        SecurityCollection sc = result[0].findCollections()[0];
        // Should list POST as an method
        Assert.assertEquals(0, sc.findOmittedMethods().length);
        Assert.assertEquals(1, sc.findMethods().length);
        Assert.assertEquals(Method.POST, sc.findMethods()[0]);
    }


    @Test
    public void testFindUncoveredHttpMethods09() {
        SecurityConstraint[] result =
                SecurityConstraint.findUncoveredHttpMethods(
                        new SecurityConstraint[] {GET_ONLY, GET_OMIT}, true,
                        DUMMY_LOG);
        Assert.assertEquals(0, result.length);
    }


    @Test
    public void testFindUncoveredHttpMethods10() {
        SecurityConstraint[] result =
                SecurityConstraint.findUncoveredHttpMethods(
                        new SecurityConstraint[] {POST_ONLY, POST_OMIT}, true,
                        DUMMY_LOG);
        Assert.assertEquals(0, result.length);
    }


    @Test
    public void testFindUncoveredHttpMethods11() {
        SecurityConstraint[] result =
                SecurityConstraint.findUncoveredHttpMethods(
                        new SecurityConstraint[] {GET_ONLY, POST_ONLY}, true,
                        DUMMY_LOG);
        Assert.assertEquals(1, result.length);
        // Should be a deny constraint
        Assert.assertTrue(result[0].getAuthConstraint());
        // Should have a single collection
        Assert.assertEquals(1, result[0].findCollections().length);
        SecurityCollection sc = result[0].findCollections()[0];
        // Should list GET and POST as omitted methods
        Assert.assertEquals(0, sc.findMethods().length);
        Assert.assertEquals(2, sc.findOmittedMethods().length);
        HashSet<String> omittedMethods = new HashSet<>();
        omittedMethods.addAll(Arrays.asList(sc.findOmittedMethods()));
        Assert.assertTrue(omittedMethods.remove(Method.GET));
        Assert.assertTrue(omittedMethods.remove(Method.POST));
    }


    @Test
    public void testFindUncoveredHttpMethods12() {
        SecurityConstraint[] result =
                SecurityConstraint.findUncoveredHttpMethods(
                        new SecurityConstraint[] {GET_OMIT, POST_OMIT}, true,
                        DUMMY_LOG);
        Assert.assertEquals(0, result.length);
    }


    @Test
    public void testFindUncoveredHttpMethods13() {
        SecurityConstraint[] result =
                SecurityConstraint.findUncoveredHttpMethods(
                        new SecurityConstraint[] {GET_ONLY, POST_OMIT}, true,
                        DUMMY_LOG);
        Assert.assertEquals(1, result.length);
        // Should be a deny constraint
        Assert.assertTrue(result[0].getAuthConstraint());
        // Should have a single collection
        Assert.assertEquals(1, result[0].findCollections().length);
        SecurityCollection sc = result[0].findCollections()[0];
        // Should list POST as a method
        Assert.assertEquals(1, sc.findMethods().length);
        Assert.assertEquals(0, sc.findOmittedMethods().length);
        Assert.assertEquals(Method.POST, sc.findMethods()[0]);
    }


    @Test
    public void testFindUncoveredHttpMethods14() {
        SecurityConstraint[] result =
                SecurityConstraint.findUncoveredHttpMethods(
                        new SecurityConstraint[] {GET_OMIT, POST_ONLY}, true,
                        DUMMY_LOG);
        Assert.assertEquals(1, result.length);
        // Should be a deny constraint
        Assert.assertTrue(result[0].getAuthConstraint());
        // Should have a single collection
        Assert.assertEquals(1, result[0].findCollections().length);
        SecurityCollection sc = result[0].findCollections()[0];
        // Should list GET as a method
        Assert.assertEquals(1, sc.findMethods().length);
        Assert.assertEquals(0, sc.findOmittedMethods().length);
        Assert.assertEquals(Method.GET, sc.findMethods()[0]);
    }
}
