// Copyright 2022 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/btm/btm_bounce_detector.h"

#include <algorithm>
#include <iterator>
#include <memory>
#include <optional>
#include <set>
#include <string>
#include <string_view>
#include <variant>
#include <vector>

#include "base/base64.h"
#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/location.h"
#include "base/memory/raw_ptr.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/strings/escape.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/to_string.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/bind.h"
#include "base/test/gmock_expected_support.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/simple_test_clock.h"
#include "base/test/test_future.h"
#include "base/test/test_timeouts.h"
#include "base/time/clock.h"
#include "base/time/time.h"
#include "base/types/expected.h"
#include "components/content_settings/core/common/features.h"
#include "components/ukm/content/source_url_recorder.h"
#include "content/browser/btm/btm_browsertest_utils.h"
#include "content/browser/btm/btm_service_impl.h"
#include "content/browser/btm/btm_storage.h"
#include "content/browser/btm/btm_test_utils.h"
#include "content/browser/btm/btm_utils.h"
#include "content/common/features.h"
#include "content/public/browser/back_forward_cache.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/btm_redirect.h"
#include "content/public/browser/btm_service.h"
#include "content/public/browser/cookie_access_details.h"
#include "content/public/browser/global_routing_id.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/network_service_instance.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_features.h"
#include "content/public/common/content_paths.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/back_forward_cache_util.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/btm_service_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_content_browser_client.h"
#include "content/public/test/content_mock_cert_verifier.h"
#include "content/public/test/fenced_frame_test_util.h"
#include "content/public/test/hit_test_region_observer.h"
#include "content/public/test/prerender_test_util.h"
#include "content/public/test/render_frame_host_test_support.h"
#include "content/public/test/test_devtools_protocol_client.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/public/test/test_utils.h"
#include "content/shell/browser/shell.h"
#include "net/base/schemeful_site.h"
#include "net/cookies/site_for_cookies.h"
#include "net/dns/mock_host_resolver.h"
#include "net/http/http_status_code.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "net/test/embedded_test_server/request_handler_util.h"
#include "net/test/test_data_directory.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/metrics/public/cpp/ukm_source.h"
#include "services/network/public/cpp/features.h"
#include "services/network/test/trust_token_request_handler.h"
#include "services/network/test/trust_token_test_util.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "third_party/blink/public/common/switches.h"
#include "third_party/blink/public/mojom/frame/frame.mojom-shared.h"
#include "url/gurl.h"
#include "url/origin.h"

#if !BUILDFLAG(IS_ANDROID)
#include "content/public/browser/scoped_authenticator_environment_for_testing.h"
#include "device/fido/virtual_ctap2_device.h"
#include "device/fido/virtual_fido_device_factory.h"
#endif  // !BUILDFLAG(IS_ANDROID)

using base::Bucket;
using testing::Contains;
using testing::ElementsAre;
using testing::Eq;
using testing::Gt;
using testing::IsEmpty;
using testing::Pair;

namespace content {

namespace {

using blink::mojom::StorageTypeAccessed;

// Returns a simplified URL representation for ease of comparison in tests.
// Just host+path.
std::string FormatURL(const GURL& url) {
  return base::StrCat({url.host(), url.path()});
}

void AppendRedirect(std::vector<std::string>* redirects,
                    const BtmRedirect& redirect,
                    const BtmRedirectChain& chain,
                    size_t redirect_index) {
  redirects->push_back(base::StringPrintf(
      "[%zu/%zu] %s -> %s (%s) -> %s", redirect_index + 1, chain.length,
      FormatURL(chain.initial_url).c_str(),
      FormatURL(redirect.redirector_url).c_str(),
      std::string(BtmDataAccessTypeToString(redirect.access_type)).c_str(),
      FormatURL(chain.final_url).c_str()));
}

void AppendRedirects(std::vector<std::string>* vec,
                     std::vector<BtmRedirectPtr> redirects,
                     BtmRedirectChainPtr chain) {
  size_t redirect_index = chain->length - redirects.size();
  for (const auto& redirect : redirects) {
    AppendRedirect(vec, *redirect, *chain, redirect_index);
    redirect_index++;
  }
}

void AppendSitesInReport(std::vector<std::string>* reports,
                         const std::set<std::string>& sites) {
  reports->push_back(base::JoinString(
      std::vector<std::string_view>(sites.begin(), sites.end()), ", "));
}

bool ContainsWrite(BtmDataAccessType access) {
  using enum BtmDataAccessType;
  return access == kWrite || access == kReadWrite;
}

// Waits for BTM to know that a cookie was written by a redirect at
// `redirect_url`, which must be the last redirect that was performed in the
// currently-in-progress redirect chain.
testing::AssertionResult WaitForRedirectCookieWrite(WebContents* web_contents,
                                                    const GURL& redirect_url) {
  RedirectChainDetector* detector =
      RedirectChainDetector::FromWebContents(web_contents);

  if (detector->CommittedRedirectContext().GetRedirectChainLength() == 0) {
    return testing::AssertionFailure() << "No redirects detected";
  }

  // Make sure the last redirect was at the expected URL.
  const BtmRedirect& redirect =
      detector->CommittedRedirectContext()
          [detector->CommittedRedirectContext().size() - 1];
  if (redirect.redirector_url != redirect_url) {
    return testing::AssertionFailure()
           << "Expected redirect at " << redirect_url << "; found "
           << redirect.redirector_url;
  }

  if (!ContainsWrite(redirect.access_type)) {
    // We haven't been notified about the cookie write from the redirect yet.
    // Wait for it to ensure the bounce is considered stateful.
    URLCookieAccessObserver(web_contents, redirect_url,
                            CookieOperation::kChange)
        .Wait();
  }

  // Return success if the cookie write was detected.
  if (ContainsWrite(redirect.access_type)) {
    return testing::AssertionSuccess();
  } else {
    return testing::AssertionFailure() << "Still no cookie write detected";
  }
}

}  // namespace

// Keeps a log of DidStartNavigation, OnCookiesAccessed, and DidFinishNavigation
// executions.
class WCOCallbackLogger : public WebContentsObserver,
                          public WebContentsUserData<WCOCallbackLogger>,
                          public SharedWorkerService::Observer,
                          public DedicatedWorkerService::Observer {
 public:
  WCOCallbackLogger(const WCOCallbackLogger&) = delete;
  WCOCallbackLogger& operator=(const WCOCallbackLogger&) = delete;

  const std::vector<std::string>& log() const { return log_; }

 private:
  explicit WCOCallbackLogger(WebContents* web_contents);
  // So WebContentsUserData::CreateForWebContents() can call the constructor.
  friend class WebContentsUserData<WCOCallbackLogger>;

  // Start WebContentsObserver overrides:
  void DidStartNavigation(NavigationHandle* navigation_handle) override;
  void OnCookiesAccessed(RenderFrameHost* render_frame_host,
                         const CookieAccessDetails& details) override;
  void OnCookiesAccessed(NavigationHandle* navigation_handle,
                         const CookieAccessDetails& details) override;
  void NotifyStorageAccessed(RenderFrameHost* render_frame_host,
                             StorageTypeAccessed storage_type,
                             bool blocked) override;
  void OnServiceWorkerAccessed(RenderFrameHost* render_frame_host,
                               const GURL& scope,
                               AllowServiceWorkerResult allowed) override;
  void OnServiceWorkerAccessed(NavigationHandle* navigation_handle,
                               const GURL& scope,
                               AllowServiceWorkerResult allowed) override;
  void DidFinishNavigation(NavigationHandle* navigation_handle) override;
  void WebAuthnAssertionRequestSucceeded(
      RenderFrameHost* render_frame_host) override;
  // End WebContentsObserver overrides.

  // Start SharedWorkerService.Observer overrides:
  void OnClientAdded(const blink::SharedWorkerToken& token,
                     GlobalRenderFrameHostId render_frame_host_id) override;
  void OnWorkerCreated(const blink::SharedWorkerToken& token,
                       ChildProcessId worker_process_id,
                       const url::Origin& security_origin,
                       const base::UnguessableToken& dev_tools_token) override {
  }
  void OnBeforeWorkerDestroyed(const blink::SharedWorkerToken& token) override {
  }
  void OnClientRemoved(const blink::SharedWorkerToken& token,
                       GlobalRenderFrameHostId render_frame_host_id) override {}
  using SharedWorkerService::Observer::OnFinalResponseURLDetermined;
  // End SharedWorkerService.Observer overrides.

  // Start DedicatedWorkerService.Observer overrides:
  void OnWorkerCreated(const blink::DedicatedWorkerToken& worker_token,
                       ChildProcessId worker_process_id,
                       const url::Origin& security_origin,
                       DedicatedWorkerCreator creator) override;
  void OnBeforeWorkerDestroyed(const blink::DedicatedWorkerToken& worker_token,
                               DedicatedWorkerCreator creator) override {}
  void OnFinalResponseURLDetermined(
      const blink::DedicatedWorkerToken& worker_token,
      const GURL& url) override {}
  // End DedicatedWorkerService.Observer overrides.

  std::vector<std::string> log_;

  WEB_CONTENTS_USER_DATA_KEY_DECL();
};

WCOCallbackLogger::WCOCallbackLogger(WebContents* web_contents)
    : WebContentsObserver(web_contents),
      WebContentsUserData<WCOCallbackLogger>(*web_contents) {}

void WCOCallbackLogger::DidStartNavigation(
    NavigationHandle* navigation_handle) {
  log_.push_back(
      base::StringPrintf("DidStartNavigation(%s)",
                         FormatURL(navigation_handle->GetURL()).c_str()));
}

void WCOCallbackLogger::OnCookiesAccessed(RenderFrameHost* render_frame_host,
                                          const CookieAccessDetails& details) {
  // Callbacks for favicons are ignored only in testing logs because their
  // ordering is variable and would cause flakiness
  if (details.url.GetPath() == "/favicon.ico") {
    return;
  }

  log_.push_back(base::StringPrintf(
      "OnCookiesAccessed(RenderFrameHost, %s: %s)",
      details.type == CookieOperation::kChange ? "Change" : "Read",
      FormatURL(details.url).c_str()));
}

void WCOCallbackLogger::OnCookiesAccessed(NavigationHandle* navigation_handle,
                                          const CookieAccessDetails& details) {
  log_.push_back(base::StringPrintf(
      "OnCookiesAccessed(NavigationHandle, %s: %s)",
      details.type == CookieOperation::kChange ? "Change" : "Read",
      FormatURL(details.url).c_str()));
}

void WCOCallbackLogger::OnServiceWorkerAccessed(
    RenderFrameHost* render_frame_host,
    const GURL& scope,
    AllowServiceWorkerResult allowed) {
  log_.push_back(
      base::StringPrintf("OnServiceWorkerAccessed(RenderFrameHost: %s)",
                         FormatURL(scope).c_str()));
}

void WCOCallbackLogger::OnServiceWorkerAccessed(
    NavigationHandle* navigation_handle,
    const GURL& scope,
    AllowServiceWorkerResult allowed) {
  log_.push_back(
      base::StringPrintf("OnServiceWorkerAccessed(NavigationHandle: %s)",
                         FormatURL(scope).c_str()));
}

void WCOCallbackLogger::OnClientAdded(
    const blink::SharedWorkerToken& token,
    GlobalRenderFrameHostId render_frame_host_id) {
  RenderFrameHost* render_frame_host =
      RenderFrameHost::FromID(render_frame_host_id);
  GURL scope;
  if (render_frame_host) {
    scope = GetFirstPartyURL(*render_frame_host);
  }

  log_.push_back(base::StringPrintf("OnSharedWorkerClientAdded(%s)",
                                    FormatURL(scope).c_str()));
}

void WCOCallbackLogger::OnWorkerCreated(
    const blink::DedicatedWorkerToken& worker_token,
    ChildProcessId worker_process_id,
    const url::Origin& security_origin,
    DedicatedWorkerCreator creator) {
  const GlobalRenderFrameHostId& render_frame_host_id =
      std::get<GlobalRenderFrameHostId>(creator);
  RenderFrameHost* render_frame_host =
      RenderFrameHost::FromID(render_frame_host_id);
  GURL scope;
  if (render_frame_host) {
    scope = GetFirstPartyURL(*render_frame_host);
  }

  log_.push_back(base::StringPrintf("OnDedicatedWorkerCreated(%s)",
                                    FormatURL(scope).c_str()));
}

void WCOCallbackLogger::DidFinishNavigation(
    NavigationHandle* navigation_handle) {
  if (!IsInPrimaryPage(*navigation_handle)) {
    return;
  }

  log_.push_back(
      base::StringPrintf("DidFinishNavigation(%s)",
                         FormatURL(navigation_handle->GetURL()).c_str()));
}

void WCOCallbackLogger::WebAuthnAssertionRequestSucceeded(
    RenderFrameHost* render_frame_host) {
  log_.push_back(base::StringPrintf(
      "WebAuthnAssertionRequestSucceeded(%s)",
      FormatURL(render_frame_host->GetLastCommittedURL()).c_str()));
}

void WCOCallbackLogger::NotifyStorageAccessed(
    RenderFrameHost* render_frame_host,
    StorageTypeAccessed storage_type,
    bool blocked) {
  log_.push_back(base::StringPrintf(
      "NotifyStorageAccessed(%s: %s)", base::ToString(storage_type).c_str(),
      FormatURL(render_frame_host->GetLastCommittedURL()).c_str()));
}

WEB_CONTENTS_USER_DATA_KEY_IMPL(WCOCallbackLogger);

class BtmBounceDetectorBrowserTest : public ContentBrowserTest {
 protected:
  BtmBounceDetectorBrowserTest()
      : prerender_test_helper_(base::BindRepeating(
            &BtmBounceDetectorBrowserTest::GetActiveWebContents,
            base::Unretained(this))) {}

  void SetUp() override {
    scoped_feature_list_.InitWithFeaturesAndParameters(enabled_features_,
                                                       disabled_features_);
    ContentBrowserTest::SetUp();
  }

  void SetUpCommandLine(base::CommandLine* command_line) override {
    // Prevents flakiness by handling clicks even before content is drawn.
    command_line->AppendSwitch(blink::switches::kAllowPreCommitInput);
  }

  void SetUpOnMainThread() override {
    browser_client_ =
        std::make_unique<ContentBrowserTestTpcBlockingBrowserClient>();
    prerender_test_helper_.RegisterServerRequestMonitor(embedded_test_server());
    net::test_server::RegisterDefaultHandlers(embedded_test_server());
    ASSERT_TRUE(embedded_test_server()->Start());
    host_resolver()->AddRule("*", "127.0.0.1");

    // Set third-party cookies to be blocked by default. If they're not blocked
    // by default, BTM will not run.
    browser_client().SetBlockThirdPartyCookiesByDefault(true);
    WebContents* web_contents = GetActiveWebContents();
    ASSERT_FALSE(btm::Are3PcsGenerallyEnabled(web_contents->GetBrowserContext(),
                                              web_contents));

    SetUpBtmWebContentsObserver();
  }

  void SetUpBtmWebContentsObserver() {
    web_contents_observer_ =
        BtmWebContentsObserver::FromWebContents(GetActiveWebContents());
    CHECK(web_contents_observer_);
  }

  WebContents* GetActiveWebContents() { return shell()->web_contents(); }

  void StartAppendingRedirectsTo(std::vector<std::string>* redirects) {
    GetRedirectChainHelper()->SetRedirectChainHandlerForTesting(
        base::BindRepeating(&AppendRedirects, redirects));
  }

  void StartAppendingReportsTo(std::vector<std::string>* reports) {
    web_contents_observer_->SetIssueReportingCallbackForTesting(
        base::BindRepeating(&AppendSitesInReport, reports));
  }

