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

#include "net/url_request/url_request_test_job.h"

#include <algorithm>
#include <list>
#include <memory>

#include "base/compiler_specific.h"
#include "base/functional/bind.h"
#include "base/lazy_instance.h"
#include "base/location.h"
#include "base/strings/string_util.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/time.h"
#include "net/base/io_buffer.h"
#include "net/base/net_errors.h"
#include "net/http/http_response_headers.h"
#include "net/http/http_util.h"

namespace net {

namespace {

typedef std::list<URLRequestTestJob*> URLRequestJobList;
base::LazyInstance<URLRequestJobList>::Leaky
    g_pending_jobs = LAZY_INSTANCE_INITIALIZER;

}  // namespace

// static getters for known URLs
GURL URLRequestTestJob::test_url_1() {
  return GURL("test:url1");
}

GURL URLRequestTestJob::test_url_2() {
  return GURL("test:url2");
}

GURL URLRequestTestJob::test_url_3() {
  return GURL("test:url3");
}

GURL URLRequestTestJob::test_url_4() {
  return GURL("test:url4");
}

GURL URLRequestTestJob::test_url_auto_advance_async_reads_1() {
  return GURL("test:url_auto_advance_async_reads_1");
}

GURL URLRequestTestJob::test_url_error() {
  return GURL("test:error");
}

GURL URLRequestTestJob::test_url_redirect_to_url_1() {
  return GURL("test:redirect_to_1");
}

GURL URLRequestTestJob::test_url_redirect_to_url_2() {
  return GURL("test:redirect_to_2");
}

// static getters for known URL responses
std::string URLRequestTestJob::test_data_1() {
  return std::string("<html><title>Test One</title></html>");
}
std::string URLRequestTestJob::test_data_2() {
  return std::string("<html><title>Test Two Two</title></html>");
}
std::string URLRequestTestJob::test_data_3() {
  return std::string("<html><title>Test Three Three Three</title></html>");
}
std::string URLRequestTestJob::test_data_4() {
  return std::string("<html><title>Test Four Four Four Four</title></html>");
}

// static getter for simple response headers
std::string URLRequestTestJob::test_headers() {
  static const char kHeaders[] =
      "HTTP/1.1 200 OK\n"
      "Content-type: text/html\n"
      "\n";
  return std::string(kHeaders, std::size(kHeaders));
}

// static getter for redirect response headers
std::string URLRequestTestJob::test_redirect_headers() {
  static const char kHeaders[] =
      "HTTP/1.1 302 MOVED\n"
      "Location: somewhere\n"
      "\n";
  return std::string(kHeaders, std::size(kHeaders));
}

// static getter for redirect response headers
std::string URLRequestTestJob::test_redirect_to_url_1_headers() {
  std::string headers = "HTTP/1.1 302 MOVED";
  headers.push_back('\n');
  headers += "Location: ";
  headers += test_url_1().spec();
  headers.push_back('\n');
  headers.push_back('\n');
  return headers;
}

// static getter for redirect response headers
std::string URLRequestTestJob::test_redirect_to_url_2_headers() {
  std::string headers = "HTTP/1.1 302 MOVED";
  headers.push_back('\n');
  headers += "Location: ";
  headers += test_url_2().spec();
  headers.push_back('\n');
  headers.push_back('\n');
  return headers;
}

// static getter for error response headers
std::string URLRequestTestJob::test_error_headers() {
  static const char kHeaders[] =
      "HTTP/1.1 500 BOO HOO\n"
      "\n";
  return std::string(kHeaders, std::size(kHeaders));
}

URLRequestTestJob::URLRequestTestJob(URLRequest* request, bool auto_advance)
    : URLRequestJob(request),
      auto_advance_(auto_advance),
      response_headers_length_(0) {}

URLRequestTestJob::URLRequestTestJob(URLRequest* request,
                                     const std::string& response_headers,
                                     const std::string& response_data,
                                     bool auto_advance)
    : URLRequestJob(request),
      auto_advance_(auto_advance),
      response_data_(response_data),
      response_headers_(base::MakeRefCounted<net::HttpResponseHeaders>(
          net::HttpUtil::AssembleRawHeaders(response_headers))),
      response_headers_length_(response_headers.size()) {}

URLRequestTestJob::~URLRequestTestJob() {
  std::erase(g_pending_jobs.Get(), this);
}

bool URLRequestTestJob::GetMimeType(std::string* mime_type) const {
  DCHECK(mime_type);
  if (!response_headers_.get())
    return false;
  return response_headers_->GetMimeType(mime_type);
}

void URLRequestTestJob::SetPriority(RequestPriority priority) {
  priority_ = priority;
}

void URLRequestTestJob::Start() {
  // Start reading asynchronously so that all error reporting and data
  // callbacks happen as they would for network requests.
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, base::BindOnce(&URLRequestTestJob::StartAsync,
                                weak_factory_.GetWeakPtr()));
}

