/*
 * Copyright 2004-2008 the original author or authors.
 *
 * Licensed 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.springframework.webflow.test;

import java.io.StringWriter;
import java.io.Writer;
import java.security.Principal;
import java.util.HashMap;
import java.util.Locale;

import org.springframework.binding.collection.SharedMapDecorator;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.webflow.context.ExternalContext;
import org.springframework.webflow.core.collection.LocalAttributeMap;
import org.springframework.webflow.core.collection.LocalSharedAttributeMap;
import org.springframework.webflow.core.collection.MutableAttributeMap;
import org.springframework.webflow.core.collection.ParameterMap;
import org.springframework.webflow.core.collection.SharedAttributeMap;

/**
 * Mock implementation of the {@link ExternalContext} interface.
 * @see ExternalContext
 * @author Keith Donald
 */
public class MockExternalContext implements ExternalContext {

	private String contextPath;

	private ParameterMap requestParameterMap = new MockParameterMap();

	private MutableAttributeMap requestMap = new LocalAttributeMap();

	private SharedAttributeMap sessionMap = new LocalSharedAttributeMap(new SharedMapDecorator(new HashMap()));

	private SharedAttributeMap globalSessionMap = sessionMap;

	private SharedAttributeMap applicationMap = new LocalSharedAttributeMap(new SharedMapDecorator(new HashMap()));

	private Object nativeContext = new Object();

	private Object nativeRequest = new Object();

	private Object nativeResponse = new Object();

	private Principal currentUser;

	private Locale locale;

	private StringWriter responseWriter = new StringWriter();

	private boolean ajaxRequest;

	private Boolean responseAllowed;

	private boolean responseComplete;

	private boolean flowExecutionRedirectRequested;

	private String flowDefinitionRedirectFlowId;

	private MutableAttributeMap flowDefinitionRedirectFlowInput;

	private String externalRedirectUrl;

	private boolean redirectInPopup;

	/**
	 * Creates a mock external context with an empty request parameter map. Allows for bean style usage.
	 */
	public MockExternalContext() {
	}

	/**
	 * Creates a mock external context with the specified parameters in the request parameter map. All other properties
	 * of the external context can be set using the appropriate setter.
	 * @param requestParameterMap the request parameters
	 */
	public MockExternalContext(ParameterMap requestParameterMap) {
		if (requestParameterMap != null) {
			this.requestParameterMap = requestParameterMap;
		}
	}

	// implementing external context

	public String getContextPath() {
		return contextPath;
	}

	public ParameterMap getRequestParameterMap() {
		return requestParameterMap;
	}

	public MutableAttributeMap getRequestMap() {
		return requestMap;
	}

	public SharedAttributeMap getSessionMap() {
		return sessionMap;
	}

	public SharedAttributeMap getGlobalSessionMap() {
		return globalSessionMap;
	}

	public SharedAttributeMap getApplicationMap() {
		return applicationMap;
	}

	public Principal getCurrentUser() {
		return currentUser;
	}

	public Locale getLocale() {
		return locale;
	}

	public Object getNativeContext() {
		return nativeContext;
	}

	public Object getNativeRequest() {
		return nativeRequest;
	}

	public Object getNativeResponse() {
		return nativeResponse;
	}

	public boolean isAjaxRequest() {
		return ajaxRequest;
	}

	public String getFlowExecutionUrl(String flowId, String flowExecutionKey) {
		return "/" + flowId + "?execution=" + flowExecutionKey;
	}

	public Writer getResponseWriter() {
		assertResponseAllowed();
		return responseWriter;
	}

	public boolean isResponseAllowed() {
		if (responseAllowed != null) {
			return responseAllowed.booleanValue();
		} else {
			return !responseComplete;
		}
	}

	public boolean isResponseComplete() {
		return responseComplete;
	}

	public void recordResponseComplete() {
		responseComplete = true;
	}

	public boolean isResponseCompleteFlowExecutionRedirect() {
		return flowExecutionRedirectRequested;
	}

	public void requestFlowExecutionRedirect() throws IllegalStateException {
		flowExecutionRedirectRequested = true;
		recordResponseComplete();
	}

