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

#include "extensions/browser/api/declarative_net_request/indexed_rule.h"

#include <algorithm>
#include <array>
#include <memory>
#include <optional>
#include <utility>

#include "base/containers/flat_set.h"
#include "base/format_macros.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/values_test_util.h"
#include "components/version_info/channel.h"
#include "extensions/browser/api/declarative_net_request/constants.h"
#include "extensions/browser/api/declarative_net_request/test_utils.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/api/declarative_net_request.h"
#include "extensions/common/api/declarative_net_request/constants.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_features.h"
#include "extensions/common/features/feature_channel.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));

namespace extensions::declarative_net_request {
namespace {

namespace flat_rule = url_pattern_index::flat;
namespace dnr_api = extensions::api::declarative_net_request;

constexpr const char* kTestExtensionId = "extensionid";

GURL GetBaseURL() {
  return Extension::GetBaseURLFromExtensionId(kTestExtensionId);
}

dnr_api::Redirect MakeRedirectUrl(const char* redirect_url) {
  dnr_api::Redirect redirect;
  redirect.url = redirect_url;
  return redirect;
}

dnr_api::Rule CreateGenericParsedRule() {
  dnr_api::Rule rule;
  rule.priority = kMinValidPriority;
  rule.id = kMinValidID;
  rule.condition.url_filter = "filter";
  rule.action.type = dnr_api::RuleActionType::kBlock;
  return rule;
}

dnr_api::HeaderInfo CreateHeaderInfo(
    std::string header,
    std::optional<std::vector<std::string>> values,
    std::optional<std::vector<std::string>> excluded_values) {
  dnr_api::HeaderInfo info;

  info.header = std::move(header);
  info.values = std::move(values);
  info.excluded_values = std::move(excluded_values);
  return info;
}

using IndexedRuleTest = ::testing::Test;

TEST_F(IndexedRuleTest, IDParsing) {
  struct TestCase {
    const int id;
    const ParseResult expected_result;
  };

  static constexpr auto cases = std::to_array<TestCase>({
      {kMinValidID - 1, ParseResult::ERROR_INVALID_RULE_ID},
      {kMinValidID, ParseResult::SUCCESS},
      {kMinValidID + 1, ParseResult::SUCCESS},
  });

  for (size_t i = 0; i < std::size(cases); ++i) {
    SCOPED_TRACE(base::StringPrintf("Testing case[%zu]", i));
    dnr_api::Rule rule = CreateGenericParsedRule();
    rule.id = cases[i].id;

    IndexedRule indexed_rule;
    ParseResult result = IndexedRule::CreateIndexedRule(
        std::move(rule), GetBaseURL(), kMinValidStaticRulesetID, &indexed_rule);

    EXPECT_EQ(cases[i].expected_result, result);
    if (result == ParseResult::SUCCESS) {
      EXPECT_EQ(base::checked_cast<uint32_t>(cases[i].id), indexed_rule.id);
    }
  }
}

TEST_F(IndexedRuleTest, PriorityParsing) {
  struct TestCase {
    dnr_api::RuleActionType action_type;
    std::optional<int> priority;
    const ParseResult expected_result;
    // Only valid if |expected_result| is SUCCESS.
    const uint32_t expected_priority;
  };

  static constexpr auto cases = std::to_array<TestCase>({
      {dnr_api::RuleActionType::kRedirect, kMinValidPriority - 1,
       ParseResult::ERROR_INVALID_RULE_PRIORITY, kDefaultPriority},
      {dnr_api::RuleActionType::kRedirect, kMinValidPriority,
       ParseResult::SUCCESS, kMinValidPriority},
      {dnr_api::RuleActionType::kRedirect, std::nullopt, ParseResult::SUCCESS,
       kDefaultPriority},
      {dnr_api::RuleActionType::kRedirect, kMinValidPriority + 1,
       ParseResult::SUCCESS, kMinValidPriority + 1},
      {dnr_api::RuleActionType::kUpgradeScheme, kMinValidPriority - 1,
       ParseResult::ERROR_INVALID_RULE_PRIORITY, kDefaultPriority},
      {dnr_api::RuleActionType::kUpgradeScheme, kMinValidPriority,
       ParseResult::SUCCESS, kMinValidPriority},
      {dnr_api::RuleActionType::kBlock, kMinValidPriority - 1,
       ParseResult::ERROR_INVALID_RULE_PRIORITY, kDefaultPriority},
      {dnr_api::RuleActionType::kBlock, kMinValidPriority, ParseResult::SUCCESS,
       kMinValidPriority},
  });

  for (size_t i = 0; i < std::size(cases); ++i) {
    SCOPED_TRACE(base::StringPrintf("Testing case[%zu]", i));
    dnr_api::Rule rule = CreateGenericParsedRule();
    rule.priority = std::move(cases[i].priority);
    rule.action.type = cases[i].action_type;

    if (cases[i].action_type == dnr_api::RuleActionType::kRedirect) {
      rule.action.redirect = MakeRedirectUrl("http://google.com");
    }

    IndexedRule indexed_rule;
    ParseResult result = IndexedRule::CreateIndexedRule(
        std::move(rule), GetBaseURL(), kMinValidStaticRulesetID, &indexed_rule);

    EXPECT_EQ(cases[i].expected_result, result);
    if (result == ParseResult::SUCCESS) {
      EXPECT_EQ(ComputeIndexedRulePriority(cases[i].expected_priority,
                                           cases[i].action_type),
                indexed_rule.priority);
    }
  }
}

TEST_F(IndexedRuleTest, OptionsParsing) {
  struct TestCase {
    const dnr_api::DomainType domain_type;
    const dnr_api::RuleActionType action_type;
    std::optional<bool> is_url_filter_case_sensitive;
    const uint8_t expected_options;
  };

  static constexpr auto cases = std::to_array<TestCase>({
      {dnr_api::DomainType::kNone, dnr_api::RuleActionType::kBlock,
       std::nullopt,
       flat_rule::OptionFlag_APPLIES_TO_THIRD_PARTY |
           flat_rule::OptionFlag_APPLIES_TO_FIRST_PARTY},
      {dnr_api::DomainType::kFirstParty, dnr_api::RuleActionType::kAllow, true,
       flat_rule::OptionFlag_IS_ALLOWLIST |
           flat_rule::OptionFlag_APPLIES_TO_FIRST_PARTY |
           flat_rule::OptionFlag_IS_MATCH_CASE},
      {dnr_api::DomainType::kFirstParty, dnr_api::RuleActionType::kAllow, false,
       flat_rule::OptionFlag_IS_ALLOWLIST |
           flat_rule::OptionFlag_APPLIES_TO_FIRST_PARTY},
  });

  for (size_t i = 0; i < std::size(cases); ++i) {
    SCOPED_TRACE(base::StringPrintf("Testing case[%zu]", i));
    dnr_api::Rule rule = CreateGenericParsedRule();
    rule.condition.domain_type = cases[i].domain_type;
    rule.action.type = cases[i].action_type;
    rule.condition.is_url_filter_case_sensitive =
        std::move(cases[i].is_url_filter_case_sensitive);

    IndexedRule indexed_rule;
    ParseResult result = IndexedRule::CreateIndexedRule(
        std::move(rule), GetBaseURL(), kMinValidStaticRulesetID, &indexed_rule);

    EXPECT_EQ(ParseResult::SUCCESS, result);
    EXPECT_EQ(cases[i].expected_options, indexed_rule.options);
  }
}

TEST_F(IndexedRuleTest, ResourceTypesParsing) {
  using ResourceTypeVec = std::vector<dnr_api::ResourceType>;

  struct TestCase {
    std::optional<ResourceTypeVec> resource_types;
    std::optional<ResourceTypeVec> excluded_resource_types;
    const ParseResult expected_result;
    // Only valid if |expected_result| is SUCCESS.
    const uint16_t expected_element_types;
  };

  const auto cases = std::to_array<TestCase>({
      {std::nullopt, std::nullopt, ParseResult::SUCCESS,
       flat_rule::ElementType_ANY & ~flat_rule::ElementType_MAIN_FRAME},
      {std::nullopt, ResourceTypeVec({dnr_api::ResourceType::kScript}),
       ParseResult::SUCCESS,
       flat_rule::ElementType_ANY & ~flat_rule::ElementType_SCRIPT},
      {ResourceTypeVec(
           {dnr_api::ResourceType::kScript, dnr_api::ResourceType::kImage}),
       std::nullopt, ParseResult::SUCCESS,
       flat_rule::ElementType_SCRIPT | flat_rule::ElementType_IMAGE},
      {ResourceTypeVec(
           {dnr_api::ResourceType::kScript, dnr_api::ResourceType::kImage}),
       ResourceTypeVec({dnr_api::ResourceType::kScript}),
       ParseResult::ERROR_RESOURCE_TYPE_DUPLICATED,
       flat_rule::ElementType_NONE},
      {std::nullopt,
       ResourceTypeVec(
           {dnr_api::ResourceType::kMainFrame, dnr_api::ResourceType::kSubFrame,
            dnr_api::ResourceType::kStylesheet, dnr_api::ResourceType::kScript,
            dnr_api::ResourceType::kImage, dnr_api::ResourceType::kFont,
            dnr_api::ResourceType::kObject,
            dnr_api::ResourceType::kXmlhttprequest,
            dnr_api::ResourceType::kPing, dnr_api::ResourceType::kCspReport,
            dnr_api::ResourceType::kMedia, dnr_api::ResourceType::kWebsocket,
            dnr_api::ResourceType::kWebtransport,
            dnr_api::ResourceType::kWebbundle, dnr_api::ResourceType::kOther}),
       ParseResult::ERROR_NO_APPLICABLE_RESOURCE_TYPES,
       flat_rule::ElementType_NONE},
      {{{}},
       {{}},
       ParseResult::ERROR_EMPTY_RESOURCE_TYPES_LIST,
       flat_rule::ElementType_NONE},
      {ResourceTypeVec({dnr_api::ResourceType::kScript}), ResourceTypeVec(),
       ParseResult::SUCCESS, flat_rule::ElementType_SCRIPT},
  });

  for (size_t i = 0; i < std::size(cases); ++i) {
    SCOPED_TRACE(base::StringPrintf("Testing case[%zu]", i));
    dnr_api::Rule rule = CreateGenericParsedRule();
    rule.condition.resource_types = std::move(cases[i].resource_types);
    rule.condition.excluded_resource_types =
        std::move(cases[i].excluded_resource_types);

    IndexedRule indexed_rule;
    ParseResult result = IndexedRule::CreateIndexedRule(
        std::move(rule), GetBaseURL(), kMinValidStaticRulesetID, &indexed_rule);

    EXPECT_EQ(cases[i].expected_result, result);
    if (result == ParseResult::SUCCESS) {
      EXPECT_EQ(cases[i].expected_element_types, indexed_rule.element_types);
    }
  }
}

TEST_F(IndexedRuleTest, UrlFilterParsing) {
  struct TestCase {
    std::optional<std::string> input_url_filter;

    // Only valid if |expected_result| is SUCCESS.
    const flat_rule::UrlPatternType expected_url_pattern_type;
    const flat_rule::AnchorType expected_anchor_left;
    const flat_rule::AnchorType expected_anchor_right;
    const std::string expected_url_pattern;

    const ParseResult expected_result;
  };

  const auto cases = std::to_array<TestCase>(
      {{std::nullopt, flat_rule::UrlPatternType_SUBSTRING,
        flat_rule::AnchorType_NONE, flat_rule::AnchorType_NONE, "",
        ParseResult::SUCCESS},
       {"", flat_rule::UrlPatternType_SUBSTRING, flat_rule::AnchorType_NONE,
        flat_rule::AnchorType_NONE, "", ParseResult::ERROR_EMPTY_URL_FILTER},
       {"|", flat_rule::UrlPatternType_SUBSTRING,
        flat_rule::AnchorType_BOUNDARY, flat_rule::AnchorType_NONE, "",
        ParseResult::SUCCESS},
       {"||", flat_rule::UrlPatternType_SUBSTRING,
        flat_rule::AnchorType_SUBDOMAIN, flat_rule::AnchorType_NONE, "",
        ParseResult::SUCCESS},
       {"|||", flat_rule::UrlPatternType_SUBSTRING,
        flat_rule::AnchorType_SUBDOMAIN, flat_rule::AnchorType_BOUNDARY, "",
        ParseResult::SUCCESS},
       {"|*|||", flat_rule::UrlPatternType_WILDCARDED,
        flat_rule::AnchorType_BOUNDARY, flat_rule::AnchorType_BOUNDARY, "*||",
        ParseResult::SUCCESS},
       {"|xyz|", flat_rule::UrlPatternType_SUBSTRING,
        flat_rule::AnchorType_BOUNDARY, flat_rule::AnchorType_BOUNDARY, "xyz",
        ParseResult::SUCCESS},
       {"||x^yz", flat_rule::UrlPatternType_WILDCARDED,
        flat_rule::AnchorType_SUBDOMAIN, flat_rule::AnchorType_NONE, "x^yz",
        ParseResult::SUCCESS},
       {"||xyz|", flat_rule::UrlPatternType_SUBSTRING,
        flat_rule::AnchorType_SUBDOMAIN, flat_rule::AnchorType_BOUNDARY, "xyz",
        ParseResult::SUCCESS},
       {"x*y|z", flat_rule::UrlPatternType_WILDCARDED,
        flat_rule::AnchorType_NONE, flat_rule::AnchorType_NONE, "x*y|z",
        ParseResult::SUCCESS},
       {"**^", flat_rule::UrlPatternType_WILDCARDED, flat_rule::AnchorType_NONE,
        flat_rule::AnchorType_NONE, "**^", ParseResult::SUCCESS},
       {"||google.com", flat_rule::UrlPatternType_SUBSTRING,
        flat_rule::AnchorType_SUBDOMAIN, flat_rule::AnchorType_NONE,
        "google.com", ParseResult::SUCCESS},
       // Url pattern with non-ascii characters -ⱴase.com.
       {base::WideToUTF8(L"\x2c74"
                         L"ase.com"),
        flat_rule::UrlPatternType_SUBSTRING, flat_rule::AnchorType_NONE,
        flat_rule::AnchorType_NONE, "",
        ParseResult::ERROR_NON_ASCII_URL_FILTER},
       // Url pattern starting with the domain anchor followed by a wildcard.
       {"||*xyz", flat_rule::UrlPatternType_WILDCARDED,
        flat_rule::AnchorType_SUBDOMAIN, flat_rule::AnchorType_NONE, "",
        ParseResult::ERROR_INVALID_URL_FILTER}});

  for (size_t i = 0; i < std::size(cases); ++i) {
    SCOPED_TRACE(base::StringPrintf("Testing case[%zu]", i));
    dnr_api::Rule rule = CreateGenericParsedRule();
    rule.condition.url_filter = std::move(cases[i].input_url_filter);

    IndexedRule indexed_rule;
    ParseResult result = IndexedRule::CreateIndexedRule(
        std::move(rule), GetBaseURL(), kMinValidStaticRulesetID, &indexed_rule);
    if (result != ParseResult::SUCCESS) {
      continue;
    }

    EXPECT_EQ(cases[i].expected_result, result);
    EXPECT_EQ(cases[i].expected_url_pattern_type,
              indexed_rule.url_pattern_type);
    EXPECT_EQ(cases[i].expected_anchor_left, indexed_rule.anchor_left);
    EXPECT_EQ(cases[i].expected_anchor_right, indexed_rule.anchor_right);
    EXPECT_EQ(cases[i].expected_url_pattern, indexed_rule.url_pattern);
  }
}

// Ensure case-insensitive patterns are lower-cased as required by
// url_pattern_index.
TEST_F(IndexedRuleTest, CaseInsensitiveLowerCased) {
  const std::string kPattern = "/QUERY";
  static constexpr struct TestCase {
    std::optional<bool> is_url_filter_case_sensitive;
    std::string_view expected_pattern;
  } test_cases[] = {
      {false, "/query"},
      {true, "/QUERY"},
      {std::nullopt, "/query"}  // By default patterns are case insensitive.
  };

  for (const auto& test_case : test_cases) {
    dnr_api::Rule rule = CreateGenericParsedRule();
    rule.condition.url_filter = kPattern;
    rule.condition.is_url_filter_case_sensitive =
        test_case.is_url_filter_case_sensitive;
    IndexedRule indexed_rule;
    ASSERT_EQ(ParseResult::SUCCESS,
              IndexedRule::CreateIndexedRule(std::move(rule), GetBaseURL(),
                                             kMinValidStaticRulesetID,
                                             &indexed_rule));
    EXPECT_EQ(test_case.expected_pattern, indexed_rule.url_pattern);
  }
}

TEST_F(IndexedRuleTest, DomainsParsing) {
  using DomainVec = std::vector<std::string>;
  struct TestCase {
    std::optional<DomainVec> domains;
    std::optional<DomainVec> excluded_domains;
    const ParseResult expected_result;
    // Only valid if |expected_result| is SUCCESS.
    const DomainVec expected_domains;
    const DomainVec expected_excluded_domains;
  };

  const auto cases = std::to_array<TestCase>({
      {std::nullopt, std::nullopt, ParseResult::SUCCESS, {}, {}},
      {{{}}, std::nullopt, ParseResult::ERROR_EMPTY_DOMAINS_LIST, {}, {}},
      {std::nullopt, {{}}, ParseResult::SUCCESS, {}, {}},
      {DomainVec({"a.com", "b.com", "a.com"}),
       DomainVec({"g.com", "XY.COM", "zzz.com", "a.com", "google.com"}),
       ParseResult::SUCCESS,
       {"a.com", "a.com", "b.com"},
       {"google.com", "zzz.com", "xy.com", "a.com", "g.com"}},
      // Domain with non-ascii characters.
      {DomainVec({base::WideToUTF8(L"abc\x2010" /*hyphen*/ L"def.com")}),
       std::nullopt,
       ParseResult::ERROR_NON_ASCII_DOMAIN,
       {},
       {}},
      // Excluded domain with non-ascii characters.
      {std::nullopt,
       DomainVec({base::WideToUTF8(L"36\x00b0c.com" /*36°c.com*/)}),
       ParseResult::ERROR_NON_ASCII_EXCLUDED_DOMAIN,
       {},
       {}},
      // Internationalized domain in punycode.
      {DomainVec({"xn--36c-tfa.com" /* punycode for 36°c.com*/}),
       std::nullopt,
       ParseResult::SUCCESS,
       {"xn--36c-tfa.com"},
       {}},
  });

  for (size_t i = 0; i < std::size(cases); ++i) {
    SCOPED_TRACE(base::StringPrintf("Testing case[%zu]", i));
    dnr_api::Rule domains_rule = CreateGenericParsedRule();
    dnr_api::Rule initiator_domains_rule = CreateGenericParsedRule();
    dnr_api::Rule request_domains_rule = CreateGenericParsedRule();
    dnr_api::Rule top_domains_rule = CreateGenericParsedRule();

    if (cases[i].domains) {
      domains_rule.condition.domains = *cases[i].domains;
      initiator_domains_rule.condition.initiator_domains = *cases[i].domains;
      request_domains_rule.condition.request_domains = *cases[i].domains;
      top_domains_rule.condition.top_domains = *cases[i].domains;
    }

    if (cases[i].excluded_domains) {
      domains_rule.condition.excluded_domains = *cases[i].excluded_domains;
      initiator_domains_rule.condition.excluded_initiator_domains =
          *cases[i].excluded_domains;
      request_domains_rule.condition.excluded_request_domains =
          *cases[i].excluded_domains;
      top_domains_rule.condition.excluded_top_domains =
          *cases[i].excluded_domains;
    }

    IndexedRule indexed_domains_rule;
    ParseResult domains_result = IndexedRule::CreateIndexedRule(
        std::move(domains_rule), GetBaseURL(), kMinValidStaticRulesetID,
        &indexed_domains_rule);

    IndexedRule indexed_initiator_domains_rule;
    ParseResult initiator_domains_result = IndexedRule::CreateIndexedRule(
        std::move(initiator_domains_rule), GetBaseURL(),
        kMinValidStaticRulesetID, &indexed_initiator_domains_rule);

    IndexedRule indexed_request_domains_rule;
    ParseResult request_domains_result = IndexedRule::CreateIndexedRule(
        std::move(request_domains_rule), GetBaseURL(), kMinValidStaticRulesetID,
        &indexed_request_domains_rule);

    IndexedRule indexed_top_domains_rule;
    ParseResult top_domains_result = IndexedRule::CreateIndexedRule(
        std::move(top_domains_rule), GetBaseURL(), kMinValidStaticRulesetID,
        &indexed_top_domains_rule);

    EXPECT_EQ(cases[i].expected_result, domains_result);

    switch (cases[i].expected_result) {
      case ParseResult::ERROR_EMPTY_DOMAINS_LIST:
        EXPECT_EQ(ParseResult::ERROR_EMPTY_INITIATOR_DOMAINS_LIST,
                  initiator_domains_result);
        EXPECT_EQ(ParseResult::ERROR_EMPTY_REQUEST_DOMAINS_LIST,
                  request_domains_result);
        EXPECT_EQ(ParseResult::ERROR_EMPTY_TOP_DOMAINS_LIST,
                  top_domains_result);
        break;
      case ParseResult::ERROR_NON_ASCII_DOMAIN:
        EXPECT_EQ(ParseResult::ERROR_NON_ASCII_INITIATOR_DOMAIN,
                  initiator_domains_result);
        EXPECT_EQ(ParseResult::ERROR_NON_ASCII_REQUEST_DOMAIN,
                  request_domains_result);
        EXPECT_EQ(ParseResult::ERROR_NON_ASCII_TOP_DOMAIN, top_domains_result);
        break;
      case ParseResult::ERROR_NON_ASCII_EXCLUDED_DOMAIN:
        EXPECT_EQ(ParseResult::ERROR_NON_ASCII_EXCLUDED_INITIATOR_DOMAIN,
                  initiator_domains_result);
        EXPECT_EQ(ParseResult::ERROR_NON_ASCII_EXCLUDED_REQUEST_DOMAIN,
                  request_domains_result);
        EXPECT_EQ(ParseResult::ERROR_NON_ASCII_EXCLUDED_TOP_DOMAIN,
                  top_domains_result);
        break;
      default:
        EXPECT_EQ(cases[i].expected_result, initiator_domains_result);
        EXPECT_EQ(cases[i].expected_result, request_domains_result);
        EXPECT_EQ(cases[i].expected_result, top_domains_result);
    }

    if (cases[i].expected_result == ParseResult::SUCCESS) {
      // The `domains` and `excluded_domains` rule conditions are deprecated and
      // mapped to `initiator_domains` and `excluded_initiator_domains`.
      EXPECT_EQ(cases[i].expected_domains,
                indexed_domains_rule.initiator_domains);
      EXPECT_EQ(cases[i].expected_excluded_domains,
                indexed_domains_rule.excluded_initiator_domains);

      EXPECT_EQ(cases[i].expected_domains,
                indexed_initiator_domains_rule.initiator_domains);
      EXPECT_EQ(cases[i].expected_excluded_domains,
                indexed_initiator_domains_rule.excluded_initiator_domains);

      EXPECT_EQ(cases[i].expected_domains,
                indexed_request_domains_rule.request_domains);
      EXPECT_EQ(cases[i].expected_excluded_domains,
                indexed_request_domains_rule.excluded_request_domains);

      EXPECT_EQ(cases[i].expected_domains,
                indexed_top_domains_rule.top_domains);
      EXPECT_EQ(cases[i].expected_excluded_domains,
                indexed_top_domains_rule.excluded_top_domains);
    }
  }

  // Test that attempting to include both domains + initiatorDomains, or
  // excludedDomains + excludedInitiatorDomains results in an parsing error.
  dnr_api::Rule both_domains_rule = CreateGenericParsedRule();
  dnr_api::Rule both_excluded_domains_rule = CreateGenericParsedRule();
  both_domains_rule.condition.domains = DomainVec({"foo"});
  both_domains_rule.condition.initiator_domains = DomainVec({"bar"});
  both_excluded_domains_rule.condition.excluded_domains = DomainVec({"flib"});
  both_excluded_domains_rule.condition.excluded_initiator_domains =
      DomainVec({"flob"});

  IndexedRule indexed_both_domains_rule;
  IndexedRule indexed_both_excluded_domains_rule;
  EXPECT_EQ(ParseResult::ERROR_DOMAINS_AND_INITIATOR_DOMAINS_BOTH_SPECIFIED,
            IndexedRule::CreateIndexedRule(
                std::move(both_domains_rule), GetBaseURL(),
                kMinValidStaticRulesetID, &indexed_both_domains_rule));
  EXPECT_EQ(
      ParseResult::
          ERROR_EXCLUDED_DOMAINS_AND_EXCLUDED_INITIATOR_DOMAINS_BOTH_SPECIFIED,
      IndexedRule::CreateIndexedRule(std::move(both_excluded_domains_rule),
                                     GetBaseURL(), kMinValidStaticRulesetID,
                                     &indexed_both_excluded_domains_rule));
}

TEST_F(IndexedRuleTest, RedirectUrlParsing) {
  static constexpr struct {
    const char* redirect_url;
    const ParseResult expected_result;
    // Only valid if |expected_result| is SUCCESS.
    const std::string_view expected_redirect_url;
  } cases[] = {
      {"", ParseResult::ERROR_INVALID_REDIRECT_URL, ""},
      {"http://google.com", ParseResult::SUCCESS, "http://google.com"},
      {"/relative/url?q=1", ParseResult::ERROR_INVALID_REDIRECT_URL, ""},
      {"abc", ParseResult::ERROR_INVALID_REDIRECT_URL, ""},
  };

  for (const auto& test_case : cases) {
    SCOPED_TRACE(test_case.redirect_url);
    dnr_api::Rule rule = CreateGenericParsedRule();
    rule.action.redirect = MakeRedirectUrl(test_case.redirect_url);
    rule.action.type = dnr_api::RuleActionType::kRedirect;

    IndexedRule indexed_rule;
    ParseResult result = IndexedRule::CreateIndexedRule(
        std::move(rule), GetBaseURL(), kMinValidStaticRulesetID, &indexed_rule);

    EXPECT_EQ(test_case.expected_result, result) << static_cast<int>(result);
    if (result == ParseResult::SUCCESS) {
      EXPECT_EQ(test_case.expected_redirect_url, indexed_rule.redirect_url);
    }
  }
}

TEST_F(IndexedRuleTest, RedirectParsing) {
  struct Cases {
    std::string redirect_dictionary_json;
    ParseResult expected_result;
    std::optional<std::string> expected_redirect_url;
  };
  auto cases = std::to_array<Cases>({
      // clang-format off
    {
      "{}",
      ParseResult::ERROR_INVALID_REDIRECT,
      std::nullopt
    },
    {
      R"({"url": "xyz"})",
      ParseResult::ERROR_INVALID_REDIRECT_URL,
      std::nullopt
    },
    {
      R"({"url": "javascript:window.alert(\"hello,world\");"})",
      ParseResult::ERROR_JAVASCRIPT_REDIRECT,
      std::nullopt
    },
    {
      R"({"url": "http://google.com"})",
      ParseResult::SUCCESS,
      std::string("http://google.com")
    },
    {
      R"({"extensionPath": "foo/xyz/"})",
      ParseResult::ERROR_INVALID_EXTENSION_PATH,
      std::nullopt
    },
    {
      R"({"extensionPath": "/foo/xyz?q=1"})",
      ParseResult::SUCCESS,
      GetBaseURL().Resolve("/foo/xyz?q=1").spec()
    },
    {
      R"(
      {
        "transform": {
          "scheme": "",
          "host": "foo.com"
        }
      })", ParseResult::ERROR_INVALID_TRANSFORM_SCHEME, std::nullopt
    },
    {
      R"(
      {
        "transform": {
          "scheme": "javascript",
          "host": "foo.com"
        }
      })", ParseResult::ERROR_INVALID_TRANSFORM_SCHEME, std::nullopt
    },
    {
      R"(
      {
        "transform": {
          "scheme": "http",
          "port": "-1"
        }
      })", ParseResult::ERROR_INVALID_TRANSFORM_PORT, std::nullopt
    },
    {
      R"(
      {
        "transform": {
          "scheme": "http",
          "query": "abc"
        }
      })", ParseResult::ERROR_INVALID_TRANSFORM_QUERY, std::nullopt
    },
    {
      R"({"transform": {"path": "abc"}})",
      ParseResult::SUCCESS,
      std::nullopt
    },
    {
      R"({"transform": {"fragment": "abc"}})",
      ParseResult::ERROR_INVALID_TRANSFORM_FRAGMENT,
      std::nullopt
    },
    {
      R"({"transform": {"path": ""}})",
      ParseResult::SUCCESS,
      std::nullopt
    },
    {
      R"(
      {
        "transform": {
          "scheme": "http",
          "query": "?abc",
          "queryTransform": {
            "removeParams": ["abc"]
          }
        }
      })", ParseResult::ERROR_QUERY_AND_TRANSFORM_BOTH_SPECIFIED, std::nullopt
    },
    {
      R"(
      {
        "transform": {
          "scheme": "https",
          "host": "foo.com",
          "port": "80",
          "path": "/foo",
          "queryTransform": {
            "removeParams": ["x1", "x2"],
            "addOrReplaceParams": [
              {"key": "y1", "value": "foo"}
            ]
          },
          "fragment": "",
          "username": "user"
        }
      })", ParseResult::SUCCESS, std::nullopt
    },
  });
  // clang-format on

  for (const auto& test_case : cases) {
    SCOPED_TRACE(test_case.redirect_dictionary_json);
    dnr_api::Rule rule = CreateGenericParsedRule();
    rule.action.type = dnr_api::RuleActionType::kRedirect;

    auto redirect = dnr_api::Redirect::FromValue(
        base::test::ParseJsonDict(test_case.redirect_dictionary_json));
    ASSERT_TRUE(redirect.has_value());
    rule.action.redirect = std::move(redirect).value();

    IndexedRule indexed_rule;
    ParseResult result = IndexedRule::CreateIndexedRule(
        std::move(rule), GetBaseURL(), kMinValidStaticRulesetID, &indexed_rule);
    EXPECT_EQ(test_case.expected_result, result) << static_cast<int>(result);
    if (result != ParseResult::SUCCESS) {
      continue;
    }

    EXPECT_TRUE(indexed_rule.url_transform || indexed_rule.redirect_url);
    EXPECT_FALSE(indexed_rule.url_transform && indexed_rule.redirect_url);
    EXPECT_EQ(test_case.expected_redirect_url, indexed_rule.redirect_url);
  }
}

