/*
 * 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 freemarker.ext.jsp;

import static org.junit.Assert.*;

import java.io.IOException;
import java.util.AbstractList;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.regex.Pattern;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.junit.Ignore;
import org.junit.Test;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import freemarker.cache.ClassTemplateLoader;
import freemarker.cache.TemplateLoader;
import freemarker.cache.WebappTemplateLoader;
import freemarker.ext.beans.BeansWrapper;
import freemarker.ext.beans.BeansWrapperBuilder;
import freemarker.ext.servlet.FreemarkerServlet;
import freemarker.template.Configuration;
import freemarker.template.DefaultObjectWrapper;
import freemarker.template.ObjectWrapper;
import freemarker.template.SimpleObjectWrapper;
import freemarker.template.TemplateExceptionHandler;
import freemarker.test.servlet.DefaultModel2TesterAction;
import freemarker.test.servlet.WebAppTestCase;

/**
 * Tests {@link FreemarkerServlet} on a real (embedded) Servlet container.
 */
@SuppressWarnings("boxing")
@SuppressFBWarnings(value="ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD", justification="Hack needed for testing only")
public class RealServletContainertTest extends WebAppTestCase {

    private static final String WEBAPP_BASIC = "basic";
    private static final String WEBAPP_TLD_DISCOVERY = "tldDiscovery";
    private static final String WEBAPP_ERRORS = "errors";
    private static final String WEBAPP_CONFIG = "config";
    private static final String WEBAPP_MULTIPLE_LOADERS = "multipleLoaders";

    @Test
    public void basicTrivial() throws Exception {
        assertJSPAndFTLOutputEquals(WEBAPP_BASIC, "tester?view=trivial");
    }

    @Test
    @Ignore  // c:forEach fails because of EL context issues
    public void basicTrivialJSTL() throws Exception {
        assertOutputsEqual(WEBAPP_BASIC, "tester?view=trivial.jsp", "tester?view=trivial-jstl-@Ignore.ftl");        
    }

    @Test
    public void basicCustomTags1() throws Exception {
        assertExpectedEqualsOutput(WEBAPP_BASIC, "customTags1.txt", "tester?view=customTags1.ftl", false);
    }

    @Test
    public void basicCustomAttributes() throws Exception {
        restartWebAppIfStarted(WEBAPP_BASIC);  // To clear the application scope attributes
        assertExpectedEqualsOutput(WEBAPP_BASIC, "attributes.txt", "tester"
                + "?action=" + AllKindOfContainersModel2Action.class.getName()
                + "&view=attributes.ftl");

        restartWebAppIfStarted(WEBAPP_BASIC);  // To clear the application scope attributes
        assertExpectedEqualsOutput(WEBAPP_BASIC, "attributes-2.3.22-future.txt", "tester"
                + "?action=" + AllKindOfContainersModel2Action.class.getName()
                + "&view=attributes.ftl&viewServlet=freemarker-2.3.22-future");
        
        restartWebAppIfStarted(WEBAPP_BASIC);  // To clear the application scope attributes
        assertExpectedEqualsOutput(WEBAPP_BASIC, "attributes-2.3.0.txt", "tester"
                + "?action=" + AllKindOfContainersModel2Action.class.getName()
                + "&view=attributes.ftl&viewServlet=freemarker-2.3.0",
                true,
                ImmutableList.of(
                        Pattern.compile("(?<=^Date-time: ).*", Pattern.MULTILINE), // Uses Date.toString, so plat. dep.
                        Pattern.compile("(?<=^MyMap: ).*", Pattern.MULTILINE)  // Uses HashMap, so order unknown
                        ));
    }

    @Test
    public void basicELFunctions() throws Exception {
        assertJSPAndFTLOutputEquals(WEBAPP_BASIC, "tester?view=customELFunctions1");
    }

    // https://issues.apache.org/jira/browse/FREEMARKER-18
    @Test
    public void basicELFunctionsTagNameClash() throws Exception {
        // System.out.println(getResponseContent(WEBAPP_BASIC, "tester?view=elFunctionsTagNameClash.jsp"));
        // System.out.println(getResponseContent(WEBAPP_BASIC, "tester?view=elFunctionsTagNameClash.ftl"));
        assertJSPAndFTLOutputEquals(WEBAPP_BASIC, "tester?view=elFunctionsTagNameClash");
    }
    
