/*
 * $Id$
 *
 * 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.struts2;

import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.FileManager;
import com.opensymphony.xwork2.FileManagerFactory;
import com.opensymphony.xwork2.conversion.impl.XWorkConverter;
import com.opensymphony.xwork2.inject.Container;
import com.opensymphony.xwork2.util.OgnlTextParser;
import com.opensymphony.xwork2.util.TextParser;
import com.opensymphony.xwork2.util.ValueStack;
import com.opensymphony.xwork2.util.finder.ClassLoaderInterface;
import com.opensymphony.xwork2.util.finder.ClassLoaderInterfaceDelegate;
import com.opensymphony.xwork2.util.fs.DefaultFileManager;
import junit.framework.TestCase;
import org.apache.commons.lang3.StringUtils;
import org.apache.struts2.dispatcher.HttpParameters;
import org.apache.struts2.jasper.runtime.InstanceHelper;
import org.apache.struts2.url.QueryStringParser;
import org.apache.struts2.url.StrutsQueryStringParser;
import org.apache.struts2.url.StrutsUrlDecoder;
import org.apache.tomcat.InstanceManager;
import org.easymock.EasyMock;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockServletConfig;
import org.springframework.mock.web.MockServletContext;

import javax.servlet.Servlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

import static org.apache.struts2.ServletActionContext.STRUTS_VALUESTACK_KEY;


public class EmbeddedJSPResultTest extends TestCase {
    private HttpServletRequest request;
    private MockHttpServletResponse response;
    private MockServletContext context;
    private MockServletConfig config;
    private EmbeddedJSPResult result;

    public void testScriptlet() throws Exception {
        result.setLocation("org/apache/struts2/scriptlet.jsp");
        result.execute(null);

        assertEquals("Saynotoscriptlets", StringUtils.deleteWhitespace(response.getContentAsString()));
    }

    public void testEmbedded() throws Exception {
        //the jsp is inside jsps.jar
        result.setLocation("dir/all.jsp");
        result.execute(null);

        assertEquals("helloJGWhoamI?XXXXXXXXXXXYThissessionisnotsecure.", StringUtils.deleteWhitespace(response.getContentAsString()));
    }

    public void testFilesAreReadOnlyOnce() throws Exception {
        //make sure that files are not read multiple times
        String jsp = "org/apache/struts2/dont-use.jsp";

        CountingClassLoaderInterface classLoaderInterface = new CountingClassLoaderInterface(this.getClass().getClassLoader());
        context.setAttribute(ClassLoaderInterface.CLASS_LOADER_INTERFACE, classLoaderInterface);
        result.setLocation(jsp);

        result.execute(null);
        Integer counter0 = classLoaderInterface.counters.get(jsp);
        assertNotNull(counter0);

        result.execute(null);
        Integer counter1 = classLoaderInterface.counters.get(jsp);
        assertNotNull(counter1);

        assertEquals(counter0, counter1);
    }

    public void testEmbeddedAbsolutePath() throws Exception {
        //the jsp is inside jsps.jar
        result.setLocation("/dir/all.jsp");
        result.execute(null);

        assertEquals("helloJGWhoamI?XXXXXXXXXXXYThissessionisnotsecure.", StringUtils.deleteWhitespace(response.getContentAsString()));
    }

    public void testSimple() throws Exception {
        result.setLocation("org/apache/struts2/simple0.jsp");
        result.execute(null);

        assertEquals("hello", response.getContentAsString().trim());
    }

    //ok i give up..i don't know why this doesn't work from maven
   /* public void testKeyInContext() throws Exception {
        result.setLocation("org/apache/struts2/scriptlet.jsp");
        result.execute(null);

        String key = JspApplicationContextImpl.class.getName() + "@" + this.getClass().getClassLoader().hashCode();
        assertNotNull(context.getAttribute(key));
    }*/

    public void testEL() throws Exception {
        result.setLocation("org/apache/struts2/el.jsp");
        result.execute(null);

        assertEquals("somethingelseText", response.getContentAsString().trim());
    }

    public void testAbsolutePath() throws Exception {
        result.setLocation("/org/apache/struts2/simple0.jsp");
        result.execute(null);

        assertEquals("hello", response.getContentAsString().trim());
    }

    public void testTag0() throws Exception {
        result.setLocation("org/apache/struts2/tag0.jsp");
        result.execute(null);

        assertEquals("Thissessionisnotsecure.OtherText", StringUtils.deleteWhitespace(response.getContentAsString()));
    }

    public void testIncludeSimple() throws Exception {
        result.setLocation("org/apache/struts2/includes0.jsp");
        result.execute(null);

        assertEquals("helloTest", StringUtils.deleteWhitespace(response.getContentAsString()));
    }

    public void testIncludeSimpleWithDirective() throws Exception {
        result.setLocation("org/apache/struts2/includes3.jsp");
        result.execute(null);

        assertEquals("helloTest", StringUtils.deleteWhitespace(response.getContentAsString()));
    }

    public void testIncludeWithSubdir() throws Exception {
        result.setLocation("org/apache/struts2/includes1.jsp");
        result.execute(null);

        assertEquals("subTest", StringUtils.deleteWhitespace(response.getContentAsString()));
    }

    public void testIncludeWithParam() throws Exception {
        result.setLocation("org/apache/struts2/includes2.jsp");
        result.execute(null);

        assertEquals("JGTest", StringUtils.deleteWhitespace(response.getContentAsString()));
    }

    public void testBroken0() throws Exception {
        try {
            result.setLocation("org/apache/struts2/broken0.jsp");
            result.execute(null);
            fail("should have failed with broken jsp");
        } catch (IllegalStateException ex) {
            //ok
        }
    }

    public void testJSTL() throws Exception {
        result.setLocation("org/apache/struts2/jstl.jsp");
        result.execute(null);

        assertEquals("XXXXXXXXXXXY", StringUtils.deleteWhitespace(response.getContentAsString()));
    }


    public void testCachedInstances() throws InterruptedException {
        ServletCache cache = new ServletCache();
        Servlet servlet1 = cache.get("org/apache/struts2/simple0.jsp");
        Servlet servlet2 = cache.get("org/apache/struts2/simple0.jsp");

        assertSame(servlet1, servlet2);
    }

    public void testCacheInstanceWithManyThreads() throws BrokenBarrierException, InterruptedException {
        //start a bunch of thread at the same time using CyclicBarrier and hit the cache
        //then wait for all the threads to end and check that they all got a reference to the same object
        //and the cache size should be 1

        DummyServletCache cache = new DummyServletCache();
        int numThreads = 70;

        CyclicBarrier startBarrier = new CyclicBarrier(numThreads + 1);
        CyclicBarrier endBarrier = new CyclicBarrier(numThreads + 1);

        List<ServletGetRunnable> runnables = new ArrayList<>(numThreads);

        //create the threads
        for (int i = 0; i < numThreads; i++) {
            ServletGetRunnable runnable = new ServletGetRunnable(cache, startBarrier, endBarrier, ActionContext.getContext());
            Thread thread = new Thread(runnable);
            runnables.add(runnable);
            thread.start();
        }

        startBarrier.await();
        endBarrier.await();
        Object servlet = cache.get("org/apache/struts2/simple0.jsp");
        assertEquals(1, cache.size());

        for (ServletGetRunnable runnable : runnables) {
            assertSame(servlet, runnable.getObject());
        }
    }

    public void testBeans() throws Exception {
        result.setLocation("org/apache/struts2/beans.jsp");
        result.execute(null);

        assertEquals("WhoamI?", StringUtils.deleteWhitespace(response.getContentAsString()));
    }

    public void testNotURLClassLoader() throws Exception {
        ClassLoader parentClassLoader = Thread.currentThread().getContextClassLoader();
        NotURLClassLoader loader = new NotURLClassLoader(parentClassLoader);
        Thread.currentThread().setContextClassLoader(loader);

        try {
            result.setLocation("org/apache/struts2/tag0.jsp");
            result.execute(null);

            assertEquals("Thissessionisnotsecure.OtherText", StringUtils.deleteWhitespace(response.getContentAsString()));
        } finally {
            Thread.currentThread().setContextClassLoader(parentClassLoader);
        }
    }

    public void testComplex() throws Exception {
        result.setLocation("org/apache/struts2/complex0.jsp");
        result.execute(null);

        String responseString = response.getContentAsString();
        assertNotNull("result is null?", responseString);
        int titleIndex = responseString.indexOf("<title>Struts2 Embedded JSP Plugin - Complex Test Page</title>");
        int responseLength = responseString.length();
        int testValue1Index = responseString.indexOf("testvalue1 set/if worked.");
        int testValue5Index = responseString.indexOf("testvalue5 set/if worked.");
        int lastGroupIndex = responseString.indexOf("End include tests<br/>");
        int lastHtmlIndex = responseString.indexOf("</html>");
        assertTrue("Did not find title index (" + titleIndex + ") ?", titleIndex > 0);
        assertTrue("Test value 1 not present or index (" + testValue1Index + ") not > title index (" + titleIndex + ") ?",
            testValue1Index > titleIndex);
        assertTrue("Test value 5 not present or index (" + testValue5Index + ") not > test value 1 index (" + testValue1Index + ") ?",
            testValue5Index > testValue1Index);
        assertTrue("Last group index not present or index (" + lastGroupIndex + ") not > test value 5 index (" + testValue5Index + ") ?",
            lastGroupIndex > testValue5Index);
        assertTrue("Last html index not present or index (" + lastHtmlIndex + ") not > last group index (" + lastGroupIndex + ") ?",
            lastHtmlIndex > lastGroupIndex);
        // complex0.jsp length 3439 in Windows and estimated 3221 in Linux/Unix (with 218 lines, Windows has around 218 additional
        //   characters (crlf vs. lf).  Test length larger than the min(Windows,Linux)), rounded down to the nearest 100.
        assertTrue("Response length (" + responseLength + ") not at least length: 3200 ?", responseLength > 3200);
    }

    public void testInstanceHelper() throws Exception {
        InstanceManager instanceManagerServlet = InstanceHelper.getServletInstanceManager(config);
        InstanceManager instanceManagerClassLoader = InstanceHelper.getClassLoaderInstanceManager(context.getClassLoader());
        assertNotNull("instanceManager (servlet) is null ?", instanceManagerServlet);
        assertNotNull("instanceManager (classloader) is null ?", instanceManagerClassLoader);
        assertEquals("instanceManager (servlet) is not equal to instanceManager (classloader) ?", instanceManagerServlet, instanceManagerClassLoader);
        final Double instanceDouble = (double) 0;
        final Long instanceLong = 0L;
        final Object instanceObject = new Object();
        final String instanceString = "test string";
        final MockHttpServletRequest intanceMockHttpServletRequest = new MockHttpServletRequest();
        intanceMockHttpServletRequest.setContextPath("context path");
        InstanceHelper.postConstruct(instanceManagerServlet, instanceDouble);
        InstanceHelper.postConstruct(instanceManagerServlet, instanceLong);
        InstanceHelper.postConstruct(instanceManagerServlet, instanceObject);
        InstanceHelper.postConstruct(instanceManagerServlet, instanceString);
        InstanceHelper.postConstruct(instanceManagerServlet, intanceMockHttpServletRequest);
        assertEquals("test string value changed after postConstruct ?", instanceString, "test string");
        assertEquals("mock servlet request context path value changed after postConstruct ?",
            intanceMockHttpServletRequest.getContextPath(), "context path");
        InstanceHelper.preDestroy(instanceManagerServlet, instanceDouble);
        InstanceHelper.preDestroy(instanceManagerServlet, instanceLong);
        InstanceHelper.preDestroy(instanceManagerServlet, instanceObject);
        InstanceHelper.preDestroy(instanceManagerServlet, instanceString);
        InstanceHelper.preDestroy(instanceManagerServlet, intanceMockHttpServletRequest);
        assertEquals("test string value changed after preDestroy ?", instanceString, "test string");
        assertEquals("mock servlet request context path value changed after preDestroy ?",
            intanceMockHttpServletRequest.getContextPath(), "context path");
    }

    @Override
    protected void setUp() throws Exception {
        super.setUp();

        result = new EmbeddedJSPResult();

        request = EasyMock.createNiceMock(HttpServletRequest.class);
        response = new MockHttpServletResponse();
        context = new MockServletContext();
        config = new MockServletConfig(context);

        final Map params = new HashMap();

        HttpSession session = EasyMock.createNiceMock(HttpSession.class);
        EasyMock.replay(session);

        //mock value stack
        ValueStack valueStack = EasyMock.createNiceMock(ValueStack.class);
        EasyMock.expect(valueStack.getActionContext()).andReturn(ActionContext.getContext()).anyTimes();
        EasyMock.replay(valueStack);

        EasyMock.expect(request.getSession()).andReturn(session).anyTimes();
        EasyMock.expect(request.getParameterMap()).andReturn(params).anyTimes();
        EasyMock.expect(request.getParameter("username")).andAnswer(() -> ActionContext.getContext().getParameters().get("username").getValue());
        EasyMock.expect(request.getAttribute(STRUTS_VALUESTACK_KEY)).andReturn(valueStack).anyTimes();
        EasyMock.expect(request.getAttribute("something")).andReturn("somethingelse").anyTimes();

        EasyMock.replay(request);

        //mock converter
        XWorkConverter converter = new DummyConverter();

        DefaultFileManager fileManager = new DefaultFileManager();
        fileManager.setReloadingConfigs(false);

        //mock container
        Container container = EasyMock.createNiceMock(Container.class);
        EasyMock.expect(container.getInstance(XWorkConverter.class)).andReturn(converter).anyTimes();
        TextParser parser = new OgnlTextParser();
        EasyMock.expect(container.getInstance(TextParser.class)).andReturn(parser).anyTimes();
        EasyMock.expect(container.getInstanceNames(FileManager.class)).andReturn(new HashSet<>()).anyTimes();
        EasyMock.expect(container.getInstance(FileManager.class)).andReturn(fileManager).anyTimes();

        QueryStringParser queryStringParser = new StrutsQueryStringParser(new StrutsUrlDecoder());
        EasyMock.expect(container.getInstance(QueryStringParser.class)).andReturn(queryStringParser).anyTimes();
        FileManagerFactory fileManagerFactory = new DummyFileManagerFactory();
        EasyMock.expect(container.getInstance(FileManagerFactory.class)).andReturn(fileManagerFactory).anyTimes();

        EasyMock.replay(container);

        ActionContext.of(new HashMap<>())
            .withParameters(HttpParameters.create(params).build())
            .withServletRequest(request)
            .withServletResponse(response)
            .withServletContext(context)
            .withContainer(container)
            .withValueStack(valueStack)
            .bind();
    }

}