TEST_F(IndexedRuleTest, RegexFilterParsing) {
  static constexpr struct {
    std::string_view regex_filter;
    ParseResult result;
  } cases[] = {{"", ParseResult::ERROR_EMPTY_REGEX_FILTER},
               // Filter with non-ascii characters.
               {"αcd", ParseResult::ERROR_NON_ASCII_REGEX_FILTER},
               // Invalid regex: Unterminated character class.
               {"x[ab", ParseResult::ERROR_INVALID_REGEX_FILTER},
               // Invalid regex: Incomplete capturing group.
               {"x(", ParseResult::ERROR_INVALID_REGEX_FILTER},
               // Invalid regex: Invalid escape sequence \x.
               {R"(ij\x1)", ParseResult::ERROR_INVALID_REGEX_FILTER},
               {R"(ij\\x1)", ParseResult::SUCCESS},
               {R"(^http://www\.(abc|def)\.xyz\.com/)", ParseResult::SUCCESS}};

  for (const auto& test_case : cases) {
    SCOPED_TRACE(test_case.regex_filter);
    dnr_api::Rule rule = CreateGenericParsedRule();
    rule.condition.url_filter.reset();
    rule.condition.regex_filter = test_case.regex_filter;

    IndexedRule indexed_rule;
    ParseResult result = IndexedRule::CreateIndexedRule(
        std::move(rule), GetBaseURL(), kMinValidStaticRulesetID, &indexed_rule);
    EXPECT_EQ(result, test_case.result);

    if (result == ParseResult::SUCCESS) {
      EXPECT_EQ(indexed_rule.url_pattern, test_case.regex_filter);
      EXPECT_EQ(flat_rule::UrlPatternType_REGEXP,
                indexed_rule.url_pattern_type);
    }
  }
}

