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

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URL;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.Random;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import javax.faces.FacesException;
import javax.faces.FactoryFinder;
import javax.faces.component.UIComponent;
import javax.faces.component.UIViewRoot;
import javax.faces.context.FacesContext;
import javax.faces.render.RenderKitFactory;
import javax.faces.render.Renderer;
import javax.servlet.http.HttpServletResponse;

import org.ajax4jsf.context.AjaxContext;
import org.ajax4jsf.context.AjaxContextImpl;
import org.ajax4jsf.renderkit.AjaxViewRootRenderer;
import org.ajax4jsf.renderkit.ChameleonRenderKitImpl;
import org.ajax4jsf.resource.InternetResource;
import org.ajax4jsf.resource.InternetResourceBuilder;
import org.ajax4jsf.tests.org.apache.shale.test.config.ConfigParser;
import org.ajax4jsf.webapp.WebXml;
import org.apache.shale.test.base.AbstractJsfTestCase;
import org.apache.shale.test.mock.MockPrintWriter;
import org.apache.shale.test.mock.MockResponseWriter;
import org.apache.shale.test.mock.MockServletOutputStream;
import org.richfaces.VersionBean;
import org.richfaces.skin.SkinBean;
import org.richfaces.skin.SkinFactory;

import com.gargoylesoftware.htmlunit.MockWebConnection;
import com.gargoylesoftware.htmlunit.Page;
import com.gargoylesoftware.htmlunit.WebClient;
import com.gargoylesoftware.htmlunit.WebRequestSettings;
import com.gargoylesoftware.htmlunit.WebResponse;
import com.gargoylesoftware.htmlunit.WebResponseData;
import com.gargoylesoftware.htmlunit.WebResponseImpl;
import com.gargoylesoftware.htmlunit.html.HtmlPage;

/**
 * Base class for testing components and renderers in a4j framework
 * Simulate a4j environment for perform requests.
 * For concrete tests MUST be overriden setUp/tearDown methods, even if no actions
 * performed - it need for junit initializations.
 * All output html collected and can be checked by htmlunit
 * @author shura (latest modification by $Author: ishabalov $)
 * @version $Revision: 1.1.2.5 $ $Date: 2007/02/20 20:58:08 $
 *
 */
public class AbstractAjax4JsfTestCase extends AbstractJsfTestCase {

	protected final static String SLASHED_RESOURCE_URI_PREFIX = "/" + WebXml.RESOURCE_URI_PREFIX;

	private File tmpRoot = null;

	/**
	 * @param name
	 */
	public AbstractAjax4JsfTestCase(String name) {
		super(name);
		// TODO Auto-generated constructor stub
	}

