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

#include "content/browser/renderer_host/recently_destroyed_hosts.h"

#include <memory>

#include "base/memory/raw_ptr.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/time/time.h"
#include "content/browser/browsing_instance.h"
#include "content/browser/isolation_context.h"
#include "content/browser/origin_agent_cluster_isolation_state.h"
#include "content/browser/process_lock.h"
#include "content/browser/site_instance_impl.h"
#include "content/browser/web_exposed_isolation_info.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/mock_render_process_host.h"
#include "content/public/test/test_browser_context.h"
#include "content/test/storage_partition_test_helpers.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace content {

class RecentlyDestroyedHostsTest : public testing::Test {
 protected:
  RecentlyDestroyedHostsTest()
      : task_environment_(BrowserTaskEnvironment::TimeSource::MOCK_TIME),
        instance_(RecentlyDestroyedHosts::GetInstance(&browser_context_)) {}

  void AddReuseInterval(base::TimeDelta interval) {
    instance_->AddReuseInterval(interval);
    task_environment_.FastForwardBy(base::Seconds(1));
  }

  std::vector<base::TimeDelta> GetReuseIntervals() {
    std::vector<base::TimeDelta> intervals;
    for (auto& reuse_interval : instance_->reuse_intervals_) {
      intervals.push_back(reuse_interval.interval);
    }
    return intervals;
  }

  BrowserTaskEnvironment task_environment_;
  TestBrowserContext browser_context_;
  raw_ptr<RecentlyDestroyedHosts> instance_;
};

