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

#include "content/browser/direct_sockets/direct_sockets_test_utils.h"

#include <string_view>

#include "base/functional/bind.h"
#include "base/json/json_reader.h"
#include "base/strings/escape.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/task/sequenced_task_runner.h"
#include "base/test/test_future.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/web_contents_tester.h"
#include "net/base/url_util.h"
#include "net/dns/host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "services/network/public/cpp/permissions_policy/permissions_policy_declaration.h"
#include "services/network/public/mojom/clear_data_filter.mojom.h"
#include "services/network/public/mojom/udp_socket.mojom.h"
#include "third_party/blink/public/mojom/navigation/navigation_params.mojom.h"
#include "url/origin.h"

namespace content::test {

// MockUDPSocket implementation

MockUDPSocket::MockUDPSocket(
    mojo::PendingRemote<network::mojom::UDPSocketListener> listener) {
  listener_.Bind(std::move(listener));
}

MockUDPSocket::~MockUDPSocket() {
  if (callback_) {
    std::move(callback_).Run(net::OK);
  }
}

void MockUDPSocket::Connect(const net::IPEndPoint& remote_addr,
                            network::mojom::UDPSocketOptionsPtr socket_options,
                            ConnectCallback callback) {
  base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE,
      base::BindOnce(std::move(callback), net::OK,
                     net::IPEndPoint{net::IPAddress::IPv4Localhost(), 0}));
}

void MockUDPSocket::Send(
    base::span<const uint8_t> data,
    const net::MutableNetworkTrafficAnnotationTag& traffic_annotation,
    SendCallback callback) {
  DCHECK(!callback_);
  callback_ = std::move(callback);
  if (additional_send_callback_) {
    std::move(additional_send_callback_).Run();
  }
  if (next_send_result_) {
    std::move(callback_).Run(*next_send_result_);
    next_send_result_.reset();
  }
}

void MockUDPSocket::MockSend(int32_t result,
                             const std::optional<base::span<uint8_t>>& data) {
  listener_->OnReceived(result, {}, data);
}

MockRestrictedUDPSocket::MockRestrictedUDPSocket(
    std::unique_ptr<network::TestUDPSocket> udp_socket,
    mojo::PendingReceiver<network::mojom::RestrictedUDPSocket> receiver)
    : network::TestRestrictedUDPSocket(std::move(udp_socket)),
      receiver_(this, std::move(receiver)) {}

MockRestrictedUDPSocket::~MockRestrictedUDPSocket() = default;

// MockNetworkContext implementation

MockNetworkContext::MockNetworkContext()
    : MockNetworkContext(/*host_mapping_rules=*/"") {}

MockNetworkContext::MockNetworkContext(std::string_view host_mapping_rules)
    : network::TestNetworkContextWithHostResolver(
          net::HostResolver::CreateStandaloneResolver(
              net::NetLog::Get(),
              /*options=*/std::nullopt,
              host_mapping_rules,
              /*enable_caching=*/false)) {}

MockNetworkContext::~MockNetworkContext() = default;

void MockNetworkContext::CreateRestrictedUDPSocket(
    const net::IPEndPoint& addr,
    network::mojom::RestrictedUDPSocketMode mode,
    const net::MutableNetworkTrafficAnnotationTag& traffic_annotation,
    network::mojom::RestrictedUDPSocketParamsPtr params,
    mojo::PendingReceiver<network::mojom::RestrictedUDPSocket> receiver,
    mojo::PendingRemote<network::mojom::UDPSocketListener> listener,
    bool allow_multicast,
    bool allow_source_specific_multicast,
    CreateRestrictedUDPSocketCallback callback) {
  auto socket = CreateMockUDPSocket(std::move(listener));
  DCHECK_EQ(mode, network::mojom::RestrictedUDPSocketMode::CONNECTED);
  socket->Connect(addr, params ? std::move(params->socket_options) : nullptr,
                  std::move(callback));
  restricted_udp_socket_ = std::make_unique<MockRestrictedUDPSocket>(
      std::move(socket), std::move(receiver));
}

std::unique_ptr<MockUDPSocket> MockNetworkContext::CreateMockUDPSocket(
    mojo::PendingRemote<network::mojom::UDPSocketListener> listener) {
  return std::make_unique<MockUDPSocket>(std::move(listener));
}

