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

import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

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

import org.apache.catalina.Context;
import org.apache.catalina.Manager;
import org.apache.catalina.Session;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.startup.TesterServlet;
import org.apache.catalina.startup.Tomcat;
import org.apache.catalina.startup.TomcatBaseTest;
import org.apache.catalina.valves.ValveBase;
import org.apache.tomcat.util.buf.ByteChunk;
import org.apache.tomcat.util.descriptor.web.LoginConfig;
import org.apache.tomcat.util.descriptor.web.SecurityCollection;
import org.apache.tomcat.util.descriptor.web.SecurityConstraint;

public class TestSSOChangeSessionId extends TomcatBaseTest {

    protected static final boolean USE_COOKIES = true;

    private static final String NOTSORANDOMID = "NOTSORANNDOMID";
    private static final String USER = "user";
    private static final String PWD = "pwd";
    private static final String ROLE = "role";

    private static final String HTTP_PREFIX = "http://localhost:";
    private static final String CONTEXT_PATH = "/test";
    private static final String URI_PROTECTED = "/protected";
    private static final String URI_AUTHENTICATION = "/authentication";

    private Context testContext;
    private SingleSignOn singleSignOn;

    private static final String SERVER_COOKIE_HEADER = "Set-Cookie";
    private static final String CLIENT_COOKIE_HEADER = "Cookie";

    private List<String> cookies;

    /*
     * setup two webapps for every test
     *
     * note: the super class tearDown method will stop tomcat
     */
    @Override
    public void setUp() throws Exception {
        super.setUp();

        // create a tomcat server using the default in-memory Realm
        final Tomcat tomcat = getTomcatInstance();

        // Must have a real docBase for webapps - just use temp
        testContext = tomcat.addContext(CONTEXT_PATH, System.getProperty("java.io.tmpdir"));

        singleSignOn = new SingleSignOn();
        tomcat.getHost().getPipeline().addValve(singleSignOn);

        testContext.getPipeline().addValve(new SessionManipulationFilter());

        // Add protected servlet to the context
        Tomcat.addServlet(testContext, "TesterServlet1", new TesterServlet());
        testContext.addServletMapping(URI_PROTECTED, "TesterServlet1");

        SecurityCollection collection1 = new SecurityCollection();
        collection1.addPattern(URI_PROTECTED);
        SecurityConstraint sc1 = new SecurityConstraint();
        sc1.addAuthRole(ROLE);
        sc1.addCollection(collection1);
        testContext.addConstraint(sc1);

        // Add Authenticator
        Tomcat.addServlet(testContext, "LoginServlet", new LoginServlet());
        testContext.addServletMapping(URI_AUTHENTICATION, "LoginServlet");

        SecurityCollection collection2 = new SecurityCollection();
        collection2.addPattern(URI_AUTHENTICATION);
        SecurityConstraint sc2 = new SecurityConstraint();
        // do not add a role - which signals access permitted without one
        sc2.addCollection(collection2);
        testContext.addConstraint(sc2);

        // Configure the authenticator and inherit the Realm from Engine
        LoginConfig lc = new LoginConfig();
        lc.setAuthMethod("NONE");
        testContext.setLoginConfig(lc);
        AuthenticatorBase nonloginAuthenticator = new NonLoginAuthenticator();
        testContext.getPipeline().addValve(nonloginAuthenticator);

        // add the test user and role to the Realm
        tomcat.addUser(USER, PWD);
        tomcat.addRole(USER, ROLE);

        tomcat.start();
    }

    private static class SessionManipulationFilter extends ValveBase {
        @Override
        public void invoke(Request request, Response response) throws IOException, ServletException {

            if (request.getPrincipal() != null) {
                final Manager manager = request.getContext().getManager();
                final Session session = manager.findSession(request.getSession().getId());
                manager.changeSessionId(session, NOTSORANDOMID);
            }

            getNext().invoke(request, response);
        }
    }

    private static class LoginServlet extends HttpServlet {

        private static final long serialVersionUID = 1L;

        @Override
        protected void doGet(HttpServletRequest request, HttpServletResponse response)
                throws ServletException, IOException {

            if (request.getUserPrincipal() == null) {
                request.login(USER, PWD);

                response.setContentType("text/plain");
                PrintWriter out = response.getWriter();
                out.print("OK");

            } else {
                response.setStatus(HttpServletResponse.SC_CONFLICT);
            }
        }
    }

