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

#include "components/performance_manager/freezing/freezing_policy.h"

#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

#include "base/byte_size.h"
#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/numerics/safe_conversions.h"
#include "base/rand_util.h"
#include "base/test/power_monitor_test.h"
#include "base/test/scoped_feature_list.h"
#include "base/time/time.h"
#include "base/unguessable_token.h"
#include "components/performance_manager/freezing/freezer.h"
#include "components/performance_manager/graph/graph_impl.h"
#include "components/performance_manager/graph/page_node_impl.h"
#include "components/performance_manager/graph/process_node_impl.h"
#include "components/performance_manager/public/decorators/page_live_state_decorator.h"
#include "components/performance_manager/public/features.h"
#include "components/performance_manager/public/freezing/cannot_freeze_reason.h"
#include "components/performance_manager/public/freezing/freezing.h"
#include "components/performance_manager/public/graph/page_node.h"
#include "components/performance_manager/public/resource_attribution/origin_in_browsing_instance_context.h"
#include "components/performance_manager/public/resource_attribution/queries.h"
#include "components/performance_manager/public/resource_attribution/query_results.h"
#include "components/performance_manager/public/resource_attribution/resource_contexts.h"
#include "components/performance_manager/test_support/graph_test_harness.h"
#include "components/ukm/test_ukm_recorder.h"
#include "content/public/browser/browsing_instance_id.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/metrics/public/cpp/ukm_source_id.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
#include "url/origin.h"

namespace performance_manager {

namespace {

using FreezingType = FreezingPolicy::FreezingType;
using freezing::CannotFreezeReason;
using freezing::CannotFreezeReasonSet;
using ::testing::ElementsAre;
using ::testing::IsEmpty;
using ::testing::Mock;

class MockFreezingPolicy : public FreezingPolicy {
 public:
  explicit MockFreezingPolicy(
      std::unique_ptr<freezing::Discarder> discarder,
      std::unique_ptr<freezing::OptOutChecker> opt_out_checker = nullptr)
      : FreezingPolicy(std::move(discarder), std::move(opt_out_checker)) {}
  ~MockFreezingPolicy() override = default;

  base::LiveTicks GenerateRandomPeriodicUnfreezePhase() const override {
    //  Make the periodic unfreeze phase non-random for tests.
    return base::LiveTicks();
  }

  MOCK_METHOD(void,
              RecordFreezingEligibilityUKMForPage,
              (ukm::SourceId source_id,
               double highest_cpu_current_interval,
               double highest_cpu_without_battery_saver_cannot_freeze,
               CannotFreezeReasonSet cannot_freeze_reasons),
              (override));
};

// Mock version of a Freezer.
class LenientMockFreezer : public Freezer {
 public:
  LenientMockFreezer() = default;
  ~LenientMockFreezer() override = default;
  LenientMockFreezer(const LenientMockFreezer& other) = delete;
  LenientMockFreezer& operator=(const LenientMockFreezer&) = delete;

  MOCK_METHOD(void,
              MaybeFreezePageNode,
              (const PageNode* page_node),
              (override));
  MOCK_METHOD(void, UnfreezePageNode, (const PageNode* page_node), (override));
};
using MockFreezer = ::testing::StrictMock<LenientMockFreezer>;

class LenientMockDiscarder : public freezing::Discarder {
 public:
  LenientMockDiscarder() = default;
  ~LenientMockDiscarder() override = default;
  LenientMockDiscarder(const LenientMockDiscarder& other) = delete;
  LenientMockDiscarder& operator=(const LenientMockDiscarder&) = delete;

  MOCK_METHOD(void,
              DiscardPages,
              (Graph * graph, std::vector<const PageNode*> page_nodes),
              (override));
};
using MockDiscarder = ::testing::StrictMock<LenientMockDiscarder>;

const base::TimeDelta kTimeBetweenUnfreezePeriods =
    features::kInfiniteTabsFreezing_UnfreezeInterval.Get() -
    features::kInfiniteTabsFreezing_UnfreezeDuration.Get();

}  // namespace

class FreezingPolicyTest_BaseWithNoPage : public GraphTestHarness {
 public:
  FreezingPolicyTest_BaseWithNoPage()
      : GraphTestHarness(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
  ~FreezingPolicyTest_BaseWithNoPage() override = default;
  FreezingPolicyTest_BaseWithNoPage(
      const FreezingPolicyTest_BaseWithNoPage& other) = delete;
  FreezingPolicyTest_BaseWithNoPage& operator=(
      const FreezingPolicyTest_BaseWithNoPage&) = delete;

  void OnGraphCreated(GraphImpl* graph) override {
    // The freezing logic relies on the existence of the page live state data.
    graph->PassToGraph(std::make_unique<PageLiveStateDecorator>());
    auto discarder = std::make_unique<MockDiscarder>();
    discarder_ = discarder.get();
    // Create the policy and pass it to the graph.
    auto policy = std::make_unique<MockFreezingPolicy>(
        std::move(discarder), CreateTestOptOutChecker());
    policy_ = policy.get();
    auto freezer = std::make_unique<MockFreezer>();
    freezer_ = freezer.get();
    policy_->SetFreezerForTesting(std::move(freezer));
    graph->PassToGraph(std::move(policy));
    process_node_ = CreateNode<ProcessNodeImpl>();
  }

  // Override to create an OptOutChecker for the test.
  virtual std::unique_ptr<freezing::OptOutChecker> CreateTestOptOutChecker() {
    return nullptr;
  }

  // Reports private memory footprint for `context` to the freezing policy, with
  // "now" as the measurement time.
  void ReportMemoryUsage(resource_attribution::ResourceContext context,
                         base::ByteSize private_footprint) {
    resource_attribution::QueryResultMap memory_result_map;
    memory_result_map[context] = resource_attribution::QueryResults{
        .memory_summary_result = resource_attribution::MemorySummaryResult{
            .metadata = resource_attribution::ResultMetadata(
                /* measurement_time=*/base::TimeTicks::Now(),
                resource_attribution::MeasurementAlgorithm::kSum),
            .resident_set_size = base::ByteSize(0),
            .private_footprint = private_footprint}};
    resource_attribution::QueryResultObserver* observer = policy();
    observer->OnResourceUsageUpdated(std::move(memory_result_map));
  }

  // Adds CPU usage for `context` to `cpu_result_map`, with "now" as the
  // measurement time. `cumulative_background_cpu` is used as cumulative
  // background CPU and `cumulative_cpu` is used as cumulative CPU
  // (`cumulative_background_cpu` is used as cumulative CPU if `cumulative_cpu`
  // is nullopt).
  void AddCPUMeasurement(
      resource_attribution::QueryResultMap& cpu_result_map,
      resource_attribution::ResourceContext context,
      base::TimeDelta cumulative_background_cpu,
      std::optional<base::TimeDelta> cumulative_cpu = std::nullopt) {
    cpu_result_map[context] = resource_attribution::QueryResults{
        .cpu_time_result = resource_attribution::CPUTimeResult{
            .metadata = resource_attribution::ResultMetadata(
                /* measurement_time=*/base::TimeTicks::Now(),
                resource_attribution::MeasurementAlgorithm::kSum),
            .start_time = base::TimeTicks(),
            .cumulative_cpu = cumulative_cpu.has_value()
                                  ? cumulative_cpu.value()
                                  : cumulative_background_cpu,
            .cumulative_background_cpu = cumulative_background_cpu}};
  }

  // Reports CPU usage for `context` to the the freezing policy, with "now" as
  // the measurement time. `cumulative_background_cpu` is used as cumulative
  // background CPU and `cumulative_cpu` is used as cumulative CPU
  // (`cumulative_background_cpu` is used as cumulative CPU if `cumulative_cpu`
  // is nullopt).
  void ReportCumulativeCPUUsage(
      resource_attribution::ResourceContext context,
      base::TimeDelta cumulative_background_cpu,
      std::optional<base::TimeDelta> cumulative_cpu = std::nullopt) {
    resource_attribution::QueryResultMap cpu_result_map;
    AddCPUMeasurement(cpu_result_map, context, cumulative_background_cpu,
                      cumulative_cpu);
    resource_attribution::QueryResultObserver* observer = policy();
    observer->OnResourceUsageUpdated(std::move(cpu_result_map));
  }

  std::pair<TestNodeWrapper<PageNodeImpl>, TestNodeWrapper<FrameNodeImpl>>
  CreatePageAndFrameWithBrowsingInstanceId(
      content::BrowsingInstanceId browsing_instance_id,
      const base::UnguessableToken& browsing_context_id =
          base::UnguessableToken()) {
    auto page =
        CreateNode<PageNodeImpl>(/*web_contents=*/nullptr, browsing_context_id);
    page->SetType(PageType::kTab);
    auto frame = CreateFrameNodeAutoId(process_node(), page.get(),
                                       /* parent_frame_node=*/nullptr,
                                       browsing_instance_id);
    return std::make_pair(std::move(page), std::move(frame));
  }

  void VerifyFreezerExpectations() {
    Mock::VerifyAndClearExpectations(freezer());
  }

  void VerifyDiscarderExpectations() {
    Mock::VerifyAndClearExpectations(discarder());
  }

  // Expect that the `CannotFreezeReason`s applicable to `freezing_type` for
  // `page_node` match `matcher`.
  template <typename MatcherType>
  void ExpectCannotFreezeReasons(
      const PageNode* page_node,
      FreezingType freezing_type,
      MatcherType matcher,
      const base::Location& location = base::Location::Current()) {
    EXPECT_THAT(
        base::Intersection(
            policy()->GetCanFreezeDetails(page_node).cannot_freeze_reasons,
            FreezingPolicy::CannotFreezeReasonsForType(freezing_type)),
        matcher)
        << location.ToString();
  }

  // Expect that the `CannotFreezeReason`s applicable to `freezing_type` for
  // pages connected to `page_node` match `matcher`.
  template <typename MatcherType>
  void ExpectConnectedCannotFreezeReasons(
      const PageNode* page_node,
      FreezingType freezing_type,
      MatcherType matcher,
      const base::Location& location = base::Location::Current()) {
    EXPECT_THAT(base::Intersection(
                    policy()
                        ->GetCanFreezeDetails(page_node)
                        .cannot_freeze_reasons_connected_pages,
                    FreezingPolicy::CannotFreezeReasonsForType(freezing_type)),
                matcher)
        << location.ToString();
  }

  ProcessNodeImpl* process_node() { return process_node_.get(); }
  MockFreezingPolicy* policy() { return policy_; }
  MockFreezer* freezer() { return freezer_; }
  MockDiscarder* discarder() { return discarder_; }

  const content::BrowsingInstanceId kBrowsingInstanceA =
      content::BrowsingInstanceId(1);
  const content::BrowsingInstanceId kBrowsingInstanceB =
      content::BrowsingInstanceId(2);
  const content::BrowsingInstanceId kBrowsingInstanceC =
      content::BrowsingInstanceId(3);
  const resource_attribution::OriginInBrowsingInstanceContext kContext{
      url::Origin(), kBrowsingInstanceA};

 private:
  TestNodeWrapper<ProcessNodeImpl> process_node_;
  raw_ptr<MockFreezer> freezer_;
  raw_ptr<MockDiscarder> discarder_;
  raw_ptr<MockFreezingPolicy> policy_;
};

class FreezingPolicyTest : public FreezingPolicyTest_BaseWithNoPage {
 public:
  FreezingPolicyTest() = default;
  ~FreezingPolicyTest() override = default;

  void OnGraphCreated(GraphImpl* graph) override {
    FreezingPolicyTest_BaseWithNoPage::OnGraphCreated(graph);

    std::tie(page_node_, frame_node_) =
        CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceA);
  }

  PageNodeImpl* page_node() { return page_node_.get(); }

 private:
  TestNodeWrapper<PageNodeImpl> page_node_;
  TestNodeWrapper<FrameNodeImpl> frame_node_;
};

// A page with no `CannotFreezeReason` that is alone in its browsing instance is
// frozen when it has a freezing vote.
TEST_F(FreezingPolicyTest, Basic) {
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  policy()->AddFreezeVote(page_node());
  VerifyFreezerExpectations();
  ExpectCannotFreezeReasons(page_node(), FreezingType::kVoting, IsEmpty());
}

// Multiple connected pages in the same browsing instance with no
// `CannotFreezeReason` are frozen when they all have a freezing vote.
TEST_F(FreezingPolicyTest, ManyPagesSameBrowsingInstance) {
  auto [page2, frame2] =
      CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceA);

  // Adding a freezing vote to each of the 2 pages in the browsing instance
  // freezes all pages.
  policy()->AddFreezeVote(page_node());
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page2.get()));
  policy()->AddFreezeVote(page2.get());
  VerifyFreezerExpectations();

  // Adding a 3rd page (with no freezing vote yet) to the browsing instance
  // unfreezes all pages.
  EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
  EXPECT_CALL(*freezer(), UnfreezePageNode(page2.get()));
  auto [page3, frame3] =
      CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceA);
  VerifyFreezerExpectations();

  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page2.get()));
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page3.get()));
  policy()->AddFreezeVote(page3.get());
  VerifyFreezerExpectations();

  // Multiple votes on the same page don't change anything.
  policy()->AddFreezeVote(page3.get());
  policy()->AddFreezeVote(page3.get());

  // Removing a freezing vote from one page unfreezes all pages.
  EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
  EXPECT_CALL(*freezer(), UnfreezePageNode(page2.get()));
  EXPECT_CALL(*freezer(), UnfreezePageNode(page3.get()));
  policy()->RemoveFreezeVote(page_node());
  VerifyFreezerExpectations();

  policy()->RemoveFreezeVote(page2.get());
  policy()->RemoveFreezeVote(page3.get());
  policy()->RemoveFreezeVote(page3.get());
  policy()->RemoveFreezeVote(page3.get());
}

