package hirondelle.fish.test.doubles;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.Principal;
import java.util.*;
import java.text.*;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletInputStream;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import hirondelle.web4j.model.ModelUtil;
import hirondelle.web4j.util.Args;
import hirondelle.web4j.util.Util;
import static hirondelle.web4j.util.Consts.EMPTY_STRING;

/**
 Fake implementation of {@link HttpServletRequest}, used 
 only for testing outside of the regular runtime environment.
 
 <P>Various methods have been added to make testing more convenient.
 
 <P>The {@link #logInAuthenticatedUser(String, List)} method allows you 
 to mimic an authenticated user.
*/
public final class FakeRequest implements HttpServletRequest {

  /** Used by factory methods to distinguish between <tt>GET</tt> and <tt>POST</tt> requests.*/
  public enum HttpMethod {GET, POST}

  /** Factory method for <tt>GET</tt> requests . */
  public static FakeRequest forGET(String aServletMatchingPath, String aQueryString){
    return new FakeRequest (aServletMatchingPath, EMPTY_STRING, aQueryString, HttpMethod.GET);
  }
  
  /** Factory method for <tt>POST</tt> requests . */
  public static FakeRequest forPOST(String aServletMatchingPath, String aQueryString){
    return new FakeRequest (aServletMatchingPath, EMPTY_STRING, aQueryString, HttpMethod.POST);
  }
  
  /** Full constructor. */
  public FakeRequest(
    String aScheme, String aServerName, Integer aServerPort, String aContextPath, 
    String aServletMatchingPath, String aExtraPath, String aQueryString, HttpMethod aMethod
  ){
    fScheme = aScheme;
    fServerName = aServerName;
    fServerPort = aServerPort;
    fContextPath = ensureNonNullStartsWithSlash(aContextPath);
    fServletMatchingPath = ensureNonNullStartsWithSlash(aServletMatchingPath);
    fExtraPath = ensureNonNullStartsWithSlash(aExtraPath);
    extractParamsFrom(aQueryString);
    fMethod = aMethod;
  }
  
  /*
   Methods added for testing. 
  */
  
  /** Method added for testing. */ 
  public void setContentType(String aContentType){ fContentType = aContentType; }
  /** Method added for testing. */ 
  public void setContentLength(int aContentLength){  fContentLength = aContentLength;  }
  /** Method added for testing. */ 
  public void setProtocol(String aProtocol){  fProtocol = aProtocol;  }
  /** Method added for testing. */ 
  public void setScheme(String aScheme){ fScheme = aScheme;  }
  /** Method added for testing. */ 
  public void setServerName(String aServerName){ fServerName = aServerName;  }
  /** Method added for testing. */ 
  public void setServerPort(int aServerPort){ fServerPort = aServerPort;  }
  /** Method added for testing. */ 
  public void setRemoteAddr(String aRemoteAddr){ fRemoteAddr = aRemoteAddr;  }
  /** Method added for testing. */ 
  public void setRemoteHost(String aRemoteHost){ fRemoteHost = aRemoteHost;  }
  /** Method added for testing. */ 
  public void setIsSecure(boolean aIsSecure){ fIsSecure = aIsSecure;  }
  /** Method added for testing. */ 
  public void setRemotePort(int aRemotePort){ fRemotePort = aRemotePort;  }
  /** Method added for testing. */ 
  public void setLocale(Locale aLocale){ fLocale = aLocale; }

  /** Method added for testing. */ 
  public void addCookie(String aName, String aValue){
    Args.checkForContent(aName);
    fCookies.put(aName, aValue);
  }

  /** Method added for testing. */ 
  public void addParameter(String aName, String aValue){
    Args.checkForContent(aName);
    List<String> existingValues = fParams.get(aName);
    if ( existingValues != null ) {
      existingValues.add(aValue);
    }
    else {
      List<String> newValues = new ArrayList<String>();
      newValues.add(aValue);
      fParams.put(aName, newValues);
    }
  }
  
  /** 
   Date/time headers must use RFC 1123 format. 
    Method added for testing. 
  */
  public void addHeader(String aName, String aValue){
    Args.checkForContent(aName);
    if( fHeaders.containsKey(aName) ){
      List<String> values = fHeaders.get(aName);
      values.add(aValue);
    }
    else {
      List<String> values = new ArrayList<String>();
      values.add(aValue);
      fHeaders.put(aName, values);
    }
  }
  
