/* 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.catalina.valves;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import jakarta.servlet.ServletContext;
import jakarta.servlet.SessionCookieConfig;
import jakarta.servlet.http.Cookie;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;

import org.apache.catalina.Context;
import org.apache.catalina.Valve;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.core.StandardPipeline;
import org.apache.tomcat.util.descriptor.web.Constants;
import org.easymock.EasyMock;
import org.easymock.IMocksControl;

@RunWith(Parameterized.class)
public class TestLoadBalancerDrainingValve {

    @Parameters(name = "{index}: activation[{0}], validSessionID[{1}], expectInvokeNext[{2}], enableIgnore[{3}], " +
            "leadingSlashes[{4}], queryString[{5}]]")
    public static Collection<Object[]> parameters() {

        String[] jkActivations = new String[] { "ACT", "DIS" };
        Boolean[] booleans = new Boolean[] { Boolean.TRUE, Boolean.FALSE };
        Integer[] integers = new Integer[] { Integer.valueOf(0), Integer.valueOf(1), Integer.valueOf(2) };
        String[] queryStrings = new String[] { null, "foo=bar" };

        List<Object[]> parameterSets = new ArrayList<>();
        for (String jkActivation : jkActivations) {
            for (Boolean validSessionId : booleans) {
                for (Boolean enableIgnore : booleans) {
                    Boolean expectInvokeNext = Boolean.valueOf(
                            "ACT".equals(jkActivation) || enableIgnore.booleanValue() || validSessionId.booleanValue());
                    for (Integer leadingSlashes : integers) {
                        for (String queryString : queryStrings) {
                            for (Boolean secureRequest : booleans) {
                                for (Boolean secureSessionConfig : booleans) {
                                    parameterSets.add(new Object[] { jkActivation, validSessionId, expectInvokeNext,
                                            enableIgnore, leadingSlashes, queryString, secureRequest,
                                            secureSessionConfig });
                                }
                            }
                        }
                    }
                }
            }
        }
        return parameterSets;
    }


    @Parameter(0)
    public String jkActivation;

    @Parameter(1)
    public boolean validSessionId;

    @Parameter(2)
    public boolean expectInvokeNext;

    @Parameter(3)
    public boolean enableIgnore;

    @Parameter(4)
    public Integer leadingSlashes;

    @Parameter(5)
    public String queryString;

    @Parameter(6)
    public Boolean secureRequest;

    @Parameter(7)
    public boolean secureSessionConfig;


    @Test
    public void runValve() throws Exception {
        IMocksControl control = EasyMock.createControl();
        ServletContext servletContext = control.createMock(ServletContext.class);
        Context ctx = control.createMock(Context.class);
        Request request = control.createMock(Request.class);
        Response response = control.createMock(Response.class);

        String sessionCookieName = "JSESSIONID";
        String sessionId = "cafebabe";
        String requestURI = "/test/path";
        SessionCookieConfig cookieConfig = new CookieConfig();
        cookieConfig.setDomain("example.com");
        cookieConfig.setName(sessionCookieName);
        cookieConfig.setPath("/");
        cookieConfig.setSecure(secureSessionConfig);

        // Valve.init requires all of this stuff
        EasyMock.expect(ctx.getMBeanKeyProperties()).andStubReturn("");
        EasyMock.expect(ctx.getName()).andStubReturn("");
        EasyMock.expect(ctx.getPipeline()).andStubReturn(new StandardPipeline());
        EasyMock.expect(ctx.getDomain()).andStubReturn("foo");
        EasyMock.expect(ctx.getLogger())
                .andStubReturn(org.apache.juli.logging.LogFactory.getLog(LoadBalancerDrainingValve.class));
        EasyMock.expect(ctx.getServletContext()).andStubReturn(servletContext);

        // Set up the actual test
        EasyMock.expect(request.getAttribute(LoadBalancerDrainingValve.ATTRIBUTE_KEY_JK_LB_ACTIVATION))
                .andStubReturn(jkActivation);
        EasyMock.expect(Boolean.valueOf(request.isRequestedSessionIdValid()))
                .andStubReturn(Boolean.valueOf(validSessionId));

        ArrayList<Cookie> cookies = new ArrayList<>();
        if (enableIgnore) {
            cookies.add(new Cookie("ignore", "true"));
        }

        if (!validSessionId && jkActivation.equals("DIS")) {
            MyCookie cookie = new MyCookie(cookieConfig.getName(), sessionId);
            cookie.setPath(cookieConfig.getPath());
            cookie.setValue(sessionId);

            cookies.add(cookie);

            EasyMock.expect(request.getRequestedSessionId()).andStubReturn(sessionId);
            EasyMock.expect(request.getRequestURI()).andStubReturn("/".repeat(leadingSlashes.intValue()) + requestURI);
            EasyMock.expect(request.getCookies()).andStubReturn(cookies.toArray(new Cookie[0]));
            EasyMock.expect(request.getContext()).andStubReturn(ctx);
            EasyMock.expect(ctx.getSessionCookieName()).andStubReturn(sessionCookieName);
            EasyMock.expect(servletContext.getSessionCookieConfig()).andStubReturn(cookieConfig);
            EasyMock.expect(request.getQueryString()).andStubReturn(queryString);
            EasyMock.expect(ctx.getSessionCookiePath()).andStubReturn("/");

            if (!enableIgnore) {
                EasyMock.expect(Boolean.valueOf(ctx.getSessionCookiePathUsesTrailingSlash()))
                        .andStubReturn(Boolean.TRUE);
                EasyMock.expect(request.getQueryString()).andStubReturn(queryString);
                // Response will have cookie deleted
                MyCookie expectedCookie = new MyCookie(cookieConfig.getName(), "");
                expectedCookie.setPath(cookieConfig.getPath());
                expectedCookie.setMaxAge(0);

                EasyMock.expect(Boolean.valueOf(request.isSecure())).andReturn(secureRequest);

                // These two lines just mean EasyMock.expect(response.addCookie) but for a void method
                response.addCookie(expectedCookie);
                EasyMock.expect(ctx.getSessionCookieName()).andReturn(sessionCookieName); // Indirect call
                String expectedRequestURI = requestURI;
                if (null != queryString) {
                    expectedRequestURI = expectedRequestURI + '?' + queryString;
                }
                response.sendRedirect(expectedRequestURI, 307);
            }
        }

        Valve next = control.createMock(Valve.class);

        if (expectInvokeNext) {
            // Expect the "next" Valve to fire
            // Next 2 lines are basically EasyMock.expect(next.invoke(req,res)) but for a void method
            next.invoke(request, response);
            EasyMock.expectLastCall();
        }

        // Get set to actually test
        control.replay();

        LoadBalancerDrainingValve valve = new LoadBalancerDrainingValve();
        valve.setContainer(ctx);
        valve.init();
        valve.setNext(next);
        valve.setIgnoreCookieName("ignore");
        valve.setIgnoreCookieValue("true");

        valve.invoke(request, response);

        control.verify();
    }


    private static class CookieConfig implements SessionCookieConfig {

        private String name;

        private final Map<String, String> attributes = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);

        @Override
        public String getName() {
            return name;
        }

        @Override
        public void setName(String name) {
            this.name = name;
        }

        @Override
        public String getDomain() {
            return attributes.get(Constants.COOKIE_DOMAIN_ATTR);
        }

        @Override
        public void setDomain(String domain) {
            attributes.put(Constants.COOKIE_DOMAIN_ATTR, domain);
        }

        @Override
        public String getPath() {
            return attributes.get(Constants.COOKIE_PATH_ATTR);
        }

        @Override
        public void setPath(String path) {
            attributes.put(Constants.COOKIE_PATH_ATTR, path);
        }

        @Override
        public String getComment() {
            return null;
        }

        @Override
        public void setComment(String comment) {
            // NO-OP
        }

        @Override
        public boolean isHttpOnly() {
            String httpOnly = getAttribute(Constants.COOKIE_HTTP_ONLY_ATTR);
            if (httpOnly == null) {
                return false;
            }
            return Boolean.parseBoolean(httpOnly);
        }

        @Override
        public void setHttpOnly(boolean httpOnly) {
            setAttribute(Constants.COOKIE_HTTP_ONLY_ATTR, Boolean.toString(httpOnly));
        }

        @Override
        public boolean isSecure() {
            String secure = getAttribute(Constants.COOKIE_SECURE_ATTR);
            if (secure == null) {
                return false;
            }
            return Boolean.parseBoolean(secure);
        }

        @Override
        public void setSecure(boolean secure) {
            setAttribute(Constants.COOKIE_SECURE_ATTR, Boolean.toString(secure));
        }

        @Override
        public int getMaxAge() {
            String maxAge = getAttribute(Constants.COOKIE_MAX_AGE_ATTR);
            if (maxAge == null) {
                return -1;
            }
            return Integer.parseInt(maxAge);
        }

        @Override
        public void setMaxAge(int maxAge) {
            setAttribute(Constants.COOKIE_MAX_AGE_ATTR, Integer.toString(maxAge));
        }

        @Override
        public void setAttribute(String name, String value) {
            attributes.put(name, value);
        }

        @Override
        public String getAttribute(String name) {
            return attributes.get(name);
        }

        @Override
        public Map<String, String> getAttributes() {
            return Collections.unmodifiableMap(attributes);
        }
    }


    // A Cookie subclass that knows how to compare itself to other Cookie objects
    private static class MyCookie extends Cookie {
        private static final long serialVersionUID = 1L;

        MyCookie(String name, String value) {
            super(name, value);
        }

        @Override
        public boolean equals(Object o) {
            if (!(o instanceof MyCookie)) {
                return false;
            }

            MyCookie mc = (MyCookie) o;
            return mc.getName().equals(this.getName()) && mc.getPath().equals(this.getPath()) &&
                    mc.getValue().equals(this.getValue()) && mc.getMaxAge() == this.getMaxAge();
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + getMaxAge();
            result = prime * result + ((getName() == null) ? 0 : getName().hashCode());
            result = prime * result + ((getPath() == null) ? 0 : getPath().hashCode());
            result = prime * result + ((getValue() == null) ? 0 : getValue().hashCode());
            return result;
        }


        @Override
        public String toString() {
            return "Cookie { name=" + getName() + ", value=" + getValue() + ", path=" + getPath() + ", maxAge=" +
                    getMaxAge() + " }";
        }
    }
}