// Similar to ManyPagesSameBrowsingInstance, except that the 1st and 3rd pages
// don't have frames in the same browsing instance (they're indirectly connected
// via the 2nd page).
TEST_F(FreezingPolicyTest, ConnectedPages) {
  auto [page2, frame2] =
      CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceA);
  auto frame2b =
      CreateFrameNodeAutoId(process_node(), page2.get(),
                            /* parent_frame_node=*/nullptr, kBrowsingInstanceB);

  // Adding a freezing vote to the 2 connected pages freezes them.
  policy()->AddFreezeVote(page_node());
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page2.get()));
  policy()->AddFreezeVote(page2.get());
  VerifyFreezerExpectations();

  // Adding a 3rd page (with no freezing vote yet) to the set of connected pages
  // unfreezes all pages.
  EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
  EXPECT_CALL(*freezer(), UnfreezePageNode(page2.get()));
  auto [page3, frame3] =
      CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceB);
  VerifyFreezerExpectations();

  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page2.get()));
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page3.get()));
  policy()->AddFreezeVote(page3.get());
  VerifyFreezerExpectations();

  // Multiple votes on the same page don't change anything.
  policy()->AddFreezeVote(page3.get());
  policy()->AddFreezeVote(page3.get());

  // Removing a freezing vote from one page unfreezes all pages.
  EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
  EXPECT_CALL(*freezer(), UnfreezePageNode(page2.get()));
  EXPECT_CALL(*freezer(), UnfreezePageNode(page3.get()));
  policy()->RemoveFreezeVote(page_node());
  VerifyFreezerExpectations();

  policy()->RemoveFreezeVote(page2.get());
  policy()->RemoveFreezeVote(page3.get());
  policy()->RemoveFreezeVote(page3.get());
  policy()->RemoveFreezeVote(page3.get());
}

// A browsing instance with many pages that each have a freeze vote is unfrozen
// when one of the pages gets a `CannotFreezeReason`.
TEST_F(FreezingPolicyTest,
       AddCannotFreezeReasonToBrowsingInstanceWithManyPages) {
  auto [page2, frame2] =
      CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceA);

  policy()->AddFreezeVote(page_node());
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page2.get()));
  policy()->AddFreezeVote(page2.get());
  VerifyFreezerExpectations();
  ExpectCannotFreezeReasons(page_node(), FreezingType::kVoting, IsEmpty());
  ExpectCannotFreezeReasons(page2.get(), FreezingType::kVoting, IsEmpty());
  ExpectConnectedCannotFreezeReasons(page_node(), FreezingType::kVoting,
                                     IsEmpty());
  ExpectConnectedCannotFreezeReasons(page2.get(), FreezingType::kVoting,
                                     IsEmpty());

  EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
  EXPECT_CALL(*freezer(), UnfreezePageNode(page2.get()));
  page_node()->SetIsHoldingWebLockForTesting(true);
  VerifyFreezerExpectations();
  ExpectCannotFreezeReasons(page_node(), FreezingType::kVoting,
                            ElementsAre(CannotFreezeReason::kHoldingWebLock));
  ExpectCannotFreezeReasons(page2.get(), FreezingType::kVoting, IsEmpty());
  ExpectConnectedCannotFreezeReasons(page_node(), FreezingType::kVoting,
                                     IsEmpty());
  ExpectConnectedCannotFreezeReasons(
      page2.get(), FreezingType::kVoting,
      ElementsAre(CannotFreezeReason::kHoldingWebLock));
}

// Similar to AddCannotFreezeReasonToBrowsingInstanceWithManyPages, except that
// the 1st and 3rd pages don't have frames in the same browsing instance
// (they're indirectly connected via the 2nd page).
TEST_F(FreezingPolicyTest, AddCannotFreezeReasonToConnectedPages) {
  auto [page2, frame2] =
      CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceA);
  auto frame2b =
      CreateFrameNodeAutoId(process_node(), page2.get(),
                            /* parent_frame_node=*/nullptr, kBrowsingInstanceB);
  auto [page3, frame3] =
      CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceB);

  policy()->AddFreezeVote(page_node());
  policy()->AddFreezeVote(page2.get());
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page2.get()));
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page3.get()));
  policy()->AddFreezeVote(page3.get());
  VerifyFreezerExpectations();
  ExpectCannotFreezeReasons(page_node(), FreezingType::kVoting, IsEmpty());
  ExpectCannotFreezeReasons(page2.get(), FreezingType::kVoting, IsEmpty());
  ExpectCannotFreezeReasons(page3.get(), FreezingType::kVoting, IsEmpty());
  ExpectConnectedCannotFreezeReasons(page_node(), FreezingType::kVoting,
                                     IsEmpty());
  ExpectConnectedCannotFreezeReasons(page2.get(), FreezingType::kVoting,
                                     IsEmpty());
  ExpectConnectedCannotFreezeReasons(page3.get(), FreezingType::kVoting,
                                     IsEmpty());

  EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
  EXPECT_CALL(*freezer(), UnfreezePageNode(page2.get()));
  EXPECT_CALL(*freezer(), UnfreezePageNode(page3.get()));
  page_node()->SetIsHoldingWebLockForTesting(true);
  VerifyFreezerExpectations();
  ExpectCannotFreezeReasons(page_node(), FreezingType::kVoting,
                            ElementsAre(CannotFreezeReason::kHoldingWebLock));
  ExpectCannotFreezeReasons(page2.get(), FreezingType::kVoting, IsEmpty());
  ExpectCannotFreezeReasons(page3.get(), FreezingType::kVoting, IsEmpty());
  ExpectConnectedCannotFreezeReasons(page_node(), FreezingType::kVoting,
                                     IsEmpty());
  ExpectConnectedCannotFreezeReasons(
      page2.get(), FreezingType::kVoting,
      ElementsAre(CannotFreezeReason::kHoldingWebLock));
  ExpectConnectedCannotFreezeReasons(
      page3.get(), FreezingType::kVoting,
      ElementsAre(CannotFreezeReason::kHoldingWebLock));
}

// A browsing instance with one page that has a `CannotFreezeReason` is not
// frozen when all its pages get a freeze vote.
TEST_F(FreezingPolicyTest,
       AddFreezeVotesToBrowsingInstanceWithManyPagesAndCannotFreezeReason) {
  auto [page2, frame2] =
      CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceA);
  page_node()->SetIsHoldingWebLockForTesting(true);
  ExpectCannotFreezeReasons(page_node(), FreezingType::kVoting,
                            ElementsAre(CannotFreezeReason::kHoldingWebLock));
  ExpectCannotFreezeReasons(page2.get(), FreezingType::kVoting, IsEmpty());
  ExpectConnectedCannotFreezeReasons(page_node(), FreezingType::kVoting,
                                     IsEmpty());
  ExpectConnectedCannotFreezeReasons(
      page2.get(), FreezingType::kVoting,
      ElementsAre(CannotFreezeReason::kHoldingWebLock));

  // Don't expect freezing.
  policy()->AddFreezeVote(page_node());
  policy()->AddFreezeVote(page2.get());
  VerifyFreezerExpectations();
}

// Similar to
// AddFreezeVotesToBrowsingInstanceWithManyPagesAndCannotFreezeReason, except
// that the 1st and 3rd pages don't have frames in the same browsing instance
// (they're indirectly connected via the 2nd page).
TEST_F(FreezingPolicyTest,
       AddFreezeVotesToConnectedPagesWithCannotFreezeReason) {
  auto [page2, frame2] =
      CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceA);
  auto frame2b =
      CreateFrameNodeAutoId(process_node(), page2.get(),
                            /* parent_frame_node=*/nullptr, kBrowsingInstanceB);
  auto [page3, frame3] =
      CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceB);
  page_node()->SetIsHoldingWebLockForTesting(true);
  ExpectCannotFreezeReasons(page_node(), FreezingType::kVoting,
                            ElementsAre(CannotFreezeReason::kHoldingWebLock));
  ExpectCannotFreezeReasons(page2.get(), FreezingType::kVoting, IsEmpty());
  ExpectCannotFreezeReasons(page3.get(), FreezingType::kVoting, IsEmpty());
  ExpectConnectedCannotFreezeReasons(page_node(), FreezingType::kVoting,
                                     IsEmpty());
  ExpectConnectedCannotFreezeReasons(
      page2.get(), FreezingType::kVoting,
      ElementsAre(CannotFreezeReason::kHoldingWebLock));
  ExpectConnectedCannotFreezeReasons(
      page3.get(), FreezingType::kVoting,
      ElementsAre(CannotFreezeReason::kHoldingWebLock));

  // Don't expect freezing.
  policy()->AddFreezeVote(page_node());
  policy()->AddFreezeVote(page2.get());
  policy()->AddFreezeVote(page3.get());
  VerifyFreezerExpectations();
}

// Verify that frozen state is correctly updated when a set of connected pages
// is broken in two by the deletion of a frame.
TEST_F(FreezingPolicyTest, BreakConnectedSet) {
  auto [page2, frame2] =
      CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceA);
  auto frame2b =
      CreateFrameNodeAutoId(process_node(), page2.get(),
                            /* parent_frame_node=*/nullptr, kBrowsingInstanceB);
  auto [page3, frame3] =
      CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceB);

  page_node()->SetIsHoldingWebLockForTesting(true);
  policy()->AddFreezeVote(page_node());
  policy()->AddFreezeVote(page2.get());
  policy()->AddFreezeVote(page3.get());
  ExpectCannotFreezeReasons(page_node(), FreezingType::kVoting,
                            ElementsAre(CannotFreezeReason::kHoldingWebLock));
  ExpectCannotFreezeReasons(page2.get(), FreezingType::kVoting, IsEmpty());
  ExpectCannotFreezeReasons(page3.get(), FreezingType::kVoting, IsEmpty());
  ExpectConnectedCannotFreezeReasons(page_node(), FreezingType::kVoting,
                                     IsEmpty());
  ExpectConnectedCannotFreezeReasons(
      page2.get(), FreezingType::kVoting,
      ElementsAre(CannotFreezeReason::kHoldingWebLock));
  ExpectConnectedCannotFreezeReasons(
      page3.get(), FreezingType::kVoting,
      ElementsAre(CannotFreezeReason::kHoldingWebLock));

  // Deleting `frame2` puts `page_node()` in a different connected set than
  // `page2` and `page3`. `page_node()` cannot be frozen because it has a
  // `CannotFreezeReason`. `page2` and `page3` can be frozen because they have
  // freeze votes and no `CannotFreezeReason`.
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page2.get()));
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page3.get()));
  frame2.reset();
  VerifyFreezerExpectations();
  ExpectCannotFreezeReasons(page_node(), FreezingType::kVoting,
                            ElementsAre(CannotFreezeReason::kHoldingWebLock));
  ExpectCannotFreezeReasons(page2.get(), FreezingType::kVoting, IsEmpty());
  ExpectCannotFreezeReasons(page3.get(), FreezingType::kVoting, IsEmpty());
  ExpectConnectedCannotFreezeReasons(page_node(), FreezingType::kVoting,
                                     IsEmpty());
  ExpectConnectedCannotFreezeReasons(page2.get(), FreezingType::kVoting,
                                     IsEmpty());
  ExpectConnectedCannotFreezeReasons(page3.get(), FreezingType::kVoting,
                                     IsEmpty());
}

// Similar to BreakConnectedSet, but the connected set left by the page from
// which a page is deleted can be frozen.
TEST_F(FreezingPolicyTest, BreakConnectedSet_LeftSetIsFrozen) {
  auto [page2, frame2] =
      CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceA);
  auto frame2b =
      CreateFrameNodeAutoId(process_node(), page2.get(),
                            /* parent_frame_node=*/nullptr, kBrowsingInstanceB);
  auto [page3, frame3] =
      CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceB);

  page2->SetIsHoldingWebLockForTesting(true);
  policy()->AddFreezeVote(page_node());
  policy()->AddFreezeVote(page2.get());
  policy()->AddFreezeVote(page3.get());
  ExpectCannotFreezeReasons(page_node(), FreezingType::kVoting, IsEmpty());
  ExpectCannotFreezeReasons(page2.get(), FreezingType::kVoting,
                            ElementsAre(CannotFreezeReason::kHoldingWebLock));
  ExpectCannotFreezeReasons(page3.get(), FreezingType::kVoting, IsEmpty());
  ExpectConnectedCannotFreezeReasons(
      page_node(), FreezingType::kVoting,
      ElementsAre(CannotFreezeReason::kHoldingWebLock));
  ExpectConnectedCannotFreezeReasons(page2.get(), FreezingType::kVoting,
                                     IsEmpty());
  ExpectConnectedCannotFreezeReasons(
      page3.get(), FreezingType::kVoting,
      ElementsAre(CannotFreezeReason::kHoldingWebLock));

  // Deleting `frame2` puts `page_node()` in a different connected set than
  // `page2` and `page3`. `page_node()` cannot be frozen because it has a
  // `CannotFreezeReason`. `page2` and `page3` can be frozen because they have
  // freeze votes and no `CannotFreezeReason`.
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  frame2.reset();
  VerifyFreezerExpectations();
  ExpectCannotFreezeReasons(page_node(), FreezingType::kVoting, IsEmpty());
  ExpectCannotFreezeReasons(page2.get(), FreezingType::kVoting,
                            ElementsAre(CannotFreezeReason::kHoldingWebLock));
  ExpectCannotFreezeReasons(page3.get(), FreezingType::kVoting, IsEmpty());
  ExpectConnectedCannotFreezeReasons(page_node(), FreezingType::kVoting,
                                     IsEmpty());
  ExpectConnectedCannotFreezeReasons(page2.get(), FreezingType::kVoting,
                                     IsEmpty());
  ExpectConnectedCannotFreezeReasons(
      page3.get(), FreezingType::kVoting,
      ElementsAre(CannotFreezeReason::kHoldingWebLock));
}

TEST_F(FreezingPolicyTest, FreezeVoteWhenVisible) {
  page_node()->SetIsVisible(true);

  // Don't expect freezing.
  policy()->AddFreezeVote(page_node());
}

TEST_F(FreezingPolicyTest, BecomesVisibleWhenFrozen) {
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  policy()->AddFreezeVote(page_node());
  VerifyFreezerExpectations();

  EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
  page_node()->SetIsVisible(true);
  VerifyFreezerExpectations();
}

TEST_F(FreezingPolicyTest, FreezeVoteWhenRecentlyVisible) {
  page_node()->SetIsVisible(true);
  page_node()->SetIsVisible(false);

  // Don't expect freezing.
  policy()->AddFreezeVote(page_node());

  // Expect freezing after visible protection time.
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  task_env().FastForwardBy(features::kFreezingVisibleProtectionTime.Get());
  VerifyFreezerExpectations();
}

TEST_F(FreezingPolicyTest, BecomesRecentlyVisibleWhenFrozen) {
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  policy()->AddFreezeVote(page_node());
  VerifyFreezerExpectations();

  EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
  page_node()->SetIsVisible(true);
  // Don't expect freezing, because the page is still "recently visible".
  page_node()->SetIsVisible(false);

  // Expect freezing after visible protection time.
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  task_env().FastForwardBy(features::kFreezingVisibleProtectionTime.Get());
  VerifyFreezerExpectations();
}

