/*
 * Copyright (C) 2011 Google Inc.
 *
 * 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 okhttp3.mockwebserver;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import okhttp3.Headers;
import okhttp3.WebSocketListener;
import okhttp3.internal.Internal;
import okhttp3.internal.http2.Settings;
import okio.Buffer;

/** A scripted response to be replayed by the mock web server. */
public final class MockResponse implements Cloneable {
  private static final String CHUNKED_BODY_HEADER = "Transfer-encoding: chunked";

  private String status;
  private Headers.Builder headers = new Headers.Builder();

  private Buffer body;

  private long throttleBytesPerPeriod = Long.MAX_VALUE;
  private long throttlePeriodAmount = 1;
  private TimeUnit throttlePeriodUnit = TimeUnit.SECONDS;

  private SocketPolicy socketPolicy = SocketPolicy.KEEP_OPEN;
  private int http2ErrorCode = -1;

  private long bodyDelayAmount = 0;
  private TimeUnit bodyDelayUnit = TimeUnit.MILLISECONDS;

  private long headersDelayAmount = 0;
  private TimeUnit headersDelayUnit = TimeUnit.MILLISECONDS;

  private List<PushPromise> promises = new ArrayList<>();
  private Settings settings;
  private WebSocketListener webSocketListener;

  /** Creates a new mock response with an empty body. */
  public MockResponse() {
    setResponseCode(200);
    setHeader("Content-Length", 0);
  }

  @Override public MockResponse clone() {
    try {
      MockResponse result = (MockResponse) super.clone();
      result.headers = headers.build().newBuilder();
      result.promises = new ArrayList<>(promises);
      return result;
    } catch (CloneNotSupportedException e) {
      throw new AssertionError();
    }
  }

  /** Returns the HTTP response line, such as "HTTP/1.1 200 OK". */
  public String getStatus() {
    return status;
  }

  public MockResponse setResponseCode(int code) {
    String reason = "Mock Response";
    if (code >= 100 && code < 200) {
      reason = "Informational";
    } else if (code >= 200 && code < 300) {
      reason = "OK";
    } else if (code >= 300 && code < 400) {
      reason = "Redirection";
    } else if (code >= 400 && code < 500) {
      reason = "Client Error";
    } else if (code >= 500 && code < 600) {
      reason = "Server Error";
    }
    return setStatus("HTTP/1.1 " + code + " " + reason);
  }

  public MockResponse setStatus(String status) {
    this.status = status;
    return this;
  }

  /** Returns the HTTP headers, such as "Content-Length: 0". */
  public Headers getHeaders() {
    return headers.build();
  }

  /**
   * Removes all HTTP headers including any "Content-Length" and "Transfer-encoding" headers that
   * were added by default.
   */
  public MockResponse clearHeaders() {
    headers = new Headers.Builder();
    return this;
  }

  /**
   * Adds {@code header} as an HTTP header. For well-formed HTTP {@code header} should contain a
   * name followed by a colon and a value.
   */
  public MockResponse addHeader(String header) {
    headers.add(header);
    return this;
  }

  /**
   * Adds a new header with the name and value. This may be used to add multiple headers with the
   * same name.
   */
  public MockResponse addHeader(String name, Object value) {
    headers.add(name, String.valueOf(value));
    return this;
  }

  /**
   * Adds a new header with the name and value. This may be used to add multiple headers with the
   * same name. Unlike {@link #addHeader(String, Object)} this does not validate the name and
   * value.
   */
  public MockResponse addHeaderLenient(String name, Object value) {
    Internal.instance.addLenient(headers, name, String.valueOf(value));
    return this;
  }

  /**
   * Removes all headers named {@code name}, then adds a new header with the name and value.
   */
  public MockResponse setHeader(String name, Object value) {
    removeHeader(name);
    return addHeader(name, value);
  }

  /** Replaces all headers with those specified in {@code headers}. */
  public MockResponse setHeaders(Headers headers) {
    this.headers = headers.newBuilder();
    return this;
  }

  /** Removes all headers named {@code name}. */
  public MockResponse removeHeader(String name) {
    headers.removeAll(name);
    return this;
  }

  /** Returns a copy of the raw HTTP payload. */
  public Buffer getBody() {
    return body != null ? body.clone() : null;
  }

  public MockResponse setBody(Buffer body) {
    setHeader("Content-Length", body.size());
    this.body = body.clone(); // Defensive copy.
    return this;
  }

  /** Sets the response body to the UTF-8 encoded bytes of {@code body}. */
  public MockResponse setBody(String body) {
    return setBody(new Buffer().writeUtf8(body));
  }