  /** Method added for testing. */ 
  public void logInAuthenticatedUser(String aUserName, List<String> aRoles){
    Args.checkForContent(aUserName);
    fUserName = aUserName;
    fUserRoles.addAll(aRoles);
  }
  
  /** Method added for testing. */ 
  public void setRequestedSessionId(String aSessionId){  fRequestedSessionId = aSessionId;  }
  
  /*
   ServletRequest methods. 
  */ 
  
  public Object getAttribute(String aName) {
    return fAttrs.get(aName);
  }

  public Enumeration getAttributeNames() {
    return Collections.enumeration(fAttrs.keySet());
  }

  public void setAttribute(String aName, Object aObject) {
    fAttrs.put(aName, aObject);
  }

  public void removeAttribute(String aName) {
    fAttrs.remove(aName);
  }

  /** Default value 'UTF-8'. */
  public String getCharacterEncoding() {  
    return fCharEncoding;  
  }

  public void setCharacterEncoding(String aEncoding) throws UnsupportedEncodingException {
    fCharEncoding = aEncoding;
  }

  /** Default value 0.  */
  public int getContentLength() {
    return fContentLength;
  }

  /** Default value 'text/html'.  */
  public String getContentType() {
    return fContentType;
  }

  public String getParameter(String aName) {
    Args.checkForContent(aName);
    String result = null;
    List<String> existingValues = fParams.get(aName);
    if ( existingValues != null ) {
      result = existingValues.get(FIRST);
    }
    return result;
  }

  public Enumeration getParameterNames() {
    return Collections.enumeration(fParams.keySet());
  }

  public String[] getParameterValues(String aName) {
    Args.checkForContent(aName);
    String[] result = null;
    List<String> existingValues = fParams.get(aName);
    if ( existingValues != null ) {
      result = existingValues.toArray(new String[0]);
    }
    return result;
  }

  public Map getParameterMap() {
    Map<String, String[]> result = new LinkedHashMap<String, String[]>();
    for(String name: fParams.keySet()) {
      String[] values = fParams.get(name).toArray(new String[0]);
      result.put(name, values);
    }
    return result;
  }

  public String getProtocol() {
    return fProtocol;
  }

  /** Default value 'http'.  */
  public String getScheme() {
    return fScheme;
  }

  /** Default value 'Test Double'.  */
  public String getServerName() {  return fServerName; }
  /** Default value 8080.  */
  public int getServerPort() {  return fServerPort;  }

  /** Default value '127.0.0.1'.  */
  public String getLocalAddr() { return fLocalAddr; }
  /** Default value '127.0.0.1'.  */
  public String getLocalName() {  return fLocalName; }
  /** Default value 8080.  */
  public int getLocalPort() {  return fLocalPort; }
  
  /** Default value '127.0.0.1'.  */
  public String getRemoteAddr() {  return fRemoteAddr; }
  /** Default value '127.0.0.1'.  */
  public String getRemoteHost() {  return fRemoteHost; }
  /** Default value '80'.  */
  public int getRemotePort() { return fRemotePort;  }


  /** Default value <tt>Locale.ENGLISH</tt>. */
  public Locale getLocale() {
    return fLocale;
  }
  /** Returns only one Locale. */
  public Enumeration getLocales() {
    Collection<Locale> locales = new ArrayList<Locale>();
    locales.add(fLocale);
    return Collections.enumeration(locales);
  }

  public boolean isSecure() { return fIsSecure; }

  /** Returns <tt>null</tt> - not implemented. */
  public RequestDispatcher getRequestDispatcher(String aPath) { return null; }
  /** Returns <tt>null</tt> - deprecated. */
  public String getRealPath(String aPath) { return null; }
  /** Returns <tt>null</tt> - not implemented. */
  public ServletInputStream getInputStream() throws IOException {  return null;  }
  /** Returns <tt>null</tt> - not implemented. */
  public BufferedReader getReader() throws IOException {  return null; }

  /*
   HttpServletRequest methods. 
  */

  /** Returns <tt>null</tt> - not authenticated. */
  public String getAuthType() { return null; }