TEST_F(RecentlyDestroyedHostsTest, RecordSubframeMetric) {
  const IsolationContext isolation_context(
      BrowsingInstanceId(1), &browser_context_,
      /*is_guest=*/false,
      /*is_fenced=*/false,
      OriginAgentClusterIsolationState::CreateForDefaultIsolation(
          &browser_context_));
  const ProcessLock process_lock = ProcessLock::Create(
      isolation_context,
      UrlInfo::CreateForTesting(GURL("https://www.google.com"),
                                CreateStoragePartitionConfigForTesting()));

  constexpr char kTimingHistogram[] =
      "SiteIsolation.ReusePendingOrCommittedSite."
      "TimeSinceReusableProcessDestroyed.Subframe2";
  constexpr char kFoundHistogram[] =
      "SiteIsolation.MissedReuseOpportunity.Found.Subframe";

  base::HistogramTester histogram_tester;

  RecentlyDestroyedHosts::RecordMetricIfReusableHostRecentlyDestroyed(
      RecentlyDestroyedHosts::Context::kSubframe, base::TimeTicks::Now(),
      process_lock, &browser_context_);

  // No entry exists, so "Found" should be false, and timing should be empty.
  histogram_tester.ExpectUniqueSample(kFoundHistogram, false, 1);
  histogram_tester.ExpectTotalCount(kTimingHistogram, 0);

  // Add a RenderProcessHost for |process_lock| to RecentlyDestroyedHosts.
  MockRenderProcessHost host(&browser_context_, /*is_for_guests_only=*/false);
  host.SetProcessLock(isolation_context, process_lock);
  RecentlyDestroyedHosts::Add(
      &host, /*time_spent_running_unload_handlers=*/base::TimeDelta(),
      &browser_context_);

  RecentlyDestroyedHosts::RecordMetricIfReusableHostRecentlyDestroyed(
      RecentlyDestroyedHosts::Context::kSubframe, base::TimeTicks::Now(),
      process_lock, &browser_context_);

  // After 2 actions, we check the cumulative state:
  histogram_tester.ExpectBucketCount(kFoundHistogram, false, 1);
  // The "true" bucket for the hit should now have 1 sample.
  histogram_tester.ExpectBucketCount(kFoundHistogram, true, 1);
  // The total count for the boolean histogram is now 2.
  histogram_tester.ExpectTotalCount(kFoundHistogram, 2);
  // The timing histogram should have its first and only sample.
  histogram_tester.ExpectUniqueTimeSample(kTimingHistogram, base::TimeDelta(),
                                          1);

  // Advance time almost to the expiration point, then re-add the host to
  // refresh its timestamp.
  task_environment_.FastForwardBy(
      RecentlyDestroyedHosts::kSubframeStorageTimeout - base::Seconds(1));
  RecentlyDestroyedHosts::Add(
      &host, /*time_spent_running_unload_handlers=*/base::TimeDelta(),
      &browser_context_);

  // Advance time a bit more and check that the host is still found.
  constexpr base::TimeDelta kReuseInterval = base::Seconds(5);
  task_environment_.FastForwardBy(kReuseInterval);
  RecentlyDestroyedHosts::RecordMetricIfReusableHostRecentlyDestroyed(
      RecentlyDestroyedHosts::Context::kSubframe, base::TimeTicks::Now(),
      process_lock, &browser_context_);

  // After 3 actions, we check the cumulative state:
  histogram_tester.ExpectBucketCount(kFoundHistogram, false, 1);
  // The "true" bucket for the hit should now have 2 sample.
  histogram_tester.ExpectBucketCount(kFoundHistogram, true, 2);
  // The total count for the boolean histogram is now 3.
  histogram_tester.ExpectTotalCount(kFoundHistogram, 3);
  // The timing histogram should a new entry for the new interval.
  histogram_tester.ExpectTimeBucketCount(kTimingHistogram, kReuseInterval, 1);
  // The total number of timing samples is now 2.
  histogram_tester.ExpectTotalCount(kTimingHistogram, 2);

  // After the storage timeout passes, host should no longer be present.
  task_environment_.FastForwardBy(
      RecentlyDestroyedHosts::kSubframeStorageTimeout + base::Seconds(1));

  RecentlyDestroyedHosts::RecordMetricIfReusableHostRecentlyDestroyed(
      RecentlyDestroyedHosts::Context::kSubframe, base::TimeTicks::Now(),
      process_lock, &browser_context_);

  // After 4 actions, we check the final cumulative state:
  // The "false" bucket now has 2 samples: the initial miss and this expiration
  // miss.
  histogram_tester.ExpectBucketCount(kFoundHistogram, false, 2);
  // The "true" bucket for the hit should now have 2 sample.
  histogram_tester.ExpectBucketCount(kFoundHistogram, true, 2);
  // The total count for the boolean histogram is now 4.
  histogram_tester.ExpectTotalCount(kFoundHistogram, 4);
  // The total number of timing samples is still 2, as the miss was not
  // recorded.
  histogram_tester.ExpectTotalCount(kTimingHistogram, 2);
}

TEST_F(RecentlyDestroyedHostsTest, AddReuseInterval) {
  const base::TimeDelta t1 = base::Seconds(4);
  const base::TimeDelta t2 = base::Seconds(5);
  const base::TimeDelta t3 = base::Seconds(6.7);
  const base::TimeDelta t4 = base::Seconds(8.2);
  const base::TimeDelta t5 = base::Seconds(10);
  const base::TimeDelta t6 = base::Seconds(11);

  AddReuseInterval(t2);
  EXPECT_THAT(GetReuseIntervals(), testing::ElementsAre(t2));

  // The list of reuse intervals should be sorted from smallest to largest.
  AddReuseInterval(t1);
  AddReuseInterval(t1);
  AddReuseInterval(t3);
  AddReuseInterval(t6);
  EXPECT_THAT(GetReuseIntervals(), testing::ElementsAre(t1, t1, t2, t3, t6));

  // When the list of reuse intervals is at its maximum size and a new entry is
  // added, it should replace the oldest entry.
  AddReuseInterval(t5);
  EXPECT_THAT(GetReuseIntervals(), testing::ElementsAre(t1, t1, t3, t5, t6));
  AddReuseInterval(t4);
  EXPECT_THAT(GetReuseIntervals(), testing::ElementsAre(t1, t3, t4, t5, t6));
}