  // Perform a browser-based navigation to terminate the current redirect chain.
  // (NOTE: tests using WCOCallbackLogger must call this *after* checking the
  // log, since this navigation will be logged.)
  //
  // By default (when `wait`=true) this waits for the BtmService to tell
  // observers that the redirect chain was handled. But some tests override
  // the handling flow so that chains don't reach the service (and so observers
  // are never notified). Such tests should pass `wait`=false.
  void EndRedirectChain(bool wait = true) {
    WebContents* web_contents = GetActiveWebContents();
    BtmService* btm_service =
        BtmService::Get(web_contents->GetBrowserContext());
    GURL expected_url = web_contents->GetLastCommittedURL();

    BtmRedirectChainObserver chain_observer(btm_service, expected_url);
    // Performing a browser-based navigation terminates the current redirect
    // chain.
    ASSERT_TRUE(NavigateToURL(
        web_contents,
        embedded_test_server()->GetURL("endthechain.test", "/title1.html")));
    if (wait) {
      chain_observer.Wait();
    }
  }

  auto* fenced_frame_test_helper() { return &fenced_frame_test_helper_; }
  auto* prerender_test_helper() { return &prerender_test_helper_; }

  RenderFrameHost* GetIFrame() {
    WebContents* web_contents = GetActiveWebContents();
    return ChildFrameAt(web_contents->GetPrimaryMainFrame(), 0);
  }

  RenderFrameHost* GetNestedIFrame() { return ChildFrameAt(GetIFrame(), 0); }

  RedirectChainDetector* GetRedirectChainHelper() {
    return RedirectChainDetector::FromWebContents(GetActiveWebContents());
  }

  void NavigateNestedIFrameTo(RenderFrameHost* parent_frame,
                              const std::string& iframe_id,
                              const GURL& url) {
    TestNavigationObserver load_observer(GetActiveWebContents());
    std::string script = base::StringPrintf(
        "var iframe = document.getElementById('%s');iframe.src='%s';",
        iframe_id.c_str(), url.spec().c_str());
    ASSERT_TRUE(ExecJs(parent_frame, script, EXECUTE_SCRIPT_NO_USER_GESTURE));
    load_observer.Wait();
  }

  void AccessCHIPSViaJSIn(RenderFrameHost* frame) {
    FrameCookieAccessObserver observer(GetActiveWebContents(), frame,
                                       CookieOperation::kChange);
    ASSERT_TRUE(ExecJs(frame,
                       "document.cookie = '__Host-foo=bar;"
                       "SameSite=None;Secure;Path=/;Partitioned';",
                       EXECUTE_SCRIPT_NO_USER_GESTURE));
    observer.Wait();
  }

  void SimulateMouseClick() {
    SimulateMouseClickAndWait(GetActiveWebContents());
  }

  void SimulateCookieWrite() {
    WebContents* web_contents = GetActiveWebContents();
    RenderFrameHost* frame = web_contents->GetPrimaryMainFrame();
    URLCookieAccessObserver cookie_observer(
        web_contents, frame->GetLastCommittedURL(), CookieOperation::kChange);
    ASSERT_TRUE(ExecJs(frame, "document.cookie = 'foo=bar';",
                       EXECUTE_SCRIPT_NO_USER_GESTURE));
    cookie_observer.Wait();
  }

  TpcBlockingBrowserClient& browser_client() { return browser_client_->impl(); }

  const base::FilePath kContentTestDataDir = GetTestDataFilePath();

  std::vector<base::test::FeatureRefAndParams> enabled_features_;
  std::vector<base::test::FeatureRef> disabled_features_;
  raw_ptr<BtmWebContentsObserver, AcrossTasksDanglingUntriaged>
      web_contents_observer_ = nullptr;

 private:
  test::PrerenderTestHelper prerender_test_helper_;
  test::FencedFrameTestHelper fenced_frame_test_helper_;
  base::test::ScopedFeatureList scoped_feature_list_;
  std::unique_ptr<ContentBrowserTestTpcBlockingBrowserClient> browser_client_;
};

IN_PROC_BROWSER_TEST_F(
    BtmBounceDetectorBrowserTest,
    // TODO(crbug.com/40924446): Re-enable this test
    DISABLED_AttributeSameSiteIframesCookieClientAccessTo1P) {
  std::vector<std::string> redirects;
  StartAppendingRedirectsTo(&redirects);

  const GURL primary_main_frame_url =
      embedded_test_server()->GetURL("a.test", "/page_with_blank_iframe.html");
  ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), primary_main_frame_url));

  const GURL iframe_url =
      embedded_test_server()->GetURL("a.test", "/title1.html");
  ASSERT_TRUE(
      NavigateIframeToURL(GetActiveWebContents(), "test_iframe", iframe_url));

  AccessCookieViaJSIn(GetActiveWebContents(), GetIFrame());

  const GURL primary_main_frame_final_url =
      embedded_test_server()->GetURL("d.test", "/title1.html");
  // Performs a Client-redirect to `primary_main_frame_final_url`.
  ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
      GetActiveWebContents(), primary_main_frame_final_url));

  CloseTab(GetActiveWebContents());
  EXPECT_THAT(redirects,
              ElementsAre(("[1/1] blank -> a.test/page_with_blank_iframe.html "
                           "(Write) -> d.test/title1.html")));
}

// TODO(crbug.com/40276415): Flaky on Mac.
#if BUILDFLAG(IS_MAC)
#define MAYBE_AttributeSameSiteIframesCookieServerAccessTo1P \
  DISABLED_AttributeSameSiteIframesCookieServerAccessTo1P
#else
#define MAYBE_AttributeSameSiteIframesCookieServerAccessTo1P \
  AttributeSameSiteIframesCookieServerAccessTo1P
#endif
IN_PROC_BROWSER_TEST_F(BtmBounceDetectorBrowserTest,
                       MAYBE_AttributeSameSiteIframesCookieServerAccessTo1P) {
  net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
  https_server.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
  https_server.AddDefaultHandlers(kContentTestDataDir);
  ASSERT_TRUE(https_server.Start());

  std::vector<std::string> redirects;
  StartAppendingRedirectsTo(&redirects);

  const GURL primary_main_frame_url =
      embedded_test_server()->GetURL("a.test", "/page_with_blank_iframe.html");
  browser_client().AllowThirdPartyCookiesOnSite(
      embedded_test_server()->GetURL("a.test", "/"));
  ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), primary_main_frame_url));

  const GURL iframe_url =
      https_server.GetURL("a.test", "/set-cookie?foo=bar;SameSite=None;Secure");
  ASSERT_TRUE(
      NavigateIframeToURL(GetActiveWebContents(), "test_iframe", iframe_url));

  const GURL primary_main_frame_final_url =
      embedded_test_server()->GetURL("d.test", "/title1.html");
  // Performs a Client-redirect to `primary_main_frame_final_url`.
  ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
      GetActiveWebContents(), primary_main_frame_final_url));

  CloseTab(GetActiveWebContents());
  EXPECT_THAT(redirects,
              ElementsAre(("[1/1] blank -> a.test/page_with_blank_iframe.html "
                           "(Write) -> d.test/title1.html")));
}

IN_PROC_BROWSER_TEST_F(BtmBounceDetectorBrowserTest,
                       Attribute3PIframesCHIPSClientAccessTo1P) {
  net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
  https_server.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
  https_server.AddDefaultHandlers(kContentTestDataDir);
  ASSERT_TRUE(https_server.Start());

  std::vector<std::string> redirects;
  StartAppendingRedirectsTo(&redirects);

  const GURL primary_main_frame_url =
      embedded_test_server()->GetURL("a.test", "/page_with_blank_iframe.html");
  ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), primary_main_frame_url));

  const GURL iframe_url = https_server.GetURL("b.test", "/title1.html");
  ASSERT_TRUE(
      NavigateIframeToURL(GetActiveWebContents(), "test_iframe", iframe_url));

  AccessCHIPSViaJSIn(GetIFrame());

  const GURL primary_main_frame_final_url =
      embedded_test_server()->GetURL("d.test", "/title1.html");
  // Performs a Client-redirect to `primary_main_frame_final_url`.
  ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
      GetActiveWebContents(), primary_main_frame_final_url));

  CloseTab(GetActiveWebContents());
  EXPECT_THAT(redirects,
              ElementsAre("[1/1] blank -> a.test/page_with_blank_iframe.html "
                          "(Write) -> d.test/title1.html"));
}

IN_PROC_BROWSER_TEST_F(BtmBounceDetectorBrowserTest,
                       // TODO(crbug.com/40287072): Re-enable this test
                       DISABLED_Attribute3PIframesCHIPSServerAccessTo1P) {
  net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
  https_server.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
  https_server.AddDefaultHandlers(kContentTestDataDir);
  ASSERT_TRUE(https_server.Start());

  std::vector<std::string> redirects;
  StartAppendingRedirectsTo(&redirects);

  const GURL primary_main_frame_url =
      https_server.GetURL("a.test", "/page_with_blank_iframe.html");
  ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), primary_main_frame_url));

  const GURL iframe_url =
      https_server.GetURL("b.test",
                          "/set-cookie?__Host-foo=bar;SameSite=None;"
                          "Secure;Path=/;Partitioned");
  ASSERT_TRUE(
      NavigateIframeToURL(GetActiveWebContents(), "test_iframe", iframe_url));

  const GURL primary_main_frame_final_url =
      embedded_test_server()->GetURL("d.test", "/title1.html");
  // Performs a Client-redirect to `primary_main_frame_final_url`.
  ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
      GetActiveWebContents(), primary_main_frame_final_url));

  CloseTab(GetActiveWebContents());
  EXPECT_THAT(redirects,
              ElementsAre(("[1/1] blank -> a.test/page_with_blank_iframe.html "
                           "(Write) -> d.test/title1.html")));
}

IN_PROC_BROWSER_TEST_F(
    BtmBounceDetectorBrowserTest,
    // TODO(crbug.com/40287072): Re-enable this test
    DISABLED_AttributeSameSiteNestedIframesCookieClientAccessTo1P) {
  std::vector<std::string> redirects;
  StartAppendingRedirectsTo(&redirects);

  const GURL primary_main_frame_url =
      embedded_test_server()->GetURL("a.test", "/page_with_blank_iframe.html");
  ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), primary_main_frame_url));

  const GURL iframe_url =
      embedded_test_server()->GetURL("a.test", "/page_with_blank_iframe.html");
  ASSERT_TRUE(
      NavigateIframeToURL(GetActiveWebContents(), "test_iframe", iframe_url));

  const GURL nested_iframe_url =
      embedded_test_server()->GetURL("a.test", "/title1.html");
  NavigateNestedIFrameTo(GetIFrame(), "test_iframe", nested_iframe_url);

  AccessCookieViaJSIn(GetActiveWebContents(), GetNestedIFrame());

  const GURL primary_main_frame_final_url =
      embedded_test_server()->GetURL("d.test", "/title1.html");
  // Performs a Client-redirect to `primary_main_frame_final_url`.
  ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
      GetActiveWebContents(), primary_main_frame_final_url));

  CloseTab(GetActiveWebContents());
  EXPECT_THAT(redirects,
              ElementsAre(("[1/1] blank -> a.test/page_with_blank_iframe.html "
                           "(Write) -> d.test/title1.html")));
}

IN_PROC_BROWSER_TEST_F(
    BtmBounceDetectorBrowserTest,
    // TODO(crbug.com/40287072): Re-enable this test
    DISABLED_AttributeSameSiteNestedIframesCookieServerAccessTo1P) {
  net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
  https_server.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
  https_server.AddDefaultHandlers(kContentTestDataDir);
  ASSERT_TRUE(https_server.Start());

  std::vector<std::string> redirects;
  StartAppendingRedirectsTo(&redirects);

  const GURL primary_main_frame_url =
      embedded_test_server()->GetURL("a.test", "/page_with_blank_iframe.html");
  ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), primary_main_frame_url));

  const GURL iframe_url =
      embedded_test_server()->GetURL("a.test", "/page_with_blank_iframe.html");
  ASSERT_TRUE(
      NavigateIframeToURL(GetActiveWebContents(), "test_iframe", iframe_url));

  const GURL nested_iframe_url =
      https_server.GetURL("a.test", "/set-cookie?foo=bar;SameSite=None;Secure");
  NavigateNestedIFrameTo(GetIFrame(), "test_iframe", nested_iframe_url);

  const GURL primary_main_frame_final_url =
      embedded_test_server()->GetURL("d.test", "/title1.html");
  // Performs a Client-redirect to `primary_main_frame_final_url`.
  ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
      GetActiveWebContents(), primary_main_frame_final_url));

  CloseTab(GetActiveWebContents());
  EXPECT_THAT(redirects,
              ElementsAre(("[1/1] blank -> a.test/page_with_blank_iframe.html "
                           "(Write) -> d.test/title1.html")));
}

IN_PROC_BROWSER_TEST_F(BtmBounceDetectorBrowserTest,
                       Attribute3PNestedIframesCHIPSClientAccessTo1P) {
  net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
  https_server.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
  https_server.AddDefaultHandlers(kContentTestDataDir);
  ASSERT_TRUE(https_server.Start());

  std::vector<std::string> redirects;
  StartAppendingRedirectsTo(&redirects);

  const GURL primary_main_frame_url =
      embedded_test_server()->GetURL("a.test", "/page_with_blank_iframe.html");
  ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), primary_main_frame_url));

  const GURL iframe_url =
      embedded_test_server()->GetURL("b.test", "/page_with_blank_iframe.html");
  ASSERT_TRUE(
      NavigateIframeToURL(GetActiveWebContents(), "test_iframe", iframe_url));

  const GURL nested_iframe_url = https_server.GetURL("c.test", "/title1.html");
  NavigateNestedIFrameTo(GetIFrame(), "test_iframe", nested_iframe_url);

  AccessCHIPSViaJSIn(GetNestedIFrame());

  const GURL primary_main_frame_final_url =
      embedded_test_server()->GetURL("d.test", "/title1.html");
  // Performs a Client-redirect to `primary_main_frame_final_url`.
  ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
      GetActiveWebContents(), primary_main_frame_final_url));

  CloseTab(GetActiveWebContents());
  EXPECT_THAT(redirects,
              ElementsAre("[1/1] blank -> a.test/page_with_blank_iframe.html "
                          "(Write) -> d.test/title1.html"));
}

IN_PROC_BROWSER_TEST_F(BtmBounceDetectorBrowserTest,
                       Attribute3PNestedIframesCHIPSServerAccessTo1P) {
  net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
  https_server.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
  https_server.AddDefaultHandlers(kContentTestDataDir);
  ASSERT_TRUE(https_server.Start());

  std::vector<std::string> redirects;
  StartAppendingRedirectsTo(&redirects);

  const GURL primary_main_frame_url =
      embedded_test_server()->GetURL("a.test", "/page_with_blank_iframe.html");
  ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), primary_main_frame_url));

  const GURL iframe_url =
      embedded_test_server()->GetURL("b.test", "/page_with_blank_iframe.html");
  ASSERT_TRUE(
      NavigateIframeToURL(GetActiveWebContents(), "test_iframe", iframe_url));

  const GURL nested_iframe_url = https_server.GetURL(
      "a.test",
      "/set-cookie?__Host-foo=bar;SameSite=None;Secure;Path=/;Partitioned");
  NavigateNestedIFrameTo(GetIFrame(), "test_iframe", nested_iframe_url);

  const GURL primary_main_frame_final_url =
      embedded_test_server()->GetURL("d.test", "/title1.html");
  // Performs a Client-redirect to `primary_main_frame_final_url`.
  ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
      GetActiveWebContents(), primary_main_frame_final_url));

  CloseTab(GetActiveWebContents());
  EXPECT_THAT(redirects,
              ElementsAre(("[1/1] blank -> a.test/page_with_blank_iframe.html "
                           "(Write) -> d.test/title1.html")));
}

