/*
 * $Id$
 *
 * License Agreement.
 *
 * Rich Faces - Natural Ajax for Java Server Faces (JSF)
 *
 * Copyright (C) 2007 Exadel, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License version 2.1 as published by the Free Software Foundation.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA
 */

package org.jboss.test.faces;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EventListener;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.LogManager;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.faces.FacesException;
import javax.faces.FactoryFinder;
import javax.faces.application.Application;
import javax.faces.application.ApplicationFactory;
import javax.faces.application.StateManager;
import javax.faces.application.ViewHandler;
import javax.faces.context.FacesContext;
import javax.faces.context.FacesContextFactory;
import javax.faces.lifecycle.Lifecycle;
import javax.faces.lifecycle.LifecycleFactory;
import javax.faces.render.ResponseStateManager;
import javax.faces.webapp.FacesServlet;
import javax.servlet.Filter;
import javax.servlet.Servlet;

import org.jboss.test.faces.staging.HttpConnection;
import org.jboss.test.faces.staging.HttpMethod;

/**
 * <p class="changed_added_4_0">
 * </p>
 * 
 * @author asmirnov@exadel.com
 * 
 */
public class FacesEnvironment {

    public static final String WEB_XML = "/WEB-INF/web.xml";
    public static final String FACES_CONFIG_XML = "/WEB-INF/faces-config.xml";

    public class FacesRequest {

        /**
         * Current virtual connection. This field populated by the {@link #setupWebContent()} method only.
         */
        private HttpConnection connection;

        /**
         * Current {@link FacesContext} instance. This field populated by the {@link #setupWebContent()} method only.
         */
        private FacesContext facesContext;

        private String viewId;

        public FacesRequest start() {
            if (connection.isStarted() || connection.isFinished()) {
                throw new IllegalStateException();
            }
            connection.start();
            FacesContextFactory facesContextFactory = (FacesContextFactory) FactoryFinder
                .getFactory(FactoryFinder.FACES_CONTEXT_FACTORY);
            facesContext = facesContextFactory.getFacesContext(facesServer.getContext(), connection.getRequest(),
                connection.getResponse(), lifecycle);
            if (null != viewId) {
                facesContext.setViewRoot(application.getViewHandler().createView(facesContext, viewId));
            }
            return this;
        }

        public FacesRequest finish() {
            if (!connection.isStarted() || connection.isFinished()) {
                throw new IllegalStateException();
            }
            connection.finish();
            return this;
		}
        
        public byte[] execute() {
            if (connection.isStarted() || connection.isFinished()) {
                throw new IllegalStateException();
            }
            connection.execute();
            return connection.getResponseBody();
        }

        public FacesRequest withViewId(String viewId) {
            if (connection.isStarted() || connection.isFinished()) {
                throw new IllegalStateException();
            }
            this.viewId = viewId;
            return this;
        }

        public FacesRequest withParameter(String name, String value) {
            this.connection.addRequestParameter(name, value);
            return this;
        }

        public String getResponseAsString() {
        	return connection.getContentAsString();
		}
        
        public void release() {
            if (null != facesContext) {
                facesContext.release();
                facesContext = null;
            }
            if (null != connection) {
                if (!connection.isFinished()) {
                    connection.finish();
                }
                connection = null;
            }
            requests.remove(this);
        }

        /**
         * <p class="changed_added_4_0">
         * </p>
         * 
         * @return the connection
         */
        public HttpConnection getConnection() {
            return this.connection;
        }

        public FacesRequest submit() throws MalformedURLException, FacesException {
            if (!connection.isFinished()) {
                throw new IllegalStateException();
            }
            // Extract VIEW_STATE value.
            Map<String, String> fields = getHiddenFields(connection.getContentAsString());
            if (!fields.containsKey(ResponseStateManager.VIEW_STATE_PARAM)) {
                throw new FacesException("No view state field in response");
            }
            FacesRequest facesRequest = createFacesRequest(connection.getRequest().getRequestURL().toString())
                .withViewId(viewId);
            facesRequest.connection.setRequestMethod(HttpMethod.POST);
            for (Map.Entry<String, String> entry : fields.entrySet()) {
                facesRequest.withParameter(entry.getKey(), entry.getValue());
            }
            return facesRequest;
        }

    }

    private List<FacesRequest> requests = new CopyOnWriteArrayList<FacesRequest>();

    private ClassLoader contextClassLoader;

    /**
     * Prepared test server instance. Populated by the default {@link #setUp()} method.
     */
    private ApplicationServer facesServer;

    /**
     * JSF {@link Lifecycle} instance. Populated by the default {@link #setUp()} method.
     */
    private Lifecycle lifecycle;