TEST_F(IndexedRuleTest, MultipleFiltersSpecified) {
  dnr_api::Rule rule = CreateGenericParsedRule();
  rule.condition.url_filter = "google";
  rule.condition.regex_filter = "example";

  IndexedRule indexed_rule;
  ParseResult result = IndexedRule::CreateIndexedRule(
      std::move(rule), GetBaseURL(), kMinValidStaticRulesetID, &indexed_rule);
  EXPECT_EQ(ParseResult::ERROR_MULTIPLE_FILTERS_SPECIFIED, result);
}

TEST_F(IndexedRuleTest, RegexSubstitutionParsing) {
  static constexpr struct {
    // |regex_filter| may be null.
    const char* regex_filter;
    std::string_view regex_substitution;
    ParseResult result;
  } cases[] = {
      {nullptr, "http://google.com",
       ParseResult::ERROR_REGEX_SUBSTITUTION_WITHOUT_FILTER},
      // \0 in |regex_substitution| refers to the entire matching text.
      {R"(^http://(.*)\.com/)", R"(https://redirect.com?referrer=\0)",
       ParseResult::SUCCESS},
      {R"(^http://google\.com?q1=(.*)&q2=(.*))",
       R"(https://redirect.com?&q1=\0&q2=\2)", ParseResult::SUCCESS},
      // Referencing invalid capture group.
      {R"(^http://google\.com?q1=(.*)&q2=(.*))",
       R"(https://redirect.com?&q1=\1&q2=\3)",
       ParseResult::ERROR_INVALID_REGEX_SUBSTITUTION},
      // Empty substitution.
      {R"(^http://(.*)\.com/)", "",
       ParseResult::ERROR_INVALID_REGEX_SUBSTITUTION},
      // '\' must always be followed by a '\' or a digit.
      {R"(^http://(.*)\.com/)", R"(https://example.com?q=\a)",
       ParseResult::ERROR_INVALID_REGEX_SUBSTITUTION},
  };

  for (const auto& test_case : cases) {
    SCOPED_TRACE(test_case.regex_substitution);

    dnr_api::Rule rule = CreateGenericParsedRule();
    rule.condition.url_filter.reset();
    if (test_case.regex_filter) {
      rule.condition.regex_filter = test_case.regex_filter;
    }

    rule.priority = kMinValidPriority;
    rule.action.type = dnr_api::RuleActionType::kRedirect;
    rule.action.redirect.emplace();
    rule.action.redirect->regex_substitution = test_case.regex_substitution;

    IndexedRule indexed_rule;
    ParseResult result = IndexedRule::CreateIndexedRule(
        std::move(rule), GetBaseURL(), kMinValidStaticRulesetID, &indexed_rule);
    EXPECT_EQ(test_case.result, result);

    if (result == ParseResult::SUCCESS) {
      EXPECT_EQ(flat_rule::UrlPatternType_REGEXP,
                indexed_rule.url_pattern_type);
      ASSERT_TRUE(indexed_rule.regex_substitution);
      EXPECT_EQ(test_case.regex_substitution, *indexed_rule.regex_substitution);
    }
  }
}

