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

#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "content/browser/back_forward_cache/back_forward_cache_impl.h"
#include "content/browser/back_forward_cache_browsertest.h"
#include "content/public/common/content_features.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/content_browser_test_utils.h"

namespace content {

class BackgroundForegroundProcessLimitBackForwardCacheBrowserTest
    : public BackForwardCacheBrowserTest {
 protected:
  void SetUpCommandLine(base::CommandLine* command_line) override {
    EnableCacheSize(kBackForwardCacheSize, kForegroundBackForwardCacheSize);
    BackForwardCacheBrowserTest::SetUpCommandLine(command_line);
  }

  void ExpectCached(const RenderFrameHostImplWrapper& rfh,
                    bool cached,
                    bool backgrounded) {
    EXPECT_FALSE(rfh.IsDestroyed());
    EXPECT_EQ(cached, rfh->IsInBackForwardCache());
    EXPECT_EQ(backgrounded, rfh->GetProcess()->GetPriority() ==
                                base::Process::Priority::kBestEffort);
  }

  size_t PruneForTesting(size_t limit,
                         BackForwardCacheMetrics::NotRestoredReason reason) {
    auto* bfc = static_cast<BackForwardCacheImpl*>(
        &web_contents()->GetController().GetBackForwardCache());
    return bfc->PruneForTesting(limit, reason);
  }

  // The number of pages the BackForwardCache can hold per tab.
  const size_t kBackForwardCacheSize = 4;
  const size_t kForegroundBackForwardCacheSize = 2;
  const size_t kPruneSize = 1u;
  const NotRestoredReason kPruneReason =
      NotRestoredReason::kCacheLimitPrunedOnModerateMemoryPressure;
};

// Test that a series of same-site navigations (which use the same process)
// uses the foreground limit.
IN_PROC_BROWSER_TEST_F(
    BackgroundForegroundProcessLimitBackForwardCacheBrowserTest,
    CacheEvictionSameSite) {
  ASSERT_TRUE(embedded_test_server()->Start());

  std::vector<RenderFrameHostImplWrapper> rfhs;

  for (size_t i = 0; i <= kBackForwardCacheSize * 2; ++i) {
    SCOPED_TRACE(i);
    GURL url(embedded_test_server()->GetURL(
        "a.com", base::StringPrintf("/title1.html?i=%zu", i)));
    ASSERT_TRUE(NavigateToURL(shell(), url));
    rfhs.emplace_back(current_frame_host());
    EXPECT_NE(rfhs.back()->GetProcess()->GetPriority(),
              base::Process::Priority::kBestEffort);

    for (size_t j = 0; j <= i; ++j) {
      SCOPED_TRACE(j);
      // The last page is active, the previous |kForegroundBackForwardCacheSize|
      // should be in the cache, any before that should be deleted.
      if (i - j <= kForegroundBackForwardCacheSize) {
        // All of the processes should be in the foreground.
        ExpectCached(rfhs[j], /*cached=*/i != j,
                     /*backgrounded=*/false);
      } else {
        ASSERT_TRUE(rfhs[j].WaitUntilRenderFrameDeleted());
      }
    }
  }

  // Navigate back but not to the initial about:blank.
  for (size_t i = 0; i <= kBackForwardCacheSize * 2 - 1; ++i) {
    SCOPED_TRACE(i);
    ASSERT_TRUE(HistoryGoBack(web_contents()));
    // The first |kBackForwardCacheSize| navigations should be restored from the
    // cache. The rest should not.
    if (i < kForegroundBackForwardCacheSize) {
      ExpectRestored(FROM_HERE);
    } else {
      ExpectNotRestored({NotRestoredReason::kForegroundCacheLimit}, {}, {}, {},
                        {}, FROM_HERE);
    }
  }
}

// Test that a series of cross-site navigations (which use different processes)
// use the background limit.
//
// TODO(crbug.com/40179515): This test is flaky. It has been re-enabled with
// improved failure output (https://crrev.com/c/2862346). It's OK to disable it
// again when it fails.
IN_PROC_BROWSER_TEST_F(
    BackgroundForegroundProcessLimitBackForwardCacheBrowserTest,
    CacheEvictionCrossSite) {
  ASSERT_TRUE(embedded_test_server()->Start());

  std::vector<RenderFrameHostImplWrapper> rfhs;

  for (size_t i = 0; i <= kBackForwardCacheSize * 2; ++i) {
    SCOPED_TRACE(i);
    // Note: do NOT use .com domains here because a4.com is on the HSTS preload
    // list, which will cause our test requests to timeout.
    GURL url(embedded_test_server()->GetURL(base::StringPrintf("a%zu.test", i),
                                            "/title1.html"));
    ASSERT_TRUE(NavigateToURL(shell(), url));
    rfhs.emplace_back(current_frame_host());
    EXPECT_NE(rfhs.back()->GetProcess()->GetPriority(),
              base::Process::Priority::kBestEffort);

    for (size_t j = 0; j <= i; ++j) {
      SCOPED_TRACE(j);
      // The last page is active, the previous |kBackgroundBackForwardCacheSize|
      // should be in the cache, any before that should be deleted.
      if (i - j <= kBackForwardCacheSize) {
        EXPECT_FALSE(rfhs[j].IsDestroyed());
        // Pages except the active one should be cached and in the background.
        ExpectCached(rfhs[j], /*cached=*/i != j,
                     /*backgrounded=*/i != j);
      } else {
        ASSERT_TRUE(rfhs[j].WaitUntilRenderFrameDeleted());
      }
    }
  }

  // Navigate back but not to the initial about:blank.
  for (size_t i = 0; i <= kBackForwardCacheSize * 2 - 1; ++i) {
    SCOPED_TRACE(i);
    ASSERT_TRUE(HistoryGoBack(web_contents()));
    // The first |kBackForwardCacheSize| navigations should be restored from the
    // cache. The rest should not.
    if (i < kBackForwardCacheSize) {
      ExpectRestored(FROM_HERE);
    } else {
      ExpectNotRestored({NotRestoredReason::kCacheLimit}, {}, {}, {}, {},
                        FROM_HERE);
    }
  }
}

// Test that pruning a series of cross-site navigations (which use different
// processes) evicts the right entries with the right reason.
IN_PROC_BROWSER_TEST_F(
    BackgroundForegroundProcessLimitBackForwardCacheBrowserTest,
    PruneCrossSite) {
  ASSERT_TRUE(embedded_test_server()->Start());

  std::vector<RenderFrameHostImplWrapper> rfhs;

  for (size_t i = 0; i < kBackForwardCacheSize; ++i) {
    SCOPED_TRACE(i);
    // Note: do NOT use .com domains here because a4.com is on the HSTS preload
    // list, which will cause our test requests to timeout.
    GURL url(embedded_test_server()->GetURL(base::StringPrintf("a%zu.test", i),
                                            "/title1.html"));
    ASSERT_TRUE(NavigateToURL(shell(), url));
    rfhs.emplace_back(current_frame_host());
    EXPECT_NE(rfhs.back()->GetProcess()->GetPriority(),
              base::Process::Priority::kBestEffort);
  }

  CHECK_LE(kPruneSize, kBackForwardCacheSize);

  // Prune the BFCache entries.
  PruneForTesting(kPruneSize, kPruneReason);

  for (int i = kBackForwardCacheSize - 1 - 1 - kPruneSize; i >= 0; --i) {
    SCOPED_TRACE(i);
    ASSERT_TRUE(rfhs[i].WaitUntilRenderFrameDeleted());
  }

  // Navigate back but not to the initial about:blank.
  for (size_t i = 0; i < kBackForwardCacheSize - 1; ++i) {
    SCOPED_TRACE(i);
    ASSERT_TRUE(HistoryGoBack(web_contents()));
    // The first `kPruneSize` navigation should be restored from the cache. The
    // rest should not.
    if (i < kPruneSize) {
      ExpectRestored(FROM_HERE);
    } else {
      ExpectNotRestored({kPruneReason}, {}, {}, {}, {}, FROM_HERE);
    }
  }
}

namespace {

const char kPrioritizedPageURL[] = "search.result";

}  // namespace

class BackForwardCacheLimitForPrioritizedPagesBrowserTest
    : public BackgroundForegroundProcessLimitBackForwardCacheBrowserTest,
      public testing::WithParamInterface<std::string> {
 protected:
  // Mock subclass of ContentBrowserClient that will determine if the url is
  // prioritized by checking against `kPrioritizedPageURL`.
  class MockContentBrowserClientWithPrioritizedBackForwardCacheEntry
      : public ContentBrowserTestContentBrowserClient {
   public:
    // ContentBrowserClient overrides:
    bool ShouldPrioritizeForBackForwardCache(BrowserContext* browser_context,
                                             const GURL& url) override {
      return url.DomainIs(kPrioritizedPageURL);
    }
  };

  void SetUpOnMainThread() override {
    BackgroundForegroundProcessLimitBackForwardCacheBrowserTest::
        SetUpOnMainThread();
    test_client_ = std::make_unique<
        MockContentBrowserClientWithPrioritizedBackForwardCacheEntry>();
  }

  void SetUpCommandLine(base::CommandLine* command_line) override {
    EnableFeatureAndSetParams(kBackForwardCachePrioritizedEntry, "level",
                              GetParam());
    BackgroundForegroundProcessLimitBackForwardCacheBrowserTest::
        SetUpCommandLine(command_line);
  }

  bool ShouldPrioritizeWhenClearAllUnlessNoEviction() {
    return GetParam() == "prioritize-unless-should-clear-all-and-no-eviction";
  }

 private:
  std::unique_ptr<MockContentBrowserClientWithPrioritizedBackForwardCacheEntry>
      test_client_;
};

INSTANTIATE_TEST_SUITE_P(
    All,
    BackForwardCacheLimitForPrioritizedPagesBrowserTest,
    testing::Values("prioritize-unless-should-clear-all",
                    "prioritize-unless-should-clear-all-and-no-eviction"));

// Test that both when pruning with size 0, if no other eviction happens, the
// prioritized entry would be evicted.
IN_PROC_BROWSER_TEST_P(BackForwardCacheLimitForPrioritizedPagesBrowserTest,
                       PruneToZero_NoOtherEvictionHappens) {
  ASSERT_TRUE(embedded_test_server()->Start());

  // We need at least 1 entries in the BFCache list for this test.
  CHECK_GE(kBackForwardCacheSize, 1u);

  ASSERT_TRUE(NavigateToURL(shell(), embedded_test_server()->GetURL(
                                         kPrioritizedPageURL, "/title1.html")));
  ASSERT_TRUE(NavigateToURL(
      shell(), embedded_test_server()->GetURL("b.test", "/title1.html")));

  // Now the BFCache entry list is: [pp, b].
  // Prune the BFCache entries to 0.
  PruneForTesting(0, kPruneReason);
  // All the entries should be evicted
  ASSERT_TRUE(HistoryGoBack(web_contents()));
  ExpectNotRestored({kPruneReason}, {}, {}, {}, {}, FROM_HERE);
}

// Test that both when pruning with size 0, if there is another entry evicted,
// the prioritized entry would be:
// - evicted if the level is prioritize-unless-should-clear-all
// - not evicted if the level is
// prioritize-unless-should-clear-all-and-no-eviction.
IN_PROC_BROWSER_TEST_P(BackForwardCacheLimitForPrioritizedPagesBrowserTest,
                       PruneToZero_OtherEvictionHappens) {
  ASSERT_TRUE(embedded_test_server()->Start());

  // We need at least 2 entries in the BFCache list for this test.
  CHECK_GE(kBackForwardCacheSize, 2u);

  ASSERT_TRUE(NavigateToURL(
      shell(), embedded_test_server()->GetURL("a.test", "/title1.html")));
  ASSERT_TRUE(NavigateToURL(shell(), embedded_test_server()->GetURL(
                                         kPrioritizedPageURL, "/title1.html")));
  ASSERT_TRUE(NavigateToURL(
      shell(), embedded_test_server()->GetURL("b.test", "/title1.html")));

  // Now the BFCache entry list is: [a, pp, b].
  // Prune the BFCache entries to 0.
  PruneForTesting(0, kPruneReason);
  ASSERT_TRUE(HistoryGoBack(web_contents()));
  if (ShouldPrioritizeWhenClearAllUnlessNoEviction()) {
    // If the level is prioritize-unless-should-clear-all-and-no-eviction, the
    // prioritized entry should be restored since the some other eviction
    // happens.
    ExpectRestored(FROM_HERE);
  } else {
    // Otherwise the prioritized entry should be evicted as well.
    ExpectNotRestored({kPruneReason}, {}, {}, {}, {}, FROM_HERE);
  }
  // The non-prioritized entry should always be evicted.
  ASSERT_TRUE(HistoryGoBack(web_contents()));
  ExpectNotRestored({kPruneReason}, {}, {}, {}, {}, FROM_HERE);
}

// Test that when pruning with a positive number size, the last prioritized
// entry outside the limit will not be evicted.
IN_PROC_BROWSER_TEST_P(BackForwardCacheLimitForPrioritizedPagesBrowserTest,
                       PruneToNonZero_PrioritizedEntryOutsideLimit) {
  ASSERT_TRUE(embedded_test_server()->Start());

  // We need at least 4 entries in the BFCache list for this test.
  CHECK_GE(kBackForwardCacheSize, 4u);

  ASSERT_TRUE(NavigateToURL(shell(), embedded_test_server()->GetURL(
                                         kPrioritizedPageURL, "/title1.html")));
  ASSERT_TRUE(NavigateToURL(
      shell(), embedded_test_server()->GetURL("a.test", "/title1.html")));
  ASSERT_TRUE(NavigateToURL(shell(), embedded_test_server()->GetURL(
                                         kPrioritizedPageURL, "/title2.html")));
  ASSERT_TRUE(NavigateToURL(
      shell(), embedded_test_server()->GetURL("b.test", "/title1.html")));
  ASSERT_TRUE(NavigateToURL(
      shell(), embedded_test_server()->GetURL("c.test", "/title1.html")));

  // Now the BFCache entry list is: [pe1, a, pe2, b].
  // Prune the BFCache entries to 1, the result should be:
  // [pe1(evicted), a(evicted), pe2(prioritized entry special rule), b].
  PruneForTesting(1, kPruneReason);

  // The last non-prioritized entry should be restored because it's within the
  // cache limit.
  ASSERT_TRUE(HistoryGoBack(web_contents()));
  ExpectRestored(FROM_HERE);
  // The last prioritized entry should be restored since it's the special
  // prioritized entry.
  ASSERT_TRUE(HistoryGoBack(web_contents()));
  ExpectRestored(FROM_HERE);
  // The other entries (including the prioritized one) should not be restored.
  for (size_t i = 0; i < 2; ++i) {
    ASSERT_TRUE(HistoryGoBack(web_contents()));
    ExpectNotRestored({kPruneReason}, {}, {}, {}, {}, FROM_HERE);
  }
}

// Test that when pruning with a positive number size, the last prioritized
// entry inside the limit should be counted as the regular cache.
IN_PROC_BROWSER_TEST_P(BackForwardCacheLimitForPrioritizedPagesBrowserTest,
                       PruneToNonZero_PrioritizedEntryInsideLimit) {
  ASSERT_TRUE(embedded_test_server()->Start());

  // We need at least 2 entries in the BFCache list for this test.
  CHECK_GE(kBackForwardCacheSize, 2u);

  ASSERT_TRUE(NavigateToURL(
      shell(), embedded_test_server()->GetURL("a.test", "/title1.html")));
  ASSERT_TRUE(NavigateToURL(shell(), embedded_test_server()->GetURL(
                                         kPrioritizedPageURL, "/title2.html")));
  ASSERT_TRUE(NavigateToURL(
      shell(), embedded_test_server()->GetURL("b.test", "/title1.html")));

  // Now the BFCache entry list is: [a, pe].
  // Prune the BFCache entries to 1, the result should be:
  // [a(evicted), pe].
  PruneForTesting(1, kPruneReason);
  ASSERT_TRUE(HistoryGoBack(web_contents()));
  ExpectRestored(FROM_HERE);
  ASSERT_TRUE(HistoryGoBack(web_contents()));
  ExpectNotRestored({kPruneReason}, {}, {}, {}, {}, FROM_HERE);
}

// Test that when pruning with a positive number size while there is already an
// old prioritized entry kept in cache before, it will be replaced by the newer
// prioritized entry.
IN_PROC_BROWSER_TEST_P(BackForwardCacheLimitForPrioritizedPagesBrowserTest,
                       PruneToNonZeroTwice) {
  ASSERT_TRUE(embedded_test_server()->Start());

  // We need at least 4 entries in the BFCache list for this test.
  CHECK_LE(kBackForwardCacheSize, 4u);

  ASSERT_TRUE(NavigateToURL(shell(), embedded_test_server()->GetURL(
                                         kPrioritizedPageURL, "/title1.html")));
  ASSERT_TRUE(NavigateToURL(
      shell(), embedded_test_server()->GetURL("a.test", "/title1.html")));
  ASSERT_TRUE(NavigateToURL(
      shell(), embedded_test_server()->GetURL("b.test", "/title1.html")));

  // Now the BFCache entry list is: [pe1, a].
  // Prune the BFCache entries to 1, the result should still be
  // [pe1(prioritized entry special rule), a].
  PruneForTesting(1, kPruneReason);

  // The last non-prioritized entry should be restored because it's within the
  // cache limit.
  ASSERT_TRUE(HistoryGoBack(web_contents()));
  ExpectRestored(FROM_HERE);
  // The last prioritized entry should be restored since it's the special
  // prioritized entry.
  ASSERT_TRUE(HistoryGoBack(web_contents()));
  ExpectRestored(FROM_HERE);

  ASSERT_TRUE(NavigateToURL(
      shell(), embedded_test_server()->GetURL("c.test", "/title1.html")));
  ASSERT_TRUE(NavigateToURL(shell(), embedded_test_server()->GetURL(
                                         kPrioritizedPageURL, "/title2.html")));
  ASSERT_TRUE(NavigateToURL(
      shell(), embedded_test_server()->GetURL("d.test", "/title1.html")));
  ASSERT_TRUE(NavigateToURL(
      shell(), embedded_test_server()->GetURL("e.test", "/title1.html")));

  // Now the BFCache entry list is: [pe1, c, pe2, d].
  // Prune the BFCache entries to 1, the result should still be
  // [pe1(evicted), a(evicted), pe2(prioritized entry special rule), d].
  PruneForTesting(1, kPruneReason);

  // The last non-prioritized entry should be restored because it's within the
  // cache limit.
  ASSERT_TRUE(HistoryGoBack(web_contents()));
  ExpectRestored(FROM_HERE);
  // The last prioritized entry should be restored since it's the special
  // prioritized entry.
  ASSERT_TRUE(HistoryGoBack(web_contents()));
  ExpectRestored(FROM_HERE);
  // The other entries (including the prioritized one) should not be restored.
  for (size_t i = 0; i < 2; ++i) {
    ASSERT_TRUE(HistoryGoBack(web_contents()));
    ExpectNotRestored({kPruneReason}, {}, {}, {}, {}, FROM_HERE);
  }
}

// Test that the prioritized BFCache entry will not be evicted even when another
// entry is stored and exceeds the limit.
IN_PROC_BROWSER_TEST_P(BackForwardCacheLimitForPrioritizedPagesBrowserTest,
                       CacheLimitReached) {
  ASSERT_TRUE(embedded_test_server()->Start());

  // We need at least 1 entry in the BFCache list for this test.
  CHECK_GE(kBackForwardCacheSize, 1u);

  ASSERT_TRUE(NavigateToURL(shell(), embedded_test_server()->GetURL(
                                         kPrioritizedPageURL, "/title1.html")));

  // Fill the BFCache with more entry and make it just exceeds the limit, the
  // result should be:
  // [pe(prioritized entry special rule), a0, a1, ...].
  for (size_t i = 0; i <= kBackForwardCacheSize; ++i) {
    ASSERT_TRUE(NavigateToURL(
        shell(), embedded_test_server()->GetURL(
                     base::StringPrintf("a%zu.test", i), "/title1.html")));
  }
  // For the entries within cache size limit, they should be restored.
  for (size_t i = 0; i < kBackForwardCacheSize; ++i) {
    ASSERT_TRUE(HistoryGoBack(web_contents()));
    ExpectRestored(FROM_HERE);
  }
  // The prioritized entry should be restored as well even if it's outside the
  // limit.
  ASSERT_TRUE(HistoryGoBack(web_contents()));
  ExpectRestored(FROM_HERE);
}

// Test that the cache responds to processes switching from background to
// foreground. We set things up so that we have
// Cached sites:
//   a0.test
//   a1.test
//   a2.test
//   a3.test
// and the active page is a4.test. Then set the process for a[1-3] to
// foregrounded so that there are 3 entries whose processes are foregrounded.
// BFCache should evict the eldest (a1) leaving a0 because despite being older,
// it is backgrounded. Setting the priority directly is not ideal but there is
// no reliable way to cause the processes to go into the foreground just by
// navigating because proactive browsing instance swap makes it impossible to
// reliably create a new a1.test renderer in the same process as the old
// a1.test.
//
// Note that we do NOT use .com domains because a4.com is on the HSTS preload
// list.  Since our test server doesn't use HTTPS, using a4.com results in the
// test timing out.
IN_PROC_BROWSER_TEST_F(
    BackgroundForegroundProcessLimitBackForwardCacheBrowserTest,
    ChangeToForeground) {
  ASSERT_TRUE(embedded_test_server()->Start());

  std::vector<RenderFrameHostImplWrapper> rfhs;

  // Navigate through a[0-3].com.
  for (size_t i = 0; i < kBackForwardCacheSize; ++i) {
    SCOPED_TRACE(i);
    GURL url(embedded_test_server()->GetURL(base::StringPrintf("a%zu.test", i),
                                            "/title1.html"));
    ASSERT_TRUE(NavigateToURL(shell(), url));
    rfhs.emplace_back(current_frame_host());
    EXPECT_NE(rfhs.back()->GetProcess()->GetPriority(),
              base::Process::Priority::kBestEffort);
  }
  // Check that a0-2 are cached and backgrounded.
  for (size_t i = 0; i < kBackForwardCacheSize - 1; ++i) {
    SCOPED_TRACE(i);
    ExpectCached(rfhs[i], /*cached=*/true, /*backgrounded=*/true);
  }

  // Navigate to a page which causes the processes for a[1-3] to be
  // foregrounded.
  GURL url(embedded_test_server()->GetURL("a4.test", "/title1.html"));
  ASSERT_TRUE(NavigateToURL(shell(), url));

  // Assert that we really have set up the situation we want where the processes
  // are shared and in the foreground.
  RenderFrameHostImpl* rfh = current_frame_host();
  ASSERT_NE(rfh->GetProcess()->GetPriority(),
            base::Process::Priority::kBestEffort);

  rfhs[1]->GetProcess()->OnMediaStreamAdded();
  rfhs[2]->GetProcess()->OnMediaStreamAdded();
  rfhs[3]->GetProcess()->OnMediaStreamAdded();

  // The page should be evicted.
  ASSERT_TRUE(rfhs[1].WaitUntilRenderFrameDeleted());

  // Check that a0 is cached and backgrounded.
  ExpectCached(rfhs[0], /*cached=*/true, /*backgrounded=*/true);
  // Check that a2-3 are cached and foregrounded.
  ExpectCached(rfhs[2], /*cached=*/true, /*backgrounded=*/false);
  ExpectCached(rfhs[3], /*cached=*/true, /*backgrounded=*/false);
}

}  // namespace content