    /**
     * JSF {@link Application} instance. Populated by the default {@link #setUp()} method.
     */
    private Application application;

    private boolean initialized = false;

    private ServletHolder facesServletContainer;

    private FilterHolder filterContainer;

    private String webXmlDefault;

    private File webRoot;

    public FacesEnvironment() {
        this(ApplicationServer.createApplicationServer());
    }

    /**
     * <p class="changed_added_4_0">
     * </p>
     */
    public FacesEnvironment(ApplicationServer applicationServer) {
        this.facesServer = applicationServer;
        setupFacesServlet();
        setupFacesListener();
        setupJsfInitParameters();
        setupWebContent();
    }

    /**
     * <p class="changed_added_4_0">
     * </p>
     * 
     * @return the facesServer
     */
    public ApplicationServer getServer() {
        return this.facesServer;
    }

    /**
     * <p class="changed_added_4_0">
     * </p>
     * 
     * @return the lifecycle
     */
    public Lifecycle getLifecycle() {
        return this.lifecycle;
    }

    /**
     * <p class="changed_added_4_0">
     * </p>
     * 
     * @return the application
     */
    public Application getApplication() {
        return this.application;
    }

    public FacesEnvironment withFilter(String name, Filter filter) {
        checkNotInitialized();
        filterContainer = new FilterHolder(facesServletContainer.getMapping(), filter);
        filterContainer.setName(name);

        return this;
    }

    public FacesEnvironment withRichFaces() {
        checkNotInitialized();
        try {
            Filter ajaxFilter = createInstance("org.ajax4jsf.Filter");
            withFilter("ajax4jsf", ajaxFilter);
            webXmlDefault = "org/jboss/test/faces/ajax-web.xml";
            return this;
        } catch (ClassNotFoundException e) {
            throw new TestException(e);
        }
    }

    public FacesEnvironment withSeam() {
        checkNotInitialized();
        try {
            Filter ajaxFilter = createInstance("org.jboss.seam.servlet.SeamFilter");
            withFilter("ajax4jsf", ajaxFilter);
            EventListener seamListener = createInstance("org.jboss.seam.servlet.SeamListener");
            facesServer.addWebListener(seamListener);
            webXmlDefault = "org/jboss/test/faces/ajax-web.xml";
            return this;
        } catch (ClassNotFoundException e) {
            throw new TestException(e);
        }
    }

    public FacesEnvironment withWebRoot(File root) {
        checkNotInitialized();
        webRoot = root;
        return this;
    }

    /**
     * <p class="changed_added_4_0">
     * </p>
     * 
     * @param path
     * @param resource
     * @see org.jboss.test.faces.staging.StagingServer#addResource(java.lang.String, java.net.URL)
     */
    public FacesEnvironment withWebRoot(URL root) {
        checkNotInitialized();
        this.facesServer.addResourcesFromDirectory("/", root);
        webRoot = null;
        return this;
    }

    /**
     * <p class="changed_added_4_0">
     * </p>
     * 
     * @param root
     * @return
     */
    public FacesEnvironment withWebRoot(String root) {
        checkNotInitialized();
        return withWebRoot(FacesEnvironment.class.getClassLoader().getResource(root));
    }

    /**
     * <p class="changed_added_4_0">
     * </p>
     * 
     * @param name
     * @param value
     * @see org.jboss.test.faces.staging.StagingServer#addInitParameter(java.lang.String, java.lang.String)
     */
    public FacesEnvironment withInitParameter(String name, String value) {
        checkNotInitialized();
        this.facesServer.addInitParameter(name, value);
        return this;
    }

    /**
     * <p class="changed_added_4_0">
     * </p>
     * 
     * @param path
     * @param resource
     * @see org.jboss.test.faces.staging.StagingServer#addResource(java.lang.String, java.lang.String)
     */
    public FacesEnvironment withResource(String path, String resource) {
        this.facesServer.addResource(path, resource);
        return this;
    }

    public FacesEnvironment withResource(String path, URL resource) {
        this.facesServer.addResource(path, resource);
        return this;
    }

    public FacesEnvironment withResourcesFromDirectory(String path, URL resource){
    	this.facesServer.addResourcesFromDirectory(path, resource);
    	return this;
    }
    /**
     * <p class="changed_added_4_0">
     * </p>
     * 
     * @param path
     * @param resource
     * @see org.jboss.test.faces.staging.StagingServer#addResource(java.lang.String, java.lang.String)
     */
    public FacesEnvironment withContent(String path, String pageContent) {
        this.facesServer.addContent(path, pageContent);
        return this;
    }

