// 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/dns/dns_config_service_win.h"

#include <optional>
#include <string>
#include <vector>

#include "base/check.h"
#include "base/compiler_specific.h"
#include "base/memory/free_deleter.h"
#include "base/test/gmock_expected_support.h"
#include "net/base/ip_address.h"
#include "net/base/ip_endpoint.h"
#include "net/dns/dns_test_util.h"
#include "net/dns/public/dns_protocol.h"
#include "net/dns/public/win_dns_system_settings.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace net {

namespace {

TEST(DnsConfigServiceWinTest, ParseSearchList) {
  const struct TestCase {
    const wchar_t* input;
    std::vector<std::string> expected;
  } cases[] = {
      {L"chromium.org", {"chromium.org"}},
      {L"chromium.org,org", {"chromium.org", "org"}},
      // Empty suffixes terminate the list
      {L"crbug.com,com,,org", {"crbug.com", "com"}},
      // IDN are converted to punycode
      {L"\u017c\xf3\u0142ta.pi\u0119\u015b\u0107.pl,pl",
       {"xn--ta-4ja03asj.xn--pi-wla5e0q.pl", "pl"}},
      // Empty search list is invalid
      {L"", {}},
      {L",,", {}},
  };

  for (const auto& t : cases) {
    EXPECT_EQ(internal::ParseSearchList(t.input), t.expected);
  }
}

TEST(DnsConfigServiceWinTest, ConvertAdapterAddresses) {
  // Check nameservers and connection-specific suffix.
  const struct TestCase {
    std::vector<AdapterInfo> input_adapters;
    std::string expected_nameservers[4];  // Empty string indicates end.
    std::string expected_suffix;
    uint16_t expected_ports[4];
  } cases[] = {
    {  // Ignore loopback and inactive adapters.
      {
        { IF_TYPE_SOFTWARE_LOOPBACK, IfOperStatusUp, L"funnyloop",
          { "2.0.0.2" } },
        { IF_TYPE_FASTETHER, IfOperStatusDormant, L"example.com",
          { "1.0.0.1" } },
        { IF_TYPE_USB, IfOperStatusUp, L"chromium.org",
          { "10.0.0.10", "2001:FFFF::1111" } },
      },
      { "10.0.0.10", "2001:FFFF::1111" },
      "chromium.org",
    },
    {  // Respect configured ports.
      {
        { IF_TYPE_USB, IfOperStatusUp, L"chromium.org",
        { "10.0.0.10", "2001:FFFF::1111" }, { 1024, 24 } },
        { 0 },
      },
      { "10.0.0.10", "2001:FFFF::1111" },
      "chromium.org",
      { 1024, 24 },
    },
    {  // Use the preferred adapter (first in binding order) and filter
       // stateless DNS discovery addresses.
      {
        { IF_TYPE_SOFTWARE_LOOPBACK, IfOperStatusUp, L"funnyloop",
          { "2.0.0.2" } },
        { IF_TYPE_FASTETHER, IfOperStatusUp, L"example.com",
          { "1.0.0.1", "fec0:0:0:ffff::2", "8.8.8.8" } },
        { IF_TYPE_USB, IfOperStatusUp, L"chromium.org",
          { "10.0.0.10", "2001:FFFF::1111" } },
      },
      { "1.0.0.1", "8.8.8.8" },
      "example.com",
    },
    {  // No usable adapters.
      {
        { IF_TYPE_SOFTWARE_LOOPBACK, IfOperStatusUp, L"localhost",
          { "2.0.0.2" } },
        { IF_TYPE_FASTETHER, IfOperStatusDormant, L"example.com",
          { "1.0.0.1" } },
        { IF_TYPE_USB, IfOperStatusUp, L"chromium.org" },
      },
    },
  };

  for (const auto& t : cases) {
    WinDnsSystemSettings settings;
    settings.addresses = CreateAdapterAddresses(t.input_adapters);
    // Default settings for the rest.
    std::vector<IPEndPoint> expected_nameservers;
    auto expected_nameservers_span = base::span(t.expected_nameservers);
    auto expected_ports_span = base::span(t.expected_ports);

    for (size_t j = 0; j < expected_nameservers_span.size() &&
                       !expected_nameservers_span[j].empty();
         ++j) {
      IPAddress ip;
      ASSERT_TRUE(ip.AssignFromIPLiteral(expected_nameservers_span[j]));
      uint16_t port = expected_ports_span[j];
      if (!port) {
        port = dns_protocol::kDefaultPort;
      }

      expected_nameservers.push_back(IPEndPoint(ip, port));
    }
    base::expected<DnsConfig, ReadWinSystemDnsSettingsError> config_or_error =
        internal::ConvertSettingsToDnsConfig(std::move(settings));
    bool expected_success = !expected_nameservers.empty();
    EXPECT_EQ(expected_success, config_or_error.has_value());
    if (config_or_error.has_value()) {
      EXPECT_EQ(expected_nameservers, config_or_error->nameservers);
      EXPECT_THAT(config_or_error->search,
                  testing::ElementsAre(t.expected_suffix));
    }
  }
}
TEST(DnsConfigServiceWinTest, ConvertSuffixSearch) {
  const std::vector<AdapterInfo> infos = {
      {IF_TYPE_USB, IfOperStatusUp, L"connection.suffix", {"1.0.0.1"}},
  };

  const struct TestCase {
    struct {
      std::optional<std::wstring> policy_search_list;
      std::optional<std::wstring> tcpip_search_list;
      std::optional<std::wstring> tcpip_domain;
      std::optional<std::wstring> primary_dns_suffix;
      WinDnsSystemSettings::DevolutionSetting policy_devolution;
      WinDnsSystemSettings::DevolutionSetting dnscache_devolution;
      WinDnsSystemSettings::DevolutionSetting tcpip_devolution;
    } input_settings;
    std::vector<std::string> expected_search;
  } cases[] = {
      {
          // Policy SearchList override.
          {
              L"policy.searchlist.a,policy.searchlist.b",
              L"tcpip.searchlist.a,tcpip.searchlist.b",
              L"tcpip.domain",
              L"primary.dns.suffix",
          },
          {"policy.searchlist.a", "policy.searchlist.b"},
      },
      {
          // User-specified SearchList override.
          {
              std::nullopt,
              L"tcpip.searchlist.a,tcpip.searchlist.b",
              L"tcpip.domain",
              L"primary.dns.suffix",
          },
          {"tcpip.searchlist.a", "tcpip.searchlist.b"},
      },
      {
          // Void SearchList. Using tcpip.domain
          {
              L",bad.searchlist,parsed.as.empty",
              L"tcpip.searchlist,good.but.overridden",
              L"tcpip.domain",
              std::nullopt,
          },
          {"tcpip.domain", "connection.suffix"},
      },
      {
          // Void SearchList. Using primary.dns.suffix
          {
              L",bad.searchlist,parsed.as.empty",
              L"tcpip.searchlist,good.but.overridden",
              L"tcpip.domain",
              L"primary.dns.suffix",
          },
          {"primary.dns.suffix", "connection.suffix"},
      },
      {
          // Void SearchList. Using tcpip.domain when primary.dns.suffix is
          // empty
          {
              L",bad.searchlist,parsed.as.empty",
              L"tcpip.searchlist,good.but.overridden",
              L"tcpip.domain",
              L"",
          },
          {"tcpip.domain", "connection.suffix"},
      },
      {
          // Void SearchList. Using tcpip.domain when primary.dns.suffix is NULL
          {
              L",bad.searchlist,parsed.as.empty",
              L"tcpip.searchlist,good.but.overridden",
              L"tcpip.domain",
              L"",
          },
          {"tcpip.domain", "connection.suffix"},
      },
      {
          // No primary suffix. Devolution does not matter.
          {
              std::nullopt,
              std::nullopt,
              L"",
              L"",
              {1, 2},
          },
          {"connection.suffix"},
      },
      {
          // Devolution enabled by policy, level by dnscache.
          {
              std::nullopt,
              std::nullopt,
              L"a.b.c.d.e",
              std::nullopt,
              {1, std::nullopt},  // policy_devolution: enabled, level
              {0, 3},             // dnscache_devolution
              {0, 1},             // tcpip_devolution
          },
          {"a.b.c.d.e", "connection.suffix", "b.c.d.e", "c.d.e"},
      },
      {
          // Devolution enabled by dnscache, level by policy.
          {
              std::nullopt,
              std::nullopt,
              L"a.b.c.d.e",
              L"f.g.i.l.j",
              {std::nullopt, 4},
              {1, std::nullopt},
              {0, 3},
          },
          {"f.g.i.l.j", "connection.suffix", "g.i.l.j"},
      },
      {
          // Devolution enabled by default.
          {
              std::nullopt,
              std::nullopt,
              L"a.b.c.d.e",
              std::nullopt,
              {std::nullopt, std::nullopt},
              {std::nullopt, 3},
              {std::nullopt, 1},
          },
          {"a.b.c.d.e", "connection.suffix", "b.c.d.e", "c.d.e"},
      },
      {
          // Devolution enabled at level = 2, but nothing to devolve.
          {
              std::nullopt,
              std::nullopt,
              L"a.b",
              std::nullopt,
              {std::nullopt, std::nullopt},
              {std::nullopt, 2},
              {std::nullopt, 2},
          },
          {"a.b", "connection.suffix"},
      },
      {
          // Devolution disabled when no explicit level.
          {
              std::nullopt,
              std::nullopt,
              L"a.b.c.d.e",
              std::nullopt,
              {1, std::nullopt},
              {1, std::nullopt},
              {1, std::nullopt},
          },
          {"a.b.c.d.e", "connection.suffix"},
      },
      {
          // Devolution disabled by policy level.
          {
              std::nullopt,
              std::nullopt,
              L"a.b.c.d.e",
              std::nullopt,
              {std::nullopt, 1},
              {1, 3},
              {1, 4},
          },
          {"a.b.c.d.e", "connection.suffix"},
      },
      {
          // Devolution disabled by user setting.
          {
              std::nullopt,
              std::nullopt,
              L"a.b.c.d.e",
              std::nullopt,
              {std::nullopt, 3},
              {std::nullopt, 3},
              {0, 3},
          },
          {"a.b.c.d.e", "connection.suffix"},
      },
  };

  for (auto& t : cases) {
    WinDnsSystemSettings settings;
    settings.addresses = CreateAdapterAddresses(infos);
    settings.policy_search_list = t.input_settings.policy_search_list;
    settings.tcpip_search_list = t.input_settings.tcpip_search_list;
    settings.tcpip_domain = t.input_settings.tcpip_domain;
    settings.primary_dns_suffix = t.input_settings.primary_dns_suffix;
    settings.policy_devolution = t.input_settings.policy_devolution;
    settings.dnscache_devolution = t.input_settings.dnscache_devolution;
    settings.tcpip_devolution = t.input_settings.tcpip_devolution;

    ASSERT_OK_AND_ASSIGN(
        DnsConfig dns_config,
        internal::ConvertSettingsToDnsConfig(std::move(settings)));
    EXPECT_THAT(dns_config,
                testing::Field(&DnsConfig::search,
                               testing::ElementsAreArray(t.expected_search)));
  }
}

TEST(DnsConfigServiceWinTest, AppendToMultiLabelName) {
  const std::vector<AdapterInfo> infos = {
      {IF_TYPE_USB, IfOperStatusUp, L"connection.suffix", {"1.0.0.1"}},
  };

  const struct TestCase {
    std::optional<DWORD> input;
    bool expected_output;
  } cases[] = {
      {0, false},
      {1, true},
      {std::nullopt, false},
  };

  for (const auto& t : cases) {
    WinDnsSystemSettings settings;
    settings.addresses = CreateAdapterAddresses(infos);
    settings.append_to_multi_label_name = t.input;
    ASSERT_OK_AND_ASSIGN(
        DnsConfig dns_config,
        internal::ConvertSettingsToDnsConfig(std::move(settings)));
    EXPECT_THAT(dns_config,
                testing::Field(&DnsConfig::append_to_multi_label_name,
                               testing::Eq(t.expected_output)));
  }
}

// Setting have_name_resolution_policy_table should set `unhandled_options`.
TEST(DnsConfigServiceWinTest, HaveNRPT) {
  const std::vector<AdapterInfo> infos = {
      {IF_TYPE_USB, IfOperStatusUp, L"connection.suffix", {"1.0.0.1"}},
  };

  const struct TestCase {
    bool have_nrpt;
    bool unhandled_options;
  } cases[] = {
      {false, false},
      {true, true},
  };

  for (const auto& t : cases) {
    WinDnsSystemSettings settings;
    settings.addresses = CreateAdapterAddresses(infos);
    settings.have_name_resolution_policy = t.have_nrpt;
    ASSERT_OK_AND_ASSIGN(
        DnsConfig dns_config,
        internal::ConvertSettingsToDnsConfig(std::move(settings)));
    EXPECT_EQ(t.unhandled_options, dns_config.unhandled_options);
    EXPECT_EQ(t.have_nrpt, dns_config.use_local_ipv6);
  }
}

// Setting have_proxy should set `unhandled_options`.
TEST(DnsConfigServiceWinTest, HaveProxy) {
  const std::vector<AdapterInfo> infos = {
      {IF_TYPE_USB, IfOperStatusUp, L"connection.suffix", {"1.0.0.1"}},
  };

  const struct TestCase {
    bool have_proxy;
    bool unhandled_options;
  } cases[] = {
      {false, false},
      {true, true},
  };

  for (const auto& t : cases) {
    WinDnsSystemSettings settings;
    settings.addresses = CreateAdapterAddresses(infos);
    settings.have_proxy = t.have_proxy;
    ASSERT_OK_AND_ASSIGN(
        DnsConfig dns_config,
        internal::ConvertSettingsToDnsConfig(std::move(settings)));
    EXPECT_THAT(dns_config, testing::Field(&DnsConfig::unhandled_options,
                                           testing::Eq(t.unhandled_options)));
  }
}

// Setting uses_vpn should set `unhandled_options`.
TEST(DnsConfigServiceWinTest, UsesVpn) {
  const std::vector<AdapterInfo> infos = {
      {IF_TYPE_USB, IfOperStatusUp, L"connection.suffix", {"1.0.0.1"}},
      {IF_TYPE_PPP, IfOperStatusUp, L"connection.suffix", {"1.0.0.1"}},
  };

  WinDnsSystemSettings settings;
  settings.addresses = CreateAdapterAddresses(infos);
  ASSERT_OK_AND_ASSIGN(
      DnsConfig dns_config,
      internal::ConvertSettingsToDnsConfig(std::move(settings)));
  EXPECT_THAT(dns_config,
              testing::Field(&DnsConfig::unhandled_options, testing::IsTrue()));
}

// Setting adapter specific nameservers should set `unhandled_options`.
TEST(DnsConfigServiceWinTest, AdapterSpecificNameservers) {
  const std::vector<AdapterInfo> infos = {
      {IF_TYPE_FASTETHER,
       IfOperStatusUp,
       L"example.com",
       {"1.0.0.1", "fec0:0:0:ffff::2", "8.8.8.8"}},
      {IF_TYPE_USB,
       IfOperStatusUp,
       L"chromium.org",
       {"10.0.0.10", "2001:FFFF::1111"}},
  };

  WinDnsSystemSettings settings;
  settings.addresses = CreateAdapterAddresses(infos);
  ASSERT_OK_AND_ASSIGN(
      DnsConfig dns_config,
      internal::ConvertSettingsToDnsConfig(std::move(settings)));
  EXPECT_THAT(dns_config,
              testing::Field(&DnsConfig::unhandled_options, testing::IsTrue()));
}

// Setting adapter specific nameservers for non operational adapter should not
// set `unhandled_options`.
TEST(DnsConfigServiceWinTest, AdapterSpecificNameserversForNo) {
  const std::vector<AdapterInfo> infos = {
      {IF_TYPE_FASTETHER,
       IfOperStatusUp,
       L"example.com",
       {"1.0.0.1", "fec0:0:0:ffff::2", "8.8.8.8"}},
      {IF_TYPE_USB,
       IfOperStatusDown,
       L"chromium.org",
       {"10.0.0.10", "2001:FFFF::1111"}},
  };

  WinDnsSystemSettings settings;
  settings.addresses = CreateAdapterAddresses(infos);
  ASSERT_OK_AND_ASSIGN(
      DnsConfig dns_config,
      internal::ConvertSettingsToDnsConfig(std::move(settings)));
  EXPECT_THAT(dns_config, testing::Field(&DnsConfig::unhandled_options,
                                         testing::IsFalse()));
}

}  // namespace

}  // namespace net