    @Test
    public void tldDiscoveryBasic() throws Exception {
        try {
            restartWebAppIfStarted(WEBAPP_TLD_DISCOVERY);
            assertExpectedEqualsOutput(WEBAPP_TLD_DISCOVERY, "test1.txt", "tester?view=test1.ftl");
        } finally {
            JspTestFreemarkerServlet.resetToDefaults();
        }
    }

    @Test
    public void tldDiscoveryBasicDefultOverride() throws Exception {
        try {
            restartWebAppIfStarted(WEBAPP_TLD_DISCOVERY);
            assertExpectedEqualsOutput(WEBAPP_TLD_DISCOVERY, "test1.txt",
                    "tester?view=test1.ftl&viewServlet=freemarker-defaultOverride");
        } finally {
            JspTestFreemarkerServlet.resetToDefaults();
        }
    }
    
    @Test
    public void tldDiscoveryEmulatedProblems1() throws Exception {
        try {
            JspTestFreemarkerServlet.emulateNoJarURLConnections = true;
            restartWebAppIfStarted(WEBAPP_TLD_DISCOVERY);
            assertExpectedEqualsOutput(WEBAPP_TLD_DISCOVERY, "test1.txt", "tester?view=test1.ftl");
        } finally {
            JspTestFreemarkerServlet.resetToDefaults();
        }       
    }

    @Test
    public void tldDiscoveryEmulatedProblems2() throws Exception {
        try {
            JspTestFreemarkerServlet.emulateNoJarURLConnections = true;
            JspTestFreemarkerServlet.emulateNoUrlToFileConversions = true;
            // Because of emulateNoUrlToFileConversions = true it won't be able to list the directories, so:
            System.setProperty(
                    FreemarkerServlet.SYSTEM_PROPERTY_CLASSPATH_TLDS,
                    "META-INF/tldDiscovery MetaInfTldSources-1.tld");
            restartWebAppIfStarted(WEBAPP_TLD_DISCOVERY);
            assertExpectedEqualsOutput(WEBAPP_TLD_DISCOVERY, "test1.txt", "tester?view=test1.ftl");
        } finally {
            JspTestFreemarkerServlet.resetToDefaults();
            System.clearProperty(FreemarkerServlet.SYSTEM_PROPERTY_CLASSPATH_TLDS);
        }
    }

    @Test
    public void tldDiscoveryClasspathOnly() throws Exception {
        try {
            System.setProperty(FreemarkerServlet.SYSTEM_PROPERTY_META_INF_TLD_SOURCES, "clear, classpath");
            restartWebAppIfStarted(WEBAPP_TLD_DISCOVERY);
            assertExpectedEqualsOutput(WEBAPP_TLD_DISCOVERY, "test1.txt", "tester?view=test1.ftl");
        } finally {
            JspTestFreemarkerServlet.resetToDefaults();
            System.clearProperty(FreemarkerServlet.SYSTEM_PROPERTY_META_INF_TLD_SOURCES);
        }
    }

    /**
     * Tests that (1) webInfPerLibJars still loads from WEB-INF/lib/*.jar, and (2) that
     * {@link FreemarkerServlet#SYSTEM_PROPERTY_META_INF_TLD_SOURCES} indeed overrides the init-param, and that the
     * Jetty container's JSTL jar-s will still be discovered.
     */
    @Test
    public void tldDiscoveryNoClasspath() throws Exception {
        try {
            System.setProperty(FreemarkerServlet.SYSTEM_PROPERTY_META_INF_TLD_SOURCES, "clear, webInfPerLibJars");
            restartWebAppIfStarted(WEBAPP_TLD_DISCOVERY);
            assertExpectedEqualsOutput(WEBAPP_TLD_DISCOVERY,
                    "test-noClasspath.txt", "tester?view=test-noClasspath.ftl");
        } finally {
            JspTestFreemarkerServlet.resetToDefaults();
            System.clearProperty(FreemarkerServlet.SYSTEM_PROPERTY_META_INF_TLD_SOURCES);
        }
    }
    