void URLRequestTestJob::StartAsync() {
  if (!response_headers_.get()) {
    SetResponseHeaders(test_headers());
    if (request_->url() == test_url_1()) {
      response_data_ = test_data_1();
      stage_ = DATA_AVAILABLE;  // Simulate a synchronous response for this one.
    } else if (request_->url() == test_url_2()) {
      response_data_ = test_data_2();
    } else if (request_->url() == test_url_3()) {
      response_data_ = test_data_3();
    } else if (request_->url() == test_url_4()) {
      response_data_ = test_data_4();
    } else if (request_->url() == test_url_auto_advance_async_reads_1()) {
      response_data_ = test_data_1();
      stage_ = DATA_AVAILABLE;  // Data is available immediately.
      async_reads_ = true;      // All reads complete asynchronously.
    } else if (request_->url() == test_url_redirect_to_url_1()) {
      SetResponseHeaders(test_redirect_to_url_1_headers());
    } else if (request_->url() == test_url_redirect_to_url_2()) {
      SetResponseHeaders(test_redirect_to_url_2_headers());
    } else {
      AdvanceJob();

      // Return an error on unexpected urls.
      NotifyStartError(ERR_INVALID_URL);
      return;
    }
  }

  AdvanceJob();

  this->NotifyHeadersComplete();
}

void URLRequestTestJob::SetResponseHeaders(
    const std::string& response_headers) {
  response_headers_ = base::MakeRefCounted<HttpResponseHeaders>(
      net::HttpUtil::AssembleRawHeaders(response_headers));
  response_headers_length_ = response_headers.size();
}

int URLRequestTestJob::CopyDataForRead(IOBuffer* buf, int buf_size) {
  int bytes_read = 0;
  if (offset_ < static_cast<int>(response_data_.length())) {
    bytes_read = buf_size;
    if (bytes_read + offset_ > static_cast<int>(response_data_.length()))
      bytes_read = static_cast<int>(response_data_.length()) - offset_;

    memcpy(buf->data(), &response_data_.c_str()[offset_], bytes_read);
    offset_ += bytes_read;
  }
  return bytes_read;
}

int URLRequestTestJob::ReadRawData(IOBuffer* buf, int buf_size) {
  if (stage_ == WAITING || async_reads_) {
    async_buf_ = buf;
    async_buf_size_ = buf_size;
    if (stage_ != WAITING) {
      stage_ = WAITING;
      base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(&URLRequestTestJob::ProcessNextOperation,
                                    weak_factory_.GetWeakPtr()));
    }
    return ERR_IO_PENDING;
  }

  return CopyDataForRead(buf, buf_size);
}

void URLRequestTestJob::GetResponseInfo(HttpResponseInfo* info) {
  if (response_headers_.get())
    info->headers = response_headers_;
}

void URLRequestTestJob::GetLoadTimingInfo(
    LoadTimingInfo* load_timing_info) const {
  // Preserve the times the URLRequest is responsible for, but overwrite all
  // the others.
  base::TimeTicks request_start = load_timing_info->request_start;
  base::Time request_start_time = load_timing_info->request_start_time;
  *load_timing_info = load_timing_info_;
  load_timing_info->request_start = request_start;
  load_timing_info->request_start_time = request_start_time;
}

int64_t URLRequestTestJob::GetTotalReceivedBytes() const {
  return response_headers_length_ + offset_;
}

bool URLRequestTestJob::IsRedirectResponse(GURL* location,
                                           int* http_status_code,
                                           bool* insecure_scheme_was_upgraded) {
  if (!response_headers_.get())
    return false;

  std::string value;
  if (!response_headers_->IsRedirect(&value))
    return false;

  *insecure_scheme_was_upgraded = false;
  *location = request_->url().Resolve(value);
  *http_status_code = response_headers_->response_code();
  return true;
}

void URLRequestTestJob::Kill() {
  stage_ = DONE;
  URLRequestJob::Kill();
  weak_factory_.InvalidateWeakPtrs();
  std::erase(g_pending_jobs.Get(), this);
}

void URLRequestTestJob::ProcessNextOperation() {
  switch (stage_) {
    case WAITING:
      // Must call AdvanceJob() prior to NotifyReadComplete() since that may
      // delete |this|.
      AdvanceJob();
      stage_ = DATA_AVAILABLE;
      // OK if ReadRawData wasn't called yet.
      if (async_buf_) {
        int result = CopyDataForRead(async_buf_.get(), async_buf_size_);
        if (result < 0)
          NOTREACHED() << "Reads should not fail in DATA_AVAILABLE.";
        if (NextReadAsync()) {
          // Make all future reads return io pending until the next
          // ProcessNextOperation().
          stage_ = WAITING;
        }
        ReadRawDataComplete(result);
      }
      break;
    case DATA_AVAILABLE:
      AdvanceJob();
      stage_ = ALL_DATA;  // done sending data
      break;
    case ALL_DATA:
      stage_ = DONE;
      return;
    case DONE:
      return;
    default:
      NOTREACHED() << "Invalid stage";
      return;
  }
}

bool URLRequestTestJob::NextReadAsync() {
  return false;
}

void URLRequestTestJob::AdvanceJob() {
  if (auto_advance_) {
    base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, base::BindOnce(&URLRequestTestJob::ProcessNextOperation,
                                  weak_factory_.GetWeakPtr()));
    return;
  }
  g_pending_jobs.Get().push_back(this);
}

// static
bool URLRequestTestJob::ProcessOnePendingMessage() {
  if (g_pending_jobs.Get().empty())
    return false;

  URLRequestTestJob* next_job(g_pending_jobs.Get().front());
  g_pending_jobs.Get().pop_front();

  DCHECK(!next_job->auto_advance());  // auto_advance jobs should be in this q
  next_job->ProcessNextOperation();
  return true;
}

}  // namespace net