IN_PROC_BROWSER_TEST_F(BtmBounceDetectorBrowserTest,
                       Attribute3PSubResourceCHIPSClientAccessTo1P) {
  net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
  https_server.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
  https_server.AddDefaultHandlers(kContentTestDataDir);
  ASSERT_TRUE(https_server.Start());

  std::vector<std::string> redirects;
  StartAppendingRedirectsTo(&redirects);

  // This block represents a navigation sequence with a CHIP access (write). It
  // might as well be happening in a separate tab from the navigation block
  // below that does the CHIP's read via subresource request.
  {
    const GURL primary_main_frame_url = embedded_test_server()->GetURL(
        "a.test", "/page_with_blank_iframe.html");
    ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), primary_main_frame_url));

    const GURL iframe_url = embedded_test_server()->GetURL(
        "b.test", "/page_with_blank_iframe.html");
    ASSERT_TRUE(
        NavigateIframeToURL(GetActiveWebContents(), "test_iframe", iframe_url));

    const GURL nested_iframe_url =
        https_server.GetURL("c.test", "/title1.html");
    NavigateNestedIFrameTo(GetIFrame(), "test_iframe", nested_iframe_url);

    AccessCHIPSViaJSIn(GetNestedIFrame());
  }

  const GURL primary_main_frame_url =
      embedded_test_server()->GetURL("a.test", "/page_with_blank_iframe.html");
  ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), primary_main_frame_url));

  GURL image_url = https_server.GetURL("c.test", "/favicon/icon.png");
  CreateImageAndWaitForCookieAccess(GetActiveWebContents(), image_url);

  const GURL primary_main_frame_final_url =
      embedded_test_server()->GetURL("d.test", "/title1.html");
  // Performs a Client-redirect to `primary_main_frame_final_url`.
  ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
      GetActiveWebContents(), primary_main_frame_final_url));

  CloseTab(GetActiveWebContents());

  EXPECT_THAT(
      redirects,
      ElementsAre(
          ("[1/1] a.test/page_with_blank_iframe.html -> "
           "a.test/page_with_blank_iframe.html (Read) -> d.test/title1.html")));
}

IN_PROC_BROWSER_TEST_F(BtmBounceDetectorBrowserTest,
                       DiscardFencedFrameCookieClientAccess) {
  std::vector<std::string> redirects;
  StartAppendingRedirectsTo(&redirects);

  const GURL primary_main_frame_url =
      embedded_test_server()->GetURL("a.test", "/title1.html");
  ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), primary_main_frame_url));

  const GURL fenced_frame_url =
      embedded_test_server()->GetURL("a.test", "/fenced_frames/title1.html");
  RenderFrameHostWrapper fenced_frame(
      fenced_frame_test_helper()->CreateFencedFrame(
          GetActiveWebContents()->GetPrimaryMainFrame(), fenced_frame_url));
  EXPECT_FALSE(fenced_frame.IsDestroyed());

  AccessCookieViaJSIn(GetActiveWebContents(), fenced_frame.get());

  const GURL primary_main_frame_final_url =
      embedded_test_server()->GetURL("d.test", "/title1.html");
  // Performs a Client-redirect to `primary_main_frame_final_url`.
  ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
      GetActiveWebContents(), primary_main_frame_final_url));

  CloseTab(GetActiveWebContents());
  EXPECT_THAT(
      redirects,
      ElementsAre(
          ("[1/1] blank -> a.test/title1.html (None) -> d.test/title1.html")));
}

IN_PROC_BROWSER_TEST_F(BtmBounceDetectorBrowserTest,
                       DiscardFencedFrameCookieServerAccess) {
  std::vector<std::string> redirects;
  StartAppendingRedirectsTo(&redirects);

  const GURL primary_main_frame_url =
      embedded_test_server()->GetURL("a.test", "/title1.html");
  ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), primary_main_frame_url));

  const GURL fenced_frame_url = embedded_test_server()->GetURL(
      "a.test", "/fenced_frames/set_cookie_header.html");
  URLCookieAccessObserver observer(GetActiveWebContents(), fenced_frame_url,
                                   CookieOperation::kChange);
  RenderFrameHostWrapper fenced_frame(
      fenced_frame_test_helper()->CreateFencedFrame(
          GetActiveWebContents()->GetPrimaryMainFrame(), fenced_frame_url));
  EXPECT_FALSE(fenced_frame.IsDestroyed());
  observer.Wait();

  const GURL primary_main_frame_final_url =
      embedded_test_server()->GetURL("d.test", "/title1.html");
  // Performs a Client-redirect to `primary_main_frame_final_url`.
  ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
      GetActiveWebContents(), primary_main_frame_final_url));

  CloseTab(GetActiveWebContents());
  EXPECT_THAT(
      redirects,
      ElementsAre(
          "[1/1] blank -> a.test/title1.html (None) -> d.test/title1.html"));
}

// TODO(crbug.com/40917101): Flaky on Mac.
#if BUILDFLAG(IS_MAC)
#define MAYBE_DiscardPrerenderedPageCookieClientAccess \
  DISABLED_DiscardPrerenderedPageCookieClientAccess
#else
#define MAYBE_DiscardPrerenderedPageCookieClientAccess \
  DiscardPrerenderedPageCookieClientAccess
#endif
IN_PROC_BROWSER_TEST_F(BtmBounceDetectorBrowserTest,
                       MAYBE_DiscardPrerenderedPageCookieClientAccess) {
  std::vector<std::string> redirects;
  StartAppendingRedirectsTo(&redirects);

  const GURL primary_main_frame_url =
      embedded_test_server()->GetURL("a.test", "/title1.html");
  ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), primary_main_frame_url));

  const GURL prerendering_url =
      embedded_test_server()->GetURL("a.test", "/title2.html");
  const PrerenderHostId host_id =
      prerender_test_helper()->AddPrerender(prerendering_url);
  prerender_test_helper()->WaitForPrerenderLoadCompletion(prerendering_url);
  test::PrerenderHostObserver observer(*GetActiveWebContents(), host_id);
  EXPECT_FALSE(observer.was_activated());
  RenderFrameHost* prerender_frame =
      prerender_test_helper()->GetPrerenderedMainFrameHost(host_id);
  EXPECT_NE(prerender_frame, nullptr);

  AccessCookieViaJSIn(GetActiveWebContents(), prerender_frame);

  prerender_test_helper()->CancelPrerenderedPage(host_id);
  observer.WaitForDestroyed();

  const GURL primary_main_frame_final_url =
      embedded_test_server()->GetURL("d.test", "/title1.html");
  // Performs a Client-redirect to `primary_main_frame_final_url`.
  ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
      GetActiveWebContents(), primary_main_frame_final_url));

  CloseTab(GetActiveWebContents());
  EXPECT_THAT(
      redirects,
      ElementsAre(
          "[1/1] blank -> a.test/title1.html (None) -> d.test/title1.html"));
}

// TODO(crbug.com/40269306): flaky test.
IN_PROC_BROWSER_TEST_F(BtmBounceDetectorBrowserTest,
                       DISABLED_DiscardPrerenderedPageCookieServerAccess) {
  std::vector<std::string> redirects;
  StartAppendingRedirectsTo(&redirects);

  const GURL primary_main_frame_url =
      embedded_test_server()->GetURL("a.test", "/title1.html");
  ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), primary_main_frame_url));

  const GURL prerendering_url =
      embedded_test_server()->GetURL("a.test", "/set_cookie_header.html");
  URLCookieAccessObserver observer(GetActiveWebContents(), prerendering_url,
                                   CookieOperation::kChange);
  const PrerenderHostId host_id =
      prerender_test_helper()->AddPrerender(prerendering_url);
  prerender_test_helper()->WaitForPrerenderLoadCompletion(prerendering_url);
  observer.Wait();

  test::PrerenderHostObserver prerender_observer(*GetActiveWebContents(),
                                                 host_id);
  EXPECT_FALSE(prerender_observer.was_activated());
  prerender_test_helper()->CancelPrerenderedPage(host_id);
  prerender_observer.WaitForDestroyed();

  const GURL primary_main_frame_final_url =
      embedded_test_server()->GetURL("d.test", "/title1.html");
  // Performs a Client-redirect to `primary_main_frame_final_url`.
  ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
      GetActiveWebContents(), primary_main_frame_final_url));

  // From the time the cookie was set by the prerendering page and now, the
  // primary main page might have accessed (read) the cookie (when sending a
  // request for a favicon after prerendering page already accessed (Write) the
  // cookie). To prevent flakiness we check for any such access and test for the
  // expected outcome accordingly.
  // TODO(crbug.com/40269100): Investigate whether Prerendering pages
  // (same-site) can be use for evasion.
  const std::string expected_access_type =
      observer.CookieAccessedInPrimaryPage() ? "Read" : "None";

  CloseTab(GetActiveWebContents());
  EXPECT_THAT(redirects,
              ElementsAre(("[1/1] blank -> a.test/title1.html (" +
                           expected_access_type + ") -> d.test/title1.html")));
}

IN_PROC_BROWSER_TEST_F(BtmBounceDetectorBrowserTest,
                       DetectStatefulBounce_ClientRedirect_SiteDataAccess) {
  std::vector<std::string> redirects;
  StartAppendingRedirectsTo(&redirects);

  // Navigate to the initial page, a.test.
  ASSERT_TRUE(
      NavigateToURL(GetActiveWebContents(),
                    embedded_test_server()->GetURL("a.test", "/title1.html")));

  // Navigate with a click (not considered to be redirect) to b.test.
  ASSERT_TRUE(NavigateToURLFromRenderer(
      GetActiveWebContents(),
      embedded_test_server()->GetURL("b.test", "/title1.html")));

  EXPECT_TRUE(AccessStorage(GetActiveWebContents()->GetPrimaryMainFrame(),
                            StorageTypeAccessed::kLocalStorage));

  // Navigate without a click (considered a client-redirect) to c.test.
  ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
      GetActiveWebContents(),
      embedded_test_server()->GetURL("c.test", "/title1.html")));

  EndRedirectChain(/*wait=*/false);

  EXPECT_THAT(redirects,
              ElementsAre("[1/1] a.test/title1.html -> b.test/title1.html "
                          "(Write) -> c.test/title1.html"));
}

// The timing of WCO::OnCookiesAccessed() execution is unpredictable for
// redirects. Sometimes it's called before WCO::DidRedirectNavigation(), and
// sometimes after. Therefore BtmBounceDetector needs to know when it's safe to
// judge an HTTP redirect as stateful (accessing cookies) or not. This test
// tries to verify that OnCookiesAccessed() is always called before
// DidFinishNavigation(), so that BtmBounceDetector can safely perform that
// judgement in DidFinishNavigation().
//
// This test also verifies that OnCookiesAccessed() is called for URLs in the
// same order that they're visited (and that for redirects that both read and
// write cookies, OnCookiesAccessed() is called with kRead before it's called
// with kChange, although BtmBounceDetector doesn't depend on that anymore.)
//
// If either assumption is incorrect, this test will be flaky. On 2022-04-27 I
// (rtarpine) ran this test 1000 times in 40 parallel jobs with no failures, so
// it seems robust.
IN_PROC_BROWSER_TEST_F(BtmBounceDetectorBrowserTest,
                       AllCookieCallbacksBeforeNavigationFinished) {
  GURL redirect_url = embedded_test_server()->GetURL(
      "a.test",
      "/cross-site/b.test/cross-site-with-cookie/c.test/cross-site-with-cookie/"
      "d.test/set-cookie?name=value");
  GURL final_url =
      embedded_test_server()->GetURL("d.test", "/set-cookie?name=value");
  WebContents* web_contents = GetActiveWebContents();

  // Set cookies on all 4 test domains
  ASSERT_TRUE(NavigateToSetCookie(web_contents, embedded_test_server(),
                                  "a.test",
                                  /*is_secure_cookie_set=*/false,
                                  /*is_ad_tagged=*/false));
  ASSERT_TRUE(NavigateToSetCookie(web_contents, embedded_test_server(),
                                  "b.test",
                                  /*is_secure_cookie_set=*/false,
                                  /*is_ad_tagged=*/false));
  ASSERT_TRUE(NavigateToSetCookie(web_contents, embedded_test_server(),
                                  "c.test",
                                  /*is_secure_cookie_set=*/false,
                                  /*is_ad_tagged=*/false));
  ASSERT_TRUE(NavigateToSetCookie(web_contents, embedded_test_server(),
                                  "d.test",
                                  /*is_secure_cookie_set=*/false,
                                  /*is_ad_tagged=*/false));

  // Start logging WebContentsObserver callbacks.
  WCOCallbackLogger::CreateForWebContents(web_contents);
  auto* logger = WCOCallbackLogger::FromWebContents(web_contents);

  // Visit the redirect.
  URLCookieAccessObserver observer(web_contents, final_url,
                                   CookieOperation::kChange);
  ASSERT_TRUE(NavigateToURL(web_contents, redirect_url, final_url));
  observer.Wait();

  // Verify that the 7 OnCookiesAccessed() executions are called in order, and
  // all between DidStartNavigation() and DidFinishNavigation().
  //
  // Note: according to web_contents_observer.h, sometimes cookie reads/writes
  // from navigations may cause the RenderFrameHost* overload of
  // OnCookiesAccessed to be called instead. We haven't seen that yet, and this
  // test will intentionally fail if it happens so that we'll notice.
  EXPECT_THAT(
      logger->log(),
      testing::ContainerEq(std::vector<std::string>(
          {("DidStartNavigation(a.test/cross-site/b.test/"
            "cross-site-with-cookie/"
            "c.test/cross-site-with-cookie/d.test/set-cookie)"),
           ("OnCookiesAccessed(NavigationHandle, Read: "
            "a.test/cross-site/b.test/cross-site-with-cookie/c.test/"
            "cross-site-with-cookie/d.test/set-cookie)"),
           ("OnCookiesAccessed(NavigationHandle, Read: "
            "b.test/cross-site-with-cookie/c.test/cross-site-with-cookie/"
            "d.test/"
            "set-cookie)"),
           ("OnCookiesAccessed(NavigationHandle, Change: "
            "b.test/cross-site-with-cookie/c.test/cross-site-with-cookie/"
            "d.test/"
            "set-cookie)"),
           ("OnCookiesAccessed(NavigationHandle, Read: "
            "c.test/cross-site-with-cookie/d.test/set-cookie)"),
           ("OnCookiesAccessed(NavigationHandle, Change: "
            "c.test/cross-site-with-cookie/d.test/set-cookie)"),
           "OnCookiesAccessed(NavigationHandle, Read: d.test/set-cookie)",
           "OnCookiesAccessed(NavigationHandle, Change: d.test/set-cookie)",
           "DidFinishNavigation(d.test/set-cookie)"})));
}

// An EmbeddedTestServer request handler for
// /cross-site-with-samesite-none-cookie URLs. Like /cross-site-with-cookie, but
// the cookie has additional Secure and SameSite=None attributes.
std::unique_ptr<net::test_server::HttpResponse>
HandleCrossSiteSameSiteNoneCookieRedirect(
    net::EmbeddedTestServer* server,
    const net::test_server::HttpRequest& request) {
  const std::string prefix = "/cross-site-with-samesite-none-cookie";
  if (!net::test_server::ShouldHandle(request, prefix)) {
    return nullptr;
  }

  std::string dest_all = base::UnescapeBinaryURLComponent(
      request.relative_url.substr(prefix.size() + 1));

  std::string dest;
  size_t delimiter = dest_all.find("/");
  if (delimiter != std::string::npos) {
    dest = base::StringPrintf(
        "//%s:%hu/%s", dest_all.substr(0, delimiter).c_str(), server->port(),
        dest_all.substr(delimiter + 1).c_str());
  }

  auto http_response = std::make_unique<net::test_server::BasicHttpResponse>();
  http_response->set_code(net::HTTP_MOVED_PERMANENTLY);
  http_response->AddCustomHeader("Location", dest);
  http_response->AddCustomHeader("Set-Cookie",
                                 "server-redirect=true; Secure; SameSite=None");
  http_response->set_content_type("text/html");
  http_response->set_content(base::StringPrintf(
      "<html><head></head><body>Redirecting to %s</body></html>",
      dest.c_str()));
  return http_response;
}