	/* (non-Javadoc)
	 * @see org.apache.shale.test.base.AbstractJsfTestCase#setUp()
	 */
	public void setUp() throws Exception {
		// This method MUST BE OVERRIDEN in any subclasses - since Junit see for it in class for call
		super.setUp();

		// Setup FacesContext with nessesary init parameters.
		this.servletContext.addInitParameter(SkinFactory.SKIN_PARAMETER, getSkinName());
		// setup VCP renderKit, create renderers.
		RenderKitFactory vcpRenderKitFactory = (RenderKitFactory) FactoryFinder.getFactory(FactoryFinder.RENDER_KIT_FACTORY);
		vcpRenderKit = new ChameleonRenderKitImpl();
		vcpRenderKitFactory.addRenderKit("HTML_CHAMELEON", vcpRenderKit);
		renderKit.addRenderer(UIViewRoot.COMPONENT_FAMILY, UIViewRoot.COMPONENT_TYPE, new AjaxViewRootRenderer());
		// setup nessesary components.
		application.addComponent("javax.faces.ViewRoot", MockViewRoot.class.getName());
		// setup AjaxContext.
		ajaxContext = new AjaxContextImpl();
		request.setAttribute(AjaxContext.AJAX_CONTEXT_KEY, ajaxContext);
		// Setup ViewHandler / ViewRoot.
		application.setViewHandler(new MockViewHandler(application.getViewHandler()));
		facesContext.setViewRoot(application.getViewHandler().createView(facesContext, facesContext.getViewRoot().getViewId()));
		// Setup Skin avd version variables.
		servletContext.setAttribute("a4j", new VersionBean());
		servletContext.setAttribute("a4jSkin", new SkinBean());
		// Setup servlet context and testing web.xml
		request.setPathElements("/testContext","/faces","/vievId.jsf","");

		File webRoot = new File(getClass().getResource("/WEB-INF/web.xml").getFile()).getParentFile().getParentFile();
		if (webRoot.exists()) {
			servletContext.setDocumentRoot(webRoot);
		} else {
			// Prepare WEB-ROOT in temp folder
			tmpRoot = File.createTempFile("TmpTestRoot" + new Random().nextInt(), null);
			tmpRoot.delete();
			tmpRoot.mkdir();
			servletContext.setDocumentRoot(tmpRoot);

			URL jarUrl = AbstractAjax4JsfTestCase.class.getProtectionDomain().getCodeSource().getLocation();
			ZipInputStream zis = new ZipInputStream(jarUrl.openStream());
			try {
				ZipEntry entry;
				byte[] buffer = new byte[8192];
				while ((entry = zis.getNextEntry()) != null) {
					String name = entry.getName();
					if (name.startsWith("WEB-INF/")) {
						File out = new File(tmpRoot, name);
						if (entry.isDirectory()) {
							out.mkdirs();
						} else {
							out.getParentFile().mkdirs();
							OutputStream os = new FileOutputStream(out);
							try {
								int count;
								while ((count = zis.read(buffer)) > 0) {
									os.write(buffer, 0, count);
								}
							} finally {
								os.close();
							}
							zis.closeEntry();
						}
					}
				}
			} catch (IOException e) {
				deleteRecursively(tmpRoot);
				throw e;
			}
			servletContext.setDocumentRoot(tmpRoot);
		}

		try {
			InternetResourceBuilder.getInstance().init();
		} catch (FacesException e) {
			InternetResourceBuilder.setInstance(null);
		}
		webXml = new WebXml();
		webXml.init(servletContext, "A4J");

		ConfigParser parser = new ConfigParser();
		parser.parse(parser.getPlatformURLs());

		Enumeration resources = getClass().getClassLoader().getResources("META-INF/faces-config.xml");
		while (resources.hasMoreElements()) {
			parser.parse((URL) resources.nextElement());
		}
		
		externalContext = new MockExternalContext(externalContext);
		facesContext.setExternalContext(externalContext);
	}

	protected WebClient createWebClient() {
		WebClient client = new WebClient();
		//disable javascript for now as HtmlUnit doesn't treat prototype.js well
		client.setJavaScriptEnabled(false);

		return client;
	}
	
	/**
	 * 
	 */
	protected void setupWebClient() {
		ajaxContext = null;
		// setup webClient for got response content.
		webClient = createWebClient();
		webConnection = new MockWebConnection(webClient) {
			public WebResponse getResponse(WebRequestSettings settings)
			throws IOException {
				String testResourceContextPath = facesContext.getExternalContext().getRequestContextPath() + 
					AbstractAjax4JsfTestCase.SLASHED_RESOURCE_URI_PREFIX;
				if (settings.getURL().getFile().startsWith(testResourceContextPath)) {
					String uri = settings.getURL().getFile().substring(testResourceContextPath.length());
					
					InternetResourceBuilder resourceBuilder = InternetResourceBuilder.getInstance();
					
					InternetResource resource = resourceBuilder.getResourceForKey(uri);
					Object resourceData = resourceBuilder.getResourceDataForKey(uri);

					final MockFacesResourceContext resourceContext = new MockFacesResourceContext(facesContext);
					resourceContext.setResourceData(resourceData);

					ByteArrayOutputStream baos = new ByteArrayOutputStream();
					response.setOutputStream(new MockServletOutputStream(baos));
					
					resource.sendHeaders(resourceContext);
					resource.send(resourceContext);
					
					return new WebResponseImpl(
							new WebResponseData(
									new ByteArrayInputStream(baos.toByteArray()),
									HttpServletResponse.SC_OK, 
									"OK", 
									resourceContext.getHeaders()), 
									settings.getURL(), 
									settings.getSubmitMethod(), 
									0) {

						public String getContentType() {
							return resourceContext.getContentType();
						}
					};

				} else {
					return super.getResponse(settings);
				}
			}
		};
		webClient.setWebConnection(webConnection);
		webConnection.setDefaultResponse("<html><body>not found</body></html>",404,"NOT FOUND","text/html");
		webClient.setThrowExceptionOnFailingStatusCode(false);
		webClient.setThrowExceptionOnScriptError(false);
	}