    @Test
    public void tldDiscoveryRelative() throws Exception {
        assertExpectedEqualsOutput(WEBAPP_TLD_DISCOVERY, "subdir/test-rel.txt", "tester?view=subdir/test-rel.ftl");
    }
    
    @Test
    public void errorStatusCodes() throws Exception {
        assertEquals(404, getResponseStatusCode(WEBAPP_ERRORS, "missing.jsp"));
        assertEquals(500, getResponseStatusCode(WEBAPP_ERRORS, "failing-runtime.jsp"));
        assertEquals(500, getResponseStatusCode(WEBAPP_ERRORS, "failing-parsetime.jsp"));
        
        assertEquals(200, getResponseStatusCode(WEBAPP_ERRORS,
                "tester?view=not-failing.ftl&viewServlet=freemarker-default-dev"));
        assertEquals(404, getResponseStatusCode(WEBAPP_ERRORS,
                "tester?view=missing.ftl&viewServlet=freemarker-default-dev"));
        assertEquals(200, getResponseStatusCode(WEBAPP_ERRORS,
                "tester?view=failing-runtime.ftl&viewServlet=freemarker-default-dev"));
        assertEquals(500, getResponseStatusCode(WEBAPP_ERRORS,
                "tester?view=failing-parsetime.ftlnv&viewServlet=freemarker-default-dev"));
        
        assertEquals(200, getResponseStatusCode(WEBAPP_ERRORS,
                "tester?view=not-failing.ftl&viewServlet=freemarker-default-prod"));
        assertEquals(404, getResponseStatusCode(WEBAPP_ERRORS,
                "tester?view=missing.ftl&viewServlet=freemarker-default-prod"));
        assertEquals(500, getResponseStatusCode(WEBAPP_ERRORS,
                "tester?view=failing-runtime.ftl&viewServlet=freemarker-default-prod"));
        assertEquals(500, getResponseStatusCode(WEBAPP_ERRORS,
                "tester?view=failing-parsetime.ftlnv&viewServlet=freemarker-default-prod"));
        
        assertEquals(200, getResponseStatusCode(WEBAPP_ERRORS,
                "tester?view=not-failing.ftl&viewServlet=freemarker-future-prod"));
        assertEquals(500, getResponseStatusCode(WEBAPP_ERRORS,
                "tester?view=missing.ftl&viewServlet=freemarker-future-prod"));
        assertEquals(500, getResponseStatusCode(WEBAPP_ERRORS,
                "tester?view=failing-runtime.ftl&viewServlet=freemarker-future-prod"));
        assertEquals(500, getResponseStatusCode(WEBAPP_ERRORS,
                "tester?view=failing-parsetime.ftlnv&viewServlet=freemarker-future-prod"));
    }
    
    @Test
    public void testTemplateLoaderConfig() throws Exception {
        assertEquals("from /WEB-INF/classes", getResponseContent(WEBAPP_CONFIG,
                "tester?view=test.ftl&viewServlet=freemarker-class-root"));
        assertEquals("from WEB-INF/lib/templates.jar/sub", getResponseContent(WEBAPP_CONFIG,
                "tester?view=sub/test2.ftl&viewServlet=freemarker-class-root"));
        assertEquals("from /WEB-INF/classes/sub", getResponseContent(WEBAPP_CONFIG,
                "tester?view=test.ftl&viewServlet=freemarker-class-sub"));
        assertEquals("from WEB-INF/lib/templates.jar/sub", getResponseContent(WEBAPP_CONFIG,
                "tester?view=test2.ftl&viewServlet=freemarker-class-sub"));
        
        assertEquals("from /WEB-INF/classes", getResponseContent(WEBAPP_CONFIG,
                "tester?view=test.ftl&viewServlet=freemarker-classpath-root"));
        assertEquals("from WEB-INF/lib/templates.jar/sub", getResponseContent(WEBAPP_CONFIG,
                "tester?view=sub/test2.ftl&viewServlet=freemarker-classpath-root"));
        assertEquals("from /WEB-INF/classes/sub", getResponseContent(WEBAPP_CONFIG,
                "tester?view=test.ftl&viewServlet=freemarker-classpath-sub"));
        assertEquals("from WEB-INF/lib/templates.jar/sub", getResponseContent(WEBAPP_CONFIG,
                "tester?view=test2.ftl&viewServlet=freemarker-classpath-sub"));
        
        assertEquals("from /WEB-INF/templates", getResponseContent(WEBAPP_CONFIG,
                "tester?view=test.ftl&viewServlet=freemarker-webinfPerTemplates"));
        assertEquals("from /", getResponseContent(WEBAPP_CONFIG,
                "tester?view=test.ftl&viewServlet=freemarker-contentRoot"));
    }
    