// Ignore iframes because their state will be partitioned under the top-level
// site anyway.
IN_PROC_BROWSER_TEST_F(BtmBounceDetectorBrowserTest,
                       IgnoreServerRedirectsInIframes) {
  // We host the iframe content on an HTTPS server, because for it to write a
  // cookie, the cookie needs to be SameSite=None and Secure.
  net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
  https_server.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
  https_server.AddDefaultHandlers(kContentTestDataDir);
  https_server.RegisterDefaultHandler(base::BindRepeating(
      &HandleCrossSiteSameSiteNoneCookieRedirect, &https_server));
  ASSERT_TRUE(https_server.Start());

  const GURL root_url =
      embedded_test_server()->GetURL("a.test", "/page_with_blank_iframe.html");
  const GURL redirect_url = https_server.GetURL(
      "b.test", "/cross-site-with-samesite-none-cookie/c.test/title1.html");
  const std::string iframe_id = "test_iframe";
  WebContents* web_contents = GetActiveWebContents();

  std::vector<std::string> redirects;
  StartAppendingRedirectsTo(&redirects);

  ASSERT_TRUE(NavigateToURL(web_contents, root_url));
  ASSERT_TRUE(NavigateIframeToURL(web_contents, iframe_id, redirect_url));
  EndRedirectChain(/*wait=*/false);

  // b.test had a stateful redirect, but because it was in an iframe, we ignored
  // it.
  EXPECT_THAT(redirects, IsEmpty());
}

// This test verifies that sites in a redirect chain with previous user
// interaction are not reported in the resulting issue when a navigation
// finishes.
IN_PROC_BROWSER_TEST_F(BtmBounceDetectorBrowserTest,
                       ReportRedirectorsInChain_OmitSitesWithInteraction) {
  WebContents* web_contents = GetActiveWebContents();

  std::vector<std::string> reports;
  StartAppendingReportsTo(&reports);

  // Record user activation on d.test.
  GURL url = embedded_test_server()->GetURL("d.test", "/title1.html");

  ASSERT_TRUE(NavigateToURL(web_contents, url));
  SimulateMouseClick();

  // Verify interaction was recorded for d.test, before proceeding.
  std::optional<StateValue> state =
      GetBtmState(GetBtmService(web_contents), url);
  ASSERT_TRUE(state.has_value());
  ASSERT_TRUE(state->user_activation_times.has_value());

  // Visit initial page on a.test.
  ASSERT_TRUE(NavigateToURL(
      web_contents, embedded_test_server()->GetURL("a.test", "/title1.html")));

  // Navigate with a click (not a redirect) to b.test, which statefully
  // S-redirects to c.test and write a cookie on c.test.
  ASSERT_TRUE(NavigateToURLFromRenderer(
      web_contents,
      embedded_test_server()->GetURL(
          "b.test", "/cross-site-with-cookie/c.test/title1.html"),
      embedded_test_server()->GetURL("c.test", "/title1.html")));
  AccessCookieViaJSIn(web_contents, web_contents->GetPrimaryMainFrame());

  // Navigate without a click (i.e. by C-redirecting) to d.test and write a
  // cookie on d.test:
  ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
      web_contents, embedded_test_server()->GetURL("d.test", "/title1.html")));
  AccessCookieViaJSIn(web_contents, web_contents->GetPrimaryMainFrame());

  // Navigate without a click (i.e. by C-redirecting) to e.test, which
  // statelessly S-redirects to f.test, which statefully S-redirects to g.test.
  ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
      web_contents,
      embedded_test_server()->GetURL(
          "e.test",
          "/cross-site/f.test/cross-site-with-cookie/g.test/"
          "title1.html"),
      embedded_test_server()->GetURL("g.test", "/title1.html")));
  EndRedirectChain();
  WaitOnStorage(GetBtmService(web_contents));

  // Verify that d.test is not reported (because it had previous user
  // interaction), but the rest of the chain is reported.
  EXPECT_THAT(reports, ElementsAre(("b.test"), ("c.test"), ("e.test, f.test")));
}

// This test verifies that a third-party cookie access doesn't cause a client
// bounce to be considered stateful.
IN_PROC_BROWSER_TEST_F(
    BtmBounceDetectorBrowserTest,
    DetectStatefulRedirect_Client_IgnoreThirdPartySubresource) {
  // We host the image on an HTTPS server, because for it to read a third-party
  // cookie, it needs to be SameSite=None and Secure.
  net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
  https_server.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
  https_server.AddDefaultHandlers(kContentTestDataDir);
  https_server.RegisterDefaultHandler(base::BindRepeating(
      &HandleCrossSiteSameSiteNoneCookieRedirect, &https_server));
  ASSERT_TRUE(https_server.Start());

  GURL initial_url = embedded_test_server()->GetURL("a.test", "/title1.html");
  GURL bounce_url = embedded_test_server()->GetURL("b.test", "/title1.html");
  GURL final_url = embedded_test_server()->GetURL("c.test", "/title1.html");
  GURL image_url = https_server.GetURL("d.test", "/favicon/icon.png");
  WebContents* web_contents = GetActiveWebContents();

  std::vector<std::string> redirects;
  StartAppendingRedirectsTo(&redirects);

  // Start logging WebContentsObserver callbacks.
  WCOCallbackLogger::CreateForWebContents(web_contents);
  auto* logger = WCOCallbackLogger::FromWebContents(web_contents);

  // Set SameSite=None cookie on d.test.
  ASSERT_TRUE(NavigateToURL(
      web_contents, https_server.GetURL(
                        "d.test", "/set-cookie?foo=bar;Secure;SameSite=None")));

  // Visit initial page
  ASSERT_TRUE(NavigateToURL(web_contents, initial_url));
  // Navigate with a click (not a redirect).
  ASSERT_TRUE(NavigateToURLFromRenderer(web_contents, bounce_url));

  // Cause a third-party cookie read.
  CreateImageAndWaitForCookieAccess(web_contents, image_url);
  // Navigate without a click (i.e. by redirecting).
  ASSERT_TRUE(
      NavigateToURLFromRendererWithoutUserGesture(web_contents, final_url));

  EXPECT_THAT(logger->log(),
              ElementsAre(
                  // Set cookie on d.test
                  ("DidStartNavigation(d.test/set-cookie)"),
                  ("OnCookiesAccessed(NavigationHandle, "
                   "Change: d.test/set-cookie)"),
                  ("DidFinishNavigation(d.test/set-cookie)"),
                  // Visit a.test
                  ("DidStartNavigation(a.test/title1.html)"),
                  ("DidFinishNavigation(a.test/title1.html)"),
                  // Bounce on b.test (reading third-party d.test cookie)
                  ("DidStartNavigation(b.test/title1.html)"),
                  ("DidFinishNavigation(b.test/title1.html)"),
                  ("OnCookiesAccessed(RenderFrameHost, "
                   "Read: d.test/favicon/icon.png)"),
                  // Land on c.test
                  ("DidStartNavigation(c.test/title1.html)"),
                  ("DidFinishNavigation(c.test/title1.html)")));
  EndRedirectChain(/*wait=*/false);

  // b.test is a bounce, but not stateful.
  EXPECT_THAT(redirects, ElementsAre("[1/1] a.test/title1.html"
                                     " -> b.test/title1.html (None)"
                                     " -> c.test/title1.html"));
}

// This test verifies that a same-site cookie access DOES cause a client
// bounce to be considered stateful.
IN_PROC_BROWSER_TEST_F(BtmBounceDetectorBrowserTest,
                       DetectStatefulRedirect_Client_FirstPartySubresource) {
  GURL initial_url = embedded_test_server()->GetURL("a.test", "/title1.html");
  GURL bounce_url = embedded_test_server()->GetURL("b.test", "/title1.html");
  GURL final_url = embedded_test_server()->GetURL("c.test", "/title1.html");
  GURL image_url =
      embedded_test_server()->GetURL("sub.b.test", "/favicon/icon.png");
  WebContents* web_contents = GetActiveWebContents();

  std::vector<std::string> redirects;
  StartAppendingRedirectsTo(&redirects);

  // Start logging WebContentsObserver callbacks.
  WCOCallbackLogger::CreateForWebContents(web_contents);
  auto* logger = WCOCallbackLogger::FromWebContents(web_contents);

  // Set cookie on sub.b.test.
  ASSERT_TRUE(NavigateToURL(
      web_contents,
      embedded_test_server()->GetURL("sub.b.test", "/set-cookie?foo=bar")));

  // Visit initial page
  ASSERT_TRUE(NavigateToURL(web_contents, initial_url));
  // Navigate with a click (not a redirect).
  ASSERT_TRUE(NavigateToURLFromRenderer(web_contents, bounce_url));

  // Cause a same-site cookie read.
  CreateImageAndWaitForCookieAccess(web_contents, image_url);
  // Navigate without a click (i.e. by redirecting).
  ASSERT_TRUE(
      NavigateToURLFromRendererWithoutUserGesture(web_contents, final_url));

  EXPECT_THAT(logger->log(),
              ElementsAre(
                  // Set cookie on sub.b.test
                  ("DidStartNavigation(sub.b.test/set-cookie)"),
                  ("OnCookiesAccessed(NavigationHandle, "
                   "Change: sub.b.test/set-cookie)"),
                  ("DidFinishNavigation(sub.b.test/set-cookie)"),
                  // Visit a.test
                  ("DidStartNavigation(a.test/title1.html)"),
                  ("DidFinishNavigation(a.test/title1.html)"),
                  // Bounce on b.test (reading same-site sub.b.test cookie)
                  ("DidStartNavigation(b.test/title1.html)"),
                  ("DidFinishNavigation(b.test/title1.html)"),
                  ("OnCookiesAccessed(RenderFrameHost, "
                   "Read: sub.b.test/favicon/icon.png)"),
                  // Land on c.test
                  ("DidStartNavigation(c.test/title1.html)"),
                  ("DidFinishNavigation(c.test/title1.html)")));
  EndRedirectChain(/*wait=*/false);

  // b.test IS considered a stateful bounce, even though the cookie was read by
  // an image hosted on sub.b.test.
  EXPECT_THAT(redirects,
              ElementsAre(("[1/1] a.test/title1.html -> b.test/title1.html "
                           "(Read) -> c.test/title1.html")));
}

// This test verifies that consecutive redirect chains are combined into one.
IN_PROC_BROWSER_TEST_F(BtmBounceDetectorBrowserTest,
                       DetectStatefulRedirect_ServerClientClientServer) {
  WebContents* web_contents = GetActiveWebContents();

  std::vector<std::string> redirects;
  StartAppendingRedirectsTo(&redirects);

  // Visit initial page on a.test
  ASSERT_TRUE(NavigateToURL(
      web_contents, embedded_test_server()->GetURL("a.test", "/title1.html")));

  // Navigate with a click (not a redirect) to b.test, which S-redirects to
  // c.test
  ASSERT_TRUE(NavigateToURLFromRenderer(
      web_contents,
      embedded_test_server()->GetURL("b.test",
                                     "/cross-site/c.test/title1.html"),
      embedded_test_server()->GetURL("c.test", "/title1.html")));

  // Navigate without a click (i.e. by C-redirecting) to d.test
  ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
      web_contents, embedded_test_server()->GetURL("d.test", "/title1.html")));

  // Navigate without a click (i.e. by C-redirecting) to e.test, which
  // S-redirects to f.test
  ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
      web_contents,
      embedded_test_server()->GetURL("e.test",
                                     "/cross-site/f.test/title1.html"),
      embedded_test_server()->GetURL("f.test", "/title1.html")));
  EndRedirectChain(/*wait=*/false);

  EXPECT_THAT(
      redirects,
      ElementsAre(("[1/4] a.test/title1.html -> "
                   "b.test/cross-site/c.test/title1.html (None) -> "
                   "f.test/title1.html"),
                  ("[2/4] a.test/title1.html -> c.test/title1.html (None) -> "
                   "f.test/title1.html"),
                  ("[3/4] a.test/title1.html -> d.test/title1.html (None) -> "
                   "f.test/title1.html"),
                  ("[4/4] a.test/title1.html -> "
                   "e.test/cross-site/f.test/title1.html (None) -> "
                   "f.test/title1.html")));
}

IN_PROC_BROWSER_TEST_F(BtmBounceDetectorBrowserTest,
                       DetectStatefulRedirect_ClosingTabEndsChain) {
  WebContents* web_contents = GetActiveWebContents();

  std::vector<std::string> redirects;
  StartAppendingRedirectsTo(&redirects);

  // Visit initial page on a.test
  ASSERT_TRUE(NavigateToURL(
      web_contents, embedded_test_server()->GetURL("a.test", "/title1.html")));

  // Navigate with a click (not a redirect) to b.test, which S-redirects to
  // c.test
  ASSERT_TRUE(NavigateToURLFromRenderer(
      web_contents,
      embedded_test_server()->GetURL("b.test",
                                     "/cross-site/c.test/title1.html"),
      embedded_test_server()->GetURL("c.test", "/title1.html")));

  EXPECT_THAT(redirects, IsEmpty());

  CloseTab(web_contents);

  EXPECT_THAT(redirects,
              ElementsAre(("[1/1] a.test/title1.html -> "
                           "b.test/cross-site/c.test/title1.html (None) -> "
                           "c.test/title1.html")));
}

// Verifies server redirects that occur while opening a link in a new tab are
// properly detected.
IN_PROC_BROWSER_TEST_F(BtmBounceDetectorBrowserTest,
                       OpenServerRedirectURLInNewTab) {
  WebContents* original_tab = GetActiveWebContents();
  GURL original_tab_url(
      embedded_test_server()->GetURL("a.test", "/title1.html"));
  ASSERT_TRUE(NavigateToURL(original_tab, original_tab_url));

  // Open a server-redirecting link in a new tab.
  GURL new_tab_url(embedded_test_server()->GetURL(
      "b.test", "/cross-site-with-cookie/c.test/title1.html"));
  ASSERT_OK_AND_ASSIGN(WebContents * new_tab,
                       OpenInNewTab(original_tab, new_tab_url));

  // Verify the tab is different from the original and at the correct URL.
  EXPECT_NE(new_tab, original_tab);
  ASSERT_EQ(new_tab->GetLastCommittedURL(),
            embedded_test_server()->GetURL("c.test", "/title1.html"));

  ASSERT_TRUE(WaitForRedirectCookieWrite(new_tab, new_tab_url));

  std::vector<std::string> redirects;
  RedirectChainDetector* tab_web_contents_observer =
      RedirectChainDetector::FromWebContents(new_tab);
  tab_web_contents_observer->SetRedirectChainHandlerForTesting(
      base::BindRepeating(&AppendRedirects, &redirects));

  WebContentsDestroyedWatcher watcher(new_tab);
  new_tab->Close();
  watcher.Wait();

  EXPECT_THAT(redirects,
              ElementsAre((
                  "[1/1] a.test/ -> " /* Note: the URL's path is lost here. */
                  "b.test/cross-site-with-cookie/c.test/title1.html (Write) -> "
                  "c.test/title1.html")));
}

// Verifies client redirects that occur while opening a link in a new tab are
// properly detected.
IN_PROC_BROWSER_TEST_F(BtmBounceDetectorBrowserTest,
                       OpenClientRedirectURLInNewTab) {
  WebContents* original_tab = GetActiveWebContents();
  GURL original_tab_url(
      embedded_test_server()->GetURL("a.test", "/title1.html"));
  ASSERT_TRUE(NavigateToURL(original_tab, original_tab_url));

  // Open link in a new tab.
  GURL new_tab_url(embedded_test_server()->GetURL("b.test", "/title1.html"));
  ASSERT_OK_AND_ASSIGN(WebContents * new_tab,
                       OpenInNewTab(original_tab, new_tab_url));

  // Verify the tab is different from the original and at the correct URL.
  EXPECT_NE(original_tab, new_tab);
  ASSERT_EQ(new_tab_url, new_tab->GetLastCommittedURL());

  std::vector<std::string> redirects;
  RedirectChainDetector* tab_web_contents_observer =
      RedirectChainDetector::FromWebContents(new_tab);
  tab_web_contents_observer->SetRedirectChainHandlerForTesting(
      base::BindRepeating(&AppendRedirects, &redirects));

  // Navigate without a click (i.e. by C-redirecting) to c.test.
  ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
      new_tab, embedded_test_server()->GetURL("c.test", "/title1.html")));
  WebContentsDestroyedWatcher watcher(new_tab);
  new_tab->Close();
  watcher.Wait();

  EXPECT_THAT(
      redirects,
      ElementsAre(("[1/1] a.test/ -> " /* Note: the URL's path is lost here. */
                   "b.test/title1.html (None) -> "
                   "c.test/title1.html")));
}