// Tests the parsing behavior when multiple keys in "Redirect" dictionary are
// specified.
TEST_F(IndexedRuleTest, MultipleRedirectKeys) {
  dnr_api::Rule rule = CreateGenericParsedRule();
  rule.priority = kMinValidPriority;
  rule.condition.url_filter.reset();
  rule.condition.regex_filter = "\\.*";
  rule.action.type = dnr_api::RuleActionType::kRedirect;
  rule.action.redirect.emplace();

  dnr_api::Redirect& redirect = *rule.action.redirect;
  redirect.url = "http://google.com";
  redirect.regex_substitution = "http://example.com";
  redirect.transform.emplace();
  redirect.transform->scheme = "https";

  IndexedRule indexed_rule;
  ParseResult result = IndexedRule::CreateIndexedRule(
      std::move(rule), GetBaseURL(), kMinValidStaticRulesetID, &indexed_rule);
  EXPECT_EQ(ParseResult::SUCCESS, result);

  // The redirect "url" is given preference in this case.
  EXPECT_FALSE(indexed_rule.url_transform);
  EXPECT_FALSE(indexed_rule.regex_substitution);
  EXPECT_EQ("http://google.com", indexed_rule.redirect_url);
}

TEST_F(IndexedRuleTest, InvalidAllowAllRequestsResourceType) {
  using ResourceTypeVec = std::vector<dnr_api::ResourceType>;

  struct TestCase {
    ResourceTypeVec resource_types;
    ResourceTypeVec excluded_resource_types;
    const ParseResult expected_result;
    // Only valid if |expected_result| is SUCCESS.
    const uint16_t expected_element_types;
  };

  const auto cases = std::to_array<TestCase>({
      {{}, {}, ParseResult::ERROR_INVALID_ALLOW_ALL_REQUESTS_RESOURCE_TYPE, 0},
      {{dnr_api::ResourceType::kSubFrame},
       {dnr_api::ResourceType::kScript},
       ParseResult::SUCCESS,
       flat_rule::ElementType_SUBDOCUMENT},
      {{dnr_api::ResourceType::kScript, dnr_api::ResourceType::kMainFrame},
       {},
       ParseResult::ERROR_INVALID_ALLOW_ALL_REQUESTS_RESOURCE_TYPE,
       0},
      {{dnr_api::ResourceType::kMainFrame, dnr_api::ResourceType::kSubFrame},
       {},
       ParseResult::SUCCESS,
       flat_rule::ElementType_MAIN_FRAME | flat_rule::ElementType_SUBDOCUMENT},
      {{dnr_api::ResourceType::kMainFrame},
       {},
       ParseResult::SUCCESS,
       flat_rule::ElementType_MAIN_FRAME},
  });

  for (size_t i = 0; i < std::size(cases); ++i) {
    SCOPED_TRACE(base::StringPrintf("Testing case[%zu]", i));
    dnr_api::Rule rule = CreateGenericParsedRule();

    if (cases[i].resource_types.empty()) {
      rule.condition.resource_types = std::nullopt;
    } else {
      rule.condition.resource_types = cases[i].resource_types;
    }

    rule.condition.excluded_resource_types = cases[i].excluded_resource_types;
    rule.action.type = dnr_api::RuleActionType::kAllowAllRequests;

    IndexedRule indexed_rule;
    ParseResult result = IndexedRule::CreateIndexedRule(
        std::move(rule), GetBaseURL(), kMinValidStaticRulesetID, &indexed_rule);

    EXPECT_EQ(cases[i].expected_result, result);
    if (result == ParseResult::SUCCESS) {
      EXPECT_EQ(cases[i].expected_element_types, indexed_rule.element_types);
    }
  }
}