	public void requestFlowDefinitionRedirect(String flowId, MutableAttributeMap input) throws IllegalStateException {
		flowDefinitionRedirectFlowId = flowId;
		flowDefinitionRedirectFlowInput = input;
		recordResponseComplete();
	}

	public void requestExternalRedirect(String uri) throws IllegalStateException {
		externalRedirectUrl = uri;
		recordResponseComplete();
	}

	public void requestRedirectInPopup() throws IllegalStateException {
		if (isRedirectRequested()) {
			redirectInPopup = true;
		} else {
			throw new IllegalStateException(
					"Only call requestRedirectInPopup after a redirect has been requested by calling requestFlowExecutionRedirect, requestFlowDefinitionRedirect, or requestExternalRedirect");
		}
	}

	/**
	 * Set the context path of the application.
	 * @param contextPath the context path
	 */
	public void setContextPath(String contextPath) {
		this.contextPath = contextPath;
	}

	/**
	 * Set the request parameter map.
	 * @see ExternalContext#getRequestParameterMap()
	 */
	public void setRequestParameterMap(ParameterMap requestParameterMap) {
		this.requestParameterMap = requestParameterMap;
	}

	/**
	 * Set the request attribute map.
	 * @see ExternalContext#getRequestMap()
	 */
	public void setRequestMap(MutableAttributeMap requestMap) {
		this.requestMap = requestMap;
	}

	/**
	 * Set the session attribute map.
	 * @see ExternalContext#getSessionMap()
	 */
	public void setSessionMap(SharedAttributeMap sessionMap) {
		this.sessionMap = sessionMap;
	}

	/**
	 * Set the global session attribute map. By default the session attribute map and the global session attribute map
	 * are one and the same.
	 * @see ExternalContext#getGlobalSessionMap()
	 */
	public void setGlobalSessionMap(SharedAttributeMap globalSessionMap) {
		this.globalSessionMap = globalSessionMap;
	}

	/**
	 * Set the application attribute map.
	 * @see ExternalContext#getApplicationMap()
	 */
	public void setApplicationMap(SharedAttributeMap applicationMap) {
		this.applicationMap = applicationMap;
	}

	/**
	 * Set the native context object.
	 * @param nativeContext the native context
	 */
	public void setNativeContext(Object nativeContext) {
		this.nativeContext = nativeContext;
	}

	/**
	 * Set the native request object.
	 * @param nativeRequest the native request object
	 */
	public void setNativeRequest(Object nativeRequest) {
		this.nativeRequest = nativeRequest;
	}

	/**
	 * Set the native response object.
	 * @param nativeResponse the native response object
	 */
	public void setNativeResponse(Object nativeResponse) {
		this.nativeResponse = nativeResponse;
	}

	/**
	 * Sets the current user principal.
	 * @param currentUser the current user
	 */
	public void setCurrentUser(Principal currentUser) {
		this.currentUser = currentUser;
	}

	/**
	 * Convenience method that sets the current user principal as a string.
	 * @param currentUser the current user name
	 */
	public void setCurrentUser(String currentUser) {
		this.currentUser = new MockPrincipal(currentUser);
	}

	/**
	 * Sets the client locale.
	 * @param locale the locale
	 */
	public void setLocale(Locale locale) {
		this.locale = locale;
	}

	// convenience helpers

	/**
	 * Returns the request parameter map as a {@link MockParameterMap} for convenient access in a unit test.
	 * @see #getRequestParameterMap()
	 */
	public MockParameterMap getMockRequestParameterMap() {
		return (MockParameterMap) requestParameterMap;
	}

	/**
	 * Puts a request parameter into the mock parameter map.
	 * @param parameterName the parameter name
	 * @param parameterValue the parameter value
	 */
	public void putRequestParameter(String parameterName, String parameterValue) {
		getMockRequestParameterMap().put(parameterName, parameterValue);
	}

	/**
	 * Puts a multi-valued request parameter into the mock parameter map.
	 * @param parameterName the parameter name
	 * @param parameterValues the parameter values
	 */
	public void putRequestParameter(String parameterName, String[] parameterValues) {
		getMockRequestParameterMap().put(parameterName, parameterValues);
	}