// Verifies the start URL of a redirect chain started by opening a link in a new
// tab is handled correctly, when that start page has an opaque origin.
IN_PROC_BROWSER_TEST_F(BtmBounceDetectorBrowserTest,
                       OpenRedirectURLInNewTab_OpaqueOriginInitiator) {
  WebContents* original_tab = GetActiveWebContents();
  GURL original_tab_url("data:text/html,<html></html>");
  ASSERT_TRUE(NavigateToURL(original_tab, original_tab_url));

  // Open a server-redirecting link in a new tab.
  GURL new_tab_url(embedded_test_server()->GetURL(
      "b.test", "/cross-site-with-cookie/c.test/title1.html"));
  ASSERT_OK_AND_ASSIGN(WebContents * new_tab,
                       OpenInNewTab(original_tab, new_tab_url));

  // Verify the tab is different from the original and at the correct URL.
  EXPECT_NE(new_tab, original_tab);
  ASSERT_EQ(new_tab->GetLastCommittedURL(),
            embedded_test_server()->GetURL("c.test", "/title1.html"));

  ASSERT_TRUE(WaitForRedirectCookieWrite(new_tab, new_tab_url));

  std::vector<std::string> redirects;
  RedirectChainDetector::FromWebContents(new_tab)
      ->SetRedirectChainHandlerForTesting(
          base::BindRepeating(&AppendRedirects, &redirects));

  WebContentsDestroyedWatcher watcher(new_tab);
  new_tab->Close();
  watcher.Wait();

  EXPECT_THAT(redirects,
              ElementsAre((
                  "[1/1] blank -> "
                  "b.test/cross-site-with-cookie/c.test/title1.html (Write) -> "
                  "c.test/title1.html")));
}

IN_PROC_BROWSER_TEST_F(BtmBounceDetectorBrowserTest,
                       RedirectInfoHttpStatusPersistence) {
  WebContents* const web_contents = GetActiveWebContents();

  // The "final" URL will not have any server redirects.
  GURL final_url = embedded_test_server()->GetURL("/echo");
  // The "302" and "303" URLs will have a server redirect to the final URL,
  // giving a 302 and 303 HTTP response code status, respectively.
  GURL redirect_303 = embedded_test_server()->GetURL("/server-redirect-303?" +
                                                     final_url.spec());
  GURL redirect_302 = embedded_test_server()->GetURL("/server-redirect-302?" +
                                                     final_url.spec());
  // The "301" URL will give a 301 response code and redirect to the "302" URL.
  GURL redirect_301 = embedded_test_server()->GetURL("/server-redirect-301?" +
                                                     redirect_302.spec());

  // Navigate to a URL that will give a 301 redirect to another URL that will
  // give a 302 redirect, before settling on a third URL.
  ASSERT_TRUE(NavigateToURL(web_contents, redirect_301, final_url));

  // Do client redirect to a URL that gives a 303 redirect.
  ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
      web_contents, redirect_303, final_url));

  RedirectChainDetector* wco =
      RedirectChainDetector::FromWebContents(web_contents);
  const BtmRedirectContext& context = wco->CommittedRedirectContext();

  ASSERT_EQ(context.size(), 4u);

  EXPECT_EQ(context[0].response_code, 301);
  EXPECT_EQ(context[1].response_code, 302);
  // The client redirect does not have an explicit HTTP response status.
  EXPECT_EQ(context[2].response_code, 0);
  EXPECT_EQ(context[3].response_code, 303);
}

class BtmSiteDataAccessDetectorTest
    : public BtmBounceDetectorBrowserTest,
      public testing::WithParamInterface<StorageTypeAccessed> {
 public:
  BtmSiteDataAccessDetectorTest(const BtmSiteDataAccessDetectorTest&) = delete;
  BtmSiteDataAccessDetectorTest& operator=(
      const BtmSiteDataAccessDetectorTest&) = delete;

  BtmSiteDataAccessDetectorTest() = default;

  void SetUpOnMainThread() override {
    host_resolver()->AddRule("*", "127.0.0.1");
    embedded_https_test_server().SetSSLConfig(
        net::EmbeddedTestServer::CERT_TEST_NAMES);
    embedded_https_test_server().AddDefaultHandlers(GetTestDataFilePath());
    ASSERT_TRUE(embedded_https_test_server().Start());
    SetUpBtmWebContentsObserver();
  }
};

IN_PROC_BROWSER_TEST_P(BtmSiteDataAccessDetectorTest,
                       DetectSiteDataAccess_Storages) {
  // Start logging `WebContentsObserver` callbacks.
  WCOCallbackLogger::CreateForWebContents(GetActiveWebContents());
  auto* logger = WCOCallbackLogger::FromWebContents(GetActiveWebContents());

  ASSERT_TRUE(NavigateToURL(
      GetActiveWebContents(),
      embedded_https_test_server().GetURL("a.test", "/title1.html")));

  ASSERT_TRUE(
      AccessStorage(GetActiveWebContents()->GetPrimaryMainFrame(), GetParam()));

  ASSERT_THAT(
      logger->log(),
      testing::ContainerEq(std::vector<std::string>(
          {"DidStartNavigation(a.test/title1.html)",
           "DidFinishNavigation(a.test/title1.html)",
           base::StringPrintf("NotifyStorageAccessed(%s: a.test/title1.html)",
                              base::ToString(GetParam()).c_str())})));
}

IN_PROC_BROWSER_TEST_P(BtmSiteDataAccessDetectorTest,
                       AttributeSameSiteIframesSiteDataAccessTo1P) {
  std::vector<std::string> redirects;
  StartAppendingRedirectsTo(&redirects);

  const GURL primary_main_frame_url = embedded_https_test_server().GetURL(
      "a.test", "/page_with_blank_iframe.html");
  ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), primary_main_frame_url));

  const GURL iframe_url =
      embedded_https_test_server().GetURL("a.test", "/title1.html");
  ASSERT_TRUE(
      NavigateIframeToURL(GetActiveWebContents(), "test_iframe", iframe_url));

  EXPECT_TRUE(AccessStorage(GetIFrame(), GetParam()));

  const GURL primary_main_frame_final_url =
      embedded_https_test_server().GetURL("d.test", "/title1.html");
  // Performs a Client-redirect to `primary_main_frame_final_url`.
  ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
      GetActiveWebContents(), primary_main_frame_final_url));

  CloseTab(GetActiveWebContents());
  EXPECT_THAT(redirects,
              ElementsAre(("[1/1] blank -> a.test/page_with_blank_iframe.html "
                           "(Write) -> d.test/title1.html")));
}

IN_PROC_BROWSER_TEST_P(BtmSiteDataAccessDetectorTest,
                       AttributeSameSiteNestedIframesSiteDataAccessTo1P) {
  std::vector<std::string> redirects;
  StartAppendingRedirectsTo(&redirects);

  const GURL primary_main_frame_url = embedded_https_test_server().GetURL(
      "a.test", "/page_with_blank_iframe.html");
  ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), primary_main_frame_url));

  const GURL iframe_url = embedded_https_test_server().GetURL(
      "a.test", "/page_with_blank_iframe.html");
  ASSERT_TRUE(
      NavigateIframeToURL(GetActiveWebContents(), "test_iframe", iframe_url));

  const GURL nested_iframe_url =
      embedded_https_test_server().GetURL("a.test", "/title1.html");
  NavigateNestedIFrameTo(GetIFrame(), "test_iframe", nested_iframe_url);

  EXPECT_TRUE(AccessStorage(GetNestedIFrame(), GetParam()));

  const GURL primary_main_frame_final_url =
      embedded_https_test_server().GetURL("d.test", "/title1.html");
  // Performs a Client-redirect to `primary_main_frame_final_url`.
  ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
      GetActiveWebContents(), primary_main_frame_final_url));

  CloseTab(GetActiveWebContents());
  EXPECT_THAT(redirects,
              ElementsAre(("[1/1] blank -> a.test/page_with_blank_iframe.html "
                           "(Write) -> d.test/title1.html")));
}

IN_PROC_BROWSER_TEST_P(BtmSiteDataAccessDetectorTest,
                       DiscardFencedFrameCookieClientAccess) {
  std::vector<std::string> redirects;
  StartAppendingRedirectsTo(&redirects);

  const GURL primary_main_frame_url =
      embedded_https_test_server().GetURL("a.test", "/title1.html");
  ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), primary_main_frame_url));

  const GURL fenced_frame_url = embedded_https_test_server().GetURL(
      "a.test", "/fenced_frames/title0.html");
  std::unique_ptr<RenderFrameHostWrapper> fenced_frame =
      std::make_unique<RenderFrameHostWrapper>(
          fenced_frame_test_helper()->CreateFencedFrame(
              GetActiveWebContents()->GetPrimaryMainFrame(), fenced_frame_url));
  EXPECT_NE(fenced_frame, nullptr);

  EXPECT_TRUE(AccessStorage(fenced_frame->get(), GetParam()));

  const GURL primary_main_frame_final_url =
      embedded_https_test_server().GetURL("d.test", "/title1.html");
  // Performs a Client-redirect to `primary_main_frame_final_url`.
  ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
      GetActiveWebContents(), primary_main_frame_final_url));

  CloseTab(GetActiveWebContents());
  EXPECT_THAT(
      redirects,
      ElementsAre(
          ("[1/1] blank -> a.test/title1.html (None) -> d.test/title1.html")));
}

IN_PROC_BROWSER_TEST_P(BtmSiteDataAccessDetectorTest,
                       DiscardPrerenderedPageCookieClientAccess) {
  // Prerendering pages do not have access to `StorageTypeAccessed::kFileSystem`
  // until activation (AKA becoming the primary page, whose test case is already
  // covered).
  if (GetParam() == StorageTypeAccessed::kFileSystem) {
    GTEST_SKIP();
  }

  std::vector<std::string> redirects;
  StartAppendingRedirectsTo(&redirects);

  const GURL primary_main_frame_url =
      embedded_https_test_server().GetURL("a.test", "/title1.html");
  ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), primary_main_frame_url));

  const GURL prerendering_url =
      embedded_https_test_server().GetURL("a.test", "/title2.html");
  const PrerenderHostId host_id =
      prerender_test_helper()->AddPrerender(prerendering_url);
  prerender_test_helper()->WaitForPrerenderLoadCompletion(prerendering_url);
  test::PrerenderHostObserver observer(*GetActiveWebContents(), host_id);
  EXPECT_FALSE(observer.was_activated());
  RenderFrameHost* prerender_frame =
      prerender_test_helper()->GetPrerenderedMainFrameHost(host_id);
  EXPECT_NE(prerender_frame, nullptr);

  EXPECT_TRUE(AccessStorage(prerender_frame, GetParam()));

  prerender_test_helper()->CancelPrerenderedPage(host_id);
  observer.WaitForDestroyed();

  const GURL primary_main_frame_final_url =
      embedded_https_test_server().GetURL("d.test", "/title1.html");
  // Performs a Client-redirect to `primary_main_frame_final_url`.
  ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
      GetActiveWebContents(), primary_main_frame_final_url));

  CloseTab(GetActiveWebContents());
  EXPECT_THAT(
      redirects,
      ElementsAre(
          ("[1/1] blank -> a.test/title1.html (None) -> d.test/title1.html")));
}

// WeLocks accesses aren't monitored by the `PageSpecificContentSettings` as
// they are not persistent.
// TODO(crbug.com/40269763): Remove `StorageTypeAccessed::kFileSystem` once
// deprecation is complete.
INSTANTIATE_TEST_SUITE_P(All,
                         BtmSiteDataAccessDetectorTest,
                         ::testing::Values(StorageTypeAccessed::kLocalStorage,
                                           StorageTypeAccessed::kSessionStorage,
                                           StorageTypeAccessed::kCacheStorage,
                                           StorageTypeAccessed::kFileSystem,
                                           StorageTypeAccessed::kIndexedDB));

// WebAuthn tests do not work on Android because there is no current way to
// install a virtual authenticator.
// NOTE: Manual testing was performed to ensure this implementation works as
// expected on Android platform.
// TODO(crbug.com/40269763): Implement automated testing once the infrastructure
// permits it (Requires mocking the Android Platform Authenticator i.e. GMS
// Core).
#if !BUILDFLAG(IS_ANDROID)
// Some refs for this test fixture:
// clang-format off
// - https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/webauthn/chrome_webauthn_browsertest.cc;drc=c4061a03f240338b42a5b84c98b1a11b62a97a9a
// - https://source.chromium.org/chromium/chromium/src/+/main:content/browser/webauth/webauth_browsertest.cc;drc=e8e4ad9096841fae7c55cea1b7d278c58f6160ff
// - https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/payments/secure_payment_confirmation_authenticator_browsertest.cc;drc=edea5c45c08d151afe67276f08a2ee13814563e1
// clang-format on
class BtmWebAuthnBrowserTest : public ContentBrowserTest {
 public:
  BtmWebAuthnBrowserTest()
      : https_server_(net::EmbeddedTestServer::TYPE_HTTPS) {}

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

  void SetUpCommandLine(base::CommandLine* command_line) override {
    mock_cert_verifier_.SetUpCommandLine(command_line);
    command_line->AppendSwitch(
        switches::kEnableExperimentalWebPlatformFeatures);
  }

  void SetUpInProcessBrowserTestFixture() override {
    mock_cert_verifier_.SetUpInProcessBrowserTestFixture();
  }

  void TearDownInProcessBrowserTestFixture() override {
    mock_cert_verifier_.TearDownInProcessBrowserTestFixture();
  }

  void SetUpOnMainThread() override {
    ContentBrowserTest::SetUpOnMainThread();

    // Allowlist all certs for the HTTPS server.
    mock_cert_verifier()->set_default_result(net::OK);

    host_resolver()->AddRule("*", "127.0.0.1");
    https_server_.ServeFilesFromSourceDirectory(GetTestDataFilePath());
    https_server_.RegisterDefaultHandler(base::BindRepeating(
        &HandleCrossSiteSameSiteNoneCookieRedirect, &https_server_));
    https_server_.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
    ASSERT_TRUE(https_server_.Start());

    auto virtual_device_factory =
        std::make_unique<device::test::VirtualFidoDeviceFactory>();

    virtual_device_factory->mutable_state()->InjectResidentKey(
        std::vector<uint8_t>{1, 2, 3, 4}, authn_hostname,
        std::vector<uint8_t>{5, 6, 7, 8}, "Foo", "Foo Bar");

    device::VirtualCtap2Device::Config config;
    config.resident_key_support = true;
    virtual_device_factory->SetCtap2Config(std::move(config));

    auth_env_ = std::make_unique<ScopedAuthenticatorEnvironmentForTesting>(
        std::move(virtual_device_factory));

    web_contents_observer_ =
        BtmWebContentsObserver::FromWebContents(GetActiveWebContents());
    CHECK(web_contents_observer_);
  }

  void TearDownOnMainThread() override {
    ContentBrowserTest::TearDownOnMainThread();
    web_contents_observer_ = nullptr;
  }

  void PostRunTestOnMainThread() override {
    auth_env_.reset();
    // web_contents_observer_.ClearAndDelete();
    ContentBrowserTest::PostRunTestOnMainThread();
  }

  auto* TestServer() { return &https_server_; }

  WebContents* GetActiveWebContents() { return shell()->web_contents(); }

  RedirectChainDetector* GetRedirectChainHelper() {
    return RedirectChainDetector::FromWebContents(GetActiveWebContents());
  }

  // Perform a browser-based navigation to terminate the current redirect chain.
  // (NOTE: tests using WCOCallbackLogger must call this *after* checking the
  // log, since this navigation will be logged.)
  void EndRedirectChain() {
    ASSERT_TRUE(NavigateToURL(GetActiveWebContents(),
                              TestServer()->GetURL("a.test", "/title1.html")));
  }

  void StartAppendingRedirectsTo(std::vector<std::string>* redirects) {
    GetRedirectChainHelper()->SetRedirectChainHandlerForTesting(
        base::BindRepeating(&AppendRedirects, redirects));
  }