TEST_F(IndexedRuleTest, ModifyHeadersParsing) {
  struct RawHeaderInfo {
    dnr_api::HeaderOperation operation;
    std::string header;
    std::optional<std::string> value;

    std::optional<std::string> regex_filter;
    std::optional<std::string> regex_substitution;
  };

  using RawHeaderInfoList = std::vector<RawHeaderInfo>;
  using ModifyHeaderInfoList = std::vector<dnr_api::ModifyHeaderInfo>;

  // A copy-able version of dnr_api::ModifyHeaderInfo is used for ease of
  // specifying test cases because elements are copied when initializing a
  // vector from an array.
  struct TestCase {
    std::optional<RawHeaderInfoList> request_headers;
    std::optional<RawHeaderInfoList> response_headers;
    ParseResult expected_result;
  };

  const auto cases = std::to_array<TestCase>({
      // Raise an error if no headers are specified.
      {std::nullopt, std::nullopt,
       ParseResult::ERROR_NO_HEADERS_TO_MODIFY_SPECIFIED},

      // Raise an error if the request or response headers list is specified,
      // but empty.
      {RawHeaderInfoList(),
       RawHeaderInfoList(
           {{dnr_api::HeaderOperation::kRemove, "set-cookie", std::nullopt}}),
       ParseResult::ERROR_EMPTY_MODIFY_REQUEST_HEADERS_LIST},

      {std::nullopt, RawHeaderInfoList(),
       ParseResult::ERROR_EMPTY_MODIFY_RESPONSE_HEADERS_LIST},

      // Raise an error if a header list contains an empty or invalid header
      // name.
      {std::nullopt,
       RawHeaderInfoList(
           {{dnr_api::HeaderOperation::kRemove, "", std::nullopt}}),
       ParseResult::ERROR_INVALID_HEADER_TO_MODIFY_NAME},

      {std::nullopt,
       RawHeaderInfoList(
           {{dnr_api::HeaderOperation::kRemove, "<<invalid>>", std::nullopt}}),
       ParseResult::ERROR_INVALID_HEADER_TO_MODIFY_NAME},

      // Raise an error if a header list contains an invalid header value.
      {std::nullopt,
       RawHeaderInfoList({{dnr_api::HeaderOperation::kAppend, "set-cookie",
                           "invalid\nvalue"}}),
       ParseResult::ERROR_INVALID_HEADER_TO_MODIFY_VALUE},

      // Raise an error if a header value is specified for a remove rule.
      {RawHeaderInfoList(
           {{dnr_api::HeaderOperation::kRemove, "cookie", "remove"}}),
       std::nullopt, ParseResult::ERROR_HEADER_VALUE_PRESENT},

      // Raise an error if no header value is specified for an append or set
      // rule.
      {RawHeaderInfoList(
           {{dnr_api::HeaderOperation::kSet, "cookie", std::nullopt}}),
       std::nullopt, ParseResult::ERROR_HEADER_VALUE_NOT_SPECIFIED},

      {std::nullopt,
       RawHeaderInfoList(
           {{dnr_api::HeaderOperation::kAppend, "set-cookie", std::nullopt}}),
       ParseResult::ERROR_HEADER_VALUE_NOT_SPECIFIED},

      // Raise an error if a rule specifies an invalid request header to be
      // appended.
      {RawHeaderInfoList(
           {{dnr_api::HeaderOperation::kAppend, "invalid-header", "value"}}),
       std::nullopt, ParseResult::ERROR_APPEND_INVALID_REQUEST_HEADER},

      {RawHeaderInfoList(
           {{dnr_api::HeaderOperation::kRemove, "cookie", std::nullopt},
            {dnr_api::HeaderOperation::kSet, "referer", ""},
            // Header matching is case-insensitive, so header names with
            // uppercase
            // characters are fine.
            {dnr_api::HeaderOperation::kAppend, "Accept-language", "en-US"}}),
       std::nullopt, ParseResult::SUCCESS},

      {RawHeaderInfoList(
           {{dnr_api::HeaderOperation::kRemove, "referer", std::nullopt}}),
       RawHeaderInfoList(
           {{dnr_api::HeaderOperation::kAppend, "set-cookie", "abcd"}}),
       ParseResult::SUCCESS},
  });

  for (size_t i = 0; i < std::size(cases); ++i) {
    SCOPED_TRACE(base::StringPrintf("Testing case[%zu]", i));
    dnr_api::Rule rule = CreateGenericParsedRule();
    rule.action.type = dnr_api::RuleActionType::kModifyHeaders;

    ModifyHeaderInfoList expected_request_headers;
    if (cases[i].request_headers) {
      rule.action.request_headers.emplace();
      for (auto header : *cases[i].request_headers) {
        rule.action.request_headers->push_back(CreateModifyHeaderInfo(
            header.operation, header.header, header.value, header.regex_filter,
            header.regex_substitution));

        expected_request_headers.push_back(CreateModifyHeaderInfo(
            header.operation, header.header, header.value, header.regex_filter,
            header.regex_substitution));
      }
    }

    ModifyHeaderInfoList expected_response_headers;
    if (cases[i].response_headers) {
      rule.action.response_headers.emplace();
      for (auto header : *cases[i].response_headers) {
        rule.action.response_headers->push_back(CreateModifyHeaderInfo(
            header.operation, header.header, header.value, header.regex_filter,
            header.regex_substitution));

        expected_response_headers.push_back(CreateModifyHeaderInfo(
            header.operation, header.header, header.value, header.regex_filter,
            header.regex_substitution));
      }
    }

    IndexedRule indexed_rule;
    ParseResult result = IndexedRule::CreateIndexedRule(
        std::move(rule), GetBaseURL(), kMinValidStaticRulesetID, &indexed_rule);
    EXPECT_EQ(cases[i].expected_result, result);
    if (result != ParseResult::SUCCESS) {
      continue;
    }

    EXPECT_EQ(dnr_api::RuleActionType::kModifyHeaders,
              indexed_rule.action_type);

    EXPECT_TRUE(std::ranges::equal(expected_request_headers,
                                   indexed_rule.request_headers_to_modify,
                                   EqualsForTesting));
    EXPECT_TRUE(std::ranges::equal(expected_response_headers,
                                   indexed_rule.response_headers_to_modify,
                                   EqualsForTesting));
  }
}

