// Copyright 2019 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/composite_matcher.h"

#include <array>
#include <string>
#include <utility>
#include <vector>

#include "base/memory/raw_ref.h"
#include "base/strings/stringprintf.h"
#include "base/test/scoped_feature_list.h"
#include "components/version_info/channel.h"
#include "extensions/browser/api/declarative_net_request/constants.h"
#include "extensions/browser/api/declarative_net_request/file_backed_ruleset_source.h"
#include "extensions/browser/api/declarative_net_request/request_action.h"
#include "extensions/browser/api/declarative_net_request/request_params.h"
#include "extensions/browser/api/declarative_net_request/ruleset_matcher.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/api/declarative_net_request/test_utils.h"
#include "extensions/common/extension_features.h"
#include "extensions/common/features/feature_channel.h"
#include "extensions/common/permissions/permissions_data.h"
#include "net/http/http_request_headers.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"

static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));

namespace extensions::declarative_net_request {
namespace {

using PageAccess = PermissionsData::PageAccess;
using ActionInfo = CompositeMatcher::ActionInfo;

namespace dnr_api = api::declarative_net_request;

using CompositeMatcherTest = ::testing::Test;

TestRule CreateModifyHeadersRule(
    int id,
    int priority,
    std::optional<std::string> url_filter,
    std::optional<std::string> regex_filter,
    std::optional<std::vector<TestHeaderInfo>> request_headers_list,
    std::optional<std::vector<TestHeaderInfo>> response_headers_list) {
  TestRule rule = CreateGenericRule();
  rule.id = id;
  rule.priority = priority;

  if (url_filter) {
    rule.condition->url_filter = url_filter;
  } else if (regex_filter) {
    rule.condition->url_filter.reset();
    rule.condition->regex_filter = regex_filter;
  }

  rule.action->type = std::string("modifyHeaders");
  if (request_headers_list) {
    rule.action->request_headers = std::move(request_headers_list);
  }
  if (response_headers_list) {
    rule.action->response_headers = std::move(response_headers_list);
  }
  return rule;
}

// Ensure that the rules in a CompositeMatcher are in the same priority space.
TEST_F(CompositeMatcherTest, SamePrioritySpace) {
  // Create the first ruleset matcher. It allows requests to google.com.
  TestRule allow_rule = CreateGenericRule();
  allow_rule.id = kMinValidID;
  allow_rule.condition->url_filter = std::string("google.com");
  allow_rule.action->type = std::string("allow");
  allow_rule.priority = 1;
  std::unique_ptr<RulesetMatcher> allow_matcher;
  RulesetID ruleset_id_one(1);
  ASSERT_TRUE(CreateVerifiedMatcher(
      {allow_rule}, CreateTemporarySource(ruleset_id_one), &allow_matcher));

  // Now create the second matcher. It blocks requests to google.com, with
  // higher priority than the allow rule.
  TestRule block_rule = allow_rule;
  block_rule.action->type = std::string("block");
  block_rule.priority = 2;
  std::unique_ptr<RulesetMatcher> block_matcher;
  RulesetID ruleset_id_two(2);
  ASSERT_TRUE(CreateVerifiedMatcher(
      {block_rule}, CreateTemporarySource(ruleset_id_two), &block_matcher));

  // Create a composite matcher with both rulesets.
  std::vector<std::unique_ptr<RulesetMatcher>> matchers;
  matchers.push_back(std::move(allow_matcher));
  matchers.push_back(std::move(block_matcher));
  auto composite_matcher = std::make_unique<CompositeMatcher>(
      std::move(matchers), /*extension_id=*/"",
      HostPermissionsAlwaysRequired::kFalse);

  GURL google_url("http://google.com");
  RequestParams params;
  params.url = &google_url;

  // The block rule should be higher priority.
  ActionInfo action_info = composite_matcher->GetAction(
      params, RulesetMatchingStage::kOnBeforeRequest, PageAccess::kAllowed);
  ASSERT_TRUE(action_info.action);
  EXPECT_EQ(action_info.action->type, RequestAction::Type::BLOCK);

  // Now swap the priority of the rules, which requires re-creating the ruleset
  // matchers and composite matcher.
  allow_rule.priority = 2;
  block_rule.priority = 1;
  ASSERT_TRUE(CreateVerifiedMatcher(
      {allow_rule}, CreateTemporarySource(ruleset_id_one), &allow_matcher));
  ASSERT_TRUE(CreateVerifiedMatcher(
      {block_rule}, CreateTemporarySource(ruleset_id_two), &block_matcher));
  matchers.clear();
  matchers.push_back(std::move(allow_matcher));
  matchers.push_back(std::move(block_matcher));
  composite_matcher = std::make_unique<CompositeMatcher>(
      std::move(matchers), /*extension_id=*/"",
      HostPermissionsAlwaysRequired::kFalse);

  // The allow rule should now have higher priority.
  action_info = composite_matcher->GetAction(
      params, RulesetMatchingStage::kOnBeforeRequest, PageAccess::kAllowed);
  ASSERT_TRUE(action_info.action);
  EXPECT_EQ(action_info.action->type, RequestAction::Type::ALLOW);
}

// Tests the GetModifyHeadersActions method.
TEST_F(CompositeMatcherTest, GetModifyHeadersActions) {
  TestRule rule_1 = CreateModifyHeadersRule(
      kMinValidID, kMinValidPriority, "google.com", std::nullopt,
      std::vector<TestHeaderInfo>(
          {TestHeaderInfo("header1", "remove", std::nullopt),
           TestHeaderInfo("header2", "set", "value2")}),
      std::nullopt);

  TestRule rule_2 = CreateModifyHeadersRule(
      kMinValidID, kMinValidPriority + 1, "/path", std::nullopt, std::nullopt,
      std::vector<TestHeaderInfo>(
          {TestHeaderInfo("header1", "remove", std::nullopt),
           TestHeaderInfo("header2", "append", "VALUE2"),
           TestHeaderInfo("header3", "set", "VALUE3")}));

  // Create the first ruleset matcher, which matches all requests from
  // |google.com|.
  const RulesetID kSource1ID(1);
  std::unique_ptr<RulesetMatcher> matcher_1;
  ASSERT_TRUE(CreateVerifiedMatcher({rule_1}, CreateTemporarySource(kSource1ID),
                                    &matcher_1));

  // Create a second ruleset matcher, which matches all requests with |/path| in
  // their URL.
  const RulesetID kSource2ID(2);
  std::unique_ptr<RulesetMatcher> matcher_2;
  ASSERT_TRUE(CreateVerifiedMatcher({rule_2}, CreateTemporarySource(kSource2ID),
                                    &matcher_2));

  // Create a composite matcher with the two rulesets.
  std::vector<std::unique_ptr<RulesetMatcher>> matchers;
  matchers.push_back(std::move(matcher_1));
  matchers.push_back(std::move(matcher_2));
  auto composite_matcher = std::make_unique<CompositeMatcher>(
      std::move(matchers), /*extension_id=*/"",
      HostPermissionsAlwaysRequired::kFalse);

  GURL google_url = GURL("http://google.com/path");
  RequestParams google_params;
  google_params.url = &google_url;
  google_params.element_type = url_pattern_index::flat::ElementType_SUBDOCUMENT;
  google_params.is_third_party = false;

  // Call GetBeforeRequestAction first to ensure that test and production code
  // paths are consistent.
  composite_matcher->GetAction(google_params,
                               RulesetMatchingStage::kOnBeforeRequest,
                               PageAccess::kAllowed);

  std::vector<RequestAction> actions =
      composite_matcher->GetModifyHeadersActions(
          google_params, RulesetMatchingStage::kOnBeforeRequest);

  // Construct expected request actions to be taken for a request to google.com.
  RequestAction action_1 =
      CreateRequestActionForTesting(RequestAction::Type::MODIFY_HEADERS,
                                    *rule_1.id, *rule_1.priority, kSource1ID);
  action_1.request_headers_to_modify = {
      RequestAction::HeaderInfo("header1", dnr_api::HeaderOperation::kRemove,
                                std::nullopt),
      RequestAction::HeaderInfo("header2", dnr_api::HeaderOperation::kSet,
                                "value2")};

  RequestAction action_2 =
      CreateRequestActionForTesting(RequestAction::Type::MODIFY_HEADERS,
                                    *rule_2.id, *rule_2.priority, kSource2ID);
  action_2.response_headers_to_modify = {
      RequestAction::HeaderInfo("header1", dnr_api::HeaderOperation::kRemove,
                                std::nullopt),
      RequestAction::HeaderInfo("header2", dnr_api::HeaderOperation::kAppend,
                                "VALUE2"),
      RequestAction::HeaderInfo("header3", dnr_api::HeaderOperation::kSet,
                                "VALUE3")};

  // |action_2| should be before |action_1| because |rule_2|
  // has a higher priority.
  EXPECT_THAT(actions, ::testing::ElementsAre(
                           ::testing::Eq(::testing::ByRef(action_2)),
                           ::testing::Eq(::testing::ByRef(action_1))));

  // Now swap the priority of the rules, which requires re-creating the ruleset
  // matchers and composite matcher.
  rule_1.priority = kMinValidPriority + 1;
  rule_2.priority = kMinValidPriority;
  ASSERT_TRUE(CreateVerifiedMatcher({rule_1}, CreateTemporarySource(kSource1ID),
                                    &matcher_1));
  ASSERT_TRUE(CreateVerifiedMatcher({rule_2}, CreateTemporarySource(kSource2ID),
                                    &matcher_2));

  matchers.clear();
  matchers.push_back(std::move(matcher_1));
  matchers.push_back(std::move(matcher_2));
  composite_matcher = std::make_unique<CompositeMatcher>(
      std::move(matchers), /*extension_id=*/"",
      HostPermissionsAlwaysRequired::kFalse);

  // Call GetBeforeRequestAction first to ensure that test and production code
  // paths are consistent.
  composite_matcher->GetAction(google_params,
                               RulesetMatchingStage::kOnBeforeRequest,
                               PageAccess::kAllowed);

  // Re-create |action_1| and |action_2| with the updated rule
  // priorities. The headers modified by each action should not change.
  actions = composite_matcher->GetModifyHeadersActions(
      google_params, RulesetMatchingStage::kOnBeforeRequest);
  action_1 =
      CreateRequestActionForTesting(RequestAction::Type::MODIFY_HEADERS,
                                    *rule_1.id, *rule_1.priority, kSource1ID);
  action_1.request_headers_to_modify = {
      RequestAction::HeaderInfo("header1", dnr_api::HeaderOperation::kRemove,
                                std::nullopt),
      RequestAction::HeaderInfo("header2", dnr_api::HeaderOperation::kSet,
                                "value2")};

  action_2 =
      CreateRequestActionForTesting(RequestAction::Type::MODIFY_HEADERS,
                                    *rule_2.id, *rule_2.priority, kSource2ID);
  action_2.response_headers_to_modify = {
      RequestAction::HeaderInfo("header1", dnr_api::HeaderOperation::kRemove,
                                std::nullopt),
      RequestAction::HeaderInfo("header2", dnr_api::HeaderOperation::kAppend,
                                "VALUE2"),
      RequestAction::HeaderInfo("header3", dnr_api::HeaderOperation::kSet,
                                "VALUE3")};

  // |action_1| should now be before |action_2| after their
  // priorities have been reversed.
  EXPECT_THAT(actions, ::testing::ElementsAre(
                           ::testing::Eq(::testing::ByRef(action_1)),
                           ::testing::Eq(::testing::ByRef(action_2))));
}

// Tests that GetModifyHeadersActions method omits rules with an equal or lower
// priority than a matched allow or allowAllRequests rule.
TEST_F(CompositeMatcherTest, GetModifyHeadersActions_Priority) {
  using HeaderInfo = RequestAction::HeaderInfo;
  int allow_rule_priority = kMinValidPriority + 1;

  TestRule allow_rule = CreateGenericRule();
  allow_rule.id = kMinValidID;
  allow_rule.condition->url_filter = std::string("google.com/1");
  allow_rule.action->type = std::string("allow");
  allow_rule.priority = allow_rule_priority;

  TestRule url_rule_1 = CreateModifyHeadersRule(
      kMinValidID + 1, allow_rule_priority - 1, "google.com", std::nullopt,
      std::vector<TestHeaderInfo>(
          {TestHeaderInfo("header1", "remove", std::nullopt)}),
      std::nullopt);

  TestRule url_rule_2 = CreateModifyHeadersRule(
      kMinValidID + 2, allow_rule_priority, "google.com", std::nullopt,
      std::vector<TestHeaderInfo>(
          {TestHeaderInfo("header2", "remove", std::nullopt)}),
      std::nullopt);

  TestRule url_rule_3 = CreateModifyHeadersRule(
      kMinValidID + 3, allow_rule_priority + 1, "google.com", std::nullopt,
      std::vector<TestHeaderInfo>(
          {TestHeaderInfo("header3", "remove", std::nullopt)}),
      std::nullopt);

  TestRule regex_rule_1 = CreateModifyHeadersRule(
      kMinValidID + 4, allow_rule_priority - 1, std::nullopt, R"(google\.com)",
      std::vector<TestHeaderInfo>(
          {TestHeaderInfo("header4", "remove", std::nullopt)}),
      std::nullopt);

  TestRule regex_rule_2 = CreateModifyHeadersRule(
      kMinValidID + 5, allow_rule_priority, std::nullopt, R"(google\.com)",
      std::vector<TestHeaderInfo>(
          {TestHeaderInfo("header5", "remove", std::nullopt)}),
      std::nullopt);

  TestRule regex_rule_3 = CreateModifyHeadersRule(
      kMinValidID + 6, allow_rule_priority + 1, std::nullopt, R"(google\.com)",
      std::vector<TestHeaderInfo>(
          {TestHeaderInfo("header6", "remove", std::nullopt)}),
      std::nullopt);

  const RulesetID kSource1ID(1);
  std::unique_ptr<RulesetMatcher> matcher_1;
  ASSERT_TRUE(
      CreateVerifiedMatcher({allow_rule, url_rule_1, url_rule_2, url_rule_3},
                            CreateTemporarySource(kSource1ID), &matcher_1));

  const RulesetID kSource2ID(2);
  std::unique_ptr<RulesetMatcher> matcher_2;
  ASSERT_TRUE(CreateVerifiedMatcher({regex_rule_1, regex_rule_2, regex_rule_3},
                                    CreateTemporarySource(kSource2ID),
                                    &matcher_2));

  // Create a CompositeMatcher with the rulesets.
  std::vector<std::unique_ptr<RulesetMatcher>> matchers;
  matchers.push_back(std::move(matcher_1));
  matchers.push_back(std::move(matcher_2));
  auto composite_matcher = std::make_unique<CompositeMatcher>(
      std::move(matchers), /*extension_id=*/"",
      HostPermissionsAlwaysRequired::kFalse);

  // Make a request to "http://google.com/1" which matches with all
  // modifyHeaders rules and |allow_rule|.
  GURL google_url = GURL("http://google.com/1");
  RequestParams google_params;
  google_params.url = &google_url;
  google_params.element_type = url_pattern_index::flat::ElementType_SUBDOCUMENT;
  google_params.is_third_party = false;

  // Call GetBeforeRequestAction first to ensure that test and production code
  // paths are consistent.
  composite_matcher->GetAction(google_params,
                               RulesetMatchingStage::kOnBeforeRequest,
                               PageAccess::kAllowed);

  std::vector<RequestAction> actions =
      composite_matcher->GetModifyHeadersActions(
          google_params, RulesetMatchingStage::kOnBeforeRequest);

  auto create_action_for_rule =
      [](const TestRule& rule, const RulesetID& ruleset_id,
         const std::vector<HeaderInfo>& request_headers) {
        RequestAction action =
            CreateRequestActionForTesting(RequestAction::Type::MODIFY_HEADERS,
                                          *rule.id, *rule.priority, ruleset_id);

        action.request_headers_to_modify = request_headers;
        return action;
      };

  RequestAction header_3_action = create_action_for_rule(
      url_rule_3, kSource1ID,
      {HeaderInfo("header3", dnr_api::HeaderOperation::kRemove, std::nullopt)});
  RequestAction header_6_action = create_action_for_rule(
      regex_rule_3, kSource2ID,
      {HeaderInfo("header6", dnr_api::HeaderOperation::kRemove, std::nullopt)});

  // For the request to "http://google.com/1", since |url_rule_3| and
  // |regex_rule_3| are the only rules with a greater priority than
  // |allow_rule|, "header3" and "header4" should be removed.
  EXPECT_THAT(actions, ::testing::UnorderedElementsAre(
                           ::testing::Eq(::testing::ByRef(header_3_action)),
                           ::testing::Eq(::testing::ByRef(header_6_action))));

  // Make a request to "http://google.com/2" which should match with all
  // modifyHeaders rules but not |allow_rule|.
  google_url = GURL("http://google.com/2");
  google_params.url = &google_url;

  // Reset the max allow rule priority cache since a new request is being made.
  google_params.max_priority_allow_action.clear();

  // Call GetBeforeRequestAction first to ensure that test and production code
  // paths are consistent.
  composite_matcher->GetAction(google_params,
                               RulesetMatchingStage::kOnBeforeRequest,
                               PageAccess::kAllowed);
  actions = composite_matcher->GetModifyHeadersActions(
      google_params, RulesetMatchingStage::kOnBeforeRequest);

  RequestAction header_1_action = create_action_for_rule(
      url_rule_1, kSource1ID,
      {HeaderInfo("header1", dnr_api::HeaderOperation::kRemove, std::nullopt)});
  RequestAction header_2_action = create_action_for_rule(
      url_rule_2, kSource1ID,
      {HeaderInfo("header2", dnr_api::HeaderOperation::kRemove, std::nullopt)});
  RequestAction header_4_action = create_action_for_rule(
      regex_rule_1, kSource2ID,
      {HeaderInfo("header4", dnr_api::HeaderOperation::kRemove, std::nullopt)});
  RequestAction header_5_action = create_action_for_rule(
      regex_rule_2, kSource2ID,
      {HeaderInfo("header5", dnr_api::HeaderOperation::kRemove, std::nullopt)});

  // For the request to "http://google.com/2", "header1" to "header6" should be
  // removed since all modifyHeaders rules are matched and there is no matching
  // allow/allowAllRequests rule.
  EXPECT_THAT(actions, ::testing::UnorderedElementsAre(
                           ::testing::Eq(::testing::ByRef(header_1_action)),
                           ::testing::Eq(::testing::ByRef(header_2_action)),
                           ::testing::Eq(::testing::ByRef(header_3_action)),
                           ::testing::Eq(::testing::ByRef(header_4_action)),
                           ::testing::Eq(::testing::ByRef(header_5_action)),
                           ::testing::Eq(::testing::ByRef(header_6_action))));
}

// Ensure CompositeMatcher detects requests to be notified based on the rule
// matched and whether the extenion has access to the request.
TEST_F(CompositeMatcherTest, NotifyWithholdFromPageAccess) {
  TestRule redirect_rule = CreateGenericRule();
  redirect_rule.condition->url_filter = std::string("google.com");
  redirect_rule.priority = kMinValidPriority;
  redirect_rule.action->type = std::string("redirect");
  redirect_rule.action->redirect.emplace();
  redirect_rule.action->redirect->url = std::string("http://ruleset1.com");
  redirect_rule.id = kMinValidID;

  TestRule upgrade_rule = CreateGenericRule();
  upgrade_rule.condition->url_filter = std::string("example.com");
  upgrade_rule.priority = kMinValidPriority + 1;
  upgrade_rule.action->type = std::string("upgradeScheme");
  upgrade_rule.id = kMinValidID + 1;

  std::unique_ptr<RulesetMatcher> matcher_1;
  ASSERT_TRUE(CreateVerifiedMatcher({redirect_rule, upgrade_rule},
                                    CreateTemporarySource(), &matcher_1));

  // Create a composite matcher.
  std::vector<std::unique_ptr<RulesetMatcher>> matchers;
  matchers.push_back(std::move(matcher_1));
  auto composite_matcher = std::make_unique<CompositeMatcher>(
      std::move(matchers), /*extension_id=*/"",
      HostPermissionsAlwaysRequired::kFalse);

  GURL google_url = GURL("http://google.com");
  GURL example_url = GURL("http://example.com");
  GURL yahoo_url = GURL("http://yahoo.com");

  GURL ruleset1_url = GURL("http://ruleset1.com");
  GURL https_example_url = GURL("https://example.com");

  struct {
    const raw_ref<GURL> request_url;
    PageAccess access;
    std::optional<GURL> expected_final_url;
    bool should_notify_withheld;
  } test_cases[] = {
      // If access to the request is allowed, we should not notify that
      // the request is withheld.
      {ToRawRef(google_url), PageAccess::kAllowed, ruleset1_url, false},
      {ToRawRef(example_url), PageAccess::kAllowed, https_example_url, false},
      {ToRawRef(yahoo_url), PageAccess::kAllowed, std::nullopt, false},

      // Notify the request is withheld if it matches with a redirect rule.
      {ToRawRef(google_url), PageAccess::kWithheld, std::nullopt, true},
      // If the page access to the request is withheld but it matches with
      // an upgrade rule, or no rule, then we should not notify.
      {ToRawRef(example_url), PageAccess::kWithheld, https_example_url, false},
      {ToRawRef(yahoo_url), PageAccess::kWithheld, std::nullopt, false},

      // If access to the request is denied instead of withheld, the extension
      // should not be notified.
      {ToRawRef(google_url), PageAccess::kDenied, std::nullopt, false},
      // If the page access to the request is denied but it matches with
      // an upgrade rule, or no rule, then we should not notify.
      {ToRawRef(example_url), PageAccess::kDenied, https_example_url, false},
      {ToRawRef(yahoo_url), PageAccess::kDenied, std::nullopt, false},
  };

  for (const auto& test_case : test_cases) {
    SCOPED_TRACE(base::StringPrintf(
        "request_url=%s, access=%d, expected_final_url=%s, "
        "should_notify_withheld=%d",
        test_case.request_url->spec().c_str(),
        static_cast<int>(test_case.access),
        test_case.expected_final_url.value_or(GURL()).spec().c_str(),
        test_case.should_notify_withheld));

    RequestParams params;
    params.url = &*test_case.request_url;
    params.element_type = url_pattern_index::flat::ElementType_SUBDOCUMENT;
    params.is_third_party = false;

    ActionInfo redirect_action_info = composite_matcher->GetAction(
        params, RulesetMatchingStage::kOnBeforeRequest, test_case.access);

    EXPECT_EQ(test_case.should_notify_withheld,
              redirect_action_info.notify_request_withheld);
  }
}

// Tests CompositeMatcher with the HostPermissionsAlwaysRequired::kTrue mode.
TEST_F(CompositeMatcherTest, HostPermissionsAlwaysRequired) {
  int rule_id = kMinValidID;

  TestRule block_rule = CreateGenericRule(rule_id++);
  block_rule.condition->url_filter = "example.com";

  TestRule block_rule_2 = CreateGenericRule(rule_id++);
  block_rule_2.condition->url_filter = "foo.com";
  block_rule.priority = 3;

  TestRule allow_rule = CreateGenericRule(rule_id++);
  allow_rule.action->type = "allow";
  allow_rule.condition->url_filter = "foo.com";
  allow_rule.priority = 5;

  TestRule upgrade_rule = CreateGenericRule(rule_id++);
  upgrade_rule.action->type = "upgradeScheme";
  upgrade_rule.condition->url_filter = "upgrade.com";

  std::unique_ptr<RulesetMatcher> matcher;
  ASSERT_TRUE(CreateVerifiedMatcher(
      {block_rule, block_rule_2, allow_rule, upgrade_rule},
      CreateTemporarySource(), &matcher));
  CompositeMatcher::MatcherList matchers;
  matchers.push_back(std::move(matcher));
  auto composite_matcher = std::make_unique<CompositeMatcher>(
      std::move(matchers), /*extension_id=*/"",
      HostPermissionsAlwaysRequired::kTrue);

  struct TestCase {
    const char* url;
    const PageAccess access;
    const bool expected_notify_withheld;
    std::optional<int> expected_matched_rule_id;
  };
  const auto cases = std::to_array<TestCase>({
      {"https://example.com", PageAccess::kAllowed, false, block_rule.id},
      {"https://example.com", PageAccess::kWithheld, true, std::nullopt},
      {"https://foo.com", PageAccess::kAllowed, false, allow_rule.id},
      // We don't expect to be notified about this (even though there's a rule
      // that would have matched) because the rule would just allow the request.
      {"https://foo.com", PageAccess::kWithheld, false, std::nullopt},
      {"http://upgrade.com", PageAccess::kAllowed, false, upgrade_rule.id},
      {"http://upgrade.com", PageAccess::kWithheld, true, std::nullopt},
      {"http://nomatch.com", PageAccess::kAllowed, false, std::nullopt},
      {"http://nomatch.com", PageAccess::kWithheld, false, std::nullopt},
  });

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

    GURL url(cases[i].url);
    RequestParams params;
    params.url = &url;

    ActionInfo info = composite_matcher->GetAction(
        params, RulesetMatchingStage::kOnBeforeRequest, cases[i].access);
    EXPECT_EQ(cases[i].expected_notify_withheld, info.notify_request_withheld);

    std::optional<int> rule_matched_id;
    if (info.action) {
      rule_matched_id = info.action->rule_id;
    }

    EXPECT_EQ(cases[i].expected_matched_rule_id, rule_matched_id);
  }
}

// Tests that the redirect url within an extension's ruleset is chosen based on
// the highest priority matching rule.
TEST_F(CompositeMatcherTest, GetRedirectUrlFromPriority) {
  TestRule abc_redirect = CreateGenericRule();
  abc_redirect.condition->url_filter = std::string("*abc*");
  abc_redirect.priority = kMinValidPriority;
  abc_redirect.action->type = std::string("redirect");
  abc_redirect.action->redirect.emplace();
  abc_redirect.action->redirect->url = std::string("http://google.com");
  abc_redirect.id = kMinValidID;

  TestRule def_upgrade = CreateGenericRule();
  def_upgrade.condition->url_filter = std::string("*def*");
  def_upgrade.priority = kMinValidPriority + 1;
  def_upgrade.action->type = std::string("upgradeScheme");
  def_upgrade.id = kMinValidID + 1;

  TestRule ghi_redirect = CreateGenericRule();
  ghi_redirect.condition->url_filter = std::string("*ghi*");
  ghi_redirect.priority = kMinValidPriority + 2;
  ghi_redirect.action->type = std::string("redirect");
  ghi_redirect.action->redirect.emplace();
  ghi_redirect.action->redirect->url = std::string("http://example.com");
  ghi_redirect.id = kMinValidID + 2;

  // In terms of priority: ghi > def > abc.

  std::unique_ptr<RulesetMatcher> matcher_1;
  ASSERT_TRUE(CreateVerifiedMatcher({abc_redirect, def_upgrade, ghi_redirect},
                                    CreateTemporarySource(), &matcher_1));

  // Create a composite matcher.
  std::vector<std::unique_ptr<RulesetMatcher>> matchers;
  matchers.push_back(std::move(matcher_1));
  auto composite_matcher = std::make_unique<CompositeMatcher>(
      std::move(matchers), /*extension_id=*/"",
      HostPermissionsAlwaysRequired::kFalse);

  struct {
    GURL request_url;
    std::optional<GURL> expected_final_url;
  } test_cases[] = {
      // Test requests which match exactly one rule.
      {GURL("http://abc.com"), GURL("http://google.com")},
      {GURL("http://def.com"), GURL("https://def.com")},
      {GURL("http://ghi.com"), GURL("http://example.com")},

      // The upgrade rule has a higher priority than the redirect rule matched
      // so the request should be upgraded.
      {GURL("http://abcdef.com"), GURL("https://abcdef.com")},

      // The upgrade rule has a lower priority than the redirect rule matched so
      // the request should be redirected.
      {GURL("http://defghi.com"), GURL("http://example.com")},

      // The request will not be redirected as it matches the upgrade rule but
      // is already https.
      {GURL("https://abcdef.com"), std::nullopt},

      {GURL("http://xyz.com"), std::nullopt},
  };

  for (const auto& test_case : test_cases) {
    SCOPED_TRACE(base::StringPrintf(
        "Test redirect from %s to %s", test_case.request_url.spec().c_str(),
        test_case.expected_final_url.value_or(GURL()).spec().c_str()));

    RequestParams params;
    params.url = &test_case.request_url;
    params.element_type = url_pattern_index::flat::ElementType_SUBDOCUMENT;
    params.is_third_party = false;

    ActionInfo redirect_action_info = composite_matcher->GetAction(
        params, RulesetMatchingStage::kOnBeforeRequest, PageAccess::kAllowed);

    if (test_case.expected_final_url) {
      ASSERT_TRUE(redirect_action_info.action);
      EXPECT_TRUE(redirect_action_info.action->IsRedirectOrUpgrade());
      EXPECT_EQ(test_case.expected_final_url,
                redirect_action_info.action->redirect_url);
    } else {
      EXPECT_FALSE(redirect_action_info.action.has_value());
    }

    EXPECT_FALSE(redirect_action_info.notify_request_withheld);
  }
}

// Ensure rule placement doesn't have side effects on matching priority.
TEST_F(CompositeMatcherTest, RulePlacement) {
  TestRule block_rule = CreateGenericRule(kMinValidID);
  block_rule.priority = 2;
  block_rule.condition->url_filter = "example.com";

  TestRule redirect_rule = CreateGenericRule(kMinValidID + 1);
  redirect_rule.priority = 3;
  redirect_rule.condition->url_filter = "example.com";
  redirect_rule.action->type = "redirect";
  redirect_rule.action->redirect.emplace();
  redirect_rule.action->redirect->url = "http://newurl.com";

  auto test_matchers = [](CompositeMatcher::MatcherList matchers) {
    auto composite_matcher = std::make_unique<CompositeMatcher>(
        std::move(matchers), /*extension_id=*/"",
        HostPermissionsAlwaysRequired::kFalse);

    GURL url("http://example.com");
    RequestParams params;
    params.url = &url;

    ActionInfo info = composite_matcher->GetAction(
        params, RulesetMatchingStage::kOnBeforeRequest, PageAccess::kAllowed);
    ASSERT_TRUE(info.action);
    EXPECT_EQ(kMinValidID + 1u, info.action->rule_id);
    EXPECT_FALSE(info.notify_request_withheld);

    // The highest priority matching rule (`redirect_rule`) needs host
    // permissions to match.
    info = composite_matcher->GetAction(
        params, RulesetMatchingStage::kOnBeforeRequest, PageAccess::kWithheld);
    EXPECT_FALSE(info.action);
    EXPECT_TRUE(info.notify_request_withheld);

    info = composite_matcher->GetAction(
        params, RulesetMatchingStage::kOnBeforeRequest, PageAccess::kDenied);
    EXPECT_FALSE(info.action);
    EXPECT_FALSE(info.notify_request_withheld);
  };

  // Case 1: Both rules are part of the same ruleset.
  {
    SCOPED_TRACE("Same ruleset");
    std::unique_ptr<RulesetMatcher> matcher;
    ASSERT_TRUE(CreateVerifiedMatcher({block_rule, redirect_rule},
                                      CreateTemporarySource(), &matcher));
    std::vector<std::unique_ptr<RulesetMatcher>> matchers;
    matchers.push_back(std::move(matcher));
    test_matchers(std::move(matchers));
  }

  // Case 2: Both rules are part of different rulesets.
  {
    SCOPED_TRACE("Different ruleset");
    std::unique_ptr<RulesetMatcher> block_matcher;
    ASSERT_TRUE(CreateVerifiedMatcher(
        {block_rule}, CreateTemporarySource(RulesetID(1)), &block_matcher));
    std::unique_ptr<RulesetMatcher> redirect_matcher;
    ASSERT_TRUE(CreateVerifiedMatcher({redirect_rule},
                                      CreateTemporarySource(RulesetID(2)),
                                      &redirect_matcher));

    std::vector<std::unique_ptr<RulesetMatcher>> matchers;
    matchers.push_back(std::move(block_matcher));
    matchers.push_back(std::move(redirect_matcher));
    test_matchers(std::move(matchers));
  }
}

class CompositeMatcherResponseHeadersTest : public CompositeMatcherTest {
 public:
  CompositeMatcherResponseHeadersTest() {
    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 that an allow rule matched in OnBeforeRequest can be returned when
// matching rules in OnHeadersReceived if said rule outprioritizes all rules
// with response header conditions.
TEST_F(CompositeMatcherResponseHeadersTest, AllowRuleMatchedAcrossStages) {
  // TODO(kelvinjiang): A lot of e2e DNR tests for response header rules test
  // the matching logic for the onHeadersReceived stage, so a header condition
  // that functionally matches on almost any header is used. Put this into
  // test_utils.
  std::vector<TestHeaderCondition> blank_header_condition =
      std::vector<TestHeaderCondition>(
          {TestHeaderCondition("nonsense-header", {}, {})});

  int rule_id = kMinValidID;

  // Add 3 rules:
  // - OnBeforeRequest allow (pri = 3)
  // - OnHeadersReceived block (pri = 2)
  // - OnHeadersReceived allow (pri = 1)
  TestRule before_request_allow = CreateGenericRule(rule_id++);
  before_request_allow.action->type = "allow";
  before_request_allow.condition->url_filter = "example.test2";
  before_request_allow.priority = 3;

  TestRule headers_received_block = CreateGenericRule(rule_id++);
  headers_received_block.condition->url_filter = "example.test";
  headers_received_block.condition->excluded_response_headers =
      blank_header_condition;
  headers_received_block.priority = 2;

  TestRule headers_received_allow = CreateGenericRule(rule_id++);
  headers_received_allow.action->type = "allow";
  headers_received_allow.condition->url_filter = "example.test";
  headers_received_allow.condition->excluded_response_headers =
      blank_header_condition;

  std::unique_ptr<RulesetMatcher> matcher;
  ASSERT_TRUE(CreateVerifiedMatcher(
      {before_request_allow, headers_received_block, headers_received_allow},
      CreateTemporarySource(), &matcher));
  CompositeMatcher::MatcherList matchers;
  matchers.push_back(std::move(matcher));
  auto composite_matcher = std::make_unique<CompositeMatcher>(
      std::move(matchers), /*extension_id=*/"",
      HostPermissionsAlwaysRequired::kTrue);

  struct {
    std::string url;
    std::optional<int> expected_matched_before_request_id;
    std::optional<int> expected_matched_headers_received_id;
  } test_cases[] = {
      // No rules are matched in OnBeforeRequest, but `headers_received_block`
      // is matched in OnHeadersReceived.
      {"https://example.test", std::nullopt, headers_received_block.id},

      // `before_request_allow` is matched in OnBeforeRequest, and that match is
      // carried over in `OnHeadersReceived` where it outprioritizes rules with
      // header conditions, so it should be matched again.
      {"https://example.test2", before_request_allow.id,
       before_request_allow.id},
  };

  for (const auto& test_case : test_cases) {
    SCOPED_TRACE(base::StringPrintf("Testing %s", test_case.url.c_str()));

    // Navigate to the given URL.
    GURL url(test_case.url);
    auto base_headers = base::MakeRefCounted<net::HttpResponseHeaders>(
        net::HttpUtil::AssembleRawHeaders("HTTP/1.0 200 OK\r\n"));
    RequestParams params =
        CreateRequestWithResponseHeaders(url, base_headers.get());

    // Match rules in the OnBeforeRequest phase and verify the matched rule ID
    // if any.
    ActionInfo before_request_info = composite_matcher->GetAction(
        params, RulesetMatchingStage::kOnBeforeRequest, PageAccess::kAllowed);

    std::optional<int> before_request_rule_id;
    if (before_request_info.action) {
      before_request_rule_id = before_request_info.action->rule_id;
    }
    EXPECT_EQ(test_case.expected_matched_before_request_id,
              before_request_rule_id);

    // Reusing `params`, simulate a request matching flow by continuing to match
    // rules in the OnHeadersReceived phase, and verify the matched rule ID if
    // any.
    ActionInfo headers_received_info = composite_matcher->GetAction(
        params, RulesetMatchingStage::kOnHeadersReceived, PageAccess::kAllowed);

    std::optional<int> headers_received_rule_id;
    if (headers_received_info.action) {
      headers_received_rule_id = headers_received_info.action->rule_id;
    }

    EXPECT_EQ(test_case.expected_matched_headers_received_id,
              headers_received_rule_id);
  }
}

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