TEST_F(FreezingPolicyTest, FreezeVoteWhenAudible) {
  page_node()->SetIsAudible(true);

  // Don't expect freezing.
  policy()->AddFreezeVote(page_node());
}

TEST_F(FreezingPolicyTest, BecomesAudibleWhenFrozen) {
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  policy()->AddFreezeVote(page_node());
  VerifyFreezerExpectations();

  EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
  page_node()->SetIsAudible(true);
  VerifyFreezerExpectations();
}

TEST_F(FreezingPolicyTest, FreezeVoteWhenRecentlyAudible) {
  page_node()->SetIsAudible(true);
  page_node()->SetIsAudible(false);

  // Don't expect freezing.
  policy()->AddFreezeVote(page_node());

  // Expect freezing after audio protection time.
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  task_env().FastForwardBy(features::kFreezingAudioProtectionTime.Get());
  VerifyFreezerExpectations();
}

TEST_F(FreezingPolicyTest, BecomesRecentlyAudibleWhenFrozen) {
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  policy()->AddFreezeVote(page_node());
  VerifyFreezerExpectations();

  EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
  page_node()->SetIsAudible(true);
  // Don't expect freezing, because the page is still "recently audible".
  page_node()->SetIsAudible(false);

  // Expect freezing after audio protection time.
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  task_env().FastForwardBy(features::kFreezingAudioProtectionTime.Get());
  VerifyFreezerExpectations();
}

TEST_F(FreezingPolicyTest, FreezeVoteWithOriginTrialOptOut) {
  page_node()->SetHasFreezingOriginTrialOptOutForTesting(true);

  // Don't expect freezing.
  policy()->AddFreezeVote(page_node());

  // Expect freezing after removing the origin trial opt-out.
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  page_node()->SetHasFreezingOriginTrialOptOutForTesting(false);
  VerifyFreezerExpectations();
}

TEST_F(FreezingPolicyTest, OriginTrialOptOutWhenFrozen) {
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  policy()->AddFreezeVote(page_node());
  VerifyFreezerExpectations();

  EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
  page_node()->SetHasFreezingOriginTrialOptOutForTesting(true);
  VerifyFreezerExpectations();
}

TEST_F(FreezingPolicyTest, FreezeVoteWhenHoldingWebLock) {
  page_node()->SetIsHoldingWebLockForTesting(true);

  // Don't expect freezing.
  policy()->AddFreezeVote(page_node());

  // Expect freezing after releasing the lock.
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  page_node()->SetIsHoldingWebLockForTesting(false);
  VerifyFreezerExpectations();
}

TEST_F(FreezingPolicyTest, AcquiresWebLockWhenFrozen) {
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  policy()->AddFreezeVote(page_node());
  VerifyFreezerExpectations();

  EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
  page_node()->SetIsHoldingWebLockForTesting(true);
  VerifyFreezerExpectations();
}

TEST_F(FreezingPolicyTest, FreezeVoteWhenHoldingBlockingIndexedDBLock) {
  page_node()->SetIsHoldingBlockingIndexedDBLockForTesting(true);

  // Don't expect freezing.
  policy()->AddFreezeVote(page_node());

  // Expect freezing after the transaction completes.
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  page_node()->SetIsHoldingBlockingIndexedDBLockForTesting(false);
  VerifyFreezerExpectations();
}

TEST_F(FreezingPolicyTest, AcquiresBlockingIndexedDBLockWhenFrozen) {
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  policy()->AddFreezeVote(page_node());
  VerifyFreezerExpectations();

  EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
  page_node()->SetIsHoldingBlockingIndexedDBLockForTesting(true);
  VerifyFreezerExpectations();
}

TEST_F(FreezingPolicyTest, FreezeVoteWhenConnectedToUSBDevice) {
  PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
      ->SetIsConnectedToUSBDeviceForTesting(true);

  // Don't expect freezing.
  policy()->AddFreezeVote(page_node());

  // Expect freezing after disconnecting from USB device.
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
      ->SetIsConnectedToUSBDeviceForTesting(false);
  VerifyFreezerExpectations();
}

TEST_F(FreezingPolicyTest, ConnectedToUSBDeviceWhenFrozen) {
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  policy()->AddFreezeVote(page_node());
  VerifyFreezerExpectations();

  EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
  PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
      ->SetIsConnectedToUSBDeviceForTesting(true);
  VerifyFreezerExpectations();
}

TEST_F(FreezingPolicyTest, FreezeVoteWhenConnectedToBluetoothDevice) {
  PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
      ->SetIsConnectedToBluetoothDeviceForTesting(true);

  // Don't expect freezing.
  policy()->AddFreezeVote(page_node());

  // Expect freezing after disconnecting from Bluetooth device.
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
      ->SetIsConnectedToBluetoothDeviceForTesting(false);
  VerifyFreezerExpectations();
}

TEST_F(FreezingPolicyTest, ConnectedToBluetoothDeviceWhenFrozen) {
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  policy()->AddFreezeVote(page_node());
  VerifyFreezerExpectations();

  EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
  PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
      ->SetIsConnectedToBluetoothDeviceForTesting(true);
  VerifyFreezerExpectations();
}

TEST_F(FreezingPolicyTest, FreezeVoteWhenConnectedToHidDevice) {
  PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
      ->SetIsConnectedToHidDeviceForTesting(true);

  // Don't expect freezing.
  policy()->AddFreezeVote(page_node());

  // Expect freezing after disconnecting from HID device.
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
      ->SetIsConnectedToHidDeviceForTesting(false);
  VerifyFreezerExpectations();
}

TEST_F(FreezingPolicyTest, ConnectedToHidDeviceWhenFrozen) {
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  policy()->AddFreezeVote(page_node());
  VerifyFreezerExpectations();

  EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
  PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
      ->SetIsConnectedToHidDeviceForTesting(true);
  VerifyFreezerExpectations();
}

TEST_F(FreezingPolicyTest, FreezeVoteWhenConnectedToSerialPort) {
  PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
      ->SetIsConnectedToSerialPortForTesting(true);

  // Don't expect freezing.
  policy()->AddFreezeVote(page_node());

  // Expect freezing after disconnecting from HID device.
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
      ->SetIsConnectedToSerialPortForTesting(false);
  VerifyFreezerExpectations();
}

TEST_F(FreezingPolicyTest, ConnectedToSerialPortWhenFrozen) {
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  policy()->AddFreezeVote(page_node());
  VerifyFreezerExpectations();

  EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
  PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
      ->SetIsConnectedToSerialPortForTesting(true);
  VerifyFreezerExpectations();
}

TEST_F(FreezingPolicyTest, FreezeVoteWhenCapturingVideo) {
  PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
      ->SetIsCapturingVideoForTesting(true);

  // Don't expect freezing.
  policy()->AddFreezeVote(page_node());

  // Expect freezing after stopping capture.
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
      ->SetIsCapturingVideoForTesting(false);
  VerifyFreezerExpectations();
}

TEST_F(FreezingPolicyTest, StartsCapturingVideoWhenFrozen) {
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  policy()->AddFreezeVote(page_node());
  VerifyFreezerExpectations();

  EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
  PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
      ->SetIsCapturingAudioForTesting(true);
  VerifyFreezerExpectations();
}

TEST_F(FreezingPolicyTest, FreezeVoteWhenCapturingAudio) {
  PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
      ->SetIsCapturingAudioForTesting(true);

  // Don't expect freezing.
  policy()->AddFreezeVote(page_node());

  // Expect freezing after stopping capture.
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
      ->SetIsCapturingAudioForTesting(false);
  VerifyFreezerExpectations();
}

TEST_F(FreezingPolicyTest, StartsCapturingAudioWhenFrozen) {
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  policy()->AddFreezeVote(page_node());
  VerifyFreezerExpectations();

  EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
  PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
      ->SetIsCapturingAudioForTesting(true);
  VerifyFreezerExpectations();
}

TEST_F(FreezingPolicyTest, FreezeVoteWhenMirrored) {
  PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
      ->SetIsBeingMirroredForTesting(true);

  // Don't expect freezing.
  policy()->AddFreezeVote(page_node());

  // Expect freezing after mirroring stops.
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
      ->SetIsBeingMirroredForTesting(false);
  VerifyFreezerExpectations();
}

TEST_F(FreezingPolicyTest, StartsBeingMirroredWhenFrozen) {
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  policy()->AddFreezeVote(page_node());
  VerifyFreezerExpectations();

  EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
  PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
      ->SetIsBeingMirroredForTesting(true);
  VerifyFreezerExpectations();
}

TEST_F(FreezingPolicyTest, FreezeVoteWhenCapturingWindow) {
  PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
      ->SetIsCapturingWindowForTesting(true);

  // Don't expect freezing.
  policy()->AddFreezeVote(page_node());

  // Expect freezing after stopping capture.
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
      ->SetIsCapturingWindowForTesting(false);
  VerifyFreezerExpectations();
}

TEST_F(FreezingPolicyTest, StartsCapturingWindowWhenFrozen) {
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  policy()->AddFreezeVote(page_node());
  VerifyFreezerExpectations();

  EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
  PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
      ->SetIsCapturingWindowForTesting(true);
  VerifyFreezerExpectations();
}

TEST_F(FreezingPolicyTest, FreezeVoteWhenCapturingDisplay) {
  PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
      ->SetIsCapturingDisplayForTesting(true);

  // Don't expect freezing.
  policy()->AddFreezeVote(page_node());

  // Expect freezing after stopping capture.
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
      ->SetIsCapturingDisplayForTesting(false);
  VerifyFreezerExpectations();
}

TEST_F(FreezingPolicyTest, StartsCapturingDisplayWhenFrozen) {
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  policy()->AddFreezeVote(page_node());
  VerifyFreezerExpectations();

  EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
  PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
      ->SetIsCapturingDisplayForTesting(true);
  VerifyFreezerExpectations();
}

TEST_F(FreezingPolicyTest, FreezeVoteWhenUsingWebRTC) {
  page_node()->SetUsesWebRTCForTesting(true);

  // Don't expect freezing.
  policy()->AddFreezeVote(page_node());

  // Expect freezing after stopping capture.
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  page_node()->SetUsesWebRTCForTesting(false);
  VerifyFreezerExpectations();
}

TEST_F(FreezingPolicyTest, StartsUsingWebRTCWhenFrozen) {
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  policy()->AddFreezeVote(page_node());
  VerifyFreezerExpectations();

  EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
  page_node()->SetUsesWebRTCForTesting(true);
  VerifyFreezerExpectations();
}

TEST_F(FreezingPolicyTest, FreezeVoteWithNotificationPermission) {
  page_node()->OnNotificationPermissionStatusChange(
      blink::mojom::PermissionStatus::GRANTED);

  // Don't expect freezing.
  policy()->AddFreezeVote(page_node());

  // Expect freezing if the permission is revoked.
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  page_node()->OnNotificationPermissionStatusChange(
      blink::mojom::PermissionStatus::DENIED);
  VerifyFreezerExpectations();
}

TEST_F(FreezingPolicyTest, NotificationPermissionWhenFrozen) {
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  policy()->AddFreezeVote(page_node());
  VerifyFreezerExpectations();

  EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
  page_node()->OnNotificationPermissionStatusChange(
      blink::mojom::PermissionStatus::GRANTED);
  VerifyFreezerExpectations();

  // Changing to ASK removes the opt-out.
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  page_node()->OnNotificationPermissionStatusChange(
      blink::mojom::PermissionStatus::ASK);
  VerifyFreezerExpectations();

  // Changing to DENIED does nothing, since there is already no opt-out.
  page_node()->OnNotificationPermissionStatusChange(
      blink::mojom::PermissionStatus::DENIED);

  // Changing to GRANTED adds the opt-out.
  EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
  page_node()->OnNotificationPermissionStatusChange(
      blink::mojom::PermissionStatus::GRANTED);
  VerifyFreezerExpectations();

  // Changing to DENIED removes the opt-out.
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  page_node()->OnNotificationPermissionStatusChange(
      blink::mojom::PermissionStatus::DENIED);
  VerifyFreezerExpectations();
}

TEST_F(FreezingPolicyTest, FreezeVoteWhenLoading) {
  page_node()->SetLoadingState(PageNode::LoadingState::kLoadedBusy);

  // Don't expect freezing.
  policy()->AddFreezeVote(page_node());

  // Expect freezing after finishing loading.
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  page_node()->SetLoadingState(PageNode::LoadingState::kLoadedIdle);
  VerifyFreezerExpectations();
}

TEST_F(FreezingPolicyTest, StartsLoadingWhenFrozen) {
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  policy()->AddFreezeVote(page_node());
  VerifyFreezerExpectations();

  EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
  page_node()->SetLoadingState(PageNode::LoadingState::kLoadedBusy);
  VerifyFreezerExpectations();
}

TEST_F(FreezingPolicyTest, DiscardGrowingPrivateMemory_Basic) {
  base::test::ScopedFeatureList feature_list{
      features::kDiscardFrozenBrowsingInstancesWithGrowingPMF};
  const base::ByteSize growth_threshold =
      base::KiB(base::checked_cast<uint64_t>(
          features::kFreezingMemoryGrowthThresholdToDiscardKb.Get()));
  // Pretend that the page is frozen.
  page_node()->SetLifecycleStateForTesting(PageNode::LifecycleState::kFrozen);

  // First memory measurement after freezing.
  constexpr base::ByteSize kInitialPMF = base::KiB(10);
  ReportMemoryUsage(kContext, kInitialPMF);

  // Another memory measurement, *not* crossing the growth threshold.
  constexpr base::ByteSize kSecondPMF = base::KiB(20);
  ASSERT_LT(kSecondPMF - kInitialPMF, growth_threshold);
  ReportMemoryUsage(kContext, kSecondPMF);

  // Another memory measurement, crossing the growth threshold. The page should
  // be discarded.
  EXPECT_CALL(*discarder(),
              DiscardPages(testing::_, testing::ElementsAre(page_node())));
  ReportMemoryUsage(kContext, kInitialPMF + growth_threshold + base::KiB(1));
  VerifyDiscarderExpectations();
}