	/**
	 * Puts a MultipartFile request parameter into the mock parameter map.
	 * @param parameterName the parameter name
	 * @param parameterValue the parameter value
	 */
	public void putRequestParameter(String parameterName, MultipartFile parameterValue) {
		getMockRequestParameterMap().put(parameterName, parameterValue);
	}

	/**
	 * Sets the id of the event that should be signaled by this context. For use when resuming a flow. This method
	 * depends on a MockViewFactory being configured for parsing the event id on a resume operation.
	 * @param eventId the id of the event to signal
	 */
	public void setEventId(String eventId) {
		putRequestParameter("_eventId", eventId);
	}

	/**
	 * Set whether this request is an ajax request.
	 * @param ajaxRequest true or false
	 */
	public void setAjaxRequest(boolean ajaxRequest) {
		this.ajaxRequest = ajaxRequest;
	}

	/**
	 * Set the response allows flag to a value for testing.
	 * @param responseAllowed true or false
	 */
	public void setResponseAllowed(boolean responseAllowed) {
		this.responseAllowed = Boolean.valueOf(responseAllowed);
	}

	/**
	 * Returns the implementation of this mock context's response writer.
	 * @return the underlying string writer to use for asserting a specific response was written
	 */
	public StringWriter getMockResponseWriter() {
		return responseWriter;
	}

	/**
	 * Returns the flag indicating if a flow execution redirect response has been requested by the flow.
	 */
	public boolean getFlowExecutionRedirectRequested() {
		return flowExecutionRedirectRequested;
	}

	/**
	 * Returns the flag indicating if a flow definition redirect response has been requested by the flow.
	 */
	public boolean getFlowDefinitionRedirectRequested() {
		return flowDefinitionRedirectFlowId != null;
	}

	/**
	 * Returns the id of the flow definition to redirect to. Only set when {@link #getFlowDefinitionRedirectRequested()}
	 * returns true.
	 */
	public String getFlowRedirectFlowId() {
		return flowDefinitionRedirectFlowId;
	}

	/**
	 * Returns the input to pass the flow definition through the redirect. Only set when
	 * {@link #getFlowDefinitionRedirectRequested()} returns true.
	 */
	public MutableAttributeMap getFlowRedirectFlowInput() {
		return flowDefinitionRedirectFlowInput;
	}

	/**
	 * Returns the flag indicating if an external redirect response has been requested by the flow.
	 */
	public boolean getExternalRedirectRequested() {
		return externalRedirectUrl != null;
	}

	/**
	 * Returns the URL to redirect to. Only set if {@link #getExternalRedirectRequested()} returns true.
	 */
	public String getExternalRedirectUrl() {
		return externalRedirectUrl;
	}

	/**
	 * If a redirect response has been requested, indicates if the redirect should be issued from a popup dialog.
	 */
	public boolean getRedirectInPopup() {
		return redirectInPopup;
	}

	// internal helpers

	private void assertResponseAllowed() throws IllegalStateException {
		if (!isResponseAllowed()) {
			if (getFlowExecutionRedirectRequested()) {
				throw new IllegalStateException(
						"A response is not allowed because a redirect has already been requested on this ExternalContext");
			}
			if (getFlowDefinitionRedirectRequested()) {
				throw new IllegalStateException(
						"A response is not allowed because a flowRedirect has already been requested on this ExternalContext");
			}
			if (getExternalRedirectRequested()) {
				throw new IllegalStateException(
						"A response is not allowed because an externalRedirect has already been requested on this ExternalContext");
			}
			if (responseComplete) {
				throw new IllegalStateException(
						"A response is not allowed because one has already been completed on this ExternalContext");
			} else {
				throw new IllegalStateException("A response is not allowed");
			}
		}
	}

	public boolean isRedirectRequested() {
		return getFlowExecutionRedirectRequested() || getFlowDefinitionRedirectRequested()
				|| getExternalRedirectRequested();
	}

	private class MockPrincipal implements Principal {
		private String name;

		private MockPrincipal(String name) {
			this.name = name;
		}

		public String getName() {
			return name;
		}

	}

}