    @Test
    public void testConfigurationDefaults() throws Exception {
        assertEquals(200, getResponseStatusCode(WEBAPP_CONFIG,
                "tester?view=test.ftl&viewServlet=freemarker-assertDefaultsFreemarkerServlet"));
        assertEquals(200, getResponseStatusCode(WEBAPP_CONFIG,
                "tester?view=test.ftl&viewServlet=freemarker-assertDefaultsIcI2322FreemarkerServlet"));
        assertEquals(200, getResponseStatusCode(WEBAPP_CONFIG,
                "tester?view=test.ftl&viewServlet=freemarker-assertCustomizedDefaultsFreemarkerServlet"));
        assertEquals(200, getResponseStatusCode(WEBAPP_CONFIG,
                "tester?view=test.ftl&viewServlet=freemarker-assertObjectWrapperDefaults1FreemarkerServlet"));
        assertEquals(200, getResponseStatusCode(WEBAPP_CONFIG,
                "tester?view=test.ftl&viewServlet=freemarker-assertObjectWrapperDefaults2FreemarkerServlet"));
    }
    
    @Test
    public void testMultipleLoaders() throws Exception {
       assertEquals("In test.ftl",
               getResponseContent(WEBAPP_MULTIPLE_LOADERS, "tester?view=test.ftl")); 
       assertEquals("In classpath-test.ftl",
               getResponseContent(WEBAPP_MULTIPLE_LOADERS, "tester?view=classpath-test.ftl")); 
    }

    public static class AllKindOfContainersModel2Action extends DefaultModel2TesterAction {

        @Override
        public String execute(HttpServletRequest req, HttpServletResponse resp) throws Exception {
            req.setAttribute("linkedList", initTestCollection(new LinkedList<Integer>()));
            req.setAttribute("arrayList", initTestCollection(new ArrayList<Integer>()));
            req.setAttribute("myList", new MyList());
            
            req.setAttribute("linkedHashMap", initTestMap(new LinkedHashMap()));
            req.setAttribute("treeMap", initTestMap(new TreeMap()));
            req.setAttribute("myMap", new MyMap());
            
            req.setAttribute("treeSet", initTestCollection(new TreeSet()));
            
            return super.execute(req, resp);
        }
        
        private Collection<Integer> initTestCollection(Collection<Integer> list) {
            for (int i = 0; i < 3; i++) list.add(i + 1);
            return list;
        }

        private Map<String, Integer> initTestMap(Map<String, Integer> map) {
            for (int i = 0; i < 3; i++) map.put(String.valueOf((char) ('a' + i)), i + 1);
            return map;
        }
        
    }
    
    public static class MyMap extends AbstractMap<String, Integer> {

        @Override
        public Set<Map.Entry<String, Integer>> entrySet() {
            return ImmutableSet.<Map.Entry<String, Integer>>of(
                    new MyEntry("a", 1), new MyEntry("b", 2), new MyEntry("c", 3));
        }
        
        private static class MyEntry implements Map.Entry<String, Integer> {
            
            private final String key;
            private final Integer value;

            public MyEntry(String key, Integer value) {
                this.key = key;
                this.value = value;
            }

            public String getKey() {
                return key;
            }

            public Integer getValue() {
                return value;
            }

            public Integer setValue(Integer value) {
                throw new UnsupportedOperationException();
            }
            
        }
        
    }
    
    public static class MyList extends AbstractList<Integer> {

        @SuppressWarnings("boxing")
        @Override
        public Integer get(int index) {
            return index + 1;
        }

        @Override
        public int size() {
            return 3;
        }
        
    }
    
    public static abstract class AssertingFreemarkerServlet extends FreemarkerServlet {

        @Override
        public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException,
                IOException {
            Configuration cfg = getConfiguration();
            try {
                doAssertions(cfg);
            } catch (Exception e) {
                throw new ServletException("Test failed", e);
            }
            
            response.setStatus(200);
            response.getWriter().write("OK");
        }