// Regression test for crbug.com/407522185: When a (non-frozen) page is added to
// a browsing instance in which all pages are frozen, the post-freezing memory
// estimates are cleared.
TEST_F(FreezingPolicyTest, DiscardGrowingPrivateMemory_PageAddedAfterFreezing) {
  base::test::ScopedFeatureList feature_list{
      features::kDiscardFrozenBrowsingInstancesWithGrowingPMF};
  const base::ByteSize growth_threshold =
      base::KiB(base::checked_cast<uint64_t>(
          features::kFreezingMemoryGrowthThresholdToDiscardKb.Get()));
  // Pretend that the page is frozen.
  page_node()->SetLifecycleStateForTesting(PageNode::LifecycleState::kFrozen);

  // First memory measurement after freezing.
  constexpr base::ByteSize kInitialPMF = base::KiB(10);
  ReportMemoryUsage(kContext, kInitialPMF);

  // Add a (non-frozen) page to the browsing instance.
  auto [page2, frame2] =
      CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceA);

  // Memory measurement crossing the growth threshold. This should not result in
  // discarding (or crash) since post-freezing memory estimates were cleared.
  const base::ByteSize kSecondPMF =
      kInitialPMF + growth_threshold + base::KiB(1);
  ReportMemoryUsage(kContext, kSecondPMF);

  // Pretend that the new page is frozen.
  page2->SetLifecycleStateForTesting(PageNode::LifecycleState::kFrozen);

  // Memory measurement crossing the growth threshold. Should not result in
  // discarding since it's the first measurement since the new page was added.
  const base::ByteSize kThirdPMF = kSecondPMF + growth_threshold + base::KiB(1);
  ReportMemoryUsage(kContext, kThirdPMF);

  // Memory measurement crossing the growth threshold. This should result in
  // discarding.
  const base::ByteSize kFourthPMFKb =
      kThirdPMF + growth_threshold + base::KiB(1);
  EXPECT_CALL(*discarder(),
              DiscardPages(testing::_, testing::UnorderedElementsAre(
                                           page_node(), page2.get())));
  ReportMemoryUsage(kContext, kFourthPMFKb);
  VerifyDiscarderExpectations();
}

TEST_F(FreezingPolicyTest, DiscardGrowingPrivateMemory_FeatureDisabled) {
  base::test::ScopedFeatureList feature_list;
  feature_list.InitAndDisableFeature(
      features::kDiscardFrozenBrowsingInstancesWithGrowingPMF);

  const base::ByteSize growth_threshold =
      base::KiB(base::checked_cast<uint64_t>(
          features::kFreezingMemoryGrowthThresholdToDiscardKb.Get()));
  // Pretend that the page is frozen.
  page_node()->SetLifecycleStateForTesting(PageNode::LifecycleState::kFrozen);

  // First memory measurement after freezing.
  constexpr base::ByteSize kInitialPMF = base::KiB(10);
  ReportMemoryUsage(kContext, kInitialPMF);

  // Another memory measurement, crossing the growth threshold. The page should
  // not be discarded since the feature is disabled.
  ReportMemoryUsage(kContext, kInitialPMF + growth_threshold + base::KiB(1));
  VerifyDiscarderExpectations();
}

TEST_F(FreezingPolicyTest,
       DiscardGrowingPrivateMemory_MultipleFrozenPagesInBrowsingInstance) {
  base::test::ScopedFeatureList feature_list{
      features::kDiscardFrozenBrowsingInstancesWithGrowingPMF};
  const base::ByteSize growth_threshold =
      base::KiB(base::checked_cast<uint64_t>(
          features::kFreezingMemoryGrowthThresholdToDiscardKb.Get()));
  auto [page2, frame2] =
      CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceA);

  // Pretend that the pages are frozen.
  page_node()->SetLifecycleStateForTesting(PageNode::LifecycleState::kFrozen);
  page2->SetLifecycleStateForTesting(PageNode::LifecycleState::kFrozen);

  // First memory measurement after freezing.
  constexpr base::ByteSize kInitialPMF = base::KiB(10);
  ReportMemoryUsage(kContext, kInitialPMF);

  // Another memory measurement, crossing the growth threshold. The 2 pages
  // should be discarded.
  EXPECT_CALL(*discarder(),
              DiscardPages(testing::_, testing::UnorderedElementsAre(
                                           page_node(), page2.get())));
  ReportMemoryUsage(kContext, kInitialPMF + growth_threshold + base::KiB(1));
  VerifyDiscarderExpectations();
}

TEST_F(FreezingPolicyTest,
       DiscardGrowingPrivateMemory_FrozenAndUnfrozenPagesInBrowsingInstance) {
  base::test::ScopedFeatureList feature_list{
      features::kDiscardFrozenBrowsingInstancesWithGrowingPMF};
  const base::ByteSize growth_threshold =
      base::KiB(base::checked_cast<uint64_t>(
          features::kFreezingMemoryGrowthThresholdToDiscardKb.Get()));
  auto [page2, frame2] =
      CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceA);

  // Pretend that the first page is frozen.
  page_node()->SetLifecycleStateForTesting(PageNode::LifecycleState::kFrozen);

  // First memory measurement after freezing the page (2nd page still unfrozen).
  constexpr base::ByteSize kInitialPMF = base::KiB(10);
  ReportMemoryUsage(kContext, kInitialPMF);

  // Pretend that the 2nd page is frozen.
  page2->SetLifecycleStateForTesting(PageNode::LifecycleState::kFrozen);

  // Another memory measurement, crossing the growth threshold since the first
  // page was frozen (but not since *all* pages were frozen). No discarding
  // expected.
  ReportMemoryUsage(kContext, kInitialPMF + growth_threshold + base::KiB(1));

  // Another memory measurement, crossing the growth threshold since all pages
  // were frozen.  The 2 pages should be discarded.
  EXPECT_CALL(*discarder(),
              DiscardPages(testing::_, testing::UnorderedElementsAre(
                                           page_node(), page2.get())));
  ReportMemoryUsage(kContext,
                    kInitialPMF + 2 * (growth_threshold + base::KiB(1)));
  VerifyDiscarderExpectations();
}

TEST_F(FreezingPolicyTest, DiscardGrowingPrivateMemory_Unfreeze) {
  base::test::ScopedFeatureList feature_list{
      features::kDiscardFrozenBrowsingInstancesWithGrowingPMF};
  const base::ByteSize growth_threshold =
      base::KiB(base::checked_cast<uint64_t>(
          features::kFreezingMemoryGrowthThresholdToDiscardKb.Get()));
  // Pretend that the page is frozen.
  page_node()->SetLifecycleStateForTesting(PageNode::LifecycleState::kFrozen);

  // First memory measurement after freezing.
  constexpr base::ByteSize kInitialPMF = base::KiB(10);
  ReportMemoryUsage(kContext, kInitialPMF);

  // Pretend that the page is unfrozen and re-frozen.
  page_node()->SetLifecycleStateForTesting(PageNode::LifecycleState::kRunning);
  page_node()->SetLifecycleStateForTesting(PageNode::LifecycleState::kFrozen);

  // Another memory measurement, crossing the growth threshold since the
  // measurement taken before unfreezing. The page should not be discarded,
  // because this is the first measurement since re-freezing.
  ReportMemoryUsage(kContext, kInitialPMF + growth_threshold + base::KiB(1));

  // Another memory measurement, crossing the growth threshold since the
  // measurement taken after re-freezing. The page should be discarded.
  EXPECT_CALL(*discarder(),
              DiscardPages(testing::_, testing::ElementsAre(page_node())));
  ReportMemoryUsage(kContext,
                    kInitialPMF + 2 * (growth_threshold + base::KiB(1)));
  VerifyDiscarderExpectations();
}

TEST_F(
    FreezingPolicyTest,
    DiscardGrowingPrivateMemory_MeasurementForNewOrigin_BelowGrowthThreshold) {
  base::test::ScopedFeatureList feature_list{
      features::kDiscardFrozenBrowsingInstancesWithGrowingPMF};
  const base::ByteSize growth_threshold =
      base::KiB(base::checked_cast<uint64_t>(
          features::kFreezingMemoryGrowthThresholdToDiscardKb.Get()));
  const resource_attribution::OriginInBrowsingInstanceContext kOtherContext{
      url::Origin(), kBrowsingInstanceA};

  // Pretend that the page is frozen.
  page_node()->SetLifecycleStateForTesting(PageNode::LifecycleState::kFrozen);

  // First memory measurement after freezing.
  constexpr base::ByteSize kInitialPMF = base::KiB(10);
  ReportMemoryUsage(kContext, kInitialPMF);

  // A memory measurement below the growth threshold for an origin not seen in
  // the first measurement. Nothing should happen.
  ReportMemoryUsage(kOtherContext,
                    (growth_threshold - base::KiB(1)).AsByteSize());
  VerifyDiscarderExpectations();

  // A second memory measurement above the growth threshold for an origin not
  // seen in the first measurement. The browsing instance should be discarded.
  EXPECT_CALL(*discarder(),
              DiscardPages(testing::_, testing::ElementsAre(page_node())));
  ReportMemoryUsage(kOtherContext, growth_threshold + base::KiB(1));
  VerifyDiscarderExpectations();
}

TEST_F(
    FreezingPolicyTest,
    DiscardGrowingPrivateMemory_MeasurementForNewOrigin_AboveGrowthThreshold) {
  base::test::ScopedFeatureList feature_list{
      features::kDiscardFrozenBrowsingInstancesWithGrowingPMF};
  const base::ByteSize growth_threshold =
      base::KiB(base::checked_cast<uint64_t>(
          features::kFreezingMemoryGrowthThresholdToDiscardKb.Get()));
  const resource_attribution::OriginInBrowsingInstanceContext kOtherContext{
      url::Origin(), kBrowsingInstanceA};

  // Pretend that the page is frozen.
  page_node()->SetLifecycleStateForTesting(PageNode::LifecycleState::kFrozen);

  // First memory measurement after freezing.
  constexpr base::ByteSize kInitialPMF = base::KiB(10);
  ReportMemoryUsage(kContext, kInitialPMF);

  // A memory measurement above the growth threshold for an origin not seen in
  // the first measurement. The browsing instance should be discarded.
  EXPECT_CALL(*discarder(),
              DiscardPages(testing::_, testing::ElementsAre(page_node())));
  ReportMemoryUsage(kOtherContext, growth_threshold + base::KiB(1));
  VerifyDiscarderExpectations();
}

TEST_F(FreezingPolicyTest,
       DiscardGrowingPrivateMemory_MeasurementForNewBrowsingInstance) {
  base::test::ScopedFeatureList feature_list{
      features::kDiscardFrozenBrowsingInstancesWithGrowingPMF};
  const base::ByteSize growth_threshold =
      base::KiB(base::checked_cast<uint64_t>(
          features::kFreezingMemoryGrowthThresholdToDiscardKb.Get()));

  const resource_attribution::OriginInBrowsingInstanceContext
      kUnknownBrowsingInstanceContext{url::Origin(), kBrowsingInstanceB};

  // Pretend that the page is frozen.
  page_node()->SetLifecycleStateForTesting(PageNode::LifecycleState::kFrozen);

  // Simulate memory usage growth above the threshold for a browsing instance
  // not known to the `FreezingPolicy`. This should be gracefully ignored.
  constexpr base::ByteSize kInitialPMF = base::KiB(10);
  ReportMemoryUsage(kUnknownBrowsingInstanceContext, kInitialPMF);
  ReportMemoryUsage(kUnknownBrowsingInstanceContext,
                    kInitialPMF + growth_threshold + base::KiB(1));
}

namespace {

class FreezingPolicyBatterySaverTest : public FreezingPolicyTest {
 public:
  FreezingPolicyBatterySaverTest() = default;
};

}  // namespace

TEST_F(FreezingPolicyBatterySaverTest, Basic) {
  policy()->ToggleFreezingOnBatterySaverMode(true);

  ReportCumulativeCPUUsage(kContext, base::Seconds(60));
  AdvanceClock(base::Seconds(60));

  // The page should be frozen when a browsing instance connected to it consumes
  // >=25% CPU in background.
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  ReportCumulativeCPUUsage(kContext, base::Seconds(75));
}