    @Test
    public void testChangeSessionId() throws Exception {
        final Map<String,SingleSignOnEntry> cache = singleSignOn.cache;
        Assert.assertTrue("No SSO entries should be present at startup", cache.isEmpty());

        // Authenticate a session
        doTest(CONTEXT_PATH + URI_AUTHENTICATION, USE_COOKIES, HttpServletResponse.SC_OK);

        Assert.assertFalse("SSO must now be present", cache.isEmpty());

        Assert.assertEquals("Only one SSO entry must be present", 1, cache.size());
        final Collection<SingleSignOnEntry> ssoEntries = cache.values();
        Assert.assertEquals("Only one session should be present, as there is only one context in the test", 1,
                ssoEntries.size());

        final SingleSignOnEntry singleSignOnEntry = ssoEntries.iterator().next();
        final Set<SingleSignOnSessionKey> sessions = singleSignOnEntry.findSessions();

        Assert.assertEquals(1, sessions.size());

        final SingleSignOnSessionKey singleSignOnSessionKey1 = sessions.iterator().next();
        Assert.assertNotEquals("A random SessionId is expected", NOTSORANDOMID, singleSignOnSessionKey1.getSessionId());

        // Perform the request that changes the session id:
        doTest(CONTEXT_PATH + URI_PROTECTED, USE_COOKIES, HttpServletResponse.SC_OK);

        Assert.assertEquals("2 sessions means we have a zombie SingleSignOnSessionKey", 1, sessions.size());

        final SingleSignOnSessionKey singleSignOnSessionKey2 = sessions.iterator().next();
        Assert.assertEquals(NOTSORANDOMID, singleSignOnSessionKey2.getSessionId());

        final Session session = testContext.getManager().findSession(NOTSORANDOMID);
        Assert.assertNotNull("We need a session in order to test expiry", session);

        session.expire();

        Assert.assertTrue("No SSO entries should be present after expiration of the session", cache.isEmpty());

        // Shouldn't this return 401?
        doTest(CONTEXT_PATH + URI_PROTECTED, USE_COOKIES, HttpServletResponse.SC_FORBIDDEN);
    }

    @After
    @Override
    public void tearDown() throws Exception {
        super.tearDown();
    }

    public void doTest(String uri, boolean useCookie, int expectedRC) throws Exception {

        Map<String,List<String>> reqHeaders = new HashMap<>();
        Map<String,List<String>> respHeaders = new HashMap<>();

        if (useCookie && (cookies != null)) {
            addCookies(reqHeaders);
        }

        ByteChunk bc = new ByteChunk();
        int rc = getUrl(HTTP_PREFIX + getPort() + uri, bc, reqHeaders, respHeaders);
        if (expectedRC != HttpServletResponse.SC_OK) {
            Assert.assertEquals(expectedRC, rc);
            Assert.assertTrue(bc.getLength() > 0);
        } else {
            Assert.assertEquals("OK", bc.toString());
            saveCookies(respHeaders);
        }
    }

    /*
     * add all saved cookies to the outgoing request
     */
    protected void addCookies(Map<String,List<String>> reqHeaders) {
        if ((cookies != null) && (cookies.size() > 0)) {
            StringBuilder cookieHeader = new StringBuilder();
            boolean first = true;
            for (String cookie : cookies) {
                if (!first) {
                    cookieHeader.append(';');
                } else {
                    first = false;
                }
                cookieHeader.append(cookie);
            }
            List<String> cookieHeaderList = new ArrayList<>(1);
            cookieHeaderList.add(cookieHeader.toString());
            reqHeaders.put(CLIENT_COOKIE_HEADER, cookieHeaderList);
        }
    }

    /*
     * extract and save the server cookies from the incoming response
     */
    protected void saveCookies(Map<String,List<String>> respHeaders) {
        // we only save the Cookie values, not header prefix
        List<String> cookieHeaders = respHeaders.get(SERVER_COOKIE_HEADER);
        if (cookieHeaders == null) {
            cookies = null;
        } else {
            cookies = new ArrayList<>(cookieHeaders.size());
            for (String cookieHeader : cookieHeaders) {
                cookies.add(cookieHeader.substring(0, cookieHeader.indexOf(';')));
            }
        }
    }
}