  void StartAppendingReportsTo(std::vector<std::string>* reports) {
    web_contents_observer_->SetIssueReportingCallbackForTesting(
        base::BindRepeating(&AppendSitesInReport, reports));
  }

  void GetWebAuthnAssertion() {
    ASSERT_EQ("OK", EvalJs(GetActiveWebContents(), R"(
    let cred_id = new Uint8Array([1,2,3,4]);
    navigator.credentials.get({
      publicKey: {
        challenge: cred_id,
        userVerification: 'preferred',
        allowCredentials: [{
          type: 'public-key',
          id: cred_id,
          transports: ['usb', 'nfc', 'ble'],
        }],
        timeout: 10000
      }
    }).then(c => 'OK',
      e => e.toString());
  )",
                           EXECUTE_SCRIPT_NO_USER_GESTURE));
  }

  ContentMockCertVerifier::CertVerifier* mock_cert_verifier() {
    return mock_cert_verifier_.mock_cert_verifier();
  }

 protected:
  const std::string authn_hostname = "b.test";

 private:
  ContentMockCertVerifier mock_cert_verifier_;
  net::EmbeddedTestServer https_server_;
  raw_ptr<BtmWebContentsObserver> web_contents_observer_ = nullptr;
  std::unique_ptr<ScopedAuthenticatorEnvironmentForTesting> auth_env_;
};

IN_PROC_BROWSER_TEST_F(BtmWebAuthnBrowserTest,
                       WebAuthnAssertion_ConfirmWCOCallback) {
  // Start logging `WebContentsObserver` callbacks.
  WCOCallbackLogger::CreateForWebContents(GetActiveWebContents());
  auto* logger = WCOCallbackLogger::FromWebContents(GetActiveWebContents());

  std::vector<std::string> redirects;
  StartAppendingRedirectsTo(&redirects);

  const GURL initial_url = TestServer()->GetURL("a.test", "/title1.html");
  ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), initial_url));

  const GURL bounce_url = TestServer()->GetURL(authn_hostname, "/title1.html");
  ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), bounce_url));

  AccessCookieViaJSIn(GetActiveWebContents(),
                      GetActiveWebContents()->GetPrimaryMainFrame());

  GetWebAuthnAssertion();

  const GURL final_url = TestServer()->GetURL("d.test", "/title1.html");
  // Performs a Client-redirect to `final_url`.
  ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
      GetActiveWebContents(), final_url));

  std::vector<std::string> expected_log = {
      "DidStartNavigation(a.test/title1.html)",
      "DidFinishNavigation(a.test/title1.html)",
      "DidStartNavigation(b.test/title1.html)",
      "DidFinishNavigation(b.test/title1.html)",
      "OnCookiesAccessed(RenderFrameHost, Change: b.test/title1.html)",
      "WebAuthnAssertionRequestSucceeded(b.test/title1.html)",
      "DidStartNavigation(d.test/title1.html)",
      "DidFinishNavigation(d.test/title1.html)"};

  EXPECT_THAT(logger->log(), testing::ContainerEq(expected_log));

  EndRedirectChain();

  std::vector<std::string> expected_redirects;
  // NOTE: The bounce detection isn't impacted (is exonerated) at this point by
  // the web authn assertion.
  expected_redirects.push_back(
      "[1/1] a.test/title1.html -> b.test/title1.html (Write) -> "
      "d.test/title1.html");
  // NOTE: Due the favicon.ico temporally iffy callbacks we could expect the
  // following outcome to help avoid flakiness.
  expected_redirects.push_back(
      "[1/1] a.test/title1.html -> b.test/title1.html (ReadWrite) -> "
      "d.test/title1.html");

  EXPECT_THAT(expected_redirects, Contains(redirects.front()));
}

// This test verifies that sites in a redirect chain with previous web authn
// assertions are not reported in the resulting issue when a navigation
// finishes.
IN_PROC_BROWSER_TEST_F(
    BtmWebAuthnBrowserTest,
    ReportRedirectorsInChain_OmitSitesWithWebAuthnAssertions) {
  WebContents* web_contents = GetActiveWebContents();

  std::vector<std::string> reports;
  StartAppendingReportsTo(&reports);

  // Visit initial page on a.test.
  ASSERT_TRUE(NavigateToURL(web_contents,
                            TestServer()->GetURL("a.test", "/title1.html")));

  GURL url = TestServer()->GetURL(authn_hostname, "/title1.html");
  ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(web_contents, url));

  GetWebAuthnAssertion();

  // Verify web authn assertion was recorded for `authn_hostname`, before
  // proceeding.
  std::optional<StateValue> state =
      GetBtmState(GetBtmService(web_contents), url);
  ASSERT_TRUE(state.has_value());
  ASSERT_FALSE(state->user_activation_times.has_value());
  ASSERT_TRUE(state->web_authn_assertion_times.has_value());

  // Navigate with a click (not a redirect) to d.test, which statefully
  // S-redirects to c.test and write a cookie on c.test.
  ASSERT_TRUE(NavigateToURLFromRenderer(
      web_contents,
      TestServer()->GetURL(
          "d.test", "/cross-site-with-samesite-none-cookie/c.test/title1.html"),
      TestServer()->GetURL("c.test", "/title1.html")));
  AccessCookieViaJSIn(web_contents, web_contents->GetPrimaryMainFrame());

  // Navigate without a click (i.e. by C-redirecting) to `authn_hostname` and
  // write a cookie on `authn_hostname`:
  ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
      web_contents, TestServer()->GetURL(authn_hostname, "/title1.html")));
  AccessCookieViaJSIn(web_contents, web_contents->GetPrimaryMainFrame());

  // Navigate without a click (i.e. by C-redirecting) to e.test, which
  // statefully S-redirects to f.test, which statefully S-redirects to g.test.
  ASSERT_TRUE(NavigateToURLFromRendererWithoutUserGesture(
      web_contents,
      TestServer()->GetURL("e.test",
                           "/cross-site-with-samesite-none-cookie/f.test/"
                           "cross-site-with-samesite-none-cookie/g.test/"
                           "title1.html"),
      TestServer()->GetURL("g.test", "/title1.html")));

  EndRedirectChain();
  WaitOnStorage(GetBtmService(web_contents));

  EXPECT_THAT(reports, ElementsAre(("a.test"), ("d.test"), ("c.test"),
                                   ("e.test, f.test")));
}
#endif  // !BUILDFLAG(IS_ANDROID)

// Verifies that a successfully registered service worker is tracked as a
// storage access.
IN_PROC_BROWSER_TEST_F(BtmBounceDetectorBrowserTest,
                       ServiceWorkerAccess_Storages) {
  // Start logging `WebContentsObserver` callbacks.
  WCOCallbackLogger::CreateForWebContents(GetActiveWebContents());
  auto* logger = WCOCallbackLogger::FromWebContents(GetActiveWebContents());

  // Navigate to URL to set service workers. This will result in a service
  // worker access from the RenderFrameHost.
  ASSERT_TRUE(NavigateToURL(GetActiveWebContents(),
                            embedded_test_server()->GetURL(
                                "/service_worker/create_service_worker.html")));

  // Register a service worker on the current page, and await its completion.
  ASSERT_EQ(true, EvalJs(GetActiveWebContents(), R"(
    (async () => {
      await navigator.serviceWorker.register('/service_worker/empty.js');
      await navigator.serviceWorker.ready;
      return true;
    })();
  )"));

  // Navigate away from and back to the URL in scope of the registered service
  // worker. This will result in a service worker access from the
  // NavigationHandle.
  ASSERT_TRUE(NavigateToURL(
      GetActiveWebContents(),
      embedded_test_server()->GetURL("/service_worker/empty.html")));
  ASSERT_TRUE(NavigateToURL(GetActiveWebContents(),
                            embedded_test_server()->GetURL(
                                "/service_worker/create_service_worker.html")));

  // Validate that the expected callbacks to WebContentsObserver were made.
  EXPECT_THAT(logger->log(),
              testing::IsSupersetOf({"OnServiceWorkerAccessed(RenderFrameHost: "
                                     "127.0.0.1/service_worker/)",
                                     "OnServiceWorkerAccessed(NavigationHandle:"
                                     " 127.0.0.1/service_worker/)"}));
}

// Verifies that adding a shared worker to a frame is tracked as a storage
// access.
IN_PROC_BROWSER_TEST_F(BtmBounceDetectorBrowserTest,
                       SharedWorkerAccess_Storages) {
  // Start logging `WebContentsObserver` callbacks.
  WCOCallbackLogger::CreateForWebContents(GetActiveWebContents());
  auto* logger = WCOCallbackLogger::FromWebContents(GetActiveWebContents());

  // Add the WCOCallbackLogger as an observer of SharedWorkerService events.
  GetActiveWebContents()
      ->GetBrowserContext()
      ->GetDefaultStoragePartition()
      ->GetSharedWorkerService()
      ->AddObserver(logger);

  // Navigate to URL for shared worker.
  ASSERT_TRUE(NavigateToURL(
      GetActiveWebContents(),
      embedded_test_server()->GetURL("a.test", "/no-favicon.html")));

  // Create and start a shared worker on the current page.
  ASSERT_EQ(true,
            EvalJs(GetActiveWebContents(), JsReplace(
                                               R"(
    (async () => {
      const worker = await new Promise((resolve, reject) => {
        const worker =
            new SharedWorker("/workers/shared_fetcher.js");
        worker.port.addEventListener("message", () => resolve(worker));
        worker.addEventListener("error", reject);
        worker.port.start();
      });

      const messagePromise = new Promise((resolve) => {
        const listener = (event) => resolve(event.data);
        worker.port.addEventListener("message", listener, { once: true });
      });

      worker.port.postMessage($1);

      const { error, ok } = await messagePromise;
      if (error !== undefined) {
        throw(error);
      }

      return ok;
    })();
  )",
                                               embedded_test_server()->GetURL(
                                                   "b.test", "/cors-ok.txt"))));

  // Validate that the expected callback to SharedWorkerService.Observer was
  // made.
  EXPECT_THAT(
      logger->log(),
      testing::Contains("OnSharedWorkerClientAdded(a.test/no-favicon.html)"));

  // Clean up the observer to avoid a dangling ptr.
  GetActiveWebContents()
      ->GetBrowserContext()
      ->GetDefaultStoragePartition()
      ->GetSharedWorkerService()
      ->RemoveObserver(logger);
}

// Verifies that adding a dedicated worker to a frame is tracked as a storage
// access.
IN_PROC_BROWSER_TEST_F(BtmBounceDetectorBrowserTest,
                       DedicatedWorkerAccess_Storages) {
  // Start logging `WebContentsObserver` callbacks.
  WCOCallbackLogger::CreateForWebContents(GetActiveWebContents());
  auto* logger = WCOCallbackLogger::FromWebContents(GetActiveWebContents());

  // Add the WCOCallbackLogger as an observer of DedicatedWorkerService events.
  GetActiveWebContents()
      ->GetBrowserContext()
      ->GetDefaultStoragePartition()
      ->GetDedicatedWorkerService()
      ->AddObserver(logger);

  // Navigate to URL for dedicated worker.
  ASSERT_TRUE(NavigateToURL(
      GetActiveWebContents(),
      embedded_test_server()->GetURL("a.test", "/no-favicon.html")));

  // Create and start a dedicated worker on the current page.
  ASSERT_EQ(true,
            EvalJs(GetActiveWebContents(), JsReplace(
                                               R"(
    (async () => {
      const worker = new Worker("/workers/fetcher.js");

      const messagePromise = new Promise((resolve) => {
        const listener = (event) => resolve(event.data);
        worker.addEventListener("message", listener, { once: true });
      });

      worker.postMessage($1);

      const { error, ok } = await messagePromise;
      if (error !== undefined) {
        throw(error);
      }

      return ok;
    })();
  )",
                                               embedded_test_server()->GetURL(
                                                   "b.test", "/cors-ok.txt"))));

  // Validate that the expected callback to DedicatedWorkerService.Observer was
  // made.
  EXPECT_THAT(
      logger->log(),
      testing::Contains("OnDedicatedWorkerCreated(a.test/no-favicon.html)"));

  // Clean up the observer to avoid a dangling ptr.
  GetActiveWebContents()
      ->GetBrowserContext()
      ->GetDefaultStoragePartition()
      ->GetDedicatedWorkerService()
      ->RemoveObserver(logger);
}

// Tests that currently only work consistently when the trigger is (any) bounce.
// TODO(crbug.com/336161248) Make these tests use stateful bounces.
class BtmBounceTriggerBrowserTest : public BtmBounceDetectorBrowserTest {
 protected:
  BtmBounceTriggerBrowserTest() {
    enabled_features_.push_back(
        {features::kBtm, {{"triggering_action", "bounce"}}});
  }

  void SetUpOnMainThread() override {
    BtmBounceDetectorBrowserTest::SetUpOnMainThread();
    // BTM will only record bounces if 3PCs are blocked.
    browser_client().SetBlockThirdPartyCookiesByDefault(true);
    WebContents* web_contents = GetActiveWebContents();
    ASSERT_FALSE(btm::Are3PcsGenerallyEnabled(web_contents->GetBrowserContext(),
                                              web_contents));
  }
};

// Verifies that a HTTP 204 (No Content) response is treated like a bounce.
IN_PROC_BROWSER_TEST_F(BtmBounceTriggerBrowserTest, NoContent) {
  WebContents* web_contents = GetActiveWebContents();

  GURL committed_url = embedded_test_server()->GetURL("a.test", "/title1.html");
  ASSERT_TRUE(NavigateToURL(web_contents, committed_url));

  BtmRedirectChainObserver observer(
      BtmService::Get(web_contents->GetBrowserContext()), committed_url);
  GURL nocontent_url = embedded_test_server()->GetURL("b.test", "/nocontent");
  ASSERT_TRUE(NavigateToURL(web_contents, nocontent_url, committed_url));
  observer.Wait();

  base::test::TestFuture<const std::vector<std::string>&> deleted_sites;
  BtmService::Get(web_contents->GetBrowserContext())
      ->DeleteEligibleSitesImmediately(deleted_sites.GetCallback());
  ASSERT_THAT(deleted_sites.Get(), ElementsAre("b.test"));
}

class BtmThrottlingBrowserTest : public BtmBounceDetectorBrowserTest {
 public:
  void SetUpOnMainThread() override {
    BtmBounceDetectorBrowserTest::SetUpOnMainThread();
    BtmWebContentsObserver::FromWebContents(GetActiveWebContents())
        ->SetClockForTesting(&test_clock_);
  }

  base::SimpleTestClock test_clock_;
};

IN_PROC_BROWSER_TEST_F(BtmThrottlingBrowserTest,
                       InteractionRecording_Throttled) {
  WebContents* web_contents = GetActiveWebContents();
  const base::Time start_time = test_clock_.Now();

  // Record user activation on a.test.
  const GURL url = embedded_test_server()->GetURL("a.test", "/title1.html");
  ASSERT_TRUE(NavigateToURL(web_contents, url));
  SimulateMouseClick();
  // Verify the interaction was recorded in the BTM DB.
  std::optional<StateValue> state =
      GetBtmState(GetBtmService(web_contents), url);
  ASSERT_THAT(state->user_activation_times,
              testing::Optional(testing::Pair(start_time, start_time)));

  // Click again, just before kBtmTimestampUpdateInterval elapses.
  test_clock_.Advance(kBtmTimestampUpdateInterval - base::Seconds(1));
  SimulateMouseClick();
  // Verify the second interaction was NOT recorded, due to throttling.
  state = GetBtmState(GetBtmService(web_contents), url);
  ASSERT_THAT(state->user_activation_times,
              testing::Optional(testing::Pair(start_time, start_time)));

  // Click a third time, after kBtmTimestampUpdateInterval has passed since the
  // first click.
  test_clock_.Advance(base::Seconds(1));
  SimulateMouseClick();
  // Verify the third interaction WAS recorded.
  state = GetBtmState(GetBtmService(web_contents), url);
  ASSERT_THAT(state->user_activation_times,
              testing::Optional(testing::Pair(
                  start_time, start_time + kBtmTimestampUpdateInterval)));
}