TEST_F(FreezingPolicyBatterySaverTest, RecordFreezingEligibilityUKM) {
  base::test::ScopedFeatureList feature_list(
      features::kRecordFreezingEligibilityUKM);
  base::MetricsSubSampler::ScopedAlwaysSampleForTesting always_sample;

  // page_node(), page2 and page3 are connected. page4 is disjoint.
  auto [page2, frame2] =
      CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceA);
  auto frame2b =
      CreateFrameNodeAutoId(process_node(), page2.get(),
                            /* parent_frame_node=*/nullptr, kBrowsingInstanceB);
  auto [page3, frame3] =
      CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceB);
  auto [page4, frame4] =
      CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceC);

  page_node()->SetUkmSourceId(ukm::AssignNewSourceId());
  page2->SetUkmSourceId(ukm::AssignNewSourceId());
  page3->SetUkmSourceId(ukm::AssignNewSourceId());
  page4->SetUkmSourceId(ukm::AssignNewSourceId());

  // kContext affects page_node(), page2 and page3. kContext2 and kContext3
  // affect page4.
  const resource_attribution::OriginInBrowsingInstanceContext kContext2{
      url::Origin(), kBrowsingInstanceC};
  const resource_attribution::OriginInBrowsingInstanceContext kContext3{
      url::Origin(), kBrowsingInstanceC};

  // A `CannotFreezeReason` applicable at the beginning of the CPU measurement
  // interval affects the UKM event.
  page_node()->SetIsHoldingWebLockForTesting(true);

  // Simulate initial CPU measurement.
  {
    resource_attribution::QueryResultMap cpu_result_map;
    AddCPUMeasurement(cpu_result_map, kContext, base::Seconds(0));
    AddCPUMeasurement(cpu_result_map, kContext2, base::Seconds(0));
    AddCPUMeasurement(cpu_result_map, kContext3, base::Seconds(0));
    resource_attribution::QueryResultObserver* observer = policy();
    observer->OnResourceUsageUpdated(std::move(cpu_result_map));
  }
  AdvanceClock(base::Seconds(60));

  page_node()->SetIsHoldingWebLockForTesting(false);

  // A `CannotFreezeReason` applicable transiently during the CPU measurement
  // interval affects the UKM event.
  page_node()->SetIsHoldingBlockingIndexedDBLockForTesting(true);
  page_node()->SetIsHoldingBlockingIndexedDBLockForTesting(false);

  // Simulate 2nd CPU measurement. Expect UKM events to be reported.
  {
    CannotFreezeReasonSet expected_cannot_freeze_reasons{
        CannotFreezeReason::kHoldingWebLock,
        CannotFreezeReason::kHoldingBlockingIndexedDBLock};

    EXPECT_CALL(*policy(),
                RecordFreezingEligibilityUKMForPage(
                    page_node()->GetUkmSourceID(),
                    /*highest_cpu_current_interval=*/0.5,
                    /*highest_cpu_without_battery_saver_cannot_freeze=*/
                    0, expected_cannot_freeze_reasons));
    EXPECT_CALL(*policy(),
                RecordFreezingEligibilityUKMForPage(
                    page2->GetUkmSourceID(),
                    /*highest_cpu_current_interval=*/0.5,
                    /*highest_cpu_without_battery_saver_cannot_freeze=*/
                    0, expected_cannot_freeze_reasons));
    EXPECT_CALL(*policy(),
                RecordFreezingEligibilityUKMForPage(
                    page3->GetUkmSourceID(),
                    /*highest_cpu_current_interval=*/0.5,
                    /*highest_cpu_without_battery_saver_cannot_freeze=*/
                    0, expected_cannot_freeze_reasons));
    EXPECT_CALL(*policy(),
                RecordFreezingEligibilityUKMForPage(
                    page4->GetUkmSourceID(),
                    /*highest_cpu_current_interval=*/1.0,
                    /*highest_cpu_without_battery_saver_cannot_freeze=*/
                    1.0, CannotFreezeReasonSet{}));

    resource_attribution::QueryResultMap cpu_result_map;
    AddCPUMeasurement(cpu_result_map, kContext, base::Seconds(30));   // 50%
    AddCPUMeasurement(cpu_result_map, kContext2, base::Seconds(45));  // 75%
    AddCPUMeasurement(cpu_result_map, kContext3, base::Seconds(60));  // 100%
    resource_attribution::QueryResultObserver* observer = policy();
    observer->OnResourceUsageUpdated(std::move(cpu_result_map));
  }
  AdvanceClock(base::Seconds(60));

  // Simulate 3rd CPU measurement (no applicable `CannotFreezeReason` this
  // time). Expect UKM events to be reported.
  {
    EXPECT_CALL(*policy(),
                RecordFreezingEligibilityUKMForPage(
                    page_node()->GetUkmSourceID(),
                    /*highest_cpu_current_interval=*/0.25,
                    /*highest_cpu_without_battery_saver_cannot_freeze=*/
                    0.25, CannotFreezeReasonSet{}));
    EXPECT_CALL(*policy(),
                RecordFreezingEligibilityUKMForPage(
                    page2->GetUkmSourceID(),
                    /*highest_cpu_current_interval=*/0.25,
                    /*highest_cpu_without_battery_saver_cannot_freeze=*/
                    0.25, CannotFreezeReasonSet{}));
    EXPECT_CALL(*policy(),
                RecordFreezingEligibilityUKMForPage(
                    page3->GetUkmSourceID(),
                    /*highest_cpu_current_interval=*/0.25,
                    /*highest_cpu_without_battery_saver_cannot_freeze=*/
                    0.25, CannotFreezeReasonSet{}));
    EXPECT_CALL(*policy(),
                RecordFreezingEligibilityUKMForPage(
                    page4->GetUkmSourceID(),
                    /*highest_cpu_current_interval=*/0.75,
                    /*highest_cpu_without_battery_saver_cannot_freeze=*/
                    1.0, CannotFreezeReasonSet{}));

    resource_attribution::QueryResultMap cpu_result_map;
    AddCPUMeasurement(cpu_result_map, kContext, base::Seconds(45));   // 25%
    AddCPUMeasurement(cpu_result_map, kContext2, base::Seconds(90));  // 75%
    AddCPUMeasurement(cpu_result_map, kContext3, base::Seconds(66));  // 10%
    resource_attribution::QueryResultObserver* observer = policy();
    observer->OnResourceUsageUpdated(std::move(cpu_result_map));
  }
}

TEST_F(FreezingPolicyBatterySaverTest,
       RecordFreezingEligibilityUKMForPageStatic) {
  // No "opt-out" field is set.
  // CPU usage is bucketed.
  {
    ukm::TestAutoSetUkmRecorder recorder;
    FreezingPolicy::RecordFreezingEligibilityUKMForPageStatic(
        ukm::SourceId(), /*highest_cpu_current_interval=*/0.1,
        /*highest_cpu_without_battery_saver_cannot_freeze=*/
        0.2, CannotFreezeReasonSet{});
    auto entries = recorder.GetEntriesByName(
        ukm::builders::PerformanceManager_FreezingEligibility::kEntryName);
    EXPECT_EQ(entries.size(), 1U);
    auto& entry = entries.front();
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "Audible", 0);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "BeingMirrored", 0);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "Capturing", 0);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "ConnectedToDevice", 0);
    ukm::TestUkmRecorder::ExpectEntryMetric(
        entry, "HighestCPUAnyIntervalWithoutOptOut", 16);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "HighestCPUCurrentInterval",
                                            8);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry,
                                            "HoldingBlockingIndexedDBLock", 0);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "HoldingWebLock", 0);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "Loading", 0);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "NotificationPermission", 0);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "OriginTrialOptOut", 0);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "RecentlyAudible", 0);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "RecentlyVisible", 0);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "Visible", 0);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "WebRTC", 0);
  }

  // All "opt-out" fields are set.
  // CPU usage is bucketed.
  {
    ukm::TestAutoSetUkmRecorder recorder;
    FreezingPolicy::RecordFreezingEligibilityUKMForPageStatic(
        ukm::SourceId(), /*highest_cpu_current_interval=*/0.16,
        /*highest_cpu_without_battery_saver_cannot_freeze=*/
        0.32,
        CannotFreezeReasonSet{
            CannotFreezeReason::kAudible, CannotFreezeReason::kBeingMirrored,
            CannotFreezeReason::kCapturingAudio,
            CannotFreezeReason::kConnectedToBluetoothDevice,
            CannotFreezeReason::kHoldingBlockingIndexedDBLock,
            CannotFreezeReason::kHoldingWebLock, CannotFreezeReason::kLoading,
            CannotFreezeReason::kNotificationPermission,
            CannotFreezeReason::kFreezingOriginTrialOptOut,
            CannotFreezeReason::kRecentlyAudible,
            CannotFreezeReason::kRecentlyVisible, CannotFreezeReason::kVisible,
            CannotFreezeReason::kWebRTC});
    auto entries = recorder.GetEntriesByName(
        ukm::builders::PerformanceManager_FreezingEligibility::kEntryName);
    EXPECT_EQ(entries.size(), 1U);
    auto& entry = entries.front();
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "Audible", 1);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "BeingMirrored", 1);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "Capturing", 1);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "ConnectedToDevice", 1);
    ukm::TestUkmRecorder::ExpectEntryMetric(
        entry, "HighestCPUAnyIntervalWithoutOptOut", 32);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "HighestCPUCurrentInterval",
                                            16);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry,
                                            "HoldingBlockingIndexedDBLock", 1);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "HoldingWebLock", 1);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "Loading", 1);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "NotificationPermission", 1);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "OriginTrialOptOut", 1);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "RecentlyAudible", 1);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "RecentlyVisible", 1);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "Visible", 1);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "WebRTC", 1);
  }

  // Opt-out fields from Audible -> Loading are set.
  // CPU usage is zero.
  {
    ukm::TestAutoSetUkmRecorder recorder;
    FreezingPolicy::RecordFreezingEligibilityUKMForPageStatic(
        ukm::SourceId(), /*highest_cpu_current_interval=*/0.0,
        /*highest_cpu_without_battery_saver_cannot_freeze=*/
        0.0,
        CannotFreezeReasonSet{
            CannotFreezeReason::kAudible, CannotFreezeReason::kBeingMirrored,
            CannotFreezeReason::kCapturingVideo,
            CannotFreezeReason::kConnectedToUsbDevice,
            CannotFreezeReason::kHoldingBlockingIndexedDBLock,
            CannotFreezeReason::kHoldingWebLock, CannotFreezeReason::kLoading});
    auto entries = recorder.GetEntriesByName(
        ukm::builders::PerformanceManager_FreezingEligibility::kEntryName);
    EXPECT_EQ(entries.size(), 1U);
    auto& entry = entries.front();
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "Audible", 1);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "BeingMirrored", 1);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "Capturing", 1);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "ConnectedToDevice", 1);
    ukm::TestUkmRecorder::ExpectEntryMetric(
        entry, "HighestCPUAnyIntervalWithoutOptOut", 0);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "HighestCPUCurrentInterval",
                                            0);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry,
                                            "HoldingBlockingIndexedDBLock", 1);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "HoldingWebLock", 1);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "Loading", 1);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "NotificationPermission", 0);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "OriginTrialOptOut", 0);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "RecentlyAudible", 0);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "RecentlyVisible", 0);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "Visible", 0);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "WebRTC", 0);
  }

  // Opt-out fields from Notification -> WebRTC are set.
  // CPU usage is very low (bucketing has no effect at this level).
  {
    ukm::TestAutoSetUkmRecorder recorder;
    FreezingPolicy::RecordFreezingEligibilityUKMForPageStatic(
        ukm::SourceId(), /*highest_cpu_current_interval=*/0.01,
        /*highest_cpu_without_battery_saver_cannot_freeze=*/
        0.02,
        CannotFreezeReasonSet{CannotFreezeReason::kNotificationPermission,
                              CannotFreezeReason::kFreezingOriginTrialOptOut,
                              CannotFreezeReason::kRecentlyAudible,
                              CannotFreezeReason::kRecentlyVisible,
                              CannotFreezeReason::kVisible,
                              CannotFreezeReason::kWebRTC});
    auto entries = recorder.GetEntriesByName(
        ukm::builders::PerformanceManager_FreezingEligibility::kEntryName);
    EXPECT_EQ(entries.size(), 1U);
    auto& entry = entries.front();
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "Audible", 0);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "BeingMirrored", 0);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "Capturing", 0);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "ConnectedToDevice", 0);
    ukm::TestUkmRecorder::ExpectEntryMetric(
        entry, "HighestCPUAnyIntervalWithoutOptOut", 2);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "HighestCPUCurrentInterval",
                                            1);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry,
                                            "HoldingBlockingIndexedDBLock", 0);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "HoldingWebLock", 0);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "Loading", 0);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "NotificationPermission", 1);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "OriginTrialOptOut", 1);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "RecentlyAudible", 1);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "RecentlyVisible", 1);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "Visible", 1);
    ukm::TestUkmRecorder::ExpectEntryMetric(entry, "WebRTC", 1);
  }
}

TEST_F(FreezingPolicyBatterySaverTest, ConnectedPages) {
  auto [page2, frame2] =
      CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceA);
  auto frame2b =
      CreateFrameNodeAutoId(process_node(), page2.get(),
                            /* parent_frame_node=*/nullptr, kBrowsingInstanceB);
  auto [page3, frame3] =
      CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceB);

  policy()->ToggleFreezingOnBatterySaverMode(true);

  ReportCumulativeCPUUsage(kContext, base::Seconds(60));
  AdvanceClock(base::Seconds(60));

  // The page should be frozen when a browsing instance connected to it consumes
  // >=25% CPU in background.
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page2.get()));
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page3.get()));
  ReportCumulativeCPUUsage(kContext, base::Seconds(75));
}

TEST_F(FreezingPolicyBatterySaverTest, CannotFreeze) {
  policy()->ToggleFreezingOnBatterySaverMode(true);

  ReportCumulativeCPUUsage(kContext, base::Seconds(60));
  AdvanceClock(base::Seconds(60));

  // Add a `CannotFreezeReason`.
  page_node()->SetIsHoldingWebLockForTesting(true);

  // The page should not be frozen when a browsing instance connected to it
  // consumes >=25% CPU in background, because it has a `CannotFreezeReason`.
  ReportCumulativeCPUUsage(kContext, base::Seconds(75));
  AdvanceClock(base::Seconds(60));

  // Remove the `CannotFreezeReason`. This should not cause the page to be
  // frozen, since there was a `CannotFreezeReason` when high CPU usage was
  // measured.
  page_node()->SetIsHoldingWebLockForTesting(false);

  // The page should not be frozen when a browsing instance connected to it
  // consumes >=25% CPU in background, because it transiently had a
  // `CannotFreezeReason` during the measurement interval.
  ReportCumulativeCPUUsage(kContext, base::Seconds(90));
  AdvanceClock(base::Seconds(60));

  // The page should be frozen when a browsing instance connected to it consumes
  // >=25% CPU in background and there was no `CannotFreezeReason` at any point
  // during the measurement interval.
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  ReportCumulativeCPUUsage(kContext, base::Seconds(105));
}

TEST_F(FreezingPolicyBatterySaverTest, CannotFreezeTransient) {
  policy()->ToggleFreezingOnBatterySaverMode(true);

  ReportCumulativeCPUUsage(kContext, base::Seconds(60));
  AdvanceClock(base::Seconds(60));

  // Transiently add a `CannotFreezeReason`.
  page_node()->SetIsHoldingWebLockForTesting(true);
  page_node()->SetIsHoldingWebLockForTesting(false);

  // The page should not be frozen when a browsing instance connected to it
  // consumes >=25% CPU in background, because it transiently had a
  // `CannotFreezeReason` during the measurement interval.
  ReportCumulativeCPUUsage(kContext, base::Seconds(75));
}

TEST_F(FreezingPolicyBatterySaverTest, BatterySaverInactive) {
  // Battery Saver is not active in this test.

  ReportCumulativeCPUUsage(kContext, base::Seconds(60));
  AdvanceClock(base::Seconds(60));

  // The page should not be frozen when a browsing instance connected to it
  // consumes >=25% CPU in background, because Battery Saver is not active.
  ReportCumulativeCPUUsage(kContext, base::Seconds(75));
}

TEST_F(FreezingPolicyBatterySaverTest, ForegroundCPU) {
  policy()->ToggleFreezingOnBatterySaverMode(true);

  ReportCumulativeCPUUsage(kContext,
                           /*cumulative_background_cpu=*/base::Seconds(60),
                           /*cumulative_cpu=*/base::Seconds(60));
  AdvanceClock(base::Seconds(60));

  // The page should not be frozen when a browsing instance connected to it
  // consumes >=25% CPU in foreground, but little CPU in background.
  ReportCumulativeCPUUsage(kContext,
                           /*cumulative_background_cpu=*/base::Seconds(62),
                           /*cumulative_cpu=*/base::Seconds(90));
}