//converter has a protected default constructor...meh
class DummyConverter extends XWorkConverter {

}

class DummyFileManagerFactory implements FileManagerFactory {

    public void setReloadingConfigs(String reloadingConfigs) {
    }

    public FileManager getFileManager() {
        return new DefaultFileManager();
    }

}

class DummyServletCache extends ServletCache {
    public int size() {
        return cache.size();
    }
}

class ServletGetRunnable implements Runnable {
    private ServletCache servletCache;
    private Object object;
    private CyclicBarrier startBarrier;
    private ActionContext actionContext;
    private CyclicBarrier endBarrier;

    ServletGetRunnable(ServletCache servletCache, CyclicBarrier startBarrier, CyclicBarrier endBarrier, ActionContext actionContext) {
        this.servletCache = servletCache;
        this.startBarrier = startBarrier;
        this.endBarrier = endBarrier;
        this.actionContext = actionContext;
    }

    public void run() {
        actionContext = ActionContext.bind(actionContext);
        //wait to start all threads at once..or try at least
        try {
            startBarrier.await();
            object = servletCache.get("org/apache/struts2/simple0.jsp");

            for (int i = 0; i < 10; i++) {
                Object object2 = servletCache.get("org/apache/struts2/simple0.jsp");
                if (object2 != object)
                    throw new RuntimeException("got different object from cache");
            }

            endBarrier.await();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public Object getObject() {
        return object;
    }
}

class CountingClassLoaderInterface extends ClassLoaderInterfaceDelegate {
    public Map<String, Integer> counters = new HashMap<>();

    public CountingClassLoaderInterface(ClassLoader classLoader) {
        super(classLoader);
    }

    @Override
    public InputStream getResourceAsStream(String name) {
        Integer counter = counters.get(name);
        counter = counter == null ? 1 : counter + 1;
        counters.put(name, counter);

        return super.getResourceAsStream(name);
    }
}

class NotURLClassLoader extends ClassLoader {

    NotURLClassLoader(ClassLoader parentClassLoader) {
        super(parentClassLoader);
    }
}