	private void deleteRecursively(File file) {
		if (file != null) {

			String[] list = file.list();
			if (list != null) {
				for (int i = 0; i < list.length; i++) {
					String name = list[i];
					File f = new File(file, name);
					if (f.isDirectory()) {
						deleteRecursively(f);
					} else {
						f.delete();
					}
				}
			}

			file.delete();
		}
	}


	/* (non-Javadoc)
	 * @see org.apache.shale.test.base.AbstractJsfTestCase#tearDown()
	 */
	public void tearDown() throws Exception {
		// This method MUST BE OVERRIDEN in any subclasses - since Junit see for it in class for call
		super.tearDown();
		vcpRenderKit = null;
//		Thread.currentThread().setContextClassLoader(threadContextClassLoader);
//		threadContextClassLoader = null;
		webClient = null;
		webConnection = null;
		writer = null;
		SkinFactory.reset();
		InternetResourceBuilder.setInstance(null);

		deleteRecursively(tmpRoot);
	}

	// Protected configurations URL's
	/**
	 * Hook method for setup current VCP skin name for test. default use "classic"
	 * @return
	 */
	protected String getSkinName() {
		return "DEFAULT";
	}

	/**
	 * Hook method for load diffrernt frameworks for this test.
	 * Can return URL's for jars with tested implementation.
	 * @return
	 */
	protected URL[] getImplementationUrls() {
		return new URL[0];
	}

	/**
	 * Create component with given render kit and unique Id from ViewRoot.
	 * @param type - component type.
	 * @param clazz - class of component.
	 * @param rendererType - name for renderer type ( can be null ).
	 * @param rendererClazz - class of renderer in HTMP_BASIC renderkit. If not null, renderer will created and registered.
	 * @param skinRendererClazz - class of renderer in Skin render-kit.
	 * @return component instance.
	 * @throws Exception
	 */
	protected UIComponent createComponent(String type, String className, String rendererType, Class rendererClazz,Class skinRendererClazz)  {
		UIComponent comp;
		try {
			comp = application.createComponent(type);
		} catch (FacesException e) {
			application.addComponent(type, className);
			comp = application.createComponent(type);
		}
		comp.setRendererType(rendererType);
		String family = comp.getFamily();
		if (null != rendererClazz) {
			Renderer renderer = renderKit.getRenderer(family, rendererType); 
			if (null == renderer) {
				try {
					renderer = (Renderer) rendererClazz.newInstance();
				} catch (InstantiationException e) {
					assertTrue("Instantiation exception for create renderer "+rendererClazz.getName(),false);
				} catch (IllegalAccessException e) {
					assertTrue("ILEGAL access exception for create renderer "+rendererClazz.getName(),false);
				}
				renderKit.addRenderer(family, rendererType, renderer);
			}
		}
		if(null != skinRendererClazz) {
			Renderer renderer = renderKit.getRenderer(family, rendererType); 
			if (null == renderer) {
				try {
					renderer = (Renderer) skinRendererClazz.newInstance();
				} catch (InstantiationException e) {
					assertTrue("Instantiation exception for create renderer "+skinRendererClazz.getName(),false);
				} catch (IllegalAccessException e) {
					assertTrue("ILEGAL access exception for create renderer "+skinRendererClazz.getName(),false);
				}
				vcpRenderKit.addRenderer(family, rendererType, renderer); 
			}			
		}
		comp.setId(facesContext.getViewRoot().createUniqueId());
		return comp;
	}