TEST_F(FreezingPolicyBatterySaverTest, DeactivateBatterySaver) {
  policy()->ToggleFreezingOnBatterySaverMode(true);

  ReportCumulativeCPUUsage(kContext, base::Seconds(60));
  AdvanceClock(base::Seconds(60));
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  ReportCumulativeCPUUsage(kContext, base::Seconds(75));
  VerifyFreezerExpectations();

  // The page should be unfrozen when Battery Saver becomes inactive.
  EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
  policy()->ToggleFreezingOnBatterySaverMode(false);
}

TEST_F(FreezingPolicyBatterySaverTest, ActivateBatterySaverAfterHighCPU) {
  // Battery Saver is not active at the beginning of this test.

  // Report high background CPU usage.
  ReportCumulativeCPUUsage(kContext, base::Seconds(60));
  AdvanceClock(base::Seconds(60));
  ReportCumulativeCPUUsage(kContext, base::Seconds(75));

  // The page should be frozen when Battery Saver becomes active.
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  policy()->ToggleFreezingOnBatterySaverMode(true);
}

namespace {

constexpr char kOptOutUrl1[] = "http://a.com/";
constexpr char kOptOutUrl2[] = "http://b.com/";
const auto kBrowsingContext1 = base::UnguessableToken::CreateForTesting(1, 1);
const auto kBrowsingContext2 = base::UnguessableToken::CreateForTesting(2, 2);

// A test implementation of OptOutChecker that opts out a single URL.
class TestOptOutChecker final : public freezing::OptOutChecker {
 public:
  TestOptOutChecker() = default;
  ~TestOptOutChecker() final = default;

  TestOptOutChecker(const TestOptOutChecker&) = delete;
  TestOptOutChecker& operator=(const TestOptOutChecker&) = delete;

  // Sets the opted out url to `url`, and notifies `browser_contexts_to_notify`
  // of the change.
  void SetOptedOutUrl(
      const std::string& url,
      const std::vector<base::UnguessableToken>& browser_contexts_to_notify);

  // OptOutChecker:
  void SetOptOutPolicyChangedCallback(
      OnPolicyChangedForBrowserContextCallback callback) final;
  bool IsPageOptedOutOfFreezing(
      const base::UnguessableToken& browser_context_id,
      const GURL& main_frame_url) final;

 private:
  OnPolicyChangedForBrowserContextCallback on_policy_changed_callback_;
  GURL opted_out_url_;
};

void TestOptOutChecker::SetOptedOutUrl(
    const std::string& url,
    const std::vector<base::UnguessableToken>& browser_contexts_to_notify =
        {}) {
  ASSERT_TRUE(on_policy_changed_callback_);
  opted_out_url_ = GURL(url);
  for (const auto& browser_context_id : browser_contexts_to_notify) {
    on_policy_changed_callback_.Run(browser_context_id);
  }
}

void TestOptOutChecker::SetOptOutPolicyChangedCallback(
    OnPolicyChangedForBrowserContextCallback callback) {
  on_policy_changed_callback_ = std::move(callback);
}

bool TestOptOutChecker::IsPageOptedOutOfFreezing(
    const base::UnguessableToken& browser_context_id,
    const GURL& main_frame_url) {
  return opted_out_url_.is_valid() && main_frame_url == opted_out_url_;
}

class FreezingPolicyOptOutTest : public FreezingPolicyTest {
 public:
  std::unique_ptr<freezing::OptOutChecker> CreateTestOptOutChecker() override {
    auto opt_out_checker = std::make_unique<TestOptOutChecker>();
    opt_out_checker_ = opt_out_checker.get();
    return opt_out_checker;
  }

  void TearDown() override {
    // Prevent dangling raw_ptr.
    opt_out_checker_ = nullptr;
    FreezingPolicyTest::TearDown();
  }

  void NavigateToUrl(PageNodeImpl* page_node, std::string_view url) {
    page_node->OnMainFrameNavigationCommitted(
        /*same_document=*/false, base::TimeTicks::Now(), next_navigation_id_++,
        GURL(url), /*contents_mime_type=*/"",
        /*notification_permission_status=*/std::nullopt);
  }

 protected:
  raw_ptr<TestOptOutChecker> opt_out_checker_ = nullptr;

  // page_node() navigates once when the GraphTestHarness is set up.
  int next_navigation_id_ = 2;
};

}  // namespace

TEST_F(FreezingPolicyOptOutTest, MainFrameUrlChanges) {
  ASSERT_TRUE(opt_out_checker_.get());
  opt_out_checker_->SetOptedOutUrl(kOptOutUrl1);

  // Can freeze before an URL is set.
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  policy()->AddFreezeVote(page_node());
  VerifyFreezerExpectations();
  ExpectCannotFreezeReasons(page_node(), FreezingType::kVoting, IsEmpty());

  // Stays frozen when navigating to an URL that's not opted out.
  NavigateToUrl(page_node(), kOptOutUrl2);
  VerifyFreezerExpectations();
  ExpectCannotFreezeReasons(page_node(), FreezingType::kVoting, IsEmpty());

  // Unfreezes when navigating to an URL that's opted out.
  EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
  NavigateToUrl(page_node(), kOptOutUrl1);
  VerifyFreezerExpectations();
  ExpectCannotFreezeReasons(page_node(), FreezingType::kVoting,
                            ElementsAre(CannotFreezeReason::kOptedOut));

  // Navigating to the same URL does nothing.
  NavigateToUrl(page_node(), kOptOutUrl1);
  VerifyFreezerExpectations();
  ExpectCannotFreezeReasons(page_node(), FreezingType::kVoting,
                            ElementsAre(CannotFreezeReason::kOptedOut));

  // Freezes when navigating away from the opted out URL.
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
  NavigateToUrl(page_node(), kOptOutUrl2);
  VerifyFreezerExpectations();
  ExpectCannotFreezeReasons(page_node(), FreezingType::kVoting, IsEmpty());
}

TEST_F(FreezingPolicyOptOutTest, OptOutPolicyChanges) {
  ASSERT_TRUE(opt_out_checker_.get());
  opt_out_checker_->SetOptedOutUrl(kOptOutUrl1);

  // Give each page a unique browsing instance so that there are no connected
  // pages. kBrowsingInstanceA is already used by page_node().
  auto [page1, frame1] = CreatePageAndFrameWithBrowsingInstanceId(
      content::BrowsingInstanceId(10), kBrowsingContext1);
  auto [page2, frame2] = CreatePageAndFrameWithBrowsingInstanceId(
      content::BrowsingInstanceId(11), kBrowsingContext1);
  auto [page3, frame3] = CreatePageAndFrameWithBrowsingInstanceId(
      content::BrowsingInstanceId(12), kBrowsingContext2);

  // Each VerifyFreezerExpectations() call is inside a SCOPED_TRACE to help
  // interpret StrictMock "unexpected function call" failures, which don't
  // include line numbers. Also dump the PageNode pointers for each failure to
  // help interpret the arguments of unexpected functions.
  SCOPED_TRACE(::testing::Message()
               << "page1: " << page1.get() << ", page2: " << page2.get()
               << ", page3: " << page3.get());

  NavigateToUrl(page1.get(), kOptOutUrl1);
  NavigateToUrl(page2.get(), kOptOutUrl2);
  NavigateToUrl(page3.get(), kOptOutUrl2);

  // Only page1's URL is opted out.
  {
    SCOPED_TRACE("add freeze votes");
    EXPECT_CALL(*freezer(), MaybeFreezePageNode(page1.get())).Times(0);
    EXPECT_CALL(*freezer(), MaybeFreezePageNode(page2.get()));
    EXPECT_CALL(*freezer(), MaybeFreezePageNode(page3.get()));
    policy()->AddFreezeVote(page1.get());
    policy()->AddFreezeVote(page2.get());
    policy()->AddFreezeVote(page3.get());
    VerifyFreezerExpectations();
    ExpectCannotFreezeReasons(page1.get(), FreezingType::kVoting,
                              ElementsAre(CannotFreezeReason::kOptedOut));
    ExpectCannotFreezeReasons(page2.get(), FreezingType::kVoting, IsEmpty());
    ExpectCannotFreezeReasons(page3.get(), FreezingType::kVoting, IsEmpty());
  }

  // Change which URL is opted out. Notify kBrowsingContext1 of the change.
  {
    SCOPED_TRACE("change opted out URL");
    EXPECT_CALL(*freezer(), UnfreezePageNode(page2.get()));
    EXPECT_CALL(*freezer(), MaybeFreezePageNode(page1.get()));
    opt_out_checker_->SetOptedOutUrl(kOptOutUrl2, {kBrowsingContext1});
    VerifyFreezerExpectations();
    ExpectCannotFreezeReasons(page1.get(), FreezingType::kVoting, IsEmpty());
    ExpectCannotFreezeReasons(page2.get(), FreezingType::kVoting,
                              ElementsAre(CannotFreezeReason::kOptedOut));
    ExpectCannotFreezeReasons(page3.get(), FreezingType::kVoting, IsEmpty());
  }

  // Now notify kBrowsingContext2.
  {
    SCOPED_TRACE("notify other context");
    EXPECT_CALL(*freezer(), UnfreezePageNode(page3.get()));
    opt_out_checker_->SetOptedOutUrl(kOptOutUrl2, {kBrowsingContext2});
    VerifyFreezerExpectations();
    ExpectCannotFreezeReasons(page1.get(), FreezingType::kVoting, IsEmpty());
    ExpectCannotFreezeReasons(page2.get(), FreezingType::kVoting,
                              ElementsAre(CannotFreezeReason::kOptedOut));
    ExpectCannotFreezeReasons(page3.get(), FreezingType::kVoting,
                              ElementsAre(CannotFreezeReason::kOptedOut));
  }

  // Remove the opt out and notify both contexts. Every page should freeze.
  {
    SCOPED_TRACE("remove opt outs");
    EXPECT_CALL(*freezer(), MaybeFreezePageNode(page2.get()));
    EXPECT_CALL(*freezer(), MaybeFreezePageNode(page3.get()));
    opt_out_checker_->SetOptedOutUrl("",
                                     {kBrowsingContext1, kBrowsingContext2});
    VerifyFreezerExpectations();
    ExpectCannotFreezeReasons(page1.get(), FreezingType::kVoting, IsEmpty());
    ExpectCannotFreezeReasons(page2.get(), FreezingType::kVoting, IsEmpty());
    ExpectCannotFreezeReasons(page3.get(), FreezingType::kVoting, IsEmpty());
  }
}

namespace {

class FreezingPolicyPeriodicUnfreezeTestBase
    : public FreezingPolicyTest_BaseWithNoPage {
 protected:
  FreezingPolicyPeriodicUnfreezeTestBase() = default;

  void OnGraphCreated(GraphImpl* graph) override {
    FreezingPolicyTest_BaseWithNoPage::OnGraphCreated(graph);

    // Start the test outside of the periodic unfreeze period.
    AdvanceToAlignedTime(base::Minutes(1));
    AdvanceClock(features::kInfiniteTabsFreezing_UnfreezeDuration.Get());

    // Create "num protected tabs" hidden pages. All pages have their own
    // browsing instance, which is not equal to `kBrowsingInstance(A|B|C)`.
    for (int i = 0; i < features::kInfiniteTabsFreezing_NumProtectedTabs.Get();
         ++i) {
      auto [page, frame] =
          CreatePageAndFrameWithBrowsingInstanceId(content::BrowsingInstanceId(
              kBrowsingInstanceC.GetUnsafeValue() + i + 1));
      ASSERT_FALSE(page->IsVisible());
      pages_.push_back(std::move(page));
      frames_.push_back(std::move(frame));
      AdvanceClock(base::Milliseconds(1));
    }
  }

  // Advances the clock to a time aligned on `interval`.
  void AdvanceToAlignedTime(base::TimeDelta interval) {
    const base::LiveTicks now = base::LiveTicks::Now();
    const base::LiveTicks next_aligned_time =
        now.SnappedToNextTick(base::LiveTicks(), interval);
    AdvanceClock(next_aligned_time - now);
  }

  std::vector<TestNodeWrapper<PageNodeImpl>> pages_;
  std::vector<TestNodeWrapper<FrameNodeImpl>> frames_;
};

// Tests with only the always-active Infinite Tabs feature enabled.
class FreezingPolicyInfiniteTabsTest
    : public FreezingPolicyPeriodicUnfreezeTestBase {
 protected:
  FreezingPolicyInfiniteTabsTest() {
    feature_list_.InitWithFeatures(
        {features::kInfiniteTabsFreezing},
        {features::kInfiniteTabsFreezingOnMemoryPressure});
  }

 private:
  base::test::ScopedFeatureList feature_list_;
};

class FreezingPolicyPeriodicUnfreezeDisabledTest
    : public FreezingPolicyPeriodicUnfreezeTestBase {
 protected:
  FreezingPolicyPeriodicUnfreezeDisabledTest() {
    feature_list_.InitWithFeatures(
        {}, {features::kInfiniteTabsFreezing,
             features::kInfiniteTabsFreezingOnMemoryPressure});
  }

 private:
  base::test::ScopedFeatureList feature_list_;
};

// Tests with only the memory-pressure-gated Infinite Tabs feature enabled.
class FreezingPolicyInfiniteTabsMemoryPressureTest
    : public FreezingPolicyPeriodicUnfreezeTestBase {
 protected:
  FreezingPolicyInfiniteTabsMemoryPressureTest() {
    // Keep the Windows-only automatic memory check from overriding pressure
    // transitions injected by this fixture.
    feature_list_.InitWithFeaturesAndParameters(
        {{features::kInfiniteTabsFreezingOnMemoryPressure,
          {{features::kInfiniteTabsFreezingOnMemoryPressureInterval.name,
            "1d"}}}},
        {features::kInfiniteTabsFreezing});
  }

 private:
  base::test::ScopedFeatureList feature_list_;
};

class FreezingPolicyInfiniteTabsBothFeaturesTest
    : public FreezingPolicyPeriodicUnfreezeTestBase {
 protected:
  FreezingPolicyInfiniteTabsBothFeaturesTest() {
    feature_list_.InitWithFeatures(
        {features::kInfiniteTabsFreezing,
         features::kInfiniteTabsFreezingOnMemoryPressure},
        {});
  }

 private:
  base::test::ScopedFeatureList feature_list_;
};

}  // namespace