IN_PROC_BROWSER_TEST_F(BtmThrottlingBrowserTest,
                       InteractionRecording_NotThrottled_AfterRefresh) {
  WebContents* web_contents = GetActiveWebContents();
  const base::Time start_time = test_clock_.Now();

  // Record user activation on a.test.
  const GURL url = embedded_test_server()->GetURL("a.test", "/title1.html");
  ASSERT_TRUE(NavigateToURL(web_contents, url));
  SimulateMouseClick();
  // Verify the interaction was recorded in the BTM DB.
  std::optional<StateValue> state =
      GetBtmState(GetBtmService(web_contents), url);
  ASSERT_THAT(state->user_activation_times,
              testing::Optional(testing::Pair(start_time, start_time)));

  // Navigate to a new page and click, only a second after the previous click.
  test_clock_.Advance(base::Seconds(1));
  const GURL url2 = embedded_test_server()->GetURL("b.test", "/title1.html");
  ASSERT_TRUE(NavigateToURL(web_contents, url2));
  SimulateMouseClick();
  // Verify the second interaction was also recorded (not throttled).
  state = GetBtmState(GetBtmService(web_contents), url2);
  ASSERT_THAT(state->user_activation_times,
              testing::Optional(testing::Pair(start_time + base::Seconds(1),
                                              start_time + base::Seconds(1))));
}


namespace {

class SiteStorage {
 public:
  constexpr SiteStorage() = default;

  virtual base::expected<std::string, std::string> ReadValue(
      RenderFrameHost* frame) const = 0;
  virtual testing::AssertionResult WriteValue(RenderFrameHost* frame,
                                              std::string_view value,
                                              bool partitioned) const = 0;

  virtual std::string_view name() const = 0;
};

class CookieStorage : public SiteStorage {
  base::expected<std::string, std::string> ReadValue(
      RenderFrameHost* frame) const override {
    EvalJsResult result =
        EvalJs(frame, "document.cookie", EXECUTE_SCRIPT_NO_USER_GESTURE);
    if (!result.is_ok()) {
      return base::unexpected(result.ExtractError());
    }
    return base::ok(result.ExtractString());
  }

  testing::AssertionResult WriteValue(RenderFrameHost* frame,
                                      std::string_view cookie,
                                      bool partitioned) const override {
    std::string value(cookie);
    if (partitioned) {
      value += ";Secure;Partitioned;SameSite=None";
    }

    FrameCookieAccessObserver obs(WebContents::FromRenderFrameHost(frame),
                                  frame, CookieOperation::kChange);
    testing::AssertionResult result =
        ExecJs(frame, JsReplace("document.cookie = $1;", value),
               EXECUTE_SCRIPT_NO_USER_GESTURE);
    if (result) {
      obs.Wait();
    }
    return result;
  }

  std::string_view name() const override { return "CookieStorage"; }
};

class LocalStorage : public SiteStorage {
  base::expected<std::string, std::string> ReadValue(
      RenderFrameHost* frame) const override {
    EvalJsResult result = EvalJs(frame, "localStorage.getItem('value')",
                                 EXECUTE_SCRIPT_NO_USER_GESTURE);
    if (!result.is_ok()) {
      return base::unexpected(result.ExtractError());
    }
    if (result == base::Value()) {
      return base::ok("");
    }
    return base::ok(result.ExtractString());
  }

  testing::AssertionResult WriteValue(RenderFrameHost* frame,
                                      std::string_view value,
                                      bool partitioned) const override {
    return ExecJs(frame, JsReplace("localStorage.setItem('value', $1);", value),
                  EXECUTE_SCRIPT_NO_USER_GESTURE);
  }

  std::string_view name() const override { return "LocalStorage"; }
};

void PrintTo(const SiteStorage* storage, std::ostream* os) {
  *os << storage->name();
}

static constexpr CookieStorage kCookieStorage;
static constexpr LocalStorage kLocalStorage;
}  // namespace

class BtmDataDeletionBrowserTest
    : public BtmBounceDetectorBrowserTest,
      public testing::WithParamInterface<const SiteStorage*> {
 public:
  void SetUpOnMainThread() override {
    BtmBounceDetectorBrowserTest::SetUpOnMainThread();
    https_server_.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
    https_server_.AddDefaultHandlers(kContentTestDataDir);
    ASSERT_TRUE(https_server_.Start());

    browser_client().SetBlockThirdPartyCookiesByDefault(true);
    WebContents* web_contents = GetActiveWebContents();
    ASSERT_FALSE(btm::Are3PcsGenerallyEnabled(web_contents->GetBrowserContext(),
                                              web_contents));
  }

  const net::EmbeddedTestServer& https_server() const { return https_server_; }

  [[nodiscard]] testing::AssertionResult WriteToPartitionedStorage(
      std::string_view first_party_hostname,
      std::string_view third_party_hostname,
      std::string_view value) {
    WebContents* web_contents = GetActiveWebContents();

    if (!NavigateToURL(web_contents,
                       https_server().GetURL(first_party_hostname,
                                             "/page_with_blank_iframe.html"))) {
      return testing::AssertionFailure() << "Failed to navigate top-level";
    }

    const std::string_view kIframeId = "test_iframe";
    if (!NavigateIframeToURL(
            web_contents, kIframeId,
            https_server().GetURL(third_party_hostname, "/title1.html"))) {
      return testing::AssertionFailure() << "Failed to navigate iframe";
    }

    RenderFrameHost* iframe = ChildFrameAt(web_contents, 0);
    if (!iframe) {
      return testing::AssertionFailure() << "Child frame not found";
    }
    return WriteValue(iframe, value, /*partitioned=*/true);
  }

  [[nodiscard]] base::expected<std::string, std::string>
  ReadFromPartitionedStorage(std::string_view first_party_hostname,
                             std::string_view third_party_hostname) {
    WebContents* web_contents = GetActiveWebContents();

    if (!NavigateToURL(web_contents,
                       https_server().GetURL(first_party_hostname,
                                             "/page_with_blank_iframe.html"))) {
      return base::unexpected("Failed to navigate top-level");
    }

    const std::string_view kIframeId = "test_iframe";
    if (!NavigateIframeToURL(
            web_contents, kIframeId,
            https_server().GetURL(third_party_hostname, "/title1.html"))) {
      return base::unexpected("Failed to navigate iframe");
    }

    RenderFrameHost* iframe = ChildFrameAt(web_contents, 0);
    if (!iframe) {
      return base::unexpected("iframe not found");
    }
    return ReadValue(iframe);
  }

  [[nodiscard]] base::expected<std::string, std::string> ReadFromStorage(
      std::string_view hostname) {
    WebContents* web_contents = GetActiveWebContents();

    if (!NavigateToURL(web_contents,
                       https_server().GetURL(hostname, "/title1.html"))) {
      return base::unexpected("Failed to navigate");
    }

    return ReadValue(web_contents);
  }

  [[nodiscard]] testing::AssertionResult WriteToStorage(
      std::string_view hostname,
      std::string_view value) {
    WebContents* web_contents = GetActiveWebContents();

    if (!NavigateToURL(web_contents,
                       https_server().GetURL(hostname, "/title1.html"))) {
      return testing::AssertionFailure() << "Failed to navigate";
    }

    return WriteValue(web_contents, value);
  }

  // Navigates to host1, then performs a stateful bounce on host2 to host3.
  [[nodiscard]] testing::AssertionResult DoStatefulBounce(
      std::string_view host1,
      std::string_view host2,
      std::string_view host3) {
    WebContents* web_contents = GetActiveWebContents();

    if (!NavigateToURL(web_contents,
                       https_server().GetURL(host1, "/title1.html"))) {
      return testing::AssertionFailure() << "Failed to navigate to " << host1;
    }

    if (!NavigateToURLFromRenderer(
            web_contents, https_server().GetURL(host2, "/title1.html"))) {
      return testing::AssertionFailure() << "Failed to navigate to " << host2;
    }

    testing::AssertionResult result = WriteValue(web_contents, "bounce=yes");
    if (!result) {
      return result;
    }

    if (!NavigateToURLFromRendererWithoutUserGesture(
            web_contents, https_server().GetURL(host3, "/title1.html"))) {
      return testing::AssertionFailure() << "Failed to navigate to " << host3;
    }

    EndRedirectChain();

    return testing::AssertionSuccess();
  }

 private:
  const SiteStorage* storage() { return GetParam(); }

  [[nodiscard]] base::expected<std::string, std::string> ReadValue(
      const ToRenderFrameHost& frame) {
    return storage()->ReadValue(frame.render_frame_host());
  }

  [[nodiscard]] testing::AssertionResult WriteValue(
      const ToRenderFrameHost& frame,
      std::string_view value,
      bool partitioned = false) {
    return storage()->WriteValue(frame.render_frame_host(), value, partitioned);
  }

  net::EmbeddedTestServer https_server_{net::EmbeddedTestServer::TYPE_HTTPS};
};

IN_PROC_BROWSER_TEST_P(BtmDataDeletionBrowserTest, DontDeleteIfTpcsEnabled) {
  // Do not block third-party cookies by default. This should make it such that
  // BTM deletion does not run.
  browser_client().SetBlockThirdPartyCookiesByDefault(false);
  WebContents* web_contents = GetActiveWebContents();
  ASSERT_TRUE(btm::Are3PcsGenerallyEnabled(web_contents->GetBrowserContext(),
                                           web_contents));

  // Perform a stateful bounce on b.test to make it eligible for deletion.
  ASSERT_TRUE(DoStatefulBounce("a.test", "b.test", "c.test"));
  // Confirm unpartitioned storage was written on b.test.
  EXPECT_THAT(ReadFromStorage("b.test"), base::test::ValueIs("bounce=yes"));
  // Navigate away from b.test since BTM won't delete its state while loaded.
  ASSERT_TRUE(NavigateToURL(web_contents,
                            https_server().GetURL("a.test", "/title1.html")));

  // Trigger BTM deletion.
  base::test::TestFuture<const std::vector<std::string>&> deleted_sites;
  BtmService::Get(web_contents->GetBrowserContext())
      ->DeleteEligibleSitesImmediately(deleted_sites.GetCallback());

  // Confirm that nothing was deleted.
  EXPECT_THAT(deleted_sites.Get(), IsEmpty());
  // Confirm b.test storage has not changed.
  EXPECT_THAT(ReadFromStorage("b.test"), base::test::ValueIs("bounce=yes"));
}

IN_PROC_BROWSER_TEST_P(BtmDataDeletionBrowserTest, DeleteDomain) {
  WebContents* web_contents = GetActiveWebContents();

  // Perform a stateful bounce on b.test to make it eligible for deletion.
  ASSERT_TRUE(DoStatefulBounce("a.test", "b.test", "c.test"));

  // Confirm unpartitioned storage was written on b.test.
  EXPECT_THAT(ReadFromStorage("b.test"), base::test::ValueIs("bounce=yes"));
  // Navigate away from b.test since BTM won't delete its state while loaded.
  ASSERT_TRUE(NavigateToURL(web_contents,
                            https_server().GetURL("a.test", "/title1.html")));

  // Trigger BTM deletion.
  base::test::TestFuture<const std::vector<std::string>&> deleted_sites;
  BtmService::Get(web_contents->GetBrowserContext())
      ->DeleteEligibleSitesImmediately(deleted_sites.GetCallback());
  ASSERT_THAT(deleted_sites.Get(), ElementsAre("b.test"));

  // Confirm b.test storage was deleted.
  EXPECT_THAT(ReadFromStorage("b.test"), base::test::ValueIs(""));
}

IN_PROC_BROWSER_TEST_P(BtmDataDeletionBrowserTest, DontDeleteOtherDomains) {
  WebContents* web_contents = GetActiveWebContents();

  // Set storage on a.test
  ASSERT_TRUE(WriteToStorage("a.test", "foo=bar"));
  // Confirm written.
  EXPECT_THAT(ReadFromStorage("a.test"), base::test::ValueIs("foo=bar"));

  // Perform a stateful bounce on b.test to make it eligible for deletion.
  ASSERT_TRUE(DoStatefulBounce("a.test", "b.test", "c.test"));

  // Trigger BTM deletion.
  base::test::TestFuture<const std::vector<std::string>&> deleted_sites;
  BtmService::Get(web_contents->GetBrowserContext())
      ->DeleteEligibleSitesImmediately(deleted_sites.GetCallback());
  ASSERT_THAT(deleted_sites.Get(), ElementsAre("b.test"));

  // Confirm a.test storage was NOT deleted.
  EXPECT_THAT(ReadFromStorage("a.test"), base::test::ValueIs("foo=bar"));
}

IN_PROC_BROWSER_TEST_P(BtmDataDeletionBrowserTest,
                       DontDeleteDomainWhenPartitioned) {
  WebContents* web_contents = GetActiveWebContents();

  // Set storage on b.test embedded in a.test.
  ASSERT_TRUE(WriteToPartitionedStorage("a.test", "b.test", "foo=bar"));
  // Confirm written.
  EXPECT_THAT(ReadFromPartitionedStorage("a.test", "b.test"),
              base::test::ValueIs("foo=bar"));

  // Perform a stateful bounce on b.test to make it eligible for deletion.
  ASSERT_TRUE(DoStatefulBounce("a.test", "b.test", "c.test"));

  // Trigger BTM deletion.
  base::test::TestFuture<const std::vector<std::string>&> deleted_sites;
  BtmService::Get(web_contents->GetBrowserContext())
      ->DeleteEligibleSitesImmediately(deleted_sites.GetCallback());
  ASSERT_THAT(deleted_sites.Get(), ElementsAre("b.test"));

  // Confirm partitioned storage was NOT deleted.
  EXPECT_THAT(ReadFromPartitionedStorage("a.test", "b.test"),
              base::test::ValueIs("foo=bar"));
}

IN_PROC_BROWSER_TEST_P(BtmDataDeletionBrowserTest, DeleteSubdomains) {
  WebContents* web_contents = GetActiveWebContents();

  // Set storage on sub.b.test
  ASSERT_TRUE(WriteToStorage("sub.b.test", "foo=bar"));
  // Confirm written.
  EXPECT_THAT(ReadFromStorage("sub.b.test"), base::test::ValueIs("foo=bar"));

  // Perform a stateful bounce on b.test to make it eligible for deletion.
  ASSERT_TRUE(DoStatefulBounce("a.test", "b.test", "c.test"));

  // Trigger BTM deletion.
  base::test::TestFuture<const std::vector<std::string>&> deleted_sites;
  BtmService::Get(web_contents->GetBrowserContext())
      ->DeleteEligibleSitesImmediately(deleted_sites.GetCallback());
  ASSERT_THAT(deleted_sites.Get(), ElementsAre("b.test"));

  // Confirm sub.b.test storage was deleted.
  EXPECT_THAT(ReadFromStorage("sub.b.test"), base::test::ValueIs(""));
}

IN_PROC_BROWSER_TEST_P(BtmDataDeletionBrowserTest, DeleteEmbedded3Ps) {
  WebContents* web_contents = GetActiveWebContents();

  // Set storage on a.test embedded in b.test.
  ASSERT_TRUE(WriteToPartitionedStorage("b.test", "a.test", "foo=bar"));
  // Confirm written.
  EXPECT_THAT(ReadFromPartitionedStorage("b.test", "a.test"),
              base::test::ValueIs("foo=bar"));

  // Perform a stateful bounce on b.test to make it eligible for deletion.
  ASSERT_TRUE(DoStatefulBounce("a.test", "b.test", "c.test"));

  // Trigger BTM deletion.
  base::test::TestFuture<const std::vector<std::string>&> deleted_sites;
  BtmService::Get(web_contents->GetBrowserContext())
      ->DeleteEligibleSitesImmediately(deleted_sites.GetCallback());
  ASSERT_THAT(deleted_sites.Get(), ElementsAre("b.test"));

  // Confirm partitioned a.test storage was deleted.
  EXPECT_THAT(ReadFromPartitionedStorage("b.test", "a.test"),
              base::test::ValueIs(""));
}

