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

#include "services/network/public/cpp/simple_host_resolver.h"

#include <string_view>

#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "net/base/host_port_pair.h"
#include "net/dns/host_resolver.h"
#include "services/network/test/test_network_context_with_host_resolver.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace network {
namespace {

net::IPEndPoint CreateExpectedEndPoint(std::string_view address,
                                       uint16_t port) {
  net::IPAddress ip_address;
  CHECK(ip_address.AssignFromIPLiteral(address));
  return net::IPEndPoint(ip_address, port);
}

std::string CreateMappingRules(
    std::vector<std::pair<std::string_view, std::string_view>>
        host_ip_address_pairs) {
  std::vector<std::string> map_rules;
  for (auto [host, ip_address] : host_ip_address_pairs) {
    map_rules.push_back(
        base::StringPrintf("MAP %s %s", host.data(), ip_address.data()));
  }
  return base::JoinString(map_rules, ",");
}

class MockNetworkContext : public TestNetworkContextWithHostResolver {
 public:
  explicit MockNetworkContext(std::unique_ptr<net::HostResolver> host_resolver)
      : TestNetworkContextWithHostResolver(std::move(host_resolver)) {}

  static std::unique_ptr<MockNetworkContext> CreateNetworkContext(
      std::string_view host_mapping_rules) {
    return std::make_unique<MockNetworkContext>(
        net::HostResolver::CreateStandaloneResolver(
            net::NetLog::Get(), /*options=*/std::nullopt, host_mapping_rules,
            /*enable_caching=*/false));
  }

  // Resets ResolveHostClient when matching |host| is supplied in ResolveHost().
  void SetResetClientFor(std::optional<net::HostPortPair> reset_client_for) {
    reset_client_for_ = std::move(reset_client_for);
  }

  void ResolveHost(
      network::mojom::HostResolverHostPtr host,
      const ::net::NetworkAnonymizationKey& network_anonymization_key,
      network::mojom::ResolveHostParametersPtr optional_parameters,
      ::mojo::PendingRemote<network::mojom::ResolveHostClient>
          pending_response_client) override {
    DCHECK(host->is_host_port_pair());
    if (reset_client_for_ == host->get_host_port_pair()) {
      pending_response_client.reset();
      return;
    }
    ResolveHostImpl(std::move(host), network_anonymization_key,
                    std::move(optional_parameters),
                    std::move(pending_response_client));
  }

  std::optional<net::HostPortPair> reset_client_for_;
};

class SimpleHostResolverTest : public testing::Test {
 public:
  SimpleHostResolverTest() = default;

 protected:
  base::test::TaskEnvironment task_environment_{
      base::test::TaskEnvironment::TimeSource::MOCK_TIME,
      base::test::TaskEnvironment::MainThreadType::IO};
};

struct HostResolverResult {
  int result;
  std::optional<net::IPEndPoint> resolved_address;
};

struct HostResolverRequest {
  net::HostPortPair host_port_pair;

  // Simulates mojo pipe break for a specific request.
  bool reset_client = false;
};

using ResolveHostFuture =
    base::test::TestFuture<int,
                           const net::ResolveErrorInfo&,
                           const net::AddressList&,
                           const net::HostResolverEndpointResults&>;

TEST_F(SimpleHostResolverTest, ResolveFourAddresses) {
  auto network_context = MockNetworkContext::CreateNetworkContext(
      CreateMappingRules({{"example.test", "98.76.54.32"},
                          {"another-example.test", "11.22.33.44"}}));
  auto simple_resolver = SimpleHostResolver::Create(network_context.get());

  // Send four ResolveHost requests:
  //   * #1 passes
  //   * #2 gets ERR_NAME_NOT_RESOLVED
  //   * #3 passes
  //   * #4 receives a pipe break and responds with ERR_FAILED
  std::vector<std::pair<HostResolverRequest, HostResolverResult>> test_cases = {
      {{.host_port_pair = net::HostPortPair("example.test", 160)},
       {.result = net::OK,
        .resolved_address = CreateExpectedEndPoint("98.76.54.32", 160)}},
      {{.host_port_pair = net::HostPortPair("unknown", 15)},
       {.result = net::ERR_NAME_NOT_RESOLVED}},
      {{.host_port_pair = net::HostPortPair("another-example.test", 85)},
       {.result = net::OK,
        .resolved_address = CreateExpectedEndPoint("11.22.33.44", 85)}},
      {{.host_port_pair = net::HostPortPair("example.test", 3800),
        .reset_client = true},
       {.result = net::ERR_FAILED}}};

  EXPECT_EQ(simple_resolver->GetNumOutstandingRequestsForTesting(), 0U);

  std::vector<std::pair<std::unique_ptr<ResolveHostFuture>, HostResolverResult>>
      futures;
  for (auto [request, result] : test_cases) {
    auto future = std::make_unique<ResolveHostFuture>();
    if (request.reset_client) {
      network_context->SetResetClientFor(request.host_port_pair);
    }
    // The ios simulator may return the NAT64 address rather than
    // the IPv4 address. The parameters specify the dns query type
    // to resolve the issue.
    auto resolver_parameters = network::mojom::ResolveHostParameters::New();
    resolver_parameters->dns_query_type = net::DnsQueryType::A;
    simple_resolver->ResolveHost(
        network::mojom::HostResolverHost::NewHostPortPair(
            request.host_port_pair),
        net::NetworkAnonymizationKey(), std::move(resolver_parameters),
        future->GetCallback());
    futures.emplace_back(std::move(future), std::move(result));
  }

  EXPECT_EQ(simple_resolver->GetNumOutstandingRequestsForTesting(), 4U);

  for (const auto& [future, resolver_result] : futures) {
    const auto& [result, resolve_error_info, resolved_addresses,
                 alternative_endpoints] = future->Get();
    EXPECT_EQ(result, resolver_result.result);
    if (!resolver_result.resolved_address) {
      EXPECT_THAT(resolved_addresses, testing::IsEmpty());
    } else {
      EXPECT_THAT(resolved_addresses.endpoints(),
                  testing::ElementsAre(*resolver_result.resolved_address));
    }
  }

  // Verify that all receivers have been erased.
  EXPECT_EQ(simple_resolver->GetNumOutstandingRequestsForTesting(), 0U);
}

}  // namespace
}  // namespace network
