// Copyright 2017 The Chromium 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 "net/test/spawned_test_server/remote_test_server_spawner_request.h"

#include <utility>

#include "base/location.h"
#include "base/logging.h"
#include "base/macros.h"
#include "base/single_thread_task_runner.h"
#include "base/synchronization/waitable_event.h"
#include "base/test/test_timeouts.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "build/build_config.h"
#include "net/base/elements_upload_data_stream.h"
#include "net/base/io_buffer.h"
#include "net/base/port_util.h"
#include "net/base/upload_bytes_element_reader.h"
#include "net/http/http_response_headers.h"
#include "net/url_request/url_request.h"
#include "net/url_request/url_request_test_util.h"
#include "url/gurl.h"

namespace net {

static const int kBufferSize = 2048;

class RemoteTestServerSpawnerRequest::Core : public URLRequest::Delegate {
 public:
  Core();
  ~Core() override;

  void SendRequest(const GURL& url, const std::string& post_data);

  // Blocks until request is finished. If |response| isn't nullptr then server
  // response is copied to *response. Returns true if the request was completed
  // successfully.
  bool WaitForCompletion(std::string* response) WARN_UNUSED_RESULT;

 private:
  // URLRequest::Delegate methods.
  void OnResponseStarted(URLRequest* request, int net_error) override;
  void OnReadCompleted(URLRequest* request, int num_bytes) override;

  void ReadResponse();
  void OnCommandCompleted(int net_error);
  void OnTimeout();

  // Request results.
  int result_code_ = 0;
  std::string data_received_;

  // WaitableEvent to notify when the request is finished.
  base::WaitableEvent event_;

  std::unique_ptr<URLRequestContext> context_;
  std::unique_ptr<URLRequest> request_;

  scoped_refptr<IOBuffer> read_buffer_;

  std::unique_ptr<base::OneShotTimer> timeout_timer_;

  THREAD_CHECKER(thread_checker_);

  DISALLOW_COPY_AND_ASSIGN(Core);
};

RemoteTestServerSpawnerRequest::Core::Core()
    : event_(base::WaitableEvent::ResetPolicy::AUTOMATIC,
             base::WaitableEvent::InitialState::NOT_SIGNALED),
      read_buffer_(base::MakeRefCounted<IOBuffer>(kBufferSize)) {
  DETACH_FROM_THREAD(thread_checker_);
}

void RemoteTestServerSpawnerRequest::Core::SendRequest(
    const GURL& url,
    const std::string& post_data) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  // Prepare the URLRequest for sending the command.
  DCHECK(!request_.get());
  context_.reset(new TestURLRequestContext);
  request_ = context_->CreateRequest(url, DEFAULT_PRIORITY, this);

  if (post_data.empty()) {
    request_->set_method("GET");
  } else {
    request_->set_method("POST");
    std::unique_ptr<UploadElementReader> reader(
        UploadOwnedBytesElementReader::CreateWithString(post_data));
    request_->set_upload(
        ElementsUploadDataStream::CreateWithReader(std::move(reader), 0));
    request_->SetExtraRequestHeaderByName(HttpRequestHeaders::kContentType,
                                          "application/json",
                                          /*override=*/true);
  }

  timeout_timer_ = std::make_unique<base::OneShotTimer>();
  timeout_timer_->Start(FROM_HERE, TestTimeouts::action_max_timeout(),
                        base::Bind(&Core::OnTimeout, base::Unretained(this)));

  request_->Start();
}

RemoteTestServerSpawnerRequest::Core::~Core() {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
}

bool RemoteTestServerSpawnerRequest::Core::WaitForCompletion(
    std::string* response) {
  // Called by RemoteTestServerSpawnerRequest::WaitForCompletion() on the main
  // thread.

  event_.Wait();
  if (response)
    *response = data_received_;
  return result_code_ == OK;
}

void RemoteTestServerSpawnerRequest::Core::OnTimeout() {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  int result = request_->CancelWithError(ERR_TIMED_OUT);
  OnCommandCompleted(result);
}

void RemoteTestServerSpawnerRequest::Core::OnCommandCompleted(int net_error) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  DCHECK_NE(ERR_IO_PENDING, net_error);
  DCHECK(!event_.IsSignaled());

  // If request has failed, return the error code.
  if (net_error != OK) {
    LOG(ERROR) << "request failed, error: " << ErrorToString(net_error);
    result_code_ = net_error;
  } else if (request_->GetResponseCode() != 200) {
    LOG(ERROR) << "Spawner server returned bad status: "
               << request_->response_headers()->GetStatusLine() << ", "
               << data_received_;
    result_code_ = ERR_FAILED;
  }

  if (result_code_ != OK)
    data_received_.clear();

  request_.reset();
  context_.reset();
  timeout_timer_.reset();

  event_.Signal();
}

void RemoteTestServerSpawnerRequest::Core::ReadResponse() {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  while (true) {
    int result = request_->Read(read_buffer_.get(), kBufferSize);
    if (result == ERR_IO_PENDING)
      return;

    if (result <= 0) {
      OnCommandCompleted(result);
      return;
    }

    data_received_.append(read_buffer_->data(), result);
  }
}

void RemoteTestServerSpawnerRequest::Core::OnResponseStarted(
    URLRequest* request,
    int net_error) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  DCHECK_NE(ERR_IO_PENDING, net_error);
  DCHECK_EQ(request, request_.get());

  if (net_error != OK) {
    OnCommandCompleted(net_error);
    return;
  }

  ReadResponse();
}

void RemoteTestServerSpawnerRequest::Core::OnReadCompleted(URLRequest* request,
                                                           int read_result) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  DCHECK_NE(ERR_IO_PENDING, read_result);
  DCHECK_EQ(request, request_.get());

  if (read_result <= 0) {
    OnCommandCompleted(read_result);
    return;
  }

  data_received_.append(read_buffer_->data(), read_result);

  ReadResponse();
}

RemoteTestServerSpawnerRequest::RemoteTestServerSpawnerRequest(
    scoped_refptr<base::SingleThreadTaskRunner> io_task_runner,
    const GURL& url,
    const std::string& post_data)
    : io_task_runner_(io_task_runner),
      core_(new Core()),
      allowed_port_(new ScopedPortException(url.EffectiveIntPort())) {
  io_task_runner_->PostTask(
      FROM_HERE, base::BindOnce(&Core::SendRequest,
                                base::Unretained(core_.get()), url, post_data));
}

RemoteTestServerSpawnerRequest::~RemoteTestServerSpawnerRequest() {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  io_task_runner_->DeleteSoon(FROM_HERE, core_.release());
}

bool RemoteTestServerSpawnerRequest::WaitForCompletion(std::string* response) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  return core_->WaitForCompletion(response);
}

}  // namespace net