    /**
     * Setup staging server instance with JSF implementation. First, this method creates a local test instance and calls
     * the other template method in the next sequence:
     * <ol>
     * <li>{@link #setupFacesServlet()}</li>
     * <li>{@link #setupFacesListener()}</li>
     * <li>{@link #setupJsfInitParameters()}</li>
     * <li>{@link #setupWebContent()}</li>
     * </ol>
     * After them, test server is initialized as well as fields {@link #lifecycle} and {@link #application} populated.
     * Also, if the resource "logging.properties" is exist in the test class package, The Java {@link LogManager} will
     * be configured with its content.
     * 
     * @throws java.lang.Exception
     */
    public FacesEnvironment start() {
        contextClassLoader = Thread.currentThread().getContextClassLoader();
        Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader());
        facesServer.addResource(WEB_XML, webXmlDefault);
        if (null != webRoot) {
            facesServer.addResourcesFromDirectory("/", webRoot);
        }

        facesServer.addServlet(facesServletContainer);

        if (filterContainer != null) {
            facesServer.addFilter(filterContainer);
        }

        facesServer.init();
        ApplicationFactory applicationFactory = (ApplicationFactory) FactoryFinder
            .getFactory(FactoryFinder.APPLICATION_FACTORY);
        application = applicationFactory.getApplication();
        LifecycleFactory lifecycleFactory = (LifecycleFactory) FactoryFinder
            .getFactory(FactoryFinder.LIFECYCLE_FACTORY);
        lifecycle = lifecycleFactory.getLifecycle(LifecycleFactory.DEFAULT_LIFECYCLE);
        initialized = true;
        return this;
    }

    /**
     * This hook method called from the {@link #setUp()} should append JSF implementation listener to the test server.
     * Default version applends "com.sun.faces.config.ConfigureListener" or
     * "org.apache.myfaces.webapp.StartupServletContextListener" for the existed SUN RI or MyFaces implementation. This
     * metod also calls appropriate {@link #setupSunFaces()} or {@link #setupMyFaces()} methods.
     */
    protected void setupFacesListener() {
        EventListener listener = null;
        try {
            // Check Sun RI configuration listener class.
            listener = createInstance("com.sun.faces.config.ConfigureListener");
            setupSunFaces();
        } catch (ClassNotFoundException e) {
            // No JSF RI listener, check MyFaces.
            try {
                listener = createInstance("org.apache.myfaces.webapp.StartupServletContextListener");
                setupMyFaces();
            } catch (ClassNotFoundException e1) {
                throw new TestException("No JSF listeners have been found", e1);
            }
        }
        facesServer.addWebListener(listener);
    }

    /**
     * This template method called from {@link #setUp()} to create {@link FacesServlet} instance. The default
     * implementation also tests presense of the "org.ajax4jsf.Filter" class. If this class is avalable, these instance
     * appended to the Faces Servlet call chain. Default mapping to the FacesServlet instance is "*.jsf"
     */
    protected void setupFacesServlet() {
        facesServletContainer = new ServletHolder("*.jsf", new FacesServlet());
        facesServletContainer.setName("Faces Servlet");
        webXmlDefault = "org/jboss/test/faces/web.xml";
    }

    /**
     * This template method called from {@link #setUp()} to append appropriate init parameters to the test server. The
     * default implementation sets state saving method to the "server", default jsf page suffix to the ".xhtml" and
     * project stage to UnitTest
     */
    protected void setupJsfInitParameters() {
        facesServer.addInitParameter(StateManager.STATE_SAVING_METHOD_PARAM_NAME,
            StateManager.STATE_SAVING_METHOD_SERVER);
        facesServer.addInitParameter(ViewHandler.DEFAULT_SUFFIX_PARAM_NAME, ".xhtml");
        // Do not use Jsf 2.0 classes directly because this environment should
        // be applicable for any JSF version.
        facesServer.addInitParameter("javax.faces.PROJECT_STAGE", "UnitTest");
    }

    /**
     * This template method called from the {@link #setupFacesListener()} if MyFaces implementation presents. The
     * default implementation does nothing.
     */
    protected void setupMyFaces() {
        // Do nothing by default.
    }

    /**
     * This template method called from the {@link #setupFacesListener()} if Sun JSF reference implementation presents.
     * The default implementation sets the "com.sun.faces.validateXml" "com.sun.faces.verifyObjects" init parameters to
     * the "true"
     */
    protected void setupSunFaces() {
        facesServer.addInitParameter("com.sun.faces.validateXml", "true");
        facesServer.addInitParameter("com.sun.faces.verifyObjects", "true");
    }

    /**
     * This template method called from the {@link #setUp()} to populate virtual server content. The default
     * implementation tries to load web content from directory pointed by the System property "webroot" or same property
     * from the "/webapp.properties" file.
     */
    protected void setupWebContent() {
        String webappDirectory = System.getProperty("webroot");
        webRoot = null;
        if (null == webappDirectory) {
            URL resource = this.getClass().getResource("/webapp.properties");
            if (null != resource && "file".equals(resource.getProtocol())) {
                Properties webProperties = new Properties();
                try {
                    InputStream inputStream = resource.openStream();
                    webProperties.load(inputStream);
                    inputStream.close();
                    webRoot = new File(resource.getPath());
                    webRoot = new File(webRoot.getParentFile(), webProperties.getProperty("webroot")).getAbsoluteFile();
                } catch (IOException e) {
                    throw new TestException(e);
                }
            }
        } else {
            webRoot = new File(webappDirectory);
        }

    }

    /**
     * Setup virtual server connection to run tests inside JSF lifecycle. The default implementation setups virtual
     * request to the "http://localhost/test.jsf" URL and creates {@link FacesContext} instance. Two template methods
     * are called :
     * <ol>
     * <li>{@link #setupConnection()} to prepare request method, parameters, headers and so</li>
     * <li>{@link #setupView()} to create default view.</li>
     * </ol>
     * 
     * @throws Exception
     */
    public FacesRequest createFacesRequest() throws Exception {
        String url = "http://localhost/test.jsf";
        return createFacesRequest(url).withViewId("/test.xhtml");
    }

    /**
     * <p class="changed_added_2_0">
     * </p>
     * 
     * @param url
     * @throws MalformedURLException
     * @throws FacesException
     */
    public FacesRequest createFacesRequest(String url) throws MalformedURLException, FacesException {
        FacesRequest request = new FacesRequest();
        request.connection = getServer().getConnection(new URL(url));
        requests.add(request);
        return request;
    }

    /**
     * JSF and Virtual server instance cleanup.
     * 
     * @throws java.lang.Exception
     */
    public void release() {
        checkInitialized();
        for (FacesRequest request : this.requests) {
            request.release();
        }
        facesServer.destroy();
        Thread.currentThread().setContextClassLoader(contextClassLoader);
        facesServer = null;
        application = null;
        lifecycle = null;
        initialized = false;
    }

    private void checkInitialized() {
        if (!initialized) {
            throw new TestException("JSF test environment has not been initialized");
        }
    }

    private void checkNotInitialized() {
        if (initialized) {
            throw new TestException("JSF test environment has already been initialized");
        }
    }

    /**
     * <p class="changed_added_4_0">
     * </p>
     * 
     * @param <T>
     * @param className
     * @return
     * @throws TestException
     * @throws ClassNotFoundException
     */
    @SuppressWarnings("unchecked")
    private <T> T createInstance(String className) throws TestException, ClassNotFoundException {
        try {
            Class<?> clazz = FacesEnvironment.class.getClassLoader().loadClass(className);
            return (T) clazz.newInstance();
        } catch (ClassNotFoundException e) {
            throw e;
        } catch (Exception e) {
            throw new TestException(e);
        }
    }

    public static FacesEnvironment createEnvironment() {
        return new FacesEnvironment();
    }

    public static FacesEnvironment createEnvironment(ApplicationServer applicationServer) {
        return new FacesEnvironment(applicationServer);
    }

    
    static final Pattern INPUT_PATTERN =Pattern.compile("<input([^>]+)>", Pattern.MULTILINE|Pattern.DOTALL);
    
    static final Pattern NAME_PATTERN =Pattern.compile("name=[\"']([^\"']*)[\"']");

    static final Pattern VALUE_PATTERN =Pattern.compile("value=[\"']([^\"']*)[\"']");

    public static Collection<String> getInputFields(String content){
        List<String> inputs = new ArrayList<String>();
        Matcher matcher = INPUT_PATTERN.matcher(content);
        while (matcher.find()) {
            inputs.add(matcher.group(1));
        }
        return inputs;
    }
    
    public static Map<String, String> getHiddenFields(String content){
        Collection<String> inputFields = getInputFields(content);
        HashMap<String, String> parameters = new HashMap<String, String>(inputFields.size());
        for (String string : inputFields) {
            if(string.contains("type='hidden'")||string.contains("type=\"hidden\"")){
                Matcher matcher = NAME_PATTERN.matcher(string);
                if(matcher.find()){
                    String name = matcher.group(1);
                    Matcher valueMatcher = VALUE_PATTERN.matcher(string);
                    if(valueMatcher.find()){
                        parameters.put(name, valueMatcher.group(1));
                    } else {
                        parameters.put(name, "");
                    }
                }
            }
        }
        return parameters;
    }

}
