// Dstl (c) Crown Copyright 2017
package uk.gov.dstl.baleen.testing.servlets;

import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.mockito.ArgumentCaptor;

import com.fasterxml.jackson.databind.ObjectMapper;

/**
 * A helper class to allow simpler testing of servlets.
 *
 * <p>Users of this class should:
 *
 * <p>1. Determine the size of the response (and then use this value in the responseBufferSize
 * constructor).
 *
 * <p>2. Add parameters
 *
 * <p>3. Call the servlet with the doGet method. This will be routined to the corresponding do
 * method on the servlet.
 *
 * <p>4. Use the getResponse* methods to examine the output.
 *
 * <p>Note that since this does not run in an HTTP server the getResponse* values will be whatever
 * was set in the servlet. If the servlet does not set a status code then no status code will be
 * available here.
 *
 * <p>This class has been implemented to the fidelity required for the current tests, and will need
 * to be enhanced over time.
 */
public class ServletCaller {

  private final HttpServletRequest request;
  private final HttpServletResponse response;
  private final Map<String, String[]> paramMap = new HashMap<>();
  private final Set<String> paramNames = new HashSet<>();
  private final BufferServletOutputStream outputStream;
  private final ObjectMapper mapper;
  private PrintWriter writer;

  /** New isntance with default response size. */
  public ServletCaller() {
    this(BufferServletOutputStream.BUFFER_SIZE);
  }

  /**
   * New instance with a specific buffer size.
   *
   * @param responseBufferSize the maximum size of response to hold (in bytes).
   */
  public ServletCaller(int responseBufferSize) {
    mapper = new ObjectMapper();

    request = mock(HttpServletRequest.class);
    response = mock(HttpServletResponse.class);

    when(request.getParameterMap()).thenReturn(paramMap);
    when(request.getParameterNames()).thenReturn(Collections.enumeration(paramNames));

    outputStream = new BufferServletOutputStream(responseBufferSize);
    writer = new PrintWriter(outputStream);
    try {
      doReturn(writer).when(response).getWriter();
      doReturn(outputStream).when(response).getOutputStream();
    } catch (IOException e) {
      // Never happens
    }
  }

  /**
   * Set the request uri.
   *
   * @param requestUri (as to be returend from request.getRequestURI())
   */
  public void setRequestUri(String requestUri) {
    doReturn(requestUri).when(request).getRequestURI();
  }

  /**
   * Set the servlet path.
   *
   * @param servletPath (as to be returend from request.getServletPath())
   */
  public void setServletPath(String servletPath) {
    doReturn(servletPath).when(request).getServletPath();
  }

  /**
   * Set the context path.
   *
   * @param contextPath (as to be returend from request.getContextPath())
   */
  public void setContextPath(String contextPath) {
    doReturn(contextPath).when(request).getContextPath();
  }

  /**
   * Add a new parameter to the request.
   *
   * @param key the parameter name
   * @param values the values of the parameter
   */
  public void addParameter(String key, String... values) {
    if (values != null && values.length > 0) {
      when(request.getParameter(key)).thenReturn(values[0]);
    }
    when(request.getParameterValues(key)).thenReturn(values);
    paramMap.put(key, values);
    paramNames.add(key);
  }

  /**
   * Get the representation of the request object.
   *
   * <p>This is not a complete implementation.
   *
   * @return
   */
  public HttpServletRequest getRequest() {
    return request;
  }

  /**
   * Get the representation of the response.
   *
   * <p>This is not a complete implementation.
   *
   * @return
   */
  public HttpServletResponse getResponse() {
    return response;
  }

  /**
   * Simulate a GET request on this servlet.
   *
   * @param servlet the servlet instance to call
   * @return the response representation (will be non-null)
   * @throws ServletException as per HttpServlet.service
   * @throws IOException as per HttpServlet.service
   */
  public HttpServletResponse doGet(HttpServlet servlet) throws Exception {
    return doMethod("GET", servlet);
  }

  /**
   * Simulate a DELETE request on this servlet.
   *
   * @param servlet the servlet instance to call
   * @return the response representation (will be non-null)
   * @throws ServletException as per HttpServlet.service
   * @throws IOException as per HttpServlet.service
   */
  public HttpServletResponse doDelete(HttpServlet servlet) throws Exception {
    return doMethod("DELETE", servlet);
  }

  /**
   * Simulate a POST request on this servlet.
   *
   * @param servlet the servlet instance to call
   * @return the response representation (will be non-null)
   * @throws ServletException as per HttpServlet.service
   * @throws IOException as per HttpServlet.service
   */
  public HttpServletResponse doPost(HttpServlet servlet) throws Exception {
    return doMethod("POST", servlet);
  }

  /**
   * Simulate a PUT request on this servlet.
   *
   * @param servlet the servlet instance to call
   * @return the response representation (will be non-null)
   * @throws ServletException as per HttpServlet.service
   * @throws IOException as per HttpServlet.service
   */
  public HttpServletResponse doPut(HttpServlet servlet) throws Exception {
    return doMethod("PUT", servlet);
  }

  private HttpServletResponse doMethod(String method, HttpServlet servlet) throws Exception {
    when(request.getMethod()).thenReturn(method);
    servlet.service(request, response);
    writer.flush();
    return response;
  }

  /**
   * The written body of the response.
   *
   * <p>Should only be called after calling doGet (or similar).
   *
   * @return the body as a UTF-8 string
   */
  public String getResponseBody() {
    return outputStream.getAsString();
  }

  /**
   * Get the response as a object by parsing JSON.
   *
   * @param clazz
   * @return object an object instance or null
   */
  public <T> T getJSONResponse(Class<T> clazz) {
    try {
      return mapper.readValue(getResponseBody(), clazz);
    } catch (Exception e) {
      return null;
    }
  }

  /**
   * The content type of the response.
   *
   * @return the content type (if set by the servlet call, or null if not)
   */
  public String getResponseType() {
    ArgumentCaptor<String> t = ArgumentCaptor.forClass(String.class);
    verify(response).setContentType(t.capture());
    return t.getValue();
  }

  /**
   * The status code of the response
   *
   * @return the status code (if set by the servlet call, or null if not)
   */
  public Integer getResponseStatus() {

    ArgumentCaptor<Integer> t = ArgumentCaptor.forClass(Integer.class);
    verify(response).setStatus(t.capture());
    return t.getValue();
  }

  /**
   * Gets the status code as set by sendError
   *
   * @return the status code.
   */
  public Integer getSentError() {
    try {
      ArgumentCaptor<Integer> c = ArgumentCaptor.forClass(Integer.class);
      ArgumentCaptor<String> m = ArgumentCaptor.forClass(String.class);
      verify(response).sendError(c.capture(), m.capture());
      return c.getValue();
    } catch (IOException e) {
      return null;
    }
  }
}