TEST_F(FreezingPolicyPeriodicUnfreezeDisabledTest, TimerIsNotStarted) {
  auto [page, frame] =
      CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceA);
  ASSERT_FALSE(page->IsVisible());

  EXPECT_FALSE(
      policy()->IsPeriodicUnfreezeTimerRunningForTesting(pages_[0].get()));

  AdvanceClock(features::kInfiniteTabsFreezing_UnfreezeInterval.Get() * 2);
  VerifyFreezerExpectations();
}

TEST_F(FreezingPolicyInfiniteTabsMemoryPressureTest, PressureTransitions) {
  auto [page, frame] =
      CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceA);
  ASSERT_FALSE(page->IsVisible());

  EXPECT_FALSE(
      policy()->IsPeriodicUnfreezeTimerRunningForTesting(pages_[0].get()));

  EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[0].get()));
  policy()->SetIsUnderMemoryPressureForTesting(true);
  VerifyFreezerExpectations();
  EXPECT_TRUE(
      policy()->IsPeriodicUnfreezeTimerRunningForTesting(pages_[0].get()));

  EXPECT_CALL(*freezer(), UnfreezePageNode(pages_[0].get()));
  policy()->SetIsUnderMemoryPressureForTesting(false);
  VerifyFreezerExpectations();
  EXPECT_FALSE(
      policy()->IsPeriodicUnfreezeTimerRunningForTesting(pages_[0].get()));

  // Enter pressure at the beginning of an unfreeze period. The page should
  // remain unfrozen until the end of the window.
  AdvanceToAlignedTime(features::kInfiniteTabsFreezing_UnfreezeInterval.Get());
  policy()->SetIsUnderMemoryPressureForTesting(true);
  EXPECT_TRUE(
      policy()->IsPeriodicUnfreezeTimerRunningForTesting(pages_[0].get()));

  EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[0].get()));
  AdvanceClock(features::kInfiniteTabsFreezing_UnfreezeDuration.Get());
  VerifyFreezerExpectations();
  EXPECT_TRUE(
      policy()->IsPeriodicUnfreezeTimerRunningForTesting(pages_[0].get()));

  EXPECT_CALL(*freezer(), UnfreezePageNode(pages_[0].get()));
  policy()->SetIsUnderMemoryPressureForTesting(false);
  VerifyFreezerExpectations();
  EXPECT_FALSE(
      policy()->IsPeriodicUnfreezeTimerRunningForTesting(pages_[0].get()));
}

TEST_F(FreezingPolicyInfiniteTabsBothFeaturesTest,
       TimerRemainsRunningAcrossPressureTransitions) {
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[0].get()));
  auto [page, frame] =
      CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceA);
  ASSERT_FALSE(page->IsVisible());
  VerifyFreezerExpectations();

  // A page that never had a reason change doesn't have a timer. Pressure
  // transitions shouldn't start one because the always-active feature already
  // controls periodic unfreezing.
  auto page_without_timer = CreateNode<PageNodeImpl>(
      /*web_contents=*/nullptr,
      /*browsing_context_id=*/base::UnguessableToken());

  EXPECT_TRUE(
      policy()->IsPeriodicUnfreezeTimerRunningForTesting(pages_[0].get()));
  EXPECT_FALSE(policy()->IsPeriodicUnfreezeTimerRunningForTesting(
      page_without_timer.get()));
  policy()->SetIsUnderMemoryPressureForTesting(true);
  EXPECT_TRUE(
      policy()->IsPeriodicUnfreezeTimerRunningForTesting(pages_[0].get()));
  EXPECT_FALSE(policy()->IsPeriodicUnfreezeTimerRunningForTesting(
      page_without_timer.get()));
  policy()->SetIsUnderMemoryPressureForTesting(false);
  EXPECT_TRUE(
      policy()->IsPeriodicUnfreezeTimerRunningForTesting(pages_[0].get()));
  EXPECT_FALSE(policy()->IsPeriodicUnfreezeTimerRunningForTesting(
      page_without_timer.get()));
  VerifyFreezerExpectations();
}

// Verify that under "Infinite Tabs Freezing", tabs are frozen if not in the
// list of most recently used.
TEST_F(FreezingPolicyInfiniteTabsTest, MostRecentlyUsed) {
  // Create a new page, which takes the spot of the `pages_[0]` in the list
  // of most recently used pages. `pages_[0]` should be frozen.
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[0].get()));
  auto [page5, frame5] =
      CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceA);
  AdvanceClock(base::Milliseconds(1));
  VerifyFreezerExpectations();

  // Create a new page, which takes the spot of `pages_[1]` in the list
  // of most recently used pages. `pages_[1]` should be frozen.
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[1].get()));
  auto [page6, frame6] =
      CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceB);
  AdvanceClock(base::Milliseconds(1));
  VerifyFreezerExpectations();

  // Make `pages_[1]` visible. It should take the spot of `pages_[2]` in the
  // list of most recently used pages. `pages_[2]` should be frozen.
  EXPECT_CALL(*freezer(), UnfreezePageNode(pages_[1].get()));
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[2].get()));
  pages_[1]->SetIsVisible(true);
  VerifyFreezerExpectations();

  // Make `pages_[2]` visible. It should take the spot of `pages_[3]]` in the
  // list of most recently used pages. `pages_[4]` should be frozen.
  EXPECT_CALL(*freezer(), UnfreezePageNode(pages_[2].get()));
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[3].get()));
  pages_[2]->SetIsVisible(true);
  VerifyFreezerExpectations();

  // Hide `pages_[3]` and `pages_[1]`. This should have no effect on freezing.
  pages_[2]->SetIsVisible(false);
  AdvanceClock(base::Milliseconds(1));
  pages_[1]->SetIsVisible(false);
  AdvanceClock(base::Milliseconds(1));

  // Create new pages. At each page creation, the least recently used protected
  // page should loose its protection and be frozen.
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[4].get()));
  auto [page7, frame7] = CreatePageAndFrameWithBrowsingInstanceId(
      content::BrowsingInstanceId(100));
  VerifyFreezerExpectations();
  AdvanceClock(base::Milliseconds(1));

  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page5.get()));
  auto [page8, frame8] = CreatePageAndFrameWithBrowsingInstanceId(
      content::BrowsingInstanceId(101));
  VerifyFreezerExpectations();
  AdvanceClock(base::Milliseconds(1));

  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page6.get()));
  auto [page9, frame9] = CreatePageAndFrameWithBrowsingInstanceId(
      content::BrowsingInstanceId(102));
  VerifyFreezerExpectations();
  AdvanceClock(base::Milliseconds(1));

  EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[2].get()));
  auto [page10, frame10] = CreatePageAndFrameWithBrowsingInstanceId(
      content::BrowsingInstanceId(103));
  VerifyFreezerExpectations();
  AdvanceClock(base::Milliseconds(1));

  EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[1].get()));
  auto [page11, frame11] = CreatePageAndFrameWithBrowsingInstanceId(
      content::BrowsingInstanceId(104));
  VerifyFreezerExpectations();
  AdvanceClock(base::Milliseconds(1));
}

// Verify that under "Infinite Tabs Freezing", a tab created visible (as opposed
// to hidden, as in other tests) doesn't have
// `CannotFreezeReason::kMostRecentlyUsed`, but still counts towards the limit
// of most recently used tabs protected against "Infinite Tabs Freezing".
TEST_F(FreezingPolicyInfiniteTabsTest, InitiallyVisible) {
  // Create a new page, which takes the spot of the `pages_[0]` in the list
  // of most recently used pages. `pages_[0]` should be frozen.
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[0].get()));
  auto page5 = CreateNode<PageNodeImpl>(
      /*web_contents=*/nullptr,
      /*browsing_context_id=*/base::UnguessableToken(), GURL(),
      PagePropertyFlags{PagePropertyFlag::kIsVisible});
  EXPECT_TRUE(page5->IsVisible());
  page5->SetType(PageType::kTab);
  auto frame5 =
      CreateFrameNodeAutoId(process_node(), page5.get(),
                            /* parent_frame_node=*/nullptr, kBrowsingInstanceA);
  VerifyFreezerExpectations();
}

// Verify that under "Infinite Tabs Freezing", when there are more visible tabs
// than the desired number of protected tabs and they subsequently become
// hidden, freezing happens (as opposed to all tabs being transferred to the
// most recently used list, exceeding the limit).
TEST_F(FreezingPolicyInfiniteTabsTest, ManyVisibleTabs) {
  // Create more visible tabs than the limit of protected tabs. All existing
  // tabs are frozen.
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[0].get()));
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[1].get()));
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[2].get()));
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[3].get()));
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[4].get()));

  std::vector<TestNodeWrapper<PageNodeImpl>> more_pages;
  std::vector<TestNodeWrapper<FrameNodeImpl>> more_frames;
  for (int i = 0;
       i < features::kInfiniteTabsFreezing_NumProtectedTabs.Get() * 2; ++i) {
    more_pages.push_back(CreateNode<PageNodeImpl>(
        /*web_contents=*/nullptr,
        /*browsing_context_id=*/base::UnguessableToken(), GURL(),
        PagePropertyFlags{PagePropertyFlag::kIsVisible}));
    more_pages.back()->SetType(PageType::kTab);
    more_frames.push_back(CreateFrameNodeAutoId(
        process_node(), more_pages.back().get(),
        /* parent_frame_node=*/nullptr, content::BrowsingInstanceId(100 + i)));
  }

  VerifyFreezerExpectations();

  // Hide half of tabs. They should be frozen immediately, not put in the list
  // of most recently used, as there are already more visible tabs that the
  // limit of protected tabs.
  for (int i = 0; i < features::kInfiniteTabsFreezing_NumProtectedTabs.Get();
       ++i) {
    EXPECT_CALL(*freezer(), MaybeFreezePageNode(more_pages[i].get()));
    more_pages[i]->SetIsVisible(false);
    VerifyFreezerExpectations();
  }
}

// Verify that under "Infinite Tabs Freezing", a `CannotFreezeReason` that
// applies to all types of freezing is honored.
TEST_F(FreezingPolicyInfiniteTabsTest, UniversalCannotFreezeReason) {
  // Create a new page, which takes the spot of the `pages_[0]` in the list
  // of most recently used pages. `pages_[0]` should be frozen.
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[0].get()));
  auto [page5, frame5] =
      CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceA);
  AdvanceClock(base::Milliseconds(1));
  VerifyFreezerExpectations();

  // Add a `CannotFreezeReason` to `pages_[0]`. It should be unfrozen.
  EXPECT_CALL(*freezer(), UnfreezePageNode(pages_[0].get()));
  pages_[0]->SetIsHoldingWebLockForTesting(true);
  VerifyFreezerExpectations();

  // Add a `CannotFreezeReason` to `pages_[1]`. This has no effect since it's
  // not frozen.
  pages_[1]->SetIsHoldingWebLockForTesting(true);

  // Create a new page, which takes the spot of `pages_[1]` in the list of most
  // recently used pages. `pages_[1]` should not be frozen since it holds a Web
  // Lock.
  auto [page6, frame6] =
      CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceB);
  AdvanceClock(base::Milliseconds(1));

  // When `pages_[0]` and `pages_[1]` release their lock, they're both frozen.
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[0].get()));
  pages_[0]->SetIsHoldingWebLockForTesting(false);
  VerifyFreezerExpectations();
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[1].get()));
  pages_[1]->SetIsHoldingWebLockForTesting(false);
}

// Verify that under "Infinite Tabs Freezing", frozen tabs are periodically
// unfrozen.
TEST_F(FreezingPolicyInfiniteTabsTest, PeriodicUnfreeze) {
  // Advance to the beginning of the next periodic unfreeze period.
  AdvanceToAlignedTime(features::kInfiniteTabsFreezing_UnfreezeInterval.Get());

  // Create a new page. This should remove
  // `CannotFreezeReason::kMostRecentlyUsed` from `pages_[0]`. However, it's not
  // frozen yet since it's still in its periodic unfreeze period.
  auto [page, frame] =
      CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceA);
  ASSERT_FALSE(page->IsVisible());
  EXPECT_TRUE(
      policy()->IsPeriodicUnfreezeTimerRunningForTesting(pages_[0].get()));

  // Advance to the end of the periodic unfreeze period. `pages_[0]` should be
  // frozen.
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[0].get()));
  AdvanceClock(features::kInfiniteTabsFreezing_UnfreezeDuration.Get());
  VerifyFreezerExpectations();
  EXPECT_TRUE(
      policy()->IsPeriodicUnfreezeTimerRunningForTesting(pages_[0].get()));

  // Advance to the beginning of the next periodic unfreeze period. `pages_[0]`
  // should be unfrozen.
  EXPECT_CALL(*freezer(), UnfreezePageNode(pages_[0].get()));
  AdvanceClock(kTimeBetweenUnfreezePeriods);
  VerifyFreezerExpectations();
  EXPECT_TRUE(
      policy()->IsPeriodicUnfreezeTimerRunningForTesting(pages_[0].get()));

  // Advance to the end of the periodic unfreeze period. `pages_[0]` should be
  // frozen again.
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[0].get()));
  AdvanceClock(features::kInfiniteTabsFreezing_UnfreezeDuration.Get());
  VerifyFreezerExpectations();
  EXPECT_TRUE(
      policy()->IsPeriodicUnfreezeTimerRunningForTesting(pages_[0].get()));

  // Add a `CannotFreezeReason`. `pages_[0]` is unfrozen.
  EXPECT_CALL(*freezer(), UnfreezePageNode(pages_[0].get()));
  pages_[0]->SetUsesWebRTCForTesting(true);
  VerifyFreezerExpectations();
  EXPECT_FALSE(
      policy()->IsPeriodicUnfreezeTimerRunningForTesting(pages_[0].get()));

  // At the next periodic unfreeze period, `pages_[0]` remains unfrozen.
  AdvanceClock(kTimeBetweenUnfreezePeriods);

  // When the periodic unfreeze period ends, `pages_[0]` is not re-frozen.
  AdvanceClock(features::kInfiniteTabsFreezing_UnfreezeDuration.Get());

  // When the `CannotFreezeReason` is removed, `pages_[0]` is frozen.
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[0].get()));
  pages_[0]->SetUsesWebRTCForTesting(false);
  VerifyFreezerExpectations();
  EXPECT_TRUE(
      policy()->IsPeriodicUnfreezeTimerRunningForTesting(pages_[0].get()));

  // The restarted timer keeps the page's original phase.
  EXPECT_CALL(*freezer(), UnfreezePageNode(pages_[0].get()));
  AdvanceClock(kTimeBetweenUnfreezePeriods);
  VerifyFreezerExpectations();
}