TEST_F(IndexedRuleTest, RequestMethodsParsing) {
  using RequestMethodVec = std::vector<dnr_api::RequestMethod>;

  struct TestCase {
    std::optional<RequestMethodVec> request_methods;
    std::optional<RequestMethodVec> excluded_request_methods;
    const ParseResult expected_result;
    // Only valid if `expected_result` is SUCCESS.
    const uint16_t expected_request_methods_mask;
  };

  const auto cases = std::to_array<TestCase>(
      {{std::nullopt, std::nullopt, ParseResult::SUCCESS,
        flat_rule::RequestMethod_ANY},
       {std::nullopt, RequestMethodVec({dnr_api::RequestMethod::kPut}),
        ParseResult::SUCCESS,
        flat_rule::RequestMethod_ANY & ~flat_rule::RequestMethod_PUT},
       {RequestMethodVec(
            {dnr_api::RequestMethod::kDelete, dnr_api::RequestMethod::kGet}),
        std::nullopt, ParseResult::SUCCESS,
        flat_rule::RequestMethod_DELETE | flat_rule::RequestMethod_GET},
       {RequestMethodVec({dnr_api::RequestMethod::kHead,
                          dnr_api::RequestMethod::kOptions,
                          dnr_api::RequestMethod::kPatch}),
        std::nullopt, ParseResult::SUCCESS,
        flat_rule::RequestMethod_HEAD | flat_rule::RequestMethod_OPTIONS |
            flat_rule::RequestMethod_PATCH},
       {RequestMethodVec({dnr_api::RequestMethod::kPost}),
        RequestMethodVec({dnr_api::RequestMethod::kPost}),
        ParseResult::ERROR_REQUEST_METHOD_DUPLICATED,
        flat_rule::RequestMethod_NONE},
       {{{}},
        std::nullopt,
        ParseResult::ERROR_EMPTY_REQUEST_METHODS_LIST,
        flat_rule::RequestMethod_NONE}});

  for (size_t i = 0; i < std::size(cases); ++i) {
    SCOPED_TRACE(base::StringPrintf("Testing case[%zu]", i));
    dnr_api::Rule rule = CreateGenericParsedRule();
    rule.condition.request_methods = std::move(cases[i].request_methods);
    rule.condition.excluded_request_methods =
        std::move(cases[i].excluded_request_methods);

    IndexedRule indexed_rule;
    ParseResult result = IndexedRule::CreateIndexedRule(
        std::move(rule), GetBaseURL(), kMinValidStaticRulesetID, &indexed_rule);

    EXPECT_EQ(cases[i].expected_result, result);
    if (result == ParseResult::SUCCESS) {
      EXPECT_EQ(cases[i].expected_request_methods_mask,
                indexed_rule.request_methods);
    }
  }
}