  public Cookie[] getCookies() {
    List<Cookie> cookies = new ArrayList<Cookie>();
    if ( ! fCookies.isEmpty() ) {
      for(String name: fCookies.keySet()){
        String value = fCookies.get(name);
        Cookie cookie = new Cookie(name, value);
        cookies.add(cookie);
      }
    }
    return cookies.isEmpty() ? null : cookies.toArray(new Cookie[0]);
  }

  public long getDateHeader(String aName) {
    Args.checkForContent(aName);
    long result = -1;
    String value = getHeader(aName);
    if(value != null) {
      SimpleDateFormat format = new SimpleDateFormat(PATTERN_RFC1123);
      try {
        Date date = format.parse(value);
        result = date.getTime();
      }
      catch (ParseException ex){
        throw new IllegalArgumentException("Cannot parse date/time header value using RFC 1123: " + Util.quote(value));
      }
    }
    return result;
  }

  public String getHeader(String aName) {
    Args.checkForContent(aName);
    String result = null;
    if( fHeaders.containsKey(aName) ) {
      result = fHeaders.get(aName).get(FIRST); 
    }
    return result;
  }

  public Enumeration getHeaders(String aName) {
    Args.checkForContent(aName);
    Collection<String> result = new ArrayList<String>();
    List<String> values = fHeaders.get(aName);
    if( values != null ) {
      result.addAll(values);
    }
    return Collections.enumeration(result);
  }

  public Enumeration getHeaderNames() {
    Collection<String> result = new ArrayList<String>();
    for(String name : fHeaders.keySet() ){
      result.add(name);
    }
    return Collections.enumeration(result);
  }

  public int getIntHeader(String aName) {
    int result = -1;
    String value = getHeader(aName);
    if(value != null) {
      result = Integer.valueOf(value);
    }
    return result;
  }

  public String getMethod() {
    return fMethod.toString();
  }

  public String getPathInfo() {
    return fExtraPath;
  }

  /** Returns <tt>null</tt> - not implemented. */
  public String getPathTranslated() {  return null; }

  public String getContextPath() {
    return fContextPath;
  }

  /**
   Created from the given request parameters.
   Includes the initial '?'. Returns <tt>null</tt> if no parameters present.
   <i>This implementation is artificial but convenient, since it makes no distinction 
   between GET and POST</i>.
  */
  public String getQueryString() {
    StringBuilder result = new StringBuilder("");
    boolean hasAddedFirstParam = false;
    for (String name : fParams.keySet()){
      if ( ! hasAddedFirstParam ) {
        result.append("?");
        hasAddedFirstParam = true;
      }
      else {
        result.append("&");
      }
      result.append(name + "=" + fParams.get(name));
    }
    return hasAddedFirstParam ? result.toString() : null;
  }

  public String getRemoteUser() {
    return fUserName;
  }

  public boolean isUserInRole(String aRole) {
    return fUserRoles.contains(aRole);
  }

  public Principal getUserPrincipal() {
    return new FakePrincipal(fUserName);
  }

  public String getRequestURI() {
    return fContextPath + fServletMatchingPath + fExtraPath;
  }

  public StringBuffer getRequestURL() {
    String result = fScheme + "://" + fServerName;
    if ( fServerPort != null ) {
      result = result + ":" + fServerPort.toString();
    }
    result = result + fContextPath + fServletMatchingPath + fExtraPath + getQueryString();
    return new StringBuffer(result);
  }

  public String getServletPath() {
    return fServletMatchingPath;
  }

  public String getRequestedSessionId() {
    return fRequestedSessionId; 
  }

  public boolean isRequestedSessionIdValid() {
    return Util.textHasContent(fRequestedSessionId) && fRequestedSessionId.equals(getSession(false).getId());
  }

  /** Hard-code to <tt>true</tt>. */
  public boolean isRequestedSessionIdFromCookie() {
    return true;
  }

  /** Hard-code to <tt>false</tt>. */
  public boolean isRequestedSessionIdFromURL() {
    return false;
  }

  /** Hard-code to <tt>false</tt>. */
  public boolean isRequestedSessionIdFromUrl() {
    return false;
  }

  public HttpSession getSession(boolean aCreateNew) {
    if( fSession == null ) {
      fSession = FakeSession.joinOrCreate(fRequestedSessionId, aCreateNew);
    }
    return fSession;
  }

