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

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequestEvent;
import jakarta.servlet.ServletRequestListener;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

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

import org.apache.catalina.Context;
import org.apache.catalina.connector.Response;
import org.apache.catalina.startup.Tomcat;
import org.apache.catalina.startup.TomcatBaseTest;
import org.apache.tomcat.util.buf.ByteChunk;
import org.apache.tomcat.util.descriptor.web.ErrorPage;

public class TestStandardHostValve extends TomcatBaseTest {

    @Test
    public void testErrorPageHandling400() throws Exception {
        doTestErrorPageHandling(400, "", "/400");
    }


    @Test
    public void testErrorPageHandling400WithException() throws Exception {
        doTestErrorPageHandling(400, "java.lang.IllegalStateException", "/400");
    }


    @Test
    public void testErrorPageHandling500() throws Exception {
        doTestErrorPageHandling(500, "", "/500");
    }


    @Test
    public void testErrorPageHandlingDefault() throws Exception {
        doTestErrorPageHandling(501, "", "/default");
    }


    private void doTestErrorPageHandling(int error, String exception, String report)
            throws Exception {

        // Set up a container
        Tomcat tomcat = getTomcatInstance();

        // No file system docBase required
        Context ctx = tomcat.addContext("", null);

        // Add the error page
        Tomcat.addServlet(ctx, "error", new ErrorServlet());
        ctx.addServletMappingDecoded("/error", "error");

        // Add the error handling page
        Tomcat.addServlet(ctx, "report", new ReportServlet());
        ctx.addServletMappingDecoded("/report/*", "report");

        // Add the handling for 400 responses
        ErrorPage errorPage400 = new ErrorPage();
        errorPage400.setErrorCode(Response.SC_BAD_REQUEST);
        errorPage400.setLocation("/report/400");
        ctx.addErrorPage(errorPage400);

        // And the handling for 500 responses
        ErrorPage errorPage500 = new ErrorPage();
        errorPage500.setErrorCode(Response.SC_INTERNAL_SERVER_ERROR);
        errorPage500.setLocation("/report/500");
        ctx.addErrorPage(errorPage500);

        // And the default error handling
        ErrorPage errorPageDefault = new ErrorPage();
        errorPageDefault.setLocation("/report/default");
        ctx.addErrorPage(errorPageDefault);

        tomcat.start();

        // Request a page that triggers an error
        ByteChunk bc = new ByteChunk();
        int rc = getUrl("http://localhost:" + getPort() + "/error?errorCode=" + error + "&exception=" + exception,
                bc, null);

        Assert.assertEquals(error, rc);
        Assert.assertEquals(report, bc.toString());
    }


    @Test(expected=IllegalArgumentException.class)
    public void testInvalidErrorPage() throws Exception {
        // Set up a container
        Tomcat tomcat = getTomcatInstance();

        // No file system docBase required
        Context ctx = tomcat.addContext("", null);

        // Add a broken error page configuration
        ErrorPage errorPage500 = new ErrorPage();
        errorPage500.setErrorCode("java.lang.Exception");
        errorPage500.setLocation("/report/500");
        ctx.addErrorPage(errorPage500);
    }


    @Test
    public void testSRLAfterError() throws Exception {
        // Set up a container
        Tomcat tomcat = getTomcatInstance();

        // No file system docBase required
        Context ctx = tomcat.addContext("", null);

        // Add the error page
        Tomcat.addServlet(ctx, "error", new ErrorServlet());
        ctx.addServletMappingDecoded("/error", "error");

        final List<String> result = new ArrayList<>();

        // Add the request listener
        ServletRequestListener servletRequestListener = new ServletRequestListener() {

            @Override
            public void requestDestroyed(ServletRequestEvent sre) {
                result.add("Visit requestDestroyed");
            }

            @Override
            public void requestInitialized(ServletRequestEvent sre) {
                result.add("Visit requestInitialized");
            }

        };
        ((StandardContext) ctx).addApplicationEventListener(servletRequestListener);

        tomcat.start();

        // Request a page that triggers an error
        ByteChunk bc = new ByteChunk();
        int rc = getUrl("http://localhost:" + getPort() + "/error?errorCode=400", bc, null);

        Assert.assertEquals(400, rc);
        Assert.assertTrue(result.contains("Visit requestInitialized"));
        Assert.assertTrue(result.contains("Visit requestDestroyed"));
    }


    @Test
    public void testIncompleteResponse() throws Exception {
        // Set up a container
        Tomcat tomcat = getTomcatInstance();

        // No file system docBase required
        Context ctx = tomcat.addContext("", null);

        // Add the error page
        Tomcat.addServlet(ctx, "error", new ExceptionServlet());
        ctx.addServletMappingDecoded("/error", "error");

        // Add the error handling page
        Tomcat.addServlet(ctx, "report", new ReportServlet());
        ctx.addServletMappingDecoded("/report/*", "report");

        // Add the handling for 500 responses
        ErrorPage errorPage500 = new ErrorPage();
        errorPage500.setErrorCode(Response.SC_INTERNAL_SERVER_ERROR);
        errorPage500.setLocation("/report/500");
        ctx.addErrorPage(errorPage500);

        // Add the default error handling
        ErrorPage errorPageDefault = new ErrorPage();
        errorPageDefault.setLocation("/report/default");
        ctx.addErrorPage(errorPageDefault);

        tomcat.start();

        // Request a page that triggers an error
        ByteChunk bc = new ByteChunk();
        Throwable t = null;
        try {
            getUrl("http://localhost:" + getPort() + "/error", bc, null);
            System.out.println(bc.toString());
        } catch (IOException ioe) {
            t = ioe;
        }
        Assert.assertNotNull(t);
    }


    private static class ErrorServlet extends HttpServlet {

        private static final long serialVersionUID = 1L;

        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp)
                throws ServletException, IOException {
            int error = Integer.parseInt(req.getParameter("errorCode"));
            resp.sendError(error);

            Throwable t = null;
            String exception = req.getParameter("exception");
            if (exception != null && exception.length() > 0) {
                try {
                    t = (Throwable) Class.forName(exception).getConstructor().newInstance();
                } catch (ReflectiveOperationException e) {
                    // Should never happen but in case it does...
                    t = new IllegalArgumentException();
                }
                req.setAttribute(RequestDispatcher.ERROR_EXCEPTION, t);
            }
        }
    }


    private static class ExceptionServlet extends HttpServlet {

        private static final long serialVersionUID = 1L;

        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp)
                throws ServletException, IOException {
            resp.flushBuffer();
            throw new IOException();
        }
    }


    private static class ReportServlet extends HttpServlet {

        private static final long serialVersionUID = 1L;

        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp)
                throws ServletException, IOException {
            String pathInfo = req.getPathInfo();
            resp.setContentType("text/plain");
            resp.getWriter().print(pathInfo);
        }
    }
}