  /**
   * Sets the response body to {@code body}, chunked every {@code maxChunkSize} bytes.
   */
  public MockResponse setChunkedBody(Buffer body, int maxChunkSize) {
    removeHeader("Content-Length");
    headers.add(CHUNKED_BODY_HEADER);

    Buffer bytesOut = new Buffer();
    while (!body.exhausted()) {
      long chunkSize = Math.min(body.size(), maxChunkSize);
      bytesOut.writeHexadecimalUnsignedLong(chunkSize);
      bytesOut.writeUtf8("\r\n");
      bytesOut.write(body, chunkSize);
      bytesOut.writeUtf8("\r\n");
    }
    bytesOut.writeUtf8("0\r\n\r\n"); // Last chunk + empty trailer + CRLF.

    this.body = bytesOut;
    return this;
  }

  /**
   * Sets the response body to the UTF-8 encoded bytes of {@code body}, chunked every {@code
   * maxChunkSize} bytes.
   */
  public MockResponse setChunkedBody(String body, int maxChunkSize) {
    return setChunkedBody(new Buffer().writeUtf8(body), maxChunkSize);
  }

  public SocketPolicy getSocketPolicy() {
    return socketPolicy;
  }

  public MockResponse setSocketPolicy(SocketPolicy socketPolicy) {
    this.socketPolicy = socketPolicy;
    return this;
  }

  public int getHttp2ErrorCode() {
    return http2ErrorCode;
  }

  /**
   * Sets the <a href="https://tools.ietf.org/html/rfc7540#section-7">HTTP/2 error code</a> to be
   * returned when resetting the stream. This is only valid with {@link
   * SocketPolicy#RESET_STREAM_AT_START}.
   */
  public MockResponse setHttp2ErrorCode(int http2ErrorCode) {
    this.http2ErrorCode = http2ErrorCode;
    return this;
  }

  /**
   * Throttles the request reader and response writer to sleep for the given period after each
   * series of {@code bytesPerPeriod} bytes are transferred. Use this to simulate network behavior.
   */
  public MockResponse throttleBody(long bytesPerPeriod, long period, TimeUnit unit) {
    this.throttleBytesPerPeriod = bytesPerPeriod;
    this.throttlePeriodAmount = period;
    this.throttlePeriodUnit = unit;
    return this;
  }

  public long getThrottleBytesPerPeriod() {
    return throttleBytesPerPeriod;
  }

  public long getThrottlePeriod(TimeUnit unit) {
    return unit.convert(throttlePeriodAmount, throttlePeriodUnit);
  }

  /**
   * Set the delayed time of the response body to {@code delay}. This applies to the response body
   * only; response headers are not affected.
   */
  public MockResponse setBodyDelay(long delay, TimeUnit unit) {
    bodyDelayAmount = delay;
    bodyDelayUnit = unit;
    return this;
  }

  public long getBodyDelay(TimeUnit unit) {
    return unit.convert(bodyDelayAmount, bodyDelayUnit);
  }

  public MockResponse setHeadersDelay(long delay, TimeUnit unit) {
    headersDelayAmount = delay;
    headersDelayUnit = unit;
    return this;
  }

  public long getHeadersDelay(TimeUnit unit) {
    return unit.convert(headersDelayAmount, headersDelayUnit);
  }

  /**
   * When {@link MockWebServer#setProtocols(java.util.List) protocols} include {@linkplain
   * okhttp3.Protocol#HTTP_2}, this attaches a pushed stream to this response.
   */
  public MockResponse withPush(PushPromise promise) {
    this.promises.add(promise);
    return this;
  }

  /** Returns the streams the server will push with this response. */
  public List<PushPromise> getPushPromises() {
    return promises;
  }

  /**
   * When {@linkplain MockWebServer#setProtocols(java.util.List) protocols} include {@linkplain
   * okhttp3.Protocol#HTTP_2 HTTP/2}, this pushes {@code settings} before writing the response.
   */
  public MockResponse withSettings(Settings settings) {
    this.settings = settings;
    return this;
  }

  public Settings getSettings() {
    return settings;
  }

  /**
   * Attempts to perform a web socket upgrade on the connection. This will overwrite any previously
   * set status or body.
   */
  public MockResponse withWebSocketUpgrade(WebSocketListener listener) {
    setStatus("HTTP/1.1 101 Switching Protocols");
    setHeader("Connection", "Upgrade");
    setHeader("Upgrade", "websocket");
    body = null;
    webSocketListener = listener;
    return this;
  }

  public WebSocketListener getWebSocketListener() {
    return webSocketListener;
  }

  @Override public String toString() {
    return status;
  }
}
