// Copyright 2014 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <brillo/http/http_transport_fake.h>

#include <utility>

#include <base/json/json_reader.h>
#include <base/json/json_writer.h>
#include <base/logging.h>
#include <brillo/bind_lambda.h>
#include <brillo/http/http_connection_fake.h>
#include <brillo/http/http_request.h>
#include <brillo/mime_utils.h>
#include <brillo/streams/memory_stream.h>
#include <brillo/strings/string_utils.h>
#include <brillo/url_utils.h>

namespace brillo {

using http::fake::Transport;
using http::fake::ServerRequestResponseBase;
using http::fake::ServerRequest;
using http::fake::ServerResponse;

Transport::Transport() {
  VLOG(1) << "fake::Transport created";
}

Transport::~Transport() {
  VLOG(1) << "fake::Transport destroyed";
}

std::shared_ptr<http::Connection> Transport::CreateConnection(
    const std::string& url,
    const std::string& method,
    const HeaderList& headers,
    const std::string& user_agent,
    const std::string& referer,
    brillo::ErrorPtr* error) {
  std::shared_ptr<http::Connection> connection;
  if (create_connection_error_) {
    if (error)
      *error = std::move(create_connection_error_);
    return connection;
  }
  HeaderList headers_copy = headers;
  if (!user_agent.empty()) {
    headers_copy.push_back(
        std::make_pair(http::request_header::kUserAgent, user_agent));
  }
  if (!referer.empty()) {
    headers_copy.push_back(
        std::make_pair(http::request_header::kReferer, referer));
  }
  connection =
      std::make_shared<http::fake::Connection>(url, method, shared_from_this());
  CHECK(connection) << "Unable to create Connection object";
  if (!connection->SendHeaders(headers_copy, error))
    connection.reset();
  request_count_++;
  return connection;
}

void Transport::RunCallbackAsync(
    const base::Location& /* from_here */,
    const base::Closure& callback) {
  if (!async_) {
    callback.Run();
    return;
  }
  async_callback_queue_.push(callback);
}

bool Transport::HandleOneAsyncRequest() {
  if (async_callback_queue_.empty())
    return false;

  base::Closure callback = async_callback_queue_.front();
  async_callback_queue_.pop();
  callback.Run();
  return true;
}

void Transport::HandleAllAsyncRequests() {
  while (!async_callback_queue_.empty())
    HandleOneAsyncRequest();
}

http::RequestID Transport::StartAsyncTransfer(
    http::Connection* /* connection */,
    const SuccessCallback& /* success_callback */,
    const ErrorCallback& /* error_callback */) {
  // Fake transport doesn't use this method.
  LOG(FATAL) << "This method should not be called on fake transport";
  return 0;
}

bool Transport::CancelRequest(RequestID /* request_id */) {
  return false;
}

void Transport::SetDefaultTimeout(base::TimeDelta /* timeout */) {
}

static inline std::string GetHandlerMapKey(const std::string& url,
                                           const std::string& method) {
  return method + ":" + url;
}

void Transport::AddHandler(const std::string& url,
                           const std::string& method,
                           const HandlerCallback& handler) {
  // Make sure we can override/replace existing handlers.
  handlers_[GetHandlerMapKey(url, method)] = handler;
}

void Transport::AddSimpleReplyHandler(const std::string& url,
                                      const std::string& method,
                                      int status_code,
                                      const std::string& reply_text,
                                      const std::string& mime_type) {
  auto handler = [](int status_code,
                    const std::string& reply_text,
                    const std::string& mime_type,
                    const ServerRequest& /* request */,
                    ServerResponse* response) {
    response->ReplyText(status_code, reply_text, mime_type);
  };
  AddHandler(
      url, method, base::Bind(handler, status_code, reply_text, mime_type));
}

Transport::HandlerCallback Transport::GetHandler(
    const std::string& url,
    const std::string& method) const {
  // First try the exact combination of URL/Method
  auto p = handlers_.find(GetHandlerMapKey(url, method));
  if (p != handlers_.end())
    return p->second;
  // If not found, try URL/*
  p = handlers_.find(GetHandlerMapKey(url, "*"));
  if (p != handlers_.end())
    return p->second;
  // If still not found, try */method
  p = handlers_.find(GetHandlerMapKey("*", method));
  if (p != handlers_.end())
    return p->second;
  // Finally, try */*
  p = handlers_.find(GetHandlerMapKey("*", "*"));
  return (p != handlers_.end()) ? p->second : HandlerCallback();
}

void ServerRequestResponseBase::SetData(StreamPtr stream) {
  data_.clear();
  if (stream) {
    uint8_t buffer[1024];
    size_t size = 0;
    if (stream->CanGetSize())
      data_.reserve(stream->GetRemainingSize());

    do {
      CHECK(stream->ReadBlocking(buffer, sizeof(buffer), &size, nullptr));
      data_.insert(data_.end(), buffer, buffer + size);
    } while (size > 0);
  }
}

std::string ServerRequestResponseBase::GetDataAsString() const {
  if (data_.empty())
    return std::string();
  auto chars = reinterpret_cast<const char*>(data_.data());
  return std::string(chars, data_.size());
}

std::unique_ptr<base::DictionaryValue>
ServerRequestResponseBase::GetDataAsJson() const {
  std::unique_ptr<base::DictionaryValue> result;
  if (brillo::mime::RemoveParameters(
          GetHeader(request_header::kContentType)) ==
      brillo::mime::application::kJson) {
    auto value = base::JSONReader::Read(GetDataAsString());
    result = base::DictionaryValue::From(std::move(value));
  }
  return result;
}

std::string ServerRequestResponseBase::GetDataAsNormalizedJsonString() const {
  std::string value;
  // Make sure we serialize the JSON back without any pretty print so
  // the string comparison works correctly.
  auto json = GetDataAsJson();
  if (json)
    base::JSONWriter::Write(*json, &value);
  return value;
}

void ServerRequestResponseBase::AddHeaders(const HeaderList& headers) {
  for (const auto& pair : headers) {
    if (pair.second.empty())
      headers_.erase(pair.first);
    else
      headers_.insert(pair);
  }
}

std::string ServerRequestResponseBase::GetHeader(
    const std::string& header_name) const {
  auto p = headers_.find(header_name);
  return p != headers_.end() ? p->second : std::string();
}

ServerRequest::ServerRequest(const std::string& url, const std::string& method)
    : method_(method) {
  auto params = brillo::url::GetQueryStringParameters(url);
  url_ = brillo::url::RemoveQueryString(url, true);
  form_fields_.insert(params.begin(), params.end());
}

std::string ServerRequest::GetFormField(const std::string& field_name) const {
  if (!form_fields_parsed_) {
    std::string mime_type = brillo::mime::RemoveParameters(
        GetHeader(request_header::kContentType));
    if (mime_type == brillo::mime::application::kWwwFormUrlEncoded &&
        !GetData().empty()) {
      auto fields = brillo::data_encoding::WebParamsDecode(GetDataAsString());
      form_fields_.insert(fields.begin(), fields.end());
    }
    form_fields_parsed_ = true;
  }
  auto p = form_fields_.find(field_name);
  return p != form_fields_.end() ? p->second : std::string();
}

void ServerResponse::Reply(int status_code,
                           const void* data,
                           size_t data_size,
                           const std::string& mime_type) {
  data_.clear();
  status_code_ = status_code;
  SetData(MemoryStream::OpenCopyOf(data, data_size, nullptr));
  AddHeaders({{response_header::kContentLength,
               brillo::string_utils::ToString(data_size)},
              {response_header::kContentType, mime_type}});
}

void ServerResponse::ReplyText(int status_code,
                               const std::string& text,
                               const std::string& mime_type) {
  Reply(status_code, text.data(), text.size(), mime_type);
}

void ServerResponse::ReplyJson(int status_code, const base::Value* json) {
  std::string text;
  base::JSONWriter::WriteWithOptions(
      *json, base::JSONWriter::OPTIONS_PRETTY_PRINT, &text);
  std::string mime_type =
      brillo::mime::AppendParameter(brillo::mime::application::kJson,
                                      brillo::mime::parameters::kCharset,
                                      "utf-8");
  ReplyText(status_code, text, mime_type);
}

void ServerResponse::ReplyJson(int status_code,
                               const http::FormFieldList& fields) {
  base::DictionaryValue json;
  for (const auto& pair : fields) {
    json.SetString(pair.first, pair.second);
  }
  ReplyJson(status_code, &json);
}

std::string ServerResponse::GetStatusText() const {
  static std::vector<std::pair<int, const char*>> status_text_map = {
      {100, "Continue"},
      {101, "Switching Protocols"},
      {102, "Processing"},
      {200, "OK"},
      {201, "Created"},
      {202, "Accepted"},
      {203, "Non-Authoritative Information"},
      {204, "No Content"},
      {205, "Reset Content"},
      {206, "Partial Content"},
      {207, "Multi-Status"},
      {208, "Already Reported"},
      {226, "IM Used"},
      {300, "Multiple Choices"},
      {301, "Moved Permanently"},
      {302, "Found"},
      {303, "See Other"},
      {304, "Not Modified"},
      {305, "Use Proxy"},
      {306, "Switch Proxy"},
      {307, "Temporary Redirect"},
      {308, "Permanent Redirect"},
      {400, "Bad Request"},
      {401, "Unauthorized"},
      {402, "Payment Required"},
      {403, "Forbidden"},
      {404, "Not Found"},
      {405, "Method Not Allowed"},
      {406, "Not Acceptable"},
      {407, "Proxy Authentication Required"},
      {408, "Request Timeout"},
      {409, "Conflict"},
      {410, "Gone"},
      {411, "Length Required"},
      {412, "Precondition Failed"},
      {413, "Request Entity Too Large"},
      {414, "Request - URI Too Long"},
      {415, "Unsupported Media Type"},
      {429, "Too Many Requests"},
      {431, "Request Header Fields Too Large"},
      {500, "Internal Server Error"},
      {501, "Not Implemented"},
      {502, "Bad Gateway"},
      {503, "Service Unavailable"},
      {504, "Gateway Timeout"},
      {505, "HTTP Version Not Supported"},
  };

  for (const auto& pair : status_text_map) {
    if (pair.first == status_code_)
      return pair.second;
  }
  return std::string();
}

}  // namespace brillo