  public HttpSession getSession() {
    if( fSession == null ) {
      fSession = FakeSession.joinOrCreate(fRequestedSessionId, true);
    }
    return fSession;
  }

  // PRIVATE //
  private Map<String, List<String>> fParams = new LinkedHashMap<String, List<String>>(); 
  private static final int FIRST = 0;
  private Map<String, Object> fAttrs = new LinkedHashMap<String, Object>();
  
  //Items for ServletRequest
  private String fCharEncoding = "UTF-8";
  private String fContentType = "text/html";
  private int fContentLength;
  private String fProtocol = "HTTP/1.1";
  private String fScheme = "http";
  
  private String fServerName = "Test Double";
  private Integer fServerPort = 8080;
  
  private String fLocalName = "Test Double";
  private int fLocalPort = 8080;
  private String fLocalAddr = "127.0.0.1";
  
  private String fRemoteAddr = "127.0.0.1";
  private String fRemoteHost = "127.0.0.1";
  private int fRemotePort = 80;
  
  private Locale fLocale = Locale.ENGLISH;
  
  private boolean fIsSecure = false;

  //Items for HttpServletRequest
  private String fRequestedSessionId = EMPTY_STRING;
  private Map<String, String> fCookies = new LinkedHashMap<String, String>();
  private Map<String, List<String>> fHeaders = new LinkedHashMap<String, List<String>>();
  private static final String PATTERN_RFC1123 =  "EEE, dd MMM yyyy HH:mm:ss zzz";
  private static final String SLASH = "/";
  private HttpMethod fMethod;
  private String fContextPath = EMPTY_STRING;
  private String fServletMatchingPath = EMPTY_STRING;
  private String fExtraPath = EMPTY_STRING;
  private String fUserName = EMPTY_STRING;
  private List<String> fUserRoles = new ArrayList<String>();
  private HttpSession fSession;

  private FakeRequest(String aServletMatchingPath, String aExtraPath, String aQueryString, HttpMethod aMethod){
    fServletMatchingPath = ensureNonNullStartsWithSlash(aServletMatchingPath);
    fExtraPath = ensureNonNullStartsWithSlash(aExtraPath);
    extractParamsFrom(aQueryString);
    fMethod = aMethod;
  }
  
  private String ensureNonNullStartsWithSlash(String aText){
    String result = aText;
    if ( Util.textHasContent(aText) ) {
      if (! aText.startsWith(SLASH)){
        throw new IllegalArgumentException("Does not start with a '/' character: " + Util.quote(aText));
      }
    }
    else {
      result = EMPTY_STRING;
    }
    return result;
  }
  
  /**
   @param aQueryString 'blah=yes', 'blah=', 'blah=yes&Id=123', 'blah=yes&Id='. 
  */
  private void extractParamsFrom(String aQueryString){
    if( Util.textHasContent(aQueryString)) {
      StringTokenizer firstParse = new StringTokenizer(aQueryString, "&");
      while ( firstParse.hasMoreElements() ) {
        String eachNameValuePair = firstParse.nextToken();
        StringTokenizer secondParse = new StringTokenizer(eachNameValuePair, "=");
        //sometimes the value is missing. in that case, coerce the value into an empty string
        List<String> items = new ArrayList<String>();
        while ( secondParse.hasMoreTokens() ) {
          items.add(secondParse.nextToken());
        }
        String name = items.get(0); //assume name always present
        String value = EMPTY_STRING; //value may not be present
        if( items.size() > 1 ) { 
          value = items.get(1); //value is present
        }
        addParameter(name, value);
      }
    }
  }
  
  private static class FakePrincipal implements Principal {
    FakePrincipal(String aName){
      fName = aName;
    }
    public String getName() {
      return fName;
    }
    @Override public String toString(){
      return fName;
    }
    @Override public boolean equals(Object aThat){
      Boolean result = ModelUtil.quickEquals(this, aThat);
      if ( result == null ){
        FakePrincipal that = (FakePrincipal) aThat;
        result = ModelUtil.equalsFor(this.getSignificantFields(), that.getSignificantFields());
      }
      return result;    
    }
    @Override public int hashCode() {
      return ModelUtil.hashCodeFor(getSignificantFields());
    }
    private String fName;
    private Object[] getSignificantFields() { return new Object[] {fName}; }
  }
}