TEST_F(RecentlyDestroyedHostsTest, RecordMainFrameMetric) {
  const IsolationContext isolation_context(
      BrowsingInstanceId(1), &browser_context_,
      /*is_guest=*/false,
      /*is_fenced=*/false,
      OriginAgentClusterIsolationState::CreateForDefaultIsolation(
          &browser_context_));
  const ProcessLock process_lock = ProcessLock::Create(
      isolation_context,
      UrlInfo::CreateForTesting(GURL("https://www.google.com"),
                                CreateStoragePartitionConfigForTesting()));

  constexpr char kFoundHistogram[] =
      "SiteIsolation.MissedReuseOpportunity.Found.MainFrame";
  constexpr char kTimingHistogram[] =
      "SiteIsolation.ReusePendingOrCommittedSite."
      "TimeSinceReusableProcessDestroyed.MainFrame2";

  base::HistogramTester histogram_tester;
  RecentlyDestroyedHosts::RecordMetricIfReusableHostRecentlyDestroyed(
      RecentlyDestroyedHosts::Context::kMainFrame, base::TimeTicks::Now(),
      process_lock, &browser_context_);

  // No entry exists, so "Found" should be false, and timing should be empty.
  histogram_tester.ExpectUniqueSample(kFoundHistogram, false, 1);
  histogram_tester.ExpectTotalCount(kTimingHistogram, 0);

  // Add a host, advance time, and check for a hit.
  MockRenderProcessHost host(&browser_context_);
  host.SetProcessLock(isolation_context, process_lock);
  RecentlyDestroyedHosts::Add(
      &host, /*time_spent_running_unload_handlers=*/base::TimeDelta(),
      &browser_context_);

  constexpr base::TimeDelta kReuseInterval = base::Minutes(10);
  task_environment_.FastForwardBy(kReuseInterval);

  RecentlyDestroyedHosts::RecordMetricIfReusableHostRecentlyDestroyed(
      RecentlyDestroyedHosts::Context::kMainFrame, base::TimeTicks::Now(),
      process_lock, &browser_context_);

  // After 2 actions, we check the cumulative state:
  // The "false" bucket for the initial miss should still have 1 sample.
  histogram_tester.ExpectBucketCount(kFoundHistogram, false, 1);
  // The "true" bucket for the hit should now have 1 sample.
  histogram_tester.ExpectBucketCount(kFoundHistogram, true, 1);
  // The total count for the boolean histogram is now 2.
  histogram_tester.ExpectTotalCount(kFoundHistogram, 2);
  // The timing histogram should have its first and only sample.
  histogram_tester.ExpectUniqueTimeSample(kTimingHistogram, kReuseInterval, 1);

  // Advance time past the 2-hour timeout and check that we get a "miss".
  task_environment_.FastForwardBy(
      RecentlyDestroyedHosts::kMainFrameStorageTimeout + base::Seconds(1));

  RecentlyDestroyedHosts::RecordMetricIfReusableHostRecentlyDestroyed(
      RecentlyDestroyedHosts::Context::kMainFrame, base::TimeTicks::Now(),
      process_lock, &browser_context_);

  // After 3 actions, we check the final cumulative state.
  // The "false" bucket now has 2 samples: the initial miss and this expiration
  // miss.
  histogram_tester.ExpectBucketCount(kFoundHistogram, false, 2);
  // The "true" bucket still has 1 sample from the previous step.
  histogram_tester.ExpectBucketCount(kFoundHistogram, true, 1);
  // The total count for the boolean histogram is now 3.
  histogram_tester.ExpectTotalCount(kFoundHistogram, 3);
  // The total number of timing samples is still 1, as the miss was not
  // recorded.
  histogram_tester.ExpectTotalCount(kTimingHistogram, 1);
}

}  // namespace content