TEST_F(FreezingPolicyInfiniteTabsTest, ConnectedPagesPeriodicUnfreeze) {
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[0].get()));
  auto [page, frame] =
      CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceA);
  VerifyFreezerExpectations();

  auto connected_page = CreateNode<PageNodeImpl>(
      /*web_contents=*/nullptr,
      /*browsing_context_id=*/base::UnguessableToken());
  connected_page->SetType(PageType::kExtension);
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(connected_page.get()));
  auto connected_frame = CreateFrameNodeAutoId(
      process_node(), connected_page.get(),
      /*parent_frame_node=*/nullptr, frames_[0]->GetBrowsingInstanceId());
  VerifyFreezerExpectations();

  EXPECT_CALL(*freezer(), UnfreezePageNode(pages_[0].get()));
  EXPECT_CALL(*freezer(), UnfreezePageNode(connected_page.get()));
  AdvanceToAlignedTime(features::kInfiniteTabsFreezing_UnfreezeInterval.Get());
  VerifyFreezerExpectations();

  EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[0].get()));
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(connected_page.get()));
  AdvanceClock(features::kInfiniteTabsFreezing_UnfreezeDuration.Get());
  VerifyFreezerExpectations();
}

TEST_F(FreezingPolicyInfiniteTabsTest, DestroyPageWithArmedTimer) {
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[0].get()));
  auto [page, frame] =
      CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceA);
  VerifyFreezerExpectations();

  EXPECT_TRUE(
      policy()->IsPeriodicUnfreezeTimerRunningForTesting(pages_[0].get()));
  frames_[0].reset();
  pages_[0].reset();

  AdvanceClock(features::kInfiniteTabsFreezing_UnfreezeInterval.Get() * 2);
  VerifyFreezerExpectations();
}

// Verify that under "Infinite Tabs Freezing", the periodic unfreeze timer is
// stopped when the system is suspended, and restarted when it's resumed.
TEST_F(FreezingPolicyInfiniteTabsTest, SystemSuspended) {
  base::test::ScopedPowerMonitorTestSource power_monitor_source;

  // Advance to the beginning of the next periodic unfreeze period.
  AdvanceToAlignedTime(features::kInfiniteTabsFreezing_UnfreezeInterval.Get());

  // Create a new page. This should remove
  // `CannotFreezeReason::kMostRecentlyUsed` from `pages_[0]`. However, it's not
  // frozen yet since it's still in its periodic unfreeze period.
  auto [page, frame] =
      CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceA);
  ASSERT_FALSE(page->IsVisible());

  // Advance to the end of the periodic unfreeze period. `pages_[0]` should be
  // frozen.
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[0].get()));
  AdvanceClock(features::kInfiniteTabsFreezing_UnfreezeDuration.Get());
  VerifyFreezerExpectations();

  power_monitor_source.Suspend();

  // Advance wall time to the beginning of the next periodic unfreeze period.
  // `pages_[0]` would normally be unfrozen, but it stays frozen because the
  // system is suspended.
  task_env().SuspendedFastForwardBy(kTimeBetweenUnfreezePeriods);
  VerifyFreezerExpectations();

  // Advance wall time to partway between unfreeze periods, to test that periods
  // after resume are aligned with system time, not wall time.
  task_env().SuspendedFastForwardBy(kTimeBetweenUnfreezePeriods * 0.25);

  // The page should not be unfrozen on resume since the periodic unfreeze timer
  // didn't advance during suspend.
  power_monitor_source.Resume();
  VerifyFreezerExpectations();

  // Advance to the beginning of the next periodic unfreeze period, not counting
  // time that was spent in suspend. `pages_[0]` stays frozen.
  AdvanceClock(kTimeBetweenUnfreezePeriods * 0.75);
  VerifyFreezerExpectations();

  // Advance to the REAL beginning of the next periodic unfreeze period. Now
  // `pages_[0]` should be unfrozen.
  EXPECT_CALL(*freezer(), UnfreezePageNode(pages_[0].get()));
  AdvanceClock(kTimeBetweenUnfreezePeriods * 0.25);
  VerifyFreezerExpectations();

  power_monitor_source.Suspend();

  // Advance wall time to the end of the periodic unfreeze period. `pages_[0]`
  // should NOT be frozen because the periodic unfreeze timer didn't advance
  // during suspend.
  task_env().SuspendedFastForwardBy(
      features::kInfiniteTabsFreezing_UnfreezeDuration.Get());
  VerifyFreezerExpectations();

  // Advance wall time to partway through an unfreeze period, to test that
  // after resume the period ends based on system time, not wall time.
  task_env().SuspendedFastForwardBy(
      features::kInfiniteTabsFreezing_UnfreezeDuration.Get() * 0.25);

  // The page should not be frozen on resume since the periodic unfreeze timer
  // didn't advance during suspend.
  power_monitor_source.Resume();
  VerifyFreezerExpectations();

  // Advance to the end of the periodic unfreeze period, not counting time that
  // was spent in suspend. `pages_[0]` stays unfrozen. (This ensures that
  // there's enough time spend unfrozen to do necessary cleanup, regardless of
  // suspend.)
  AdvanceClock(features::kInfiniteTabsFreezing_UnfreezeDuration.Get() * 0.75);
  VerifyFreezerExpectations();

  // Advance to the REAL end of the periodic unfreeze period. Now `pages_[0]`
  // should be frozen.
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[0].get()));
  AdvanceClock(features::kInfiniteTabsFreezing_UnfreezeDuration.Get() * 0.25);
  VerifyFreezerExpectations();
}

// Verify that a page with a freeze vote can be frozen even if it's in the list
// of most recently used tabs (this list only affects Infinite Tabs Freezing).
TEST_F(FreezingPolicyInfiniteTabsTest, InteractionWithVoting) {
  EXPECT_EQ(policy()->GetCanFreezeDetails(pages_[0].get()).can_freeze,
            freezing::CanFreeze::kVaries);
  ExpectCannotFreezeReasons(pages_[0].get(), FreezingType::kVoting, IsEmpty());
  ExpectCannotFreezeReasons(pages_[0].get(), FreezingType::kInfiniteTabs,
                            ElementsAre(CannotFreezeReason::kMostRecentlyUsed));

  EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[0].get()));
  policy()->AddFreezeVote(pages_[0].get());
  VerifyFreezerExpectations();
}

// Verify that a page which is CPU-intensive in the background while Battery
// Saver is active can be frozen even if it's in the list of most recently
// used tabs (this list only affects Infinite Tabs Freezing).
TEST_F(FreezingPolicyInfiniteTabsTest, InteractionWithBatterySaver) {
  policy()->ToggleFreezingOnBatterySaverMode(true);

  EXPECT_EQ(policy()->GetCanFreezeDetails(pages_[0].get()).can_freeze,
            freezing::CanFreeze::kVaries);
  ExpectCannotFreezeReasons(pages_[0].get(), FreezingType::kBatterySaver,
                            IsEmpty());
  ExpectCannotFreezeReasons(pages_[0].get(), FreezingType::kInfiniteTabs,
                            ElementsAre(CannotFreezeReason::kMostRecentlyUsed));

  const resource_attribution::OriginInBrowsingInstanceContext kPage0Context{
      url::Origin(), frames_[0]->GetBrowsingInstanceId()};

  ReportCumulativeCPUUsage(kPage0Context, base::Seconds(60));
  AdvanceClock(base::Seconds(60));
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[0].get()));
  ReportCumulativeCPUUsage(kPage0Context, base::Seconds(75));

  VerifyFreezerExpectations();
}

// Verify that visible pages which are not tabs don't affect Infinite Tabs
// Freezing.
TEST_F(FreezingPolicyInfiniteTabsTest, NonTab) {
  // Create a new page of type `kTab` which takes the spot of the `pages_[0]` in
  // the list of most recently used pages. `pages_[0]` should be frozen.
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[0].get()));
  auto [page5, frame5] =
      CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceA);
  EXPECT_EQ(page5->GetType(), PageType::kTab);
  AdvanceClock(base::Milliseconds(1));
  VerifyFreezerExpectations();

  // Create a new page of type `kExtension`. Unlike the previous case, this
  // should have no effect on freezing.
  auto non_tab_page = CreateNode<PageNodeImpl>(
      /*web_contents=*/nullptr,
      /*browsing_context_id=*/base::UnguessableToken(), GURL(),
      PagePropertyFlags{PagePropertyFlag::kIsVisible});
  non_tab_page->SetType(PageType::kExtension);
  auto non_tab_frame =
      CreateFrameNodeAutoId(process_node(), non_tab_page.get(),
                            /* parent_frame_node=*/nullptr, kBrowsingInstanceB);
}

TEST_F(FreezingPolicyTest, FreezeVoteWhenWebUI) {
  // Navigate to a WebUI URL.
  page_node()->OnMainFrameNavigationCommitted(
      /*same_document=*/false, base::TimeTicks::Now(), /*navigation_id=*/2,
      GURL("chrome://settings"), /*contents_mime_type=*/"",
      /*notification_permission_status=*/std::nullopt);

  // Adding a freeze vote should NOT trigger MaybeFreezePageNode because of
  // CannotFreezeReason::kWebUI.
  policy()->AddFreezeVote(page_node());
  VerifyFreezerExpectations();
  ExpectCannotFreezeReasons(page_node(), FreezingType::kVoting,
                            ElementsAre(CannotFreezeReason::kWebUI));

  // Since there is still a freeze vote and the CannotFreezeReason::kWebUI is
  // about to be removed, the page should be frozen as part of the navigation.
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));

  // Navigate to a non-WebUI URL.
  page_node()->OnMainFrameNavigationCommitted(
      /*same_document=*/false, base::TimeTicks::Now(), /*navigation_id=*/3,
      GURL("https://example.com"), /*contents_mime_type=*/"",
      /*notification_permission_status=*/std::nullopt);

  VerifyFreezerExpectations();
  ExpectCannotFreezeReasons(page_node(), FreezingType::kVoting, IsEmpty());
}

TEST_F(FreezingPolicyTest, FreezeVoteWhenWebUiDialog) {
  // Create a new page representing a WebDialog (it remains PageType::kUnknown).
  auto dialog_page = CreateNode<PageNodeImpl>(
      /*web_contents=*/nullptr,
      /*browsing_context_id=*/base::UnguessableToken(), GURL());
  auto dialog_frame =
      CreateFrameNodeAutoId(process_node(), dialog_page.get(),
                            /* parent_frame_node=*/nullptr, kBrowsingInstanceA);

  // Navigate to a WebUI URL (representing the constrained dialog opening
  // chrome://print).
  dialog_page->OnMainFrameNavigationCommitted(
      /*same_document=*/false, base::TimeTicks::Now(), /*navigation_id=*/2,
      GURL("chrome://print"), /*contents_mime_type=*/"",
      /*notification_permission_status=*/std::nullopt);

  // Verify that adding a freeze vote does NOT freeze the page because it has
  // the CannotFreezeReason::kWebUI reason.
  policy()->AddFreezeVote(dialog_page.get());
  VerifyFreezerExpectations();
  ExpectCannotFreezeReasons(dialog_page.get(), FreezingType::kVoting,
                            ElementsAre(CannotFreezeReason::kWebUI));
}

TEST_F(FreezingPolicyInfiniteTabsTest, NonTabWebUI) {
  // Create a new page of type `kNonTabWebUI`.
  auto webui_page = CreateNode<PageNodeImpl>(
      /*web_contents=*/nullptr,
      /*browsing_context_id=*/base::UnguessableToken(), GURL(),
      PagePropertyFlags{PagePropertyFlag::kIsVisible});
  webui_page->SetType(PageType::kNonTabWebUI);
  auto webui_frame =
      CreateFrameNodeAutoId(process_node(), webui_page.get(),
                            /* parent_frame_node=*/nullptr, kBrowsingInstanceB);

  // Navigate to a WebUI URL.
  webui_page->OnMainFrameNavigationCommitted(
      /*same_document=*/false, base::TimeTicks::Now(), /*navigation_id=*/2,
      GURL("chrome://tab-search"), /*contents_mime_type=*/"",
      /*notification_permission_status=*/std::nullopt);

  // It should have both kVisible and kWebUI cannot freeze reasons.
  ExpectCannotFreezeReasons(
      webui_page.get(), FreezingType::kInfiniteTabs,
      ElementsAre(CannotFreezeReason::kVisible, CannotFreezeReason::kWebUI));

  // When it becomes hidden, it should not be frozen because of the cannot
  // freeze reason.
  webui_page->SetIsVisible(false);
  AdvanceClock(base::Milliseconds(1));
  VerifyFreezerExpectations();

  // Now it should only have the kWebUI cannot freeze reason.
  ExpectCannotFreezeReasons(webui_page.get(), FreezingType::kInfiniteTabs,
                            ElementsAre(CannotFreezeReason::kWebUI));
}

// Verify that disabling tab freezing by user unfreezes tabs frozen for
// infinite tabs freezing, and re-enabling re-freezes them.
TEST_F(FreezingPolicyInfiniteTabsTest, SetFreezingEnabledByUser) {
  // Create a new page which pushes pages_[0] out of the protected list,
  // freezing it.
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[0].get()));
  auto [page5, frame5] =
      CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceA);
  AdvanceClock(base::Milliseconds(1));
  VerifyFreezerExpectations();

  // User disables tab freezing. pages_[0] should be unfrozen.
  EXPECT_CALL(*freezer(), UnfreezePageNode(pages_[0].get()));
  policy()->SetFreezingEnabledByUser(false);
  VerifyFreezerExpectations();

  // User re-enables tab freezing. pages_[0] should be frozen again.
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[0].get()));
  policy()->SetFreezingEnabledByUser(true);
  VerifyFreezerExpectations();
}

// Verify that freeze votes (kVoting) continue to freeze pages even when user
// tab freezing is disabled.
TEST_F(FreezingPolicyInfiniteTabsTest,
       SetFreezingEnabledByUser_FreezeVotePreserved) {
  policy()->SetFreezingEnabledByUser(false);

  // Adding a freeze vote on a protected tab should still freeze it.
  EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[1].get()));
  policy()->AddFreezeVote(pages_[1].get());
  VerifyFreezerExpectations();

  // Removing the freeze vote unfreezes it.
  EXPECT_CALL(*freezer(), UnfreezePageNode(pages_[1].get()));
  policy()->RemoveFreezeVote(pages_[1].get());
  VerifyFreezerExpectations();
}

}  // namespace performance_manager
