/**
 * 
 */
package org.richfaces.test;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.EventListener;
import java.util.logging.LogManager;

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.component.UIViewRoot;
import javax.faces.context.FacesContext;
import javax.faces.context.FacesContextFactory;
import javax.faces.lifecycle.Lifecycle;
import javax.faces.lifecycle.LifecycleFactory;
import javax.faces.webapp.FacesServlet;
import javax.servlet.Filter;

import junit.framework.TestCase;

import org.junit.After;
import org.junit.Before;
import org.richfaces.test.staging.FilterContainer;
import org.richfaces.test.staging.ServletContainer;
import org.richfaces.test.staging.StagingConnection;
import org.richfaces.test.staging.StagingServer;

/**
 * Base class for all JSF test cases. 
 * @author asmirnov
 * 
 */
public abstract class AbstractFacesTest extends TestCase {

	private ClassLoader contextClassLoader;

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

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

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

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

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

	/**
	 * 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
	 */
	@Before
	public void setUp() throws Exception {
		contextClassLoader = Thread.currentThread().getContextClassLoader();
		Thread.currentThread().setContextClassLoader(
				this.getClass().getClassLoader());
		InputStream stream = this.getClass().getResourceAsStream(
				"logging.properties");
		if (null != stream) {
			try {
				LogManager.getLogManager().readConfiguration(stream);
			} catch (Exception e) {
				// Ignore it.
			} finally {
				try {
					stream.close();
				} catch (IOException e) {
					// Ignore it.
				}
			}
		}
		facesServer = new StagingServer();
		setupFacesServlet();
		setupFacesListener();
		setupJsfInitParameters();
		setupWebContent();
		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);
	}

	/**
	 * 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.
			Class<? extends EventListener> listenerClass = contextClassLoader
					.loadClass("com.sun.faces.config.ConfigureListener")
					.asSubclass(EventListener.class);
			listener = listenerClass.newInstance();
			setupSunFaces();
		} catch (ClassNotFoundException e) {
			// No JSF RI listener, check MyFaces.
			Class<? extends EventListener> listenerClass;
			try {
				listenerClass = contextClassLoader
						.loadClass(
								"org.apache.myfaces.webapp.StartupServletContextListener")
						.asSubclass(EventListener.class);
				listener = listenerClass.newInstance();
				setupMyFaces();
			} catch (ClassNotFoundException e1) {
				throw new TestException("No JSF listeners have been found", e1);
			} catch (Exception e2) {
				throw new TestException("Error instantiate MyFaces listener",
						e2);
			}
		} catch (Exception e) {
			throw new TestException("Error instantiate JSF RI listener", e);
		}
		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() {
		ServletContainer facesServletContainer = new ServletContainer("*.jsf",
				new FacesServlet());
		facesServletContainer.setName("Faces Servlet");
		try {
			// Check for an ajax4jsf filter.
			Class<? extends Filter> ajaxFilterClass = contextClassLoader
					.loadClass("org.ajax4jsf.Filter").asSubclass(Filter.class);
			Filter ajaxFilter = ajaxFilterClass.newInstance();
			FilterContainer filterContainer = new FilterContainer(ajaxFilter,
					facesServletContainer);
			filterContainer.setName("ajax4jsf");
			facesServer.addResource("/WEB-INF/web.xml",
					"org/richfaces/test/ajax-web.xml");
			facesServer.addServlet(filterContainer);
		} catch (ClassNotFoundException e) {
			// No Richfaces filter, uses servlet directly.
			facesServer.addResource("/WEB-INF/web.xml",
					"org/richfaces/test/web.xml");
			facesServer.addServlet(facesServletContainer);
		} catch (Exception e) {
			throw new TestException(e);
		}
	}

	/**
	 * 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" and default jsf page suffix to the ".xhtml"
	 */
	protected void setupJsfInitParameters() {
		facesServer.addInitParameter(
				StateManager.STATE_SAVING_METHOD_PARAM_NAME,
				StateManager.STATE_SAVING_METHOD_SERVER);
		facesServer.addInitParameter(ViewHandler.DEFAULT_SUFFIX_PARAM_NAME,
				".xhtml");
	}

	/**
	 * 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 do nothing.
	 */
	protected void setupWebContent() {

	}

	/**
	 * 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
	 */
	protected void setupFacesRequest() throws Exception {
		connection = facesServer.getConnection(new URL(
				"http://localhost/test.jsf"));
		setupConnection();
		connection.start();
		FacesContextFactory facesContextFactory = (FacesContextFactory) FactoryFinder
				.getFactory(FactoryFinder.FACES_CONTEXT_FACTORY);
		facesContext = facesContextFactory.getFacesContext(facesServer
				.getContext(), connection.getRequest(), connection
				.getResponse(), lifecycle);
		UIViewRoot viewRoot = setupView();
		if (null != viewRoot) {
			facesContext.setViewRoot(viewRoot);
		}
	}

	/**
	 * This template method called from the {@link #setupFacesRequest()} to create components view tree in the virtual request. 
	 * The default implementation is only creates {@link UIViewRoot} instance for view ID "/test.xhtml".
	 * @return
	 */
	protected UIViewRoot setupView() {
		UIViewRoot viewRoot = (UIViewRoot) application.createComponent(UIViewRoot.COMPONENT_TYPE);
		viewRoot.setViewId("/test.xhtml");
		return viewRoot;
	}

	/**
	 * This template method called from the {@link #setupFacesRequest()} to setup additional virtual connection parameters.
	 * The default implementation does nothing.
	 */
	protected void setupConnection() {

	}

	/**
	 * Virtual server instance cleanup.
	 * @throws java.lang.Exception
	 */
	@After
	public void tearDown() throws Exception {
		if (null != facesContext) {
			facesContext.release();
			facesContext = null;
		}
		if (null != connection) {
			if (!connection.isFinished()) {
				connection.finish();
			}
			connection = null;
		}
		facesServer.destroy();
		Thread.currentThread().setContextClassLoader(contextClassLoader);
		facesServer = null;
		application = null;
		lifecycle = null;
	}

}