	/**
	 * Render all children for given component.
	 * @param component
	 * @throws IOException
	 */
	protected void renderChildren(FacesContext context,
			UIComponent component) throws IOException {
		if (component.getChildCount() > 0) {
			for (Iterator it = component.getChildren().iterator(); it.hasNext();) {
				UIComponent child = (UIComponent) it.next();
				renderChild(context,child);
			}
		}
	}


	/**
	 * Render one component and it childrens
	 * @param child
	 * @throws IOException
	 */
	protected void renderChild(FacesContext context, UIComponent child)
	throws IOException {
		if (!child.isRendered()) {
			return;
		}

		child.encodeBegin(context);
		if (child.getRendersChildren()) {
			child.encodeChildren(context);
		} else {
			renderChildren(context, child);
		}
		child.encodeEnd(context);
	}

	/**
	 * Render test view and parse to htmlunit page structure.
	 * @return
	 * @throws Exception
	 */
	protected HtmlPage renderView() throws Exception {
		setupResponseWriter();
		renderChild(facesContext, facesContext.getViewRoot());
		return processResponseWriter();
	}

	/**
	 * Finish response processing by call {@link javax.faces.context.ResponseWriter#endDocument()} and parse
	 * result to htmlunit {@link HtmlPage}
	 * @return
	 * @throws IOException
	 * @throws Exception
	 */
	protected HtmlPage processResponseWriter() throws IOException, Exception {
		writer.endDocument();
		return (HtmlPage) processResponse();
	}

	/**
	 * Setup Faces {@link javax.faces.context.ResponseWriter} and prepare rendering ( startDocument ).
	 * after this method, any jsf rendering methods can be performed, and results can be checked by {@link #processResponseWriter()}
	 * @throws IOException
	 */
	protected void setupResponseWriter() throws IOException {
		writer = (MockResponseWriter) renderKit.createResponseWriter(response.getWriter(),"text/html",null);
		facesContext.setResponseWriter(writer);
		writer.startDocument();
	}

	/**
	 * Parse collected content of mock  response to Page instance, used for check rendered html.
	 * @return
	 * @throws Exception
	 */
	protected Page processResponse() throws Exception {
		setupWebClient();
		URL page = new URL( "http:"+ facesContext.getExternalContext().getRequestContextPath() + facesContext.getViewRoot().getViewId());
		try {
			char[] content = ((MockPrintWriter) response.getWriter()).content();
			webConnection.setResponse(page,String.valueOf(content),response.getStatus(), "OK", response.getContentType(), Collections.EMPTY_LIST);
		} catch (IllegalStateException e) {
			byte[] content = ((MockServletOutputStream)response.getOutputStream()).content();
			webConnection.setResponse(page,content, response.getStatus(), "OK", response.getContentType(), Collections.EMPTY_LIST);
		}
		return webClient.getPage(page);
	}

	protected AjaxContext ajaxContext = null;

	/**
	 * Initialised instance of VCP render kit.
	 */
	protected ChameleonRenderKitImpl vcpRenderKit = null;

	/**
	 * Instance of Mock {@link javax.faces.context.ResponseWriter} , created by setupResponseWriter 
	 */
	protected MockResponseWriter writer;

	/**
	 * <p>The htmlunit web client for this test case. </p>
	 * 
	 */
	protected WebClient webClient = null;

	/**
	 * Mock web connection for accept stored content of JSF encoding. For testing JavaScript code, all URL's for scripts must be
	 * rregistered by {@link MockWebConnection#setResponse(java.net.URL, byte[], int, java.lang.String, java.lang.String, java.util.List)} method
	 * By default, for unregistered pages return 404 - not found.
	 */
	protected MockWebConnection webConnection = null;


	// Thread context class loader saved and restored after each test
	private ClassLoader threadContextClassLoader = null;

	protected WebXml webXml;

}