// AsyncJsRunner implementation

AsyncJsRunner::AsyncJsRunner(content::WebContents* web_contents)
    : WebContentsObserver(web_contents) {}

AsyncJsRunner::~AsyncJsRunner() = default;

base::test::TestFuture<std::string> AsyncJsRunner::RunScript(
    const std::string& async_script) {
  // Do not leave behind hanging futures from previous invocations.
  DCHECK(!future_callback_);
  base::test::TestFuture<std::string> future;

  token_ = base::Token::CreateRandom();
  future_callback_ = future.GetCallback();
  const std::string wrapped_script =
      MakeScriptSendResultToDomQueue(async_script);
  ExecuteScriptAsync(web_contents(), wrapped_script);

  return future;
}

void AsyncJsRunner::DomOperationResponse(RenderFrameHost* render_frame_host,
                                         const std::string& json_string) {
  // Check that future is valid and not yet fulfilled.
  DCHECK(future_callback_);

  auto parsed = base::JSONReader::ReadAndReturnValueWithError(
      json_string, base::JSON_ALLOW_TRAILING_COMMAS);
  DCHECK(parsed.has_value());
  DCHECK_EQ(parsed->type(), base::Value::Type::LIST);

  const auto& list = parsed->GetList();
  DCHECK_EQ(list.size(), 2U);
  DCHECK(list[0].is_string());
  DCHECK(list[1].is_string());

  if (list[1].GetString() == token_.ToString()) {
    std::move(future_callback_).Run(list[0].GetString());
  }
}

std::string AsyncJsRunner::MakeScriptSendResultToDomQueue(
    const std::string& script) const {
  DCHECK(!script.empty());
  return WrapAsync(base::StringPrintf(
      R"(
        let result = await %s;
        window.domAutomationController.send([result, '%s']);
      )",
      script.c_str(), token_.ToString().c_str()));
}

SetHeaderWithFileUrlBuilder::SetHeaderWithFileUrlBuilder(std::string_view path)
    : path_(path) {}

SetHeaderWithFileUrlBuilder::~SetHeaderWithFileUrlBuilder() = default;

SetHeaderWithFileUrlBuilder& SetHeaderWithFileUrlBuilder::WithCOIHeaders() {
  headers_.push_back("Cross-Origin-Opener-Policy: same-origin");
  headers_.push_back("Cross-Origin-Embedder-Policy: require-corp");
  return *this;
}

SetHeaderWithFileUrlBuilder& SetHeaderWithFileUrlBuilder::WithPermissionsPolicy(
    std::string_view feature,
    std::string_view value) {
  permissions_policy_[std::string(feature)].emplace_back(value);
  return *this;
}

GURL SetHeaderWithFileUrlBuilder::Build(net::EmbeddedTestServer* server) const {
  std::vector<std::string> all_headers = headers_;
  if (!permissions_policy_.empty()) {
    std::vector<std::string> features;
    for (const auto& [feature, values] : permissions_policy_) {
      features.push_back(base::StringPrintf(
          "%s=%s", feature.c_str(), base::JoinString(values, " ").c_str()));
    }
    all_headers.push_back(base::StrCat(
        {"Permissions-Policy: ", base::JoinString(features, ", ")}));
  }

  std::string query;
  for (const auto& header : all_headers) {
    if (!query.empty()) {
      query += "&";
    }
    query += base::EscapeQueryParamValue(header, /*use_plus=*/false);
  }

  return server->GetURL(base::StrCat(
      {"/set-header-with-file/content/test/data", path_, "?", query}));
}

SetHeaderWithFileUrlBuilder FileWithHeaders(std::string_view path) {
  return SetHeaderWithFileUrlBuilder{path};
}

IsolatedWebAppContentBrowserClient::IsolatedWebAppContentBrowserClient(
    const url::Origin& isolated_app_origin)
    : isolated_app_origin_(isolated_app_origin) {}

bool IsolatedWebAppContentBrowserClient::ShouldUrlUseApplicationIsolationLevel(
    BrowserContext* browser_context,
    const GURL& url) {
  return isolated_app_origin_ == url::Origin::Create(url);
}

// misc
std::string WrapAsync(const std::string& script) {
  DCHECK(!script.empty());
  return base::StringPrintf(R"((async () => {%s})())", script.c_str());
}

}  // namespace content::test