TEST_F(IndexedRuleTest, TabID) {
  using IntVec = std::vector<int>;
  struct TestCase {
    std::optional<IntVec> tab_ids;
    std::optional<IntVec> excluded_tab_ids;
    RulesetID ruleset_id;
    ParseResult expected_result;

    // Only relevant if `expected_result` is ParseResult::SUCCESS.
    base::flat_set<int> expected_tab_ids;
    base::flat_set<int> expected_excluded_tab_ids;
  };

  const auto cases = std::to_array<TestCase>({
      {std::nullopt, std::nullopt, kSessionRulesetID, ParseResult::SUCCESS},
      {IntVec(), IntVec({3, 4, 4}), kSessionRulesetID,
       ParseResult::ERROR_EMPTY_TAB_IDS_LIST},
      {IntVec({1, 2}),
       IntVec({3, 4, 3}),
       kSessionRulesetID,
       ParseResult::SUCCESS,
       {1, 2},
       {}},
      {std::nullopt,
       IntVec({3, 4, 3}),
       kSessionRulesetID,
       ParseResult::SUCCESS,
       {},
       {3, 4}},
      {IntVec({1, 2, 3}), IntVec({5, 2}), kSessionRulesetID,
       ParseResult::ERROR_TAB_ID_DUPLICATED},
      {IntVec({1, 2}), std::nullopt, kDynamicRulesetID,
       ParseResult::ERROR_TAB_IDS_ON_NON_SESSION_RULE},
      {IntVec({1, 2}), IntVec({3}), kMinValidStaticRulesetID,
       ParseResult::ERROR_TAB_IDS_ON_NON_SESSION_RULE},
  });

  for (size_t i = 0; i < std::size(cases); ++i) {
    SCOPED_TRACE(base::StringPrintf("Testing case[%zu]", i));
    dnr_api::Rule rule = CreateGenericParsedRule();
    if (cases[i].tab_ids) {
      rule.condition.tab_ids = *cases[i].tab_ids;
    }

    if (cases[i].excluded_tab_ids) {
      rule.condition.excluded_tab_ids = *cases[i].excluded_tab_ids;
    }

    IndexedRule indexed_rule;
    ParseResult result = IndexedRule::CreateIndexedRule(
        std::move(rule), GetBaseURL(), cases[i].ruleset_id, &indexed_rule);
    EXPECT_EQ(cases[i].expected_result, result);
    if (result == ParseResult::SUCCESS) {
      EXPECT_EQ(cases[i].expected_tab_ids, indexed_rule.tab_ids);
      EXPECT_EQ(cases[i].expected_excluded_tab_ids,
                indexed_rule.excluded_tab_ids);
    }
  }
}

class IndexedResponseHeaderRuleTest : public IndexedRuleTest {
 public:
  IndexedResponseHeaderRuleTest() {
    scoped_feature_list_.InitAndEnableFeature(
        extensions_features::kDeclarativeNetRequestResponseHeaderMatching);
  }

 private:
  // TODO(crbug.com/40727004): Once feature is launched to stable and feature
  // flag can be removed, replace usages of this test class with just
  // DeclarativeNetRequestBrowserTest.
  base::test::ScopedFeatureList scoped_feature_list_;
  ScopedCurrentChannel current_channel_override_{version_info::Channel::DEV};
};

// Test the validation of rules that specify response header matching
// conditions.
TEST_F(IndexedResponseHeaderRuleTest, MatchingResponseHeaders) {
  struct RawHeaderInfo {
    explicit RawHeaderInfo(std::string header) : header(std::move(header)) {}
    RawHeaderInfo(std::string header,
                  std::optional<std::vector<std::string>> values,
                  std::optional<std::vector<std::string>> excluded_values)
        : header(std::move(header)),
          values(std::move(values)),
          excluded_values(std::move(excluded_values)) {}

    std::string header;
    std::optional<std::vector<std::string>> values;
    std::optional<std::vector<std::string>> excluded_values;
  };

  using HeaderValues = std::vector<std::string>;
  using HeaderInfoList = std::vector<RawHeaderInfo>;
  struct TestCase {
    std::optional<HeaderInfoList> response_headers;
    std::optional<HeaderInfoList> excluded_response_headers;
    ParseResult expected_result;
  };

  const auto cases = std::to_array<TestCase>({
      // No response headers included or excluded; should parse successfully.
      {std::nullopt, std::nullopt, ParseResult::SUCCESS},

      // only header1 specified, matching on name only should parse
      // successfully.
      {HeaderInfoList({RawHeaderInfo("header1")}), std::nullopt,
       ParseResult::SUCCESS},

      // Valid included and excluded response headers with values and excluded
      // values should parse successfully.
      {HeaderInfoList(
           {{"header1", HeaderValues({"value-1", "value-2"}), std::nullopt},
            {"header1", std::nullopt, HeaderValues({"excluded-value"})}}),
       HeaderInfoList({RawHeaderInfo("excluded-header")}),
       ParseResult::SUCCESS},

      // An empty response header value should parse successfully.
      {HeaderInfoList({{"header", HeaderValues({""}), std::nullopt}}),
       std::nullopt, ParseResult::SUCCESS},

      // An empty matching response header list should trigger an error.
      {HeaderInfoList(), std::nullopt,
       ParseResult::ERROR_EMPTY_RESPONSE_HEADER_MATCHING_LIST},

      // An empty matching excluded response header list should trigger an
      // error.
      {std::nullopt, HeaderInfoList(),
       ParseResult::ERROR_EMPTY_EXCLUDED_RESPONSE_HEADER_MATCHING_LIST},

      // Test that a rule with an empty or invalid response header name will
      // return an error.
      {HeaderInfoList({RawHeaderInfo("")}), std::nullopt,
       ParseResult::ERROR_INVALID_MATCHING_RESPONSE_HEADER_NAME},

      {std::nullopt, HeaderInfoList({RawHeaderInfo("<<invalid_header>>")}),
       ParseResult::ERROR_INVALID_MATCHING_EXCLUDED_RESPONSE_HEADER_NAME},

      // Test that a rule with an invalid response header value will return an
      // error.
      {std::nullopt,
       HeaderInfoList({{"invalid-header-value",
                        HeaderValues({"value\nwith\nnewline"}), std::nullopt}}),
       ParseResult::ERROR_INVALID_MATCHING_RESPONSE_HEADER_VALUE},

      // Test that a rule cannot specify the same header in `response_headers`
      // and `excluded_response_headers` if that header is to be matched based
      // on name only in `excluded_response_headers`.
      {HeaderInfoList({{"repeated-header", HeaderValues({"specific-value"}),
                        std::nullopt}}),
       HeaderInfoList({RawHeaderInfo("repeated-header")}),
       ParseResult::ERROR_MATCHING_RESPONSE_HEADER_DUPLICATED},

      // Test that a rule CAN specify the same header in `response_headers` and
      // `excluded_response_headers` if that header is matched on name AND value
      // in `excluded_response_headers`. In practice, the below rule will match
      // a request if its `excluded_response_headers` condition does not match
      // and its `response_headers` condition matches.
      {HeaderInfoList({{"repeated-header", HeaderValues({"specific-value"}),
                        std::nullopt}}),
       HeaderInfoList({{"repeated-header", HeaderValues({"excluded-value"}),
                        std::nullopt}}),
       ParseResult::SUCCESS},
  });

  auto get_header_info_matcher = [](const RawHeaderInfo& info) {
    return testing::AllOf(
        testing::Field(&dnr_api::HeaderInfo::header, info.header),
        testing::Field(&dnr_api::HeaderInfo::values, info.values),
        testing::Field(&dnr_api::HeaderInfo::excluded_values,
                       info.excluded_values));
  };

  for (size_t i = 0; i < std::size(cases); ++i) {
    SCOPED_TRACE(base::StringPrintf("Testing case[%zu]", i));
    dnr_api::Rule rule = CreateGenericParsedRule();

    std::vector<testing::Matcher<dnr_api::HeaderInfo>> response_header_matchers;
    if (cases[i].response_headers) {
      rule.condition.response_headers.emplace();
      for (const auto& header : *cases[i].response_headers) {
        rule.condition.response_headers->push_back(CreateHeaderInfo(
            header.header, header.values, header.excluded_values));
        response_header_matchers.push_back(get_header_info_matcher(header));
      }
    }

    std::vector<testing::Matcher<dnr_api::HeaderInfo>>
        expected_excluded_response_headers;
    if (cases[i].excluded_response_headers) {
      rule.condition.excluded_response_headers.emplace();
      for (const auto& header : *cases[i].excluded_response_headers) {
        rule.condition.excluded_response_headers->push_back(CreateHeaderInfo(
            header.header, header.values, header.excluded_values));
        expected_excluded_response_headers.push_back(
            get_header_info_matcher(header));
      }
    }

    IndexedRule indexed_rule;
    ParseResult result = IndexedRule::CreateIndexedRule(
        std::move(rule), GetBaseURL(), kMinValidStaticRulesetID, &indexed_rule);
    EXPECT_EQ(cases[i].expected_result, result);
    if (result != ParseResult::SUCCESS) {
      continue;
    }

    // If parsing is successful, test that the `response_headers` and
    // `excluded_response_headers` from `indexed_rule` are the same as what's
    // specified in the test case.
    EXPECT_THAT(indexed_rule.response_headers,
                testing::UnorderedElementsAreArray(response_header_matchers));
    EXPECT_THAT(
        indexed_rule.excluded_response_headers,
        testing::UnorderedElementsAreArray(expected_excluded_response_headers));
  }
}