        protected abstract void doAssertions(Configuration cfg) throws Exception;
        
    }
    
    public static class AssertDefaultsFreemarkerServlet extends AssertingFreemarkerServlet {

        @Override
        protected void doAssertions(Configuration cfg) {
            assertEquals(Configuration.VERSION_2_3_22, cfg.getIncompatibleImprovements());
            
            assertSame(cfg.getTemplateExceptionHandler(), TemplateExceptionHandler.HTML_DEBUG_HANDLER);
            
            assertFalse(cfg.getLogTemplateExceptions());
            
            {
                ObjectWrapper ow = cfg.getObjectWrapper();
                assertTrue(ow instanceof DefaultObjectWrapper);
                assertEquals(Configuration.VERSION_2_3_22, ((DefaultObjectWrapper) ow).getIncompatibleImprovements());
            }
            
            {
                TemplateLoader tl = cfg.getTemplateLoader();
                assertTrue(tl instanceof ClassTemplateLoader);
                assertEquals("/", ((ClassTemplateLoader) tl).getBasePackagePath());
            }
            
        }
        
    }

    public static class AssertDefaultsIcI2322FreemarkerServlet extends AssertDefaultsFreemarkerServlet {

        @Override
        protected Configuration createConfiguration() {
            Configuration cfg = new Configuration(Configuration.VERSION_2_3_22);
            return cfg;
        }
        
    }
    
    public static class AssertCustomizedDefaultsFreemarkerServlet extends AssertingFreemarkerServlet {

        @Override
        protected Configuration createConfiguration() {
            Configuration cfg = new Configuration(Configuration.VERSION_2_3_20);
            cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
            cfg.setLogTemplateExceptions(true);
            cfg.setObjectWrapper(new BeansWrapperBuilder(Configuration.VERSION_2_3_21).build());
            cfg.setTemplateLoader(new WebappTemplateLoader(getServletContext()));
            return cfg;
        }

        @Override
        protected void doAssertions(Configuration cfg) {
            assertEquals(Configuration.VERSION_2_3_20, cfg.getIncompatibleImprovements());
            
            assertSame(cfg.getTemplateExceptionHandler(), TemplateExceptionHandler.RETHROW_HANDLER);
            
            assertTrue(cfg.getLogTemplateExceptions());
            
            {
                ObjectWrapper ow = cfg.getObjectWrapper();
                assertSame(BeansWrapper.class, ow.getClass());
                assertEquals(Configuration.VERSION_2_3_21, ((BeansWrapper) ow).getIncompatibleImprovements());
            }
            
            {
                TemplateLoader tl = cfg.getTemplateLoader();
                assertTrue(tl instanceof WebappTemplateLoader);
            }
            
        }
        
    }
    
    public static class AssertObjectWrapperDefaults1FreemarkerServlet extends AssertingFreemarkerServlet {

        @Override
        protected void doAssertions(Configuration cfg) throws Exception {
            ObjectWrapper ow = cfg.getObjectWrapper();
            assertSame(BeansWrapper.class, ow.getClass());
            assertEquals(Configuration.VERSION_2_3_21, ((BeansWrapper) ow).getIncompatibleImprovements());
        }

        @Override
        protected ObjectWrapper createDefaultObjectWrapper() {
            return new BeansWrapperBuilder(Configuration.VERSION_2_3_21).build();
        }
        
    }

    public static class AssertObjectWrapperDefaults2FreemarkerServlet extends
            AssertObjectWrapperDefaults1FreemarkerServlet {

        @Override
        protected Configuration createConfiguration() {
            Configuration cfg = new Configuration(Configuration.VERSION_2_3_20);
            cfg.setObjectWrapper(new SimpleObjectWrapper(Configuration.VERSION_2_3_22));
            return cfg;
        }
        
        @Override
        protected void doAssertions(Configuration cfg) throws Exception {
            ObjectWrapper ow = cfg.getObjectWrapper();
            assertSame(SimpleObjectWrapper.class, ow.getClass());
            assertEquals(Configuration.VERSION_2_3_22, ((BeansWrapper) ow).getIncompatibleImprovements());
        }
        
    }
    
}