IN_PROC_BROWSER_TEST_P(BtmDataDeletionBrowserTest,
                       DeleteEmbedded3Ps_Subdomain) {
  WebContents* web_contents = GetActiveWebContents();

  // Set storage on a.test embedded in sub.b.test.
  ASSERT_TRUE(WriteToPartitionedStorage("sub.b.test", "a.test", "foo=bar"));
  // Confirm written.
  EXPECT_THAT(ReadFromPartitionedStorage("sub.b.test", "a.test"),
              base::test::ValueIs("foo=bar"));

  // Perform a stateful bounce on b.test to make it eligible for deletion.
  ASSERT_TRUE(DoStatefulBounce("a.test", "b.test", "c.test"));

  // Trigger BTM deletion.
  base::test::TestFuture<const std::vector<std::string>&> deleted_sites;
  BtmService::Get(web_contents->GetBrowserContext())
      ->DeleteEligibleSitesImmediately(deleted_sites.GetCallback());
  ASSERT_THAT(deleted_sites.Get(), ElementsAre("b.test"));

  // Confirm partitioned a.test storage was deleted.
  EXPECT_THAT(ReadFromPartitionedStorage("sub.b.test", "a.test"),
              base::test::ValueIs(""));
}

IN_PROC_BROWSER_TEST_P(BtmDataDeletionBrowserTest, DeleteEmbedded1Ps) {
  content::WebContents* web_contents = GetActiveWebContents();

  // Set storage on a.test embedded in another a.test.
  ASSERT_TRUE(WriteToPartitionedStorage("a.test", "a.test", "foo=bar"));
  // Confirm written.
  EXPECT_THAT(ReadFromPartitionedStorage("a.test", "a.test"),
              base::test::ValueIs("foo=bar"));

  // Perform a stateful bounce on a.test to make it eligible for deletion.
  ASSERT_TRUE(DoStatefulBounce("b.test", "a.test", "c.test"));

  // Trigger BTM deletion.
  base::test::TestFuture<const std::vector<std::string>&> deleted_sites;
  BtmService::Get(web_contents->GetBrowserContext())
      ->DeleteEligibleSitesImmediately(deleted_sites.GetCallback());
  ASSERT_THAT(deleted_sites.Get(), ElementsAre("a.test"));

  // Confirm partitioned a.test storage was deleted.
  EXPECT_THAT(ReadFromPartitionedStorage("a.test", "a.test"),
              base::test::ValueIs(""));
}

INSTANTIATE_TEST_SUITE_P(All,
                         BtmDataDeletionBrowserTest,
                         ::testing::Values(&kCookieStorage, &kLocalStorage));

class BtmBounceDetectorBFCacheTest : public BtmBounceDetectorBrowserTest,
                                     public testing::WithParamInterface<bool> {
 public:
  void SetUp() override {
    if (IsBFCacheEnabled() &&
        !base::FeatureList::IsEnabled(features::kBackForwardCache)) {
      GTEST_SKIP() << "BFCache disabled";
    }
    BtmBounceDetectorBrowserTest::SetUp();
  }
  bool IsBFCacheEnabled() const { return GetParam(); }
  void SetUpOnMainThread() override {
    if (!IsBFCacheEnabled()) {
      DisableBackForwardCacheForTesting(
          GetActiveWebContents(),
          BackForwardCache::DisableForTestingReason::TEST_REQUIRES_NO_CACHING);
    }

    BtmBounceDetectorBrowserTest::SetUpOnMainThread();
  }
};

// Confirm that BTM records a bounce, even if the user immediately navigates
// away.
// TODO(https://crbug.com/425717555): Very flaky if BF Cache is disabled.
#if BUILDFLAG(IS_ANDROID)
#define MAYBE_LateCookieAccessTest DISABLED_LateCookieAccessTest
#else
#define MAYBE_LateCookieAccessTest LateCookieAccessTest
#endif
IN_PROC_BROWSER_TEST_P(BtmBounceDetectorBFCacheTest,
                       MAYBE_LateCookieAccessTest) {
  const GURL bounce_url =
      embedded_test_server()->GetURL("b.test", "/empty.html");
  const GURL final_url =
      embedded_test_server()->GetURL("c.test", "/empty.html");

  WebContents* const web_contents = GetActiveWebContents();
  RedirectChainDetector* wco =
      RedirectChainDetector::FromWebContents(web_contents);

  ASSERT_TRUE(NavigateToURL(
      web_contents, embedded_test_server()->GetURL("a.test", "/empty.html")));

  ASSERT_TRUE(NavigateToURLFromRenderer(web_contents, bounce_url));
  ASSERT_TRUE(ExecJs(web_contents, "document.cookie = 'bounce=true';",
                     EXECUTE_SCRIPT_NO_USER_GESTURE));

  ASSERT_TRUE(
      NavigateToURLFromRendererWithoutUserGesture(web_contents, final_url));
  URLCookieAccessObserver cookie_observer(web_contents, final_url,
                                          CookieOperation::kChange);

  ASSERT_TRUE(ExecJs(web_contents, "document.cookie = 'final=yes';",
                     EXECUTE_SCRIPT_NO_USER_GESTURE));
  cookie_observer.Wait();
  // Since cookies are reported serially, both cookie writes should have been
  // reported by now.

  const BtmRedirectContext& context = wco->CommittedRedirectContext();
  ASSERT_EQ(context.size(), 1u);
  const BtmRedirect& redirect = context[0];
  EXPECT_EQ(redirect.redirector_url, bounce_url);
  // A request to /favicon.ico may cause a cookie read in addition to the write
  // we explicitly performed.
  EXPECT_THAT(
      redirect.access_type,
      testing::AnyOf(BtmDataAccessType::kWrite, BtmDataAccessType::kReadWrite));
}

// Confirm that WCO::OnCookiesAccessed() is always called even if the user
// immediately navigates away.
IN_PROC_BROWSER_TEST_P(BtmBounceDetectorBFCacheTest, CookieAccessReported) {
  const GURL url1 = embedded_test_server()->GetURL("a.test", "/empty.html");
  const GURL url2 = embedded_test_server()->GetURL("b.test", "/empty.html");
  const GURL url3 = embedded_test_server()->GetURL("c.test", "/empty.html");

  WebContents* const web_contents = GetActiveWebContents();
  WCOCallbackLogger::CreateForWebContents(web_contents);
  auto* logger = WCOCallbackLogger::FromWebContents(web_contents);

  ASSERT_TRUE(NavigateToURL(web_contents, url1));
  ASSERT_TRUE(ExecJs(web_contents, "document.cookie = 'initial=true';",
                     EXECUTE_SCRIPT_NO_USER_GESTURE));
  ASSERT_TRUE(NavigateToURL(web_contents, url2));
  ASSERT_TRUE(NavigateToURL(web_contents, url3));
  URLCookieAccessObserver cookie_observer(web_contents, url3,
                                          CookieOperation::kChange);
  ASSERT_TRUE(ExecJs(web_contents, "document.cookie = 'final=yes';",
                     EXECUTE_SCRIPT_NO_USER_GESTURE));
  cookie_observer.Wait();

  EXPECT_THAT(
      logger->log(),
      testing::Contains(
          "OnCookiesAccessed(RenderFrameHost, Change: a.test/empty.html)"));
}

// Confirm that BTM records an interaction, even if the user immediately
// navigates away.
//
// TODO: crbug.com/376625002 - After moving to //content, this test was flaky
// because the navigation to final_url unexpectedly sometimes has a user
// gesture. Because there's no indication of a fault in BTM, we disabled this
// test to get the move done, but we should try to fix and re-enable it.
IN_PROC_BROWSER_TEST_P(BtmBounceDetectorBFCacheTest,
                       DISABLED_LateInteractionTest) {
  const GURL bounce_url =
      embedded_test_server()->GetURL("b.test", "/empty.html");
  const GURL final_url =
      embedded_test_server()->GetURL("c.test", "/empty.html");
  WebContents* const web_contents = GetActiveWebContents();
  RedirectChainDetector* wco =
      RedirectChainDetector::FromWebContents(web_contents);

  ASSERT_TRUE(NavigateToURL(
      web_contents, embedded_test_server()->GetURL("a.test", "/empty.html")));

  ASSERT_TRUE(NavigateToURLFromRenderer(web_contents, bounce_url));
  ::content::SimulateMouseClick(web_contents, 0,
                                blink::WebMouseEvent::Button::kLeft);
  // Consume the transient user activation so the next navigation is not
  // considered to be user-initiated and will be judged a bounce.
  if (EvalJs(web_contents, "!open('about:blank')",
             EXECUTE_SCRIPT_NO_USER_GESTURE)
          .ExtractBool()) {
    // Due to a race condition, the open() call might be executed before the
    // click is processed, causing open() to fail and leaving the window with
    // transient user activation. In such a case, just skip the test. (If we
    // used UserActivationObserver::Wait() here, it would defeat the purpose of
    // this test, which is to verify that BTM sees the interaction even if the
    // test doesn't wait for it.)
    GTEST_SKIP();
  }
  ASSERT_FALSE(
      web_contents->GetPrimaryMainFrame()->HasTransientUserActivation());

  ASSERT_TRUE(
      NavigateToURLFromRendererWithoutUserGesture(web_contents, final_url));
  UserActivationObserver interaction_observer(
      web_contents, web_contents->GetPrimaryMainFrame());
  ::content::SimulateMouseClick(web_contents, 0,
                                blink::WebMouseEvent::Button::kLeft);
  interaction_observer.Wait();

  const BtmRedirectContext& context = wco->CommittedRedirectContext();
  ASSERT_EQ(context.size(), 1u);
  const BtmRedirect& redirect = context[0];
  EXPECT_EQ(redirect.redirector_url, bounce_url);
  EXPECT_THAT(redirect.has_sticky_activation, true);
}

IN_PROC_BROWSER_TEST_P(BtmBounceDetectorBFCacheTest, IsOrWasInPrimaryPage) {
  WebContents* const web_contents = GetActiveWebContents();

  ASSERT_TRUE(NavigateToURL(
      web_contents, embedded_test_server()->GetURL("a.test", "/empty.html")));
  RenderFrameHost* rfh = web_contents->GetPrimaryMainFrame();
  EXPECT_TRUE(IsInPrimaryPage(*rfh));
  EXPECT_TRUE(btm::IsOrWasInPrimaryPage(*rfh));
  const GlobalRenderFrameHostId rfh_id = rfh->GetGlobalId();

  ASSERT_TRUE(NavigateToURL(
      web_contents, embedded_test_server()->GetURL("b.test", "/empty.html")));
  // Attempt to get a pointer to the RFH of the a.test page, although
  rfh = RenderFrameHost::FromID(rfh_id);
  if (IsBFCacheEnabled()) {
    // If the bfcache is enabled, the RFH should be in the cache.
    ASSERT_TRUE(rfh);
    EXPECT_TRUE(rfh->IsInLifecycleState(
        RenderFrameHost::LifecycleState::kInBackForwardCache));
    // The page is no longer primary, but it used to be:
    EXPECT_FALSE(IsInPrimaryPage(*rfh));
    EXPECT_TRUE(btm::IsOrWasInPrimaryPage(*rfh));
  } else {
    // If the bfcache is disabled, the RFH may or may not be in memory. If it
    // still is, it's only because it's pending deletion.
    if (rfh) {
      EXPECT_TRUE(rfh->IsInLifecycleState(
          RenderFrameHost::LifecycleState::kPendingDeletion));
      // The page is no longer primary, but it used to be:
      EXPECT_FALSE(IsInPrimaryPage(*rfh));
      EXPECT_TRUE(btm::IsOrWasInPrimaryPage(*rfh));
    }
  }
}

// For waiting until prerendering starts.
class PrerenderingObserver : public WebContentsObserver {
 public:
  explicit PrerenderingObserver(WebContents* web_contents)
      : WebContentsObserver(web_contents) {}

  void Wait() { run_loop_.Run(); }

  GlobalRenderFrameHostId rfh_id() const {
    CHECK(rfh_id_.has_value());
    return rfh_id_.value();
  }

 private:
  base::RunLoop run_loop_;
  std::optional<GlobalRenderFrameHostId> rfh_id_;

  void RenderFrameCreated(RenderFrameHost* render_frame_host) override;
};

void PrerenderingObserver::RenderFrameCreated(
    RenderFrameHost* render_frame_host) {
  if (render_frame_host->IsInLifecycleState(
          RenderFrameHost::LifecycleState::kPrerendering)) {
    rfh_id_ = render_frame_host->GetGlobalId();
    run_loop_.Quit();
  }
}

// Confirm that IsOrWasInPrimaryPage() returns false for prerendered pages that
// are never activated.
IN_PROC_BROWSER_TEST_P(BtmBounceDetectorBFCacheTest,
                       PrerenderedPagesAreNotPrimary) {
  WebContents* const web_contents = GetActiveWebContents();

  ASSERT_TRUE(NavigateToURL(
      web_contents,
      embedded_test_server()->GetURL("a.test", "/empty.html?primary")));

  PrerenderingObserver observer(web_contents);
  ASSERT_TRUE(ExecJs(web_contents, R"(
    const elt = document.createElement('script');
    elt.setAttribute('type', 'speculationrules');
    elt.textContent = JSON.stringify({
      prerender: [{'urls': ['empty.html?prerendered']}]
    });    document.body.appendChild(elt);
  )"));
  observer.Wait();
  ASSERT_FALSE(testing::Test::HasFailure())
      << "Failed waiting for prerendering";

  RenderFrameHost* rfh = RenderFrameHost::FromID(observer.rfh_id());
  ASSERT_TRUE(rfh);
  EXPECT_FALSE(btm::IsOrWasInPrimaryPage(*rfh));

  // Navigating to another site may trigger destruction of the frame.
  ASSERT_TRUE(NavigateToURL(
      web_contents, embedded_test_server()->GetURL("b.test", "/empty.html")));

  rfh = RenderFrameHost::FromID(observer.rfh_id());
  if (rfh) {
    // Even if it's still in memory, it was never primary.
    EXPECT_FALSE(btm::IsOrWasInPrimaryPage(*rfh));
  }
}

// Confirm that IsOrWasInPrimaryPage() returns true for prerendered pages that
// get activated.
IN_PROC_BROWSER_TEST_P(BtmBounceDetectorBFCacheTest,
                       PrerenderedPagesCanBecomePrimary) {
  WebContents* const web_contents = GetActiveWebContents();

  ASSERT_TRUE(NavigateToURL(
      web_contents,
      embedded_test_server()->GetURL("a.test", "/empty.html?primary")));

  PrerenderingObserver observer(web_contents);
  ASSERT_TRUE(ExecJs(web_contents, R"(
    const elt = document.createElement('script');
    elt.setAttribute('type', 'speculationrules');
    elt.textContent = JSON.stringify({
      prerender: [{'urls': ['empty.html?prerendered']}]
    });
    document.body.appendChild(elt);
  )"));
  observer.Wait();
  ASSERT_FALSE(testing::Test::HasFailure())
      << "Failed waiting for prerendering";

  RenderFrameHost* rfh = RenderFrameHost::FromID(observer.rfh_id());
  ASSERT_TRUE(rfh);
  EXPECT_FALSE(btm::IsOrWasInPrimaryPage(*rfh));

  // Navigate to the prerendered page.
  ASSERT_TRUE(NavigateToURLFromRenderer(
      web_contents,
      embedded_test_server()->GetURL("a.test", "/empty.html?prerendered")));
  // Navigate to another page, so the prerendered page is no longer active.
  ASSERT_TRUE(NavigateToURL(
      web_contents, embedded_test_server()->GetURL("b.test", "/empty.html")));

  rfh = RenderFrameHost::FromID(observer.rfh_id());
  if (rfh) {
    EXPECT_FALSE(IsInPrimaryPage(*rfh));
    EXPECT_TRUE(btm::IsOrWasInPrimaryPage(*rfh));
  }
}

INSTANTIATE_TEST_SUITE_P(All, BtmBounceDetectorBFCacheTest, ::testing::Bool());

}  // namespace content
