// Copyright 2018 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 <fuchsia/net/oldhttp/cpp/fidl.h>
#include <lib/fidl/cpp/binding.h>

#include "base/fuchsia/component_context.h"
#include "base/fuchsia/scoped_service_binding.h"
#include "base/fuchsia/service_directory.h"
#include "base/run_loop.h"
#include "base/test/scoped_task_environment.h"
#include "net/base/net_errors.h"
#include "net/test/embedded_test_server/default_handlers.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "webrunner/net_http/http_service_impl.h"
#include "webrunner/net_http/url_loader_impl.h"

namespace net_http {

namespace oldhttp = ::fuchsia::net::oldhttp;

namespace {

const base::FilePath::CharType kTestFilePath[] =
    FILE_PATH_LITERAL("webrunner/net_http/testdata");

// Capacity, in bytes, for buffers used to read data off the URLResponse.
const size_t kBufferCapacity = 1024;

using ResponseHeaders = std::multimap<std::string, std::string>;

class HttpServiceTest : public ::testing::Test {
 public:
  HttpServiceTest()
      : task_environment_(
            base::test::ScopedTaskEnvironment::MainThreadType::IO),
        binding_(&http_service_server_) {
    // Initialize the test server.
    test_server_.AddDefaultHandlers(
        base::FilePath(FILE_PATH_LITERAL(kTestFilePath)));
    net::test_server::RegisterDefaultHandlers(&test_server_);
  }

 protected:
  base::test::ScopedTaskEnvironment task_environment_;

  void SetUp() override {
    ASSERT_TRUE(test_server_.Start());

    // Bind the service with the client-side interface.
    binding_.Bind(http_service_interface_.NewRequest());
  }

  void TearDown() override {
    // Disconnect the client and wait for the service to shut down.
    base::RunLoop run_loop;
    binding_.set_error_handler(
        [&run_loop](zx_status_t status) { run_loop.Quit(); });
    http_service_interface_.Unbind();
    run_loop.Run();
    binding_.set_error_handler(nullptr);

    // Check there are no pending requests.
    EXPECT_EQ(URLLoaderImpl::GetNumActiveRequestsForTests(), 0);
  }

  // Helper method to start |request| on |url_loader|
  void ExecuteRequest(const oldhttp::URLLoaderPtr& url_loader,
                      oldhttp::URLRequest request) {
    base::RunLoop run_loop;

    url_loader->Start(std::move(request),
                      [this, &run_loop](oldhttp::URLResponse response) {
                        run_loop.Quit();
                        url_response_ = std::move(response);
                      });
    run_loop.Run();
  }

  net::EmbeddedTestServer* http_test_server() { return &test_server_; }
  oldhttp::HttpServicePtr& http_service() { return http_service_interface_; }
  oldhttp::URLResponse& url_response() { return url_response_; }

 private:
  net::EmbeddedTestServer test_server_;

  HttpServiceImpl http_service_server_;
  oldhttp::HttpServicePtr http_service_interface_;
  fidl::Binding<oldhttp::HttpService> binding_;
  oldhttp::URLResponse url_response_;