// Test that response header matching rules may only modify response headers.
TEST_F(IndexedResponseHeaderRuleTest, MatchingResponseHeaders_ModifyHeaders) {
  struct RawModifyHeaderInfo {
    dnr_api::HeaderOperation operation;
    std::string header;
    std::optional<std::string> value;
  };

  using ModifyHeaderInfoList = std::vector<RawModifyHeaderInfo>;
  struct TestCase {
    std::optional<ModifyHeaderInfoList> request_headers_to_modify;
    std::optional<ModifyHeaderInfoList> response_headers_to_modify;
    ParseResult expected_result;
  };

  const auto cases = std::to_array<TestCase>({
      // Two test cases here: one for a rule that tries to modify request
      // headers, one for response headers. The first rule is disallowed since
      // request headers cannot be further modified when it comes time to match
      // on response headers.
      {ModifyHeaderInfoList({{dnr_api::HeaderOperation::kRemove,
                              "request-header", std::nullopt}}),
       std::nullopt,
       ParseResult::ERROR_RESPONSE_HEADER_RULE_CANNOT_MODIFY_REQUEST_HEADERS},

      {std::nullopt,
       ModifyHeaderInfoList(
           {{dnr_api::HeaderOperation::kSet, "response-header", "new-value"}}),
       ParseResult::SUCCESS},
  });

  for (size_t i = 0; i < std::size(cases); ++i) {
    SCOPED_TRACE(base::StringPrintf("Testing case[%zu]", i));
    dnr_api::Rule rule = CreateGenericParsedRule();
    rule.condition.response_headers.emplace();
    rule.condition.response_headers->push_back(
        CreateHeaderInfo("header", std::nullopt, std::nullopt));

    rule.action.type = dnr_api::RuleActionType::kModifyHeaders;
    if (cases[i].request_headers_to_modify) {
      rule.action.request_headers.emplace();
      for (const auto& header : *cases[i].request_headers_to_modify) {
        rule.action.request_headers->push_back(CreateModifyHeaderInfo(
            header.operation, header.header, header.value));
      }
    }

    if (cases[i].response_headers_to_modify) {
      rule.action.response_headers.emplace();
      for (const auto& header : *cases[i].response_headers_to_modify) {
        rule.action.response_headers->push_back(CreateModifyHeaderInfo(
            header.operation, header.header, header.value));
      }
    }

    IndexedRule indexed_rule;
    ParseResult result = IndexedRule::CreateIndexedRule(
        std::move(rule), GetBaseURL(), kMinValidStaticRulesetID, &indexed_rule);
    EXPECT_EQ(cases[i].expected_result, result);
  }
}

class IndexedHeaderSubstitutionRuleTest : public IndexedRuleTest {
 public:
  IndexedHeaderSubstitutionRuleTest() {
    scoped_feature_list_.InitAndEnableFeature(
        extensions_features::kDeclarativeNetRequestHeaderSubstitution);
  }

 private:
  // TODO(crbug.com/352093575): Once feature is launched and feature flag can be
  // removed, replace usages of this test class with just
  // DeclarativeNetRequestBrowserTest.
  base::test::ScopedFeatureList scoped_feature_list_;
};

// Test parsing for regex filters and substitutions inside ModifyHeaderInfo.
TEST_F(IndexedHeaderSubstitutionRuleTest,
       ModifyHeaderInfoRegexFilterAndSubstitutionParsing) {
  struct TestCase {
    std::optional<std::string> regex_filter;
    std::optional<std::string> regex_substitution;
    ParseResult expected_result;
  };

  const auto cases = std::to_array<TestCase>({
      // Test valid cases:
      {"bad-cookie", std::nullopt, ParseResult::SUCCESS},
      {"bad-cookie", "good-cookie=phew", ParseResult::SUCCESS},

      // TODO(crbug.com/352093575): When the feature is about to launch, add a
      // case which requires a regex substitution if the operation is not
      // specified.

      // Test some invalid regex filter cases.
      {"", "new-cookie=coconut", ParseResult::ERROR_EMPTY_REGEX_FILTER},
      {"αcd", "new-cookie=coconut", ParseResult::ERROR_NON_ASCII_REGEX_FILTER},
      // Invalid regex: Incomplete capturing group.
      {"x(", "new-cookie=coconut", ParseResult::ERROR_INVALID_REGEX_FILTER},

      // Test an invalid regex substitution case, where the substitution
      // references more capture groups than what the filter captures.
      {R"(^http://google\.com?q1=(.*)&q2=(.*))",
       R"(https://redirect.com?&q1=\1&q2=\3)",
       ParseResult::ERROR_INVALID_REGEX_SUBSTITUTION},
  });

  for (size_t i = 0; i < std::size(cases); ++i) {
    SCOPED_TRACE(base::StringPrintf("Testing case[%zu]", i));
    dnr_api::Rule rule = CreateGenericParsedRule();
    rule.action.type = dnr_api::RuleActionType::kModifyHeaders;

    rule.action.request_headers.emplace();
    rule.action.request_headers->push_back(CreateModifyHeaderInfo(
        dnr_api::HeaderOperation::kRemove, "cookie", std::nullopt,
        cases[i].regex_filter, cases[i].regex_substitution));

    dnr_api::ModifyHeaderInfo expected_request_header;
    expected_request_header = CreateModifyHeaderInfo(
        dnr_api::HeaderOperation::kRemove, "cookie", std::nullopt,
        cases[i].regex_filter, cases[i].regex_substitution);

    IndexedRule indexed_rule;
    ParseResult result = IndexedRule::CreateIndexedRule(
        std::move(rule), GetBaseURL(), kMinValidStaticRulesetID, &indexed_rule);
    EXPECT_EQ(cases[i].expected_result, result);
  }
}

}  // namespace
}  // namespace extensions::declarative_net_request