  DISALLOW_COPY_AND_ASSIGN(HttpServiceTest);
};

void CheckResponseStream(const oldhttp::URLResponse& response,
                         const std::string& expected_response) {
  EXPECT_TRUE(response.body->is_stream());

  zx::socket stream = std::move(response.body->stream());
  size_t offset = 0;

  while (true) {
    std::array<char, kBufferCapacity> buffer;
    size_t size = 0;
    zx_status_t result = stream.read(0, buffer.data(), kBufferCapacity, &size);

    if (result == ZX_ERR_SHOULD_WAIT) {
      zx_signals_t observed;
      stream.wait_one(ZX_SOCKET_READABLE | ZX_SOCKET_PEER_CLOSED,
                      zx::time::infinite(), &observed);
      if (observed & ZX_SOCKET_READABLE) {
        // Attempt to read again now that the socket is readable.
        continue;
      } else if (observed & ZX_SOCKET_PEER_CLOSED) {
        // Done reading.
        break;
      } else {
        NOTREACHED();
      }
    } else if (result == ZX_ERR_PEER_CLOSED) {
      // Done reading.
      break;
    }
    EXPECT_EQ(result, ZX_OK);

    EXPECT_TRUE(std::equal(buffer.begin(), buffer.begin() + size,
                           expected_response.begin() + offset));
    offset += size;
  }

  EXPECT_EQ(offset, expected_response.length());
}

void CheckResponseBuffer(const oldhttp::URLResponse& response,
                         const std::string& expected_response) {
  EXPECT_TRUE(response.body->is_buffer());

  fuchsia::mem::Buffer mem_buffer = std::move(response.body->buffer());
  size_t response_size = mem_buffer.size;
  EXPECT_EQ(mem_buffer.size, expected_response.length());

  std::array<char, kBufferCapacity> buffer;
  size_t offset = 0;
  while (offset != mem_buffer.size) {
    size_t length = std::min(response_size - offset, kBufferCapacity);
    zx_status_t result = mem_buffer.vmo.read(buffer.data(), offset, length);
    EXPECT_EQ(result, ZX_OK);

    EXPECT_TRUE(std::equal(buffer.begin(), buffer.begin() + response_size,
                           expected_response.begin() + offset));
    offset += response_size;
  }

  EXPECT_EQ(offset, expected_response.length());
}

void CheckResponseHeaders(const oldhttp::URLResponse& response,
                          ResponseHeaders* expected_headers) {
  for (auto& header : response.headers.get()) {
    const std::string header_name = header.name->data();
    const std::string header_value = header.value->data();
    auto iter = std::find_if(expected_headers->begin(), expected_headers->end(),
                             [&header_name, &header_value](auto& elt) -> bool {
                               return elt.first.compare(header_name) == 0 &&
                                      elt.second.compare(header_value) == 0;
                             });
    EXPECT_NE(iter, expected_headers->end())
        << "Unexpected header: \"" << header_name << "\" with value: \""
        << header_value << "\".";
    if (iter != expected_headers->end()) {
      expected_headers->erase(iter);
    }
  }
  EXPECT_TRUE(expected_headers->empty());
}

}  // namespace

// Check a basic end-to-end request resolution with the response being streamed
// is handled properly.
TEST_F(HttpServiceTest, BasicRequestStream) {
  oldhttp::URLLoaderPtr url_loader;
  http_service()->CreateURLLoader(url_loader.NewRequest());

  oldhttp::URLRequest request;
  request.url = http_test_server()->GetURL("/simple.html").spec();
  request.method = "GET";
  request.response_body_mode = oldhttp::ResponseBodyMode::STREAM;

  ExecuteRequest(url_loader, std::move(request));
  EXPECT_EQ(url_response().status_code, 200u);
  CheckResponseStream(url_response(), "hello");
}

// Check a basic end-to-end request resolution with the response being
// buffered is handled properly.
TEST_F(HttpServiceTest, BasicRequestBuffer) {
  oldhttp::URLLoaderPtr url_loader;
  http_service()->CreateURLLoader(url_loader.NewRequest());

  oldhttp::URLRequest request;
  request.url = http_test_server()->GetURL("/simple.html").spec();
  request.method = "GET";
  request.response_body_mode = oldhttp::ResponseBodyMode::BUFFER;

  ExecuteRequest(url_loader, std::move(request));
  EXPECT_EQ(url_response().status_code, 200u);
  CheckResponseBuffer(url_response(), "hello");
}

// Check network request headers are received properly.
TEST_F(HttpServiceTest, RequestWithHeaders) {
  oldhttp::URLLoaderPtr url_loader;
  http_service()->CreateURLLoader(url_loader.NewRequest());

  oldhttp::URLRequest request;
  request.url = http_test_server()->GetURL("/with-headers.html").spec();
  request.method = "GET";
  request.response_body_mode = oldhttp::ResponseBodyMode::STREAM;

  ExecuteRequest(url_loader, std::move(request));
  EXPECT_EQ(url_response().status_code, 200u);
  CheckResponseStream(
      url_response(),
      "This file is boring; all the action's in the .mock-http-headers.\n");
  ResponseHeaders expected_headers = {
      {"Cache-Control", "private"},
      {"Content-Type", "text/html; charset=ISO-8859-1"},
      {"X-Multiple-Entries", "a"},
      {"X-Multiple-Entries", "b"},
  };
  CheckResponseHeaders(url_response(), &expected_headers);
}

// Check duplicate network request headers are received properly.
TEST_F(HttpServiceTest, RequestWithDuplicateHeaders) {
  oldhttp::URLLoaderPtr url_loader;
  http_service()->CreateURLLoader(url_loader.NewRequest());

  oldhttp::URLRequest request;
  request.url =
      http_test_server()->GetURL("/with-duplicate-headers.html").spec();
  request.method = "GET";
  request.response_body_mode = oldhttp::ResponseBodyMode::STREAM;

  ExecuteRequest(url_loader, std::move(request));
  EXPECT_EQ(url_response().status_code, 200u);
  CheckResponseStream(
      url_response(),
      "This file is boring; all the action's in the .mock-http-headers.\n");
  ResponseHeaders expected_headers = {
      {"Cache-Control", "private"},
      {"Content-Type", "text/html; charset=ISO-8859-1"},
      {"X-Multiple-Entries", "a"},
      {"X-Multiple-Entries", "a"},
      {"X-Multiple-Entries", "b"},
  };
  CheckResponseHeaders(url_response(), &expected_headers);
}

// Check a request with automatic redirect resolution is handled properly.
TEST_F(HttpServiceTest, AutoRedirect) {
  oldhttp::URLLoaderPtr url_loader;
  http_service()->CreateURLLoader(url_loader.NewRequest());

  oldhttp::URLRequest request;
  request.url = http_test_server()->GetURL("/redirect-test.html").spec();
  request.method = "GET";
  request.response_body_mode = oldhttp::ResponseBodyMode::STREAM;
  request.auto_follow_redirects = true;

  ExecuteRequest(url_loader, std::move(request));
  EXPECT_EQ(url_response().status_code, 200u);
  EXPECT_EQ(url_response().url,
            http_test_server()->GetURL("/with-headers.html").spec());
}

// Check a request with manual redirect resolution is handled properly.
TEST_F(HttpServiceTest, ManualRedirect) {
  oldhttp::URLLoaderPtr url_loader;
  http_service()->CreateURLLoader(url_loader.NewRequest());
  std::string request_url =
      http_test_server()->GetURL("/redirect-test.html").spec();

  oldhttp::URLRequest request;
  request.url = request_url;
  request.method = "GET";
  request.response_body_mode = oldhttp::ResponseBodyMode::STREAM;
  request.auto_follow_redirects = false;

  ExecuteRequest(url_loader, std::move(request));
  std::string final_url =
      http_test_server()->GetURL("/with-headers.html").spec();
  EXPECT_EQ(url_response().status_code, 302u);
  EXPECT_EQ(url_response().url, request_url);
  EXPECT_EQ(url_response().redirect_url, final_url);

  base::RunLoop run_loop;
  url_loader->FollowRedirect(
      [&run_loop, &final_url](oldhttp::URLResponse response) {
        EXPECT_EQ(response.status_code, 200u);
        EXPECT_EQ(response.url, final_url);
        run_loop.Quit();
      });
  run_loop.Run();
}

// Check HTTP error codes are properly populated.
TEST_F(HttpServiceTest, HttpErrorCode) {
  oldhttp::URLLoaderPtr url_loader;
  http_service()->CreateURLLoader(url_loader.NewRequest());

  oldhttp::URLRequest request;
  request.url = http_test_server()
                    ->base_url()
                    .Resolve("/non_existent_cooper.html")
                    .spec();
  request.method = "GET";
  request.response_body_mode = oldhttp::ResponseBodyMode::STREAM;

  ExecuteRequest(url_loader, std::move(request));
  EXPECT_EQ(url_response().status_code, 404u);
}

// Check network error codes are properly populated.
TEST_F(HttpServiceTest, InvalidURL) {
  oldhttp::URLLoaderPtr url_loader;
  http_service()->CreateURLLoader(url_loader.NewRequest());

  oldhttp::URLRequest request;
  request.url = "ht\\tp://test.test/";
  request.method = "GET";
  request.response_body_mode = oldhttp::ResponseBodyMode::STREAM;

  ExecuteRequest(url_loader, std::move(request));
  EXPECT_EQ(url_response().error->code, net::ERR_INVALID_URL);
}

// Ensure the service can handle multiple concurrent requests.
TEST_F(HttpServiceTest, MultipleRequests) {
  oldhttp::URLLoaderPtr url_loaders[100];
  for (int i = 0; i < 100; i++) {
    http_service()->CreateURLLoader(url_loaders[i].NewRequest());
  }

  base::RunLoop run_loop;
  int requests_done = 0;
  for (int i = 0; i < 100; i++) {
    oldhttp::URLRequest request;
    request.url = http_test_server()->GetURL("/simple.html").spec();
    request.method = "GET";
    request.response_body_mode = (i % 2) == 0
                                     ? oldhttp::ResponseBodyMode::STREAM
                                     : oldhttp::ResponseBodyMode::BUFFER;
    url_loaders[i]->Start(
        std::move(request),
        [&requests_done, &run_loop](oldhttp::URLResponse response) {
          EXPECT_EQ(response.status_code, 200u);
          if (response.body->is_buffer()) {
            CheckResponseBuffer(response, "hello");
          } else {
            CheckResponseStream(response, "hello");
          }
          requests_done++;
          if (requests_done == 100) {
            // Last request signals the run_loop to exit.
            run_loop.Quit();
          }
        });
  }
  run_loop.Run();
}

// Check QueryStatus works as expected when a request is loading.
// Also checks the request is properly deleted after the binding is destroyed.
TEST_F(HttpServiceTest, QueryStatus) {
  oldhttp::URLLoaderPtr url_loader;
  http_service()->CreateURLLoader(url_loader.NewRequest());

  oldhttp::URLRequest request;
  request.url = http_test_server()->GetURL("/hung-after-headers").spec();
  request.method = "GET";
  request.response_body_mode = oldhttp::ResponseBodyMode::STREAM;

  // In socket mode, we should still get the response headers.
  ExecuteRequest(url_loader, std::move(request));
  EXPECT_EQ(url_response().status_code, 200u);

  base::RunLoop run_loop;
  url_loader->QueryStatus([&run_loop](oldhttp::URLLoaderStatus status) {
    EXPECT_TRUE(status.is_loading);
    run_loop.Quit();
  });
  run_loop.Run();
}

// Check the response error is properly set if the server disconnects early.
TEST_F(HttpServiceTest, CloseSocket) {
  oldhttp::URLLoaderPtr url_loader;
  http_service()->CreateURLLoader(url_loader.NewRequest());

  oldhttp::URLRequest request;
  request.url = http_test_server()->GetURL("/close-socket").spec();
  request.method = "GET";
  request.response_body_mode = oldhttp::ResponseBodyMode::STREAM;

  ExecuteRequest(url_loader, std::move(request));
  EXPECT_EQ(url_response().error->code, net::ERR_EMPTY_RESPONSE);
}

}  // namespace net_http
