// 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/public/browser/btm_service.h"

#include <optional>
#include <string_view>

#include "base/containers/span.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/simple_test_clock.h"
#include "base/test/test_file_util.h"
#include "base/time/default_clock.h"
#include "base/time/time.h"
#include "base/types/pass_key.h"
#include "components/ukm/test_ukm_recorder.h"
#include "content/browser/browser_context_impl.h"
#include "content/browser/btm/btm_bounce_detector.h"
#include "content/browser/btm/btm_service_impl.h"
#include "content/browser/btm/btm_state.h"
#include "content/browser/btm/btm_test_utils.h"
#include "content/browser/btm/btm_utils.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/btm_redirect.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_features.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/btm_service_test_utils.h"
#include "content/public/test/mock_browsing_data_remover_delegate.h"
#include "content/public/test/test_browser_context.h"
#include "net/base/schemeful_site.h"
#include "net/cookies/cookie_partition_key.h"
#include "net/http/http_status_code.h"
#include "services/metrics/public/cpp/ukm_source_id.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/features_generated.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
#include "url/gurl.h"

using testing::AllOf;
using testing::ElementsAre;
using testing::IsEmpty;
using testing::Pair;

namespace content {

class BtmServiceTest : public testing::Test {
 protected:
  base::PassKey<BtmServiceTest> PassKey() { return {}; }

  void RecordBounce(
      BrowserContext* browser_context,
      std::string_view url,
      std::string_view initial_url,
      std::string_view final_url,
      base::Time time,
      bool stateful,
      BtmServiceImpl::StatefulBounceCallback stateful_bounce_callback) {
    BtmRedirectChain chain(
        GURL(initial_url), ukm::AssignNewSourceId(), GURL(final_url),
        ukm::AssignNewSourceId(),
        /*length=*/3,
        /*is_partial_chain=*/false,
        btm::Are3PcsGenerallyEnabled(browser_context, nullptr));

    BtmRedirectPtr redirect = BtmRedirect::CreateForServer(
        GURL(url), ukm::AssignNewSourceId(),
        stateful ? BtmDataAccessType::kWrite : BtmDataAccessType::kRead, time,
        /*was_response_cached=*/false,
        /*response_code=*/net::HTTP_FOUND,
        /*server_bounce_delay=*/base::TimeDelta());

    btm::Populate3PcExceptions(browser_context,
                               /*web_contents=*/nullptr, GURL(initial_url),
                               GURL(final_url), base::span_from_ref(redirect));
    redirect->chain_index = 1;
    redirect->chain_id = chain.chain_id;

    BtmServiceImpl::Get(browser_context)
        ->RecordBounceForTesting(*redirect, chain, stateful_bounce_callback);
  }

 private:
  BrowserTaskEnvironment task_environment_;
};

TEST_F(BtmServiceTest, CreateServiceIfFeatureEnabled) {
  ScopedInitBtmFeature init_btm(true);

  TestBrowserContext profile;
  EXPECT_NE(BtmServiceImpl::Get(&profile), nullptr);
}

TEST_F(BtmServiceTest, DontCreateServiceIfFeatureDisabled) {
  ScopedInitBtmFeature init_btm(false);

  TestBrowserContext profile;
  EXPECT_EQ(BtmServiceImpl::Get(&profile), nullptr);
}

// Verifies that if the BTM feature is enabled, BTM database is created when a
// (non-OTR) profile is created.
TEST_F(BtmServiceTest, CreateBTMDatabaseIfBtmEnabled) {
  base::FilePath data_path = base::CreateUniqueTempDirectoryScopedToTest();
  BtmServiceImpl* service;
  std::unique_ptr<TestBrowserContext> profile;

  // Ensure the BTM feature is enabled.
  base::test::ScopedFeatureList feature_list(features::kBtm);

  profile = std::make_unique<TestBrowserContext>(data_path);
  service = BtmServiceImpl::Get(profile.get());
  ASSERT_NE(service, nullptr);

  // Ensure the database files have been created since the BTM feature is
  // enabled.
  WaitOnStorage(service);
  BrowserContextImpl::From(profile.get())->WaitForBtmCleanupForTesting();
#if BUILDFLAG(IS_FUCHSIA) && defined(IS_WEB_ENGINE)
  // See crbug.com/434764000, file based BTM is disabled on web engine on
  // fuchsia due to the storage constraint.
  EXPECT_FALSE(base::PathExists(GetBtmFilePath(profile.get())));
#else
  EXPECT_TRUE(base::PathExists(GetBtmFilePath(profile.get())));
#endif
}

#if BUILDFLAG(IS_FUCHSIA) && defined(IS_WEB_ENGINE)
// See crbug.com/434764000, file based BTM is disabled on web engine on fuchsia
// due to the storage constraint.
#define MAYBE_PreserveRegularProfileDbFiles \
  DISABLED_PreserveRegularProfileDbFiles
#else
#define MAYBE_PreserveRegularProfileDbFiles PreserveRegularProfileDbFiles
#endif
// Verifies that when an OTR profile is opened, the BTM database file for
// the underlying regular profile is NOT deleted.
TEST_F(BtmServiceTest, MAYBE_PreserveRegularProfileDbFiles) {
  base::FilePath data_path = base::CreateUniqueTempDirectoryScopedToTest();

  // Ensure the BTM feature is enabled.
  base::test::ScopedFeatureList feature_list(features::kBtm);

  // Build a regular profile.
  std::unique_ptr<TestBrowserContext> profile =
      std::make_unique<TestBrowserContext>(data_path);
  BtmServiceImpl* service = BtmServiceImpl::Get(profile.get());
  ASSERT_NE(service, nullptr);

  // Ensure the regular profile's database files have been created since the
  // BTM feature is enabled.
  WaitOnStorage(service);
  BrowserContextImpl::From(profile.get())->WaitForBtmCleanupForTesting();
  ASSERT_TRUE(base::PathExists(GetBtmFilePath(profile.get())));

  // Build an off-the-record profile based on `profile`.
  std::unique_ptr<TestBrowserContext> otr_profile =
      std::make_unique<TestBrowserContext>(profile->GetPath());
  otr_profile->set_is_off_the_record(true);
  BtmServiceImpl* otr_service = BtmServiceImpl::Get(otr_profile.get());
  ASSERT_NE(otr_service, nullptr);

  // Ensure the OTR profile's database has been initialized and any file
  // deletion tasks have finished (although there shouldn't be any).
  WaitOnStorage(otr_service);
  BrowserContextImpl::From(otr_profile.get())->WaitForBtmCleanupForTesting();

  // Ensure the regular profile's database files were NOT deleted.
  EXPECT_TRUE(base::PathExists(GetBtmFilePath(profile.get())));

  // Every TestBrowserContext normally deletes its folder when it's destroyed.
  // But since `otr_profile` is sharing `profile`'s directory, we don't want it
  // to delete that folder (`profile` will).
  otr_profile->TakePath();
}
#if BUILDFLAG(IS_FUCHSIA) && defined(IS_WEB_ENGINE)
// See crbug.com/434764000, file based BTM is disabled on web engine on
// fuchsia due to the storage constraint. But the leftover file previously
// created should be deleted.
TEST_F(BtmServiceTest, DeleteLeftoverDatabaseFileOnWebEngineOnFuchsia) {
  base::FilePath user_data_dir;
  base::FilePath db_path;

  // First, create a browser context and create a mock database file at the
  // correct path.
  {
    TestBrowserContext browser_context;
    db_path = GetBtmFilePath(&browser_context);
    // Ensure the BtmService (and its database) are initialized.
    BrowserContextImpl::From(&browser_context)
        ->GetBtmService()
        ->WaitForFuchsiaCleanupForTesting();

    // Create a mock database file where one would be if the platform wasn't
    // WebEngine on Fuchsia.
    ASSERT_TRUE(base::WriteFile(db_path, "test"));
    ASSERT_TRUE(base::PathExists(db_path));

    // Take ownership of the browser context's directory so we can reuse it.
    user_data_dir = browser_context.TakePath();

    // Confirm that WaitForBtmCleanupForTesting() returns and the file still
    // exists.
    BrowserContextImpl::From(&browser_context)->WaitForBtmCleanupForTesting();
    ASSERT_TRUE(base::PathExists(db_path));
  }

  // Confirm the file still exists after the browser context is destroyed.
  ASSERT_TRUE(base::PathExists(db_path));

  // Create another browser context for the same directory and confirm the
  // database file is deleted.
  {
    TestBrowserContext browser_context(user_data_dir);
    BrowserContextImpl::From(&browser_context)
        ->GetBtmService()
        ->WaitForFuchsiaCleanupForTesting();
    ASSERT_FALSE(base::PathExists(db_path));
  }
}

TEST_F(BtmServiceTest, BtmServiceCanStartWithoutDatabaseFile) {
  TestBrowserContext browser_context;
  base::FilePath db_path = GetBtmFilePath(&browser_context);
  ASSERT_FALSE(base::PathExists(db_path));
  // Wait for the database to be created.
  BrowserContextImpl::From(&browser_context)
      ->GetBtmService()
      ->storage()
      ->FlushPostedTasksForTesting();
  ASSERT_FALSE(base::PathExists(db_path));
}
#endif

#if BUILDFLAG(IS_FUCHSIA) && defined(IS_WEB_ENGINE)
// See crbug.com/434764000, file based BTM is disabled on web engine on
// fuchsia due to the storage constraint.
#define MAYBE_DatabaseFileIsDeletedIfFeatureIsDisabled \
  DISABLED_DatabaseFileIsDeletedIfFeatureIsDisabled
#else
#define MAYBE_DatabaseFileIsDeletedIfFeatureIsDisabled \
  DatabaseFileIsDeletedIfFeatureIsDisabled
#endif
TEST_F(BtmServiceTest, MAYBE_DatabaseFileIsDeletedIfFeatureIsDisabled) {
  base::FilePath user_data_dir;
  base::FilePath db_path;

  // First, create a browser context while BTM is enabled, and confirm a
  // database file is created.
  {
    TestBrowserContext browser_context;
    db_path = GetBtmFilePath(&browser_context);
    // Wait for the database to be created.
    BrowserContextImpl::From(&browser_context)
        ->GetBtmService()
        ->storage()
        ->FlushPostedTasksForTesting();
    ASSERT_TRUE(base::PathExists(db_path));

    // Take ownership of the browser context's directory so we can reuse it.
    user_data_dir = browser_context.TakePath();

    // Confirm that WaitForBtmCleanupForTesting() returns even if the file is
    // not deleted.
    BrowserContextImpl::From(&browser_context)->WaitForBtmCleanupForTesting();
    ASSERT_TRUE(base::PathExists(db_path));
  }

  // Confirm the file still exists after the browser context is destroyed.
  ASSERT_TRUE(base::PathExists(db_path));

  // Create another browser context for the same directory, while BTM is
  // disabled. Confirm the database file is deleted.
  {
    ScopedInitBtmFeature disable_btm(false);
    TestBrowserContext browser_context(user_data_dir);
    ASSERT_FALSE(BrowserContextImpl::From(&browser_context)->GetBtmService());
    BrowserContextImpl::From(&browser_context)->WaitForBtmCleanupForTesting();
    ASSERT_FALSE(base::PathExists(db_path));
  }
}

TEST_F(BtmServiceTest, EmptySiteEventsIgnored) {
  base::test::ScopedFeatureList feature_list;
  feature_list.InitAndEnableFeature(features::kBtm);
  std::unique_ptr<TestBrowserContext> profile =
      std::make_unique<TestBrowserContext>();
  BtmServiceImpl* service = BtmServiceImpl::Get(profile.get());

  // Record a bounce for an empty URL.
  GURL url;
  base::Time bounce = base::Time::FromSecondsSinceUnixEpoch(2);
  RecordBounce(profile.get(), url.spec(), "https://initial.com",
               "https://final.com", bounce, false, base::DoNothing());
  WaitOnStorage(service);

  // Verify that an entry is not returned when querying for an empty URL,
  StateForURLCallback callback = base::BindLambdaForTesting(
      [&](BtmState state) { EXPECT_FALSE(state.was_loaded()); });
  service->storage()
      ->AsyncCall(&BtmStorage::Read)
      .WithArgs(url)
      .Then(std::move(callback));
  WaitOnStorage(service);
}

class BtmServiceStateRemovalTest : public testing::Test {
 public:
  BtmServiceStateRemovalTest()
      : profile_(std::make_unique<TestBrowserContext>()),
        service_(BtmServiceImpl::Get(GetProfile())) {
    SetBrowserClientForTesting(&browser_client_);
  }

  base::TimeDelta grace_period;
  base::TimeDelta interaction_ttl;
  base::TimeDelta tiny_delta = base::Milliseconds(1);

  BrowserContext* GetProfile() { return profile_.get(); }
  BtmServiceImpl* GetService() { return service_; }

 protected:
  TpcBlockingBrowserClient browser_client_;
  BrowserTaskEnvironment task_environment_;
  MockBrowsingDataRemoverDelegate delegate_;

  // Test setup.
  void SetUp() override {
    grace_period = features::kBtmGracePeriod.Get();
    interaction_ttl = features::kBtmInteractionTtl.Get();
    ASSERT_LT(tiny_delta, grace_period);

    GetProfile()->GetBrowsingDataRemover()->SetEmbedderDelegate(&delegate_);
    browser_client_.SetBlockThirdPartyCookiesByDefault(true);
    ASSERT_FALSE(Are3PcsGenerallyEnabled());

    DCHECK(service_);
    service_->SetStorageClockForTesting(&clock_);
    WaitOnStorage(GetService());
  }

  void TearDown() override {
    profile_.reset();
    base::RunLoop().RunUntilIdle();
  }

  void AdvanceTimeTo(base::Time now) {
    ASSERT_GE(now, clock_.Now());
    clock_.SetNow(now);
  }

  base::Time Now() { return clock_.Now(); }
  void SetNow(base::Time now) { clock_.SetNow(now); }

  void AdvanceTimeBy(base::TimeDelta delta) { clock_.Advance(delta); }

  void FireBtmTimer() {
    service_->OnTimerFiredForTesting();
    WaitOnStorage(GetService());
  }

  // Add an exception to the third-party cookie blocking rule for
  // |third_party_url| embedded by |first_party_url|.
  void Add3PCException(const GURL& first_party_url,
                       const GURL& third_party_url) {
    browser_client_.SetThirdPartyCookieAccess(third_party_url, first_party_url,
                                              CONTENT_SETTING_ALLOW);

    auto* client = GetContentClientForTesting()->browser();
    EXPECT_TRUE(client->IsFullCookieAccessAllowed(
        profile_.get(), nullptr, third_party_url,
        blink::StorageKey::CreateFirstParty(
            url::Origin::Create(first_party_url)),
        /*overrides=*/{}));
    EXPECT_FALSE(client->IsFullCookieAccessAllowed(
        profile_.get(), nullptr, first_party_url,
        blink::StorageKey::CreateFirstParty(
            url::Origin::Create(third_party_url)),
        /*overrides=*/{}));
  }

  void RecordBounce(
      std::string_view url,
      std::string_view initial_url,
      std::string_view final_url,
      base::Time time,
      bool stateful,
      BtmServiceImpl::StatefulBounceCallback stateful_bounce_callback) {
    BtmRedirectChain chain(GURL(initial_url), ukm::AssignNewSourceId(),
                           GURL(final_url), ukm::AssignNewSourceId(),
                           /*length=*/3,
                           /*is_partial_chain=*/false,
                           Are3PcsGenerallyEnabled());

    BtmRedirectPtr redirect = BtmRedirect::CreateForServer(
        GURL(url), ukm::AssignNewSourceId(),
        stateful ? BtmDataAccessType::kWrite : BtmDataAccessType::kRead, time,
        /*was_response_cached=*/false,
        /*response_code=*/net::HTTP_FOUND,
        /*server_bounce_delay=*/base::TimeDelta());

    btm::Populate3PcExceptions(GetProfile(),
                               /*web_contents=*/nullptr, GURL(initial_url),
                               GURL(final_url), base::span_from_ref(redirect));
    redirect->chain_index = 1;
    redirect->chain_id = chain.chain_id;

    GetService()->RecordBounceForTesting(*redirect, chain,
                                         stateful_bounce_callback);
  }

  bool Are3PcsGenerallyEnabled() {
    return btm::Are3PcsGenerallyEnabled(profile_.get(), nullptr);
  }

 private:
  base::SimpleTestClock clock_;

  std::unique_ptr<TestBrowserContext> profile_;
  raw_ptr<BtmServiceImpl, DanglingUntriaged> service_ = nullptr;
};

namespace {
class RedirectChainCounter : public BtmService::Observer {
 public:
  explicit RedirectChainCounter(BtmService* service) { obs_.Observe(service); }

  size_t count() const { return count_; }

 private:
  void OnChainHandled(const std::vector<BtmRedirectPtr>& redirects,
                      const BtmRedirectChainPtr& chain) override {
    count_++;
  }

  size_t count_ = 0;
  base::ScopedObservation<BtmService, Observer> obs_{this};
};
}  // namespace

TEST_F(BtmServiceStateRemovalTest,
       CompleteChain_NotifiesBtmRedirectChainObservers) {
  GetService()->SetStorageClockForTesting(base::DefaultClock::GetInstance());
  RedirectChainCounter chain_counter(GetService());

  std::vector<BtmRedirectPtr> complete_redirects;
  complete_redirects.push_back(BtmRedirect::CreateForServer(
      /*redirector_url=*/GURL("http://b.test/"),
      /*redirector_source_id=*/ukm::AssignNewSourceId(),
      /*access_type=*/BtmDataAccessType::kNone,
      /*time=*/Now(),
      /*was_response_cached=*/false,
      /*response_code=*/net::HTTP_FOUND,
      /*server_bounce_delay=*/base::TimeDelta()));
  auto complete_chain = std::make_unique<BtmRedirectChain>(
      /*initial_url=*/GURL("http://a.test/"),
      /*initial_source_id=*/ukm::AssignNewSourceId(),
      /*final_url=*/GURL("http://c.test/"),
      /*final_source_id*/ ukm::AssignNewSourceId(),
      /*length=*/1, /*is_partial_chain=*/false, Are3PcsGenerallyEnabled());

  btm::Populate3PcExceptions(GetProfile(), /*web_contents=*/nullptr,
                             complete_chain->initial_url,
                             complete_chain->final_url, complete_redirects);
  GetService()->HandleRedirectChain(std::move(complete_redirects),
                                    std::move(complete_chain),
                                    base::DoNothing());
  WaitOnStorage(GetService());
  // Expect one call to Observer.OnChainHandled when handling a complete chain.
  EXPECT_EQ(chain_counter.count(), 1u);
}

TEST_F(BtmServiceStateRemovalTest,
       PartialChain_DoesNotNotifyBtmRedirectChainObservers) {
  GetService()->SetStorageClockForTesting(base::DefaultClock::GetInstance());
  RedirectChainCounter chain_counter(GetService());

  std::vector<BtmRedirectPtr> partial_redirects;
  partial_redirects.push_back(BtmRedirect::CreateForServer(
      /*redirector_url=*/GURL("http://b.test/"),
      /*redirector_source_id=*/ukm::AssignNewSourceId(),
      /*access_type=*/BtmDataAccessType::kNone,
      /*time=*/Now(),
      /*was_response_cached=*/false,
      /*response_code=*/net::HTTP_FOUND,
      /*server_bounce_delay=*/base::TimeDelta()));
  auto partial_chain = std::make_unique<BtmRedirectChain>(
      /*initial_url=*/GURL("http://a.test/"),
      /*initial_source_id=*/ukm::AssignNewSourceId(),
      /*final_url=*/GURL("http://c.test/"),
      /*final_source_id=*/ukm::AssignNewSourceId(),
      /*length=*/1, /*is_partial_chain=*/true, Are3PcsGenerallyEnabled());

  btm::Populate3PcExceptions(GetProfile(), /*web_contents=*/nullptr,
                             partial_chain->initial_url,
                             partial_chain->final_url, partial_redirects);
  GetService()->HandleRedirectChain(std::move(partial_redirects),
                                    std::move(partial_chain),
                                    base::DoNothing());
  WaitOnStorage(GetService());
  // Expect no calls to Observer.OnChainHandled when handling a partial chain.
  EXPECT_EQ(chain_counter.count(), 0u);
}

// NOTE: The use of a MockBrowsingDataRemoverDelegate in this test fixture
// means that when BTM deletion is enabled, the row for 'url' is not actually
// removed from the BTM db since 'delegate_' doesn't actually carryout the
// removal task.
TEST_F(BtmServiceStateRemovalTest, DISABLED_BrowsingDataDeletion_Enabled) {
  ukm::TestAutoSetUkmRecorder ukm_recorder;
  base::test::ScopedFeatureList feature_list;
  feature_list.InitAndEnableFeatureWithParameters(
      features::kBtm, {{"triggering_action", "bounce"}});

  // Record a bounce.
  GURL url("https://example.com");
  base::Time bounce = base::Time::FromSecondsSinceUnixEpoch(2);
  RecordBounce(url.spec(), "https://initial.com", "https://final.com", bounce,
               false, base::DoNothing());
  WaitOnStorage(GetService());
  EXPECT_TRUE(GetBtmState(GetService(), url).has_value());

  // Set the current time to just after the bounce happened.
  AdvanceTimeTo(bounce + tiny_delta);
  FireBtmTimer();
  task_environment_.RunUntilIdle();

  // Verify a removal task was not posted to the BrowsingDataRemover(Delegate).
  delegate_.VerifyAndClearExpectations();

  auto filter_builder = BrowsingDataFilterBuilder::Create(
      BrowsingDataFilterBuilder::Mode::kDelete);
  filter_builder->AddRegisterableDomain(GetSiteForBtm(url));
  filter_builder->SetCookiePartitionKeyCollection(
      net::CookiePartitionKeyCollection());
  delegate_.ExpectCall(
      base::Time::Min(), base::Time::Max(),
      (ContentBrowserClient::kDefaultBtmRemoveMask &
       ~BrowsingDataRemover::DATA_TYPE_PRIVACY_SANDBOX) |
          BrowsingDataRemover::DATA_TYPE_AVOID_CLOSING_CONNECTIONS,
      BrowsingDataRemover::ORIGIN_TYPE_UNPROTECTED_WEB |
          BrowsingDataRemover::ORIGIN_TYPE_PROTECTED_WEB,
      filter_builder.get());
  // We don't test the filter builder for partitioned cookies here because it's
  // messy. The browser tests ensure that it behaves as expected.
  delegate_.ExpectCallDontCareAboutFilterBuilder(
      base::Time::Min(), base::Time::Max(),
      BrowsingDataRemover::DATA_TYPE_COOKIES,
      BrowsingDataRemover::ORIGIN_TYPE_UNPROTECTED_WEB |
          BrowsingDataRemover::ORIGIN_TYPE_PROTECTED_WEB);

  // Time-travel to after the grace period has ended for the bounce.
  AdvanceTimeTo(bounce + grace_period + tiny_delta);
  FireBtmTimer();
  task_environment_.RunUntilIdle();

  // Verify that a removal task was posted to the BrowsingDataRemover(Delegate)
  // for 'url'.
  delegate_.VerifyAndClearExpectations();
  // Because this test fixture uses a MockBrowsingDataRemoverDelegate the BTM
  // entry should not actually be removed. However, in practice it would be.
  EXPECT_TRUE(GetBtmState(GetService(), url).has_value());

  EXPECT_THAT(ukm_recorder,
              EntryUrlsAre("DIPS.Deletion", {"http://example.com/"}));
}

TEST_F(BtmServiceStateRemovalTest,
       BrowsingDataDeletion_Respects3PExceptionsFor3PC) {
  ukm::TestAutoSetUkmRecorder ukm_recorder;
  base::test::ScopedFeatureList feature_list;
  feature_list.InitAndEnableFeatureWithParameters(
      features::kBtm, {{"triggering_action", "bounce"}});

  GURL excepted_3p_url("https://excepted-as-3p.com");
  GURL non_excepted_url("https://not-excepted.com");

  browser_client_.GrantCookieAccessTo3pSite(excepted_3p_url);

  int stateful_bounce_count = 0;
  BtmServiceImpl::StatefulBounceCallback increment_bounce =
      base::BindLambdaForTesting(
          [&](const GURL& final_url) { stateful_bounce_count++; });

  // Bounce through both tracking sites.
  base::Time bounce = base::Time::FromSecondsSinceUnixEpoch(2);
  RecordBounce(excepted_3p_url.spec(), "https://initial.com",
               "https://final.com", bounce, true, increment_bounce);
  RecordBounce(non_excepted_url.spec(), "https://initial.com",
               "https://final.com", bounce, true, increment_bounce);
  WaitOnStorage(GetService());

  // Verify that the bounce was not recorded for the excepted 3P URL.
  EXPECT_FALSE(GetBtmState(GetService(), excepted_3p_url).has_value());
  EXPECT_TRUE(GetBtmState(GetService(), non_excepted_url).has_value());

  // Time-travel to after the grace period has ended for the bounce.
  AdvanceTimeTo(bounce + grace_period + tiny_delta);
  FireBtmTimer();
  task_environment_.RunUntilIdle();

  // Only the non-excepted site should be reported to UKM.
  EXPECT_THAT(ukm_recorder,
              EntryUrlsAre("DIPS.Deletion", {"http://not-excepted.com/"}));

  // Expect one recorded bounce, for the stateful redirect through the
  // non-excepted site.
  EXPECT_EQ(stateful_bounce_count, 1);
}

TEST_F(BtmServiceStateRemovalTest,
       BrowsingDataDeletion_Respects1PExceptionsFor3PC) {
  ukm::TestAutoSetUkmRecorder ukm_recorder;
  base::test::ScopedFeatureList feature_list;
  feature_list.InitAndEnableFeatureWithParameters(
      features::kBtm, {{"triggering_action", "bounce"}});

  GURL excepted_1p_url("https://excepted-as-1p.com");
  GURL scoped_excepted_1p_url("https://excepted-as-1p-with-3p.com");
  GURL non_excepted_url("https://not-excepted.com");
  GURL redirect_url_1("https://redirect-1.com");
  GURL redirect_url_2("https://redirect-2.com");
  GURL redirect_url_3("https://redirect-3.com");

  browser_client_.AllowThirdPartyCookiesOnSite(excepted_1p_url);
  Add3PCException(scoped_excepted_1p_url, redirect_url_1);

  int stateful_bounce_count = 0;
  BtmServiceImpl::StatefulBounceCallback increment_bounce =
      base::BindLambdaForTesting(
          [&](const GURL& final_url) { stateful_bounce_count++; });

  base::Time bounce = base::Time::FromSecondsSinceUnixEpoch(2);
  // Record a bounce through redirect_url_1 that starts on an excepted
  // URL.
  RecordBounce(redirect_url_1.spec(), excepted_1p_url.spec(),
               non_excepted_url.spec(), bounce, true, increment_bounce);
  // Record a bounce through redirect_url_1 that ends on an excepted
  // URL.
  RecordBounce(redirect_url_1.spec(), non_excepted_url.spec(),
               excepted_1p_url.spec(), bounce, true, increment_bounce);
  // Record a bounce through redirect_url_1 that ends on a URL with an exception
  // scoped to redirect_url_1.
  RecordBounce(redirect_url_1.spec(), non_excepted_url.spec(),
               scoped_excepted_1p_url.spec(), bounce, true, increment_bounce);
  // Record a bounce through redirect_url_2 that does not start or
  // end on an excepted URL.
  RecordBounce(redirect_url_2.spec(), non_excepted_url.spec(),
               non_excepted_url.spec(), bounce, true, increment_bounce);
  // Record a bounce through redirect_url_3 that does not start or
  // end on an excepted URL. Record an interaction on this URL as well.
  RecordBounce(redirect_url_3.spec(), non_excepted_url.spec(),
               non_excepted_url.spec(), bounce, true, increment_bounce);
  GetService()
      ->storage()
      ->AsyncCall(&BtmStorage::RecordUserActivation)
      .WithArgs(redirect_url_3, bounce);
  WaitOnStorage(GetService());

  // Expect no recorded BtmState for redirect_url_1, since every
  // recorded bounce started or ended on an excepted site.
  EXPECT_FALSE(GetBtmState(GetService(), redirect_url_1).has_value());
  EXPECT_TRUE(GetBtmState(GetService(), redirect_url_2).has_value());
  EXPECT_TRUE(GetBtmState(GetService(), redirect_url_3).has_value());

  // Record a bounce through redirect_url_2 that starts on an
  // excepted URL. This should clear the DB entry for redirect_url_2.
  RecordBounce(redirect_url_2.spec(), excepted_1p_url.spec(),
               non_excepted_url.spec(), bounce, true, increment_bounce);
  EXPECT_FALSE(GetBtmState(GetService(), redirect_url_2).has_value());

  // Record a bounce through redirect_url_3 that starts on an
  // excepted URL. This should not clear the DB entry for redirect_url_3 as it
  // has a recorded interaction.
  RecordBounce(redirect_url_3.spec(), excepted_1p_url.spec(),
               non_excepted_url.spec(), bounce, true, increment_bounce);
  EXPECT_TRUE(GetBtmState(GetService(), redirect_url_3).has_value());

  // Expect two non-excepted stateful redirects: the first bounces through
  // redirect_url_2 and redirect_url_3.
  EXPECT_EQ(stateful_bounce_count, 2);
}

// TODO: crbug.com/376625002 - temporarily disabled for the move to //content,
// where there's no HostContentSettingsMap. Find an appropriate way to implement
// this test in //content or move it back to //chrome.
TEST_F(BtmServiceStateRemovalTest,
       DISABLED_BrowsingDataDeletion_RespectsStorageAccessGrantExceptions) {
  ukm::TestAutoSetUkmRecorder ukm_recorder;
  std::vector<base::test::FeatureRefAndParams> enabled_features;
  enabled_features.push_back(
      {features::kBtm, {{"triggering_action", "bounce"}}});
  base::test::ScopedFeatureList feature_list;
  feature_list.InitWithFeaturesAndParameters(enabled_features, {});

  GURL storage_access_grant_url("https://storage-access-granted.com");
  GURL top_level_storage_access_grant_url(
      "https://top-level-storage-access-granted.com");
  GURL no_grant_url("https://no-storage-access-grant.com");
  GURL redirect_url_1("https://redirect-1.com");
  GURL redirect_url_2("https://redirect-2.com");
  GURL redirect_url_3("https://redirect-3.com");

  // Create Storage Access grants for the required sites.
  /*
  HostContentSettingsMap* map =
      HostContentSettingsMapFactory::GetForProfile(GetProfile());
  map->SetContentSettingCustomScope(
      ContentSettingsPattern::Wildcard(),
      ContentSettingsPattern::FromString("[*.]" +
                                         storage_access_grant_url.host()),
      ContentSettingsType::STORAGE_ACCESS, CONTENT_SETTING_ALLOW);
  map->SetContentSettingCustomScope(
      ContentSettingsPattern::Wildcard(),
      ContentSettingsPattern::FromString(
          "[*.]" + top_level_storage_access_grant_url.host()),
      ContentSettingsType::TOP_LEVEL_STORAGE_ACCESS, CONTENT_SETTING_ALLOW);
  */
  int stateful_bounce_count = 0;
  BtmServiceImpl::StatefulBounceCallback increment_bounce =
      base::BindLambdaForTesting(
          [&](const GURL& final_url) { stateful_bounce_count++; });

  base::Time bounce = base::Time::FromSecondsSinceUnixEpoch(2);
  // Record a bounce through redirect_url_1 that starts on a URL with an SA
  // grant.
  RecordBounce(redirect_url_1.spec(), storage_access_grant_url.spec(),
               no_grant_url.spec(), bounce, true, increment_bounce);
  // Record a bounce through redirect_url_1 that ends on a URL with a top-level
  // SA grant.
  RecordBounce(redirect_url_1.spec(), no_grant_url.spec(),
               top_level_storage_access_grant_url.spec(), bounce, true,
               increment_bounce);
  // Record a bounce through redirect_url_2 that does not start or
  // end on a URL with an SA grant.
  RecordBounce(redirect_url_2.spec(), no_grant_url.spec(), no_grant_url.spec(),
               bounce, true, increment_bounce);
  // Record a bounce through redirect_url_3 that does not start or
  // end on a URL with an SA grant. Record an interaction on this URL as well.
  RecordBounce(redirect_url_3.spec(), no_grant_url.spec(), no_grant_url.spec(),
               bounce, true, increment_bounce);
  GetService()
      ->storage()
      ->AsyncCall(&BtmStorage::RecordUserActivation)
      .WithArgs(redirect_url_3, bounce);
  WaitOnStorage(GetService());

  // Expect no recorded BtmState for redirect_url_1, since every
  // recorded bounce started or ended on a site with an SA grant.
  EXPECT_FALSE(GetBtmState(GetService(), redirect_url_1).has_value());
  EXPECT_TRUE(GetBtmState(GetService(), redirect_url_2).has_value());
  EXPECT_TRUE(GetBtmState(GetService(), redirect_url_3).has_value());

  // Record a bounce through redirect_url_2 that starts on a URL with an SA
  // grant. This should clear the DB entry for redirect_url_2.
  RecordBounce(redirect_url_2.spec(), storage_access_grant_url.spec(),
               no_grant_url.spec(), bounce, true, increment_bounce);
  EXPECT_FALSE(GetBtmState(GetService(), redirect_url_2).has_value());

  // Record a bounce through redirect_url_3 that starts on a URL with an SA
  // grant. This should not clear the DB entry for redirect_url_3 as it has a
  // recorded interaction.
  RecordBounce(redirect_url_3.spec(), storage_access_grant_url.spec(),
               no_grant_url.spec(), bounce, true, increment_bounce);
  EXPECT_TRUE(GetBtmState(GetService(), redirect_url_3).has_value());

  // Expect two non-SA stateful redirects: the first bounces through
  // redirect_url_2 and redirect_url_3.
  EXPECT_EQ(stateful_bounce_count, 2);
}

// When third-party cookies are globally allowed, bounces should be recorded for
// sites which have an exception to block 3PC, but not by default.
TEST_F(
    BtmServiceStateRemovalTest,
    BrowsingDataDeletion_Respects1PExceptionsForBlocking3PCWhenDefaultAllowed) {
  browser_client_.SetBlockThirdPartyCookiesByDefault(false);
  ASSERT_TRUE(Are3PcsGenerallyEnabled());

  ukm::TestAutoSetUkmRecorder ukm_recorder;
  base::test::ScopedFeatureList feature_list;
  feature_list.InitAndEnableFeatureWithParameters(
      features::kBtm, {{"triggering_action", "bounce"}});

  GURL blocked_1p_url("https://excepted-as-1p.com");
  GURL scoped_blocked_1p_url("https://excepted-as-1p-with-3p.com");
  GURL non_blocked_url("https://not-excepted.com");
  GURL redirect_url_1("https://redirect-1.com");
  GURL redirect_url_2("https://redirect-2.com");
  GURL redirect_url_3("https://redirect-3.com");
  GURL redirect_url_4("https://redirect-4.com");

  // Exceptions to block third-party cookies.
  browser_client_.BlockThirdPartyCookiesOnSite(blocked_1p_url);
  browser_client_.SetThirdPartyCookieAccess(
      redirect_url_1, scoped_blocked_1p_url, CONTENT_SETTING_BLOCK);

  int stateful_bounce_count = 0;
  BtmServiceImpl::StatefulBounceCallback increment_bounce =
      base::BindLambdaForTesting(
          [&](const GURL& final_url) { stateful_bounce_count++; });

  base::Time bounce = base::Time::FromSecondsSinceUnixEpoch(2);
  // Record a bounce through redirect_url_1 that starts and ends on blocked
  // URLs.
  RecordBounce(redirect_url_1.spec(), blocked_1p_url.spec(),
               scoped_blocked_1p_url.spec(), bounce, true, increment_bounce);
  // Record a bounce through redirect_url_2 that starts and ends on blocked
  // URLs. Record an interaction on this URL as well.
  RecordBounce(redirect_url_2.spec(), blocked_1p_url.spec(),
               blocked_1p_url.spec(), bounce, true, increment_bounce);
  GetService()
      ->storage()
      ->AsyncCall(&BtmStorage::RecordUserActivation)
      .WithArgs(redirect_url_2, bounce);
  WaitOnStorage(GetService());
  // Record a bounce through redirect_url_3 that starts on a non-blocked URL.
  RecordBounce(redirect_url_3.spec(), non_blocked_url.spec(),
               blocked_1p_url.spec(), bounce, true, increment_bounce);
  // Record a bounce through redirect_url_4 that ends on a non-blocked URL.
  RecordBounce(redirect_url_4.spec(), blocked_1p_url.spec(),
               non_blocked_url.spec(), bounce, true, increment_bounce);

  // Expect a recorded BtmState for redirect_url_1 and redirect_url_2, since
  // they were bounced through with blocking exceptions on both the initial and
  // final URL. The other two trackers were only bounced through from
  // default-allowed sites.
  EXPECT_TRUE(GetBtmState(GetService(), redirect_url_1).has_value());
  EXPECT_TRUE(GetBtmState(GetService(), redirect_url_2).has_value());
  EXPECT_FALSE(GetBtmState(GetService(), redirect_url_3).has_value());
  EXPECT_FALSE(GetBtmState(GetService(), redirect_url_4).has_value());

  // Record a bounce through redirect_url_1 that starts on a non-blocked URL.
  // This should clear the DB entry for redirect_url_1.
  RecordBounce(redirect_url_1.spec(), non_blocked_url.spec(),
               blocked_1p_url.spec(), bounce, true, increment_bounce);
  EXPECT_FALSE(GetBtmState(GetService(), redirect_url_1).has_value());

  // Record a bounce through redirect_url_2 that starts on a
  // blocked URL. This should not clear the DB entry for redirect_url_2 as it
  // has a recorded interaction.
  RecordBounce(redirect_url_2.spec(), non_blocked_url.spec(),
               blocked_1p_url.spec(), bounce, true, increment_bounce);
  EXPECT_TRUE(GetBtmState(GetService(), redirect_url_2).has_value());

  // Expect two recorded stateful redirects: the first bounces through
  // redirect_url_1 and redirect_url_2.
  EXPECT_EQ(stateful_bounce_count, 2);
}

TEST_F(BtmServiceStateRemovalTest, ImmediateEnforcement) {
  base::test::ScopedFeatureList feature_list;
  feature_list.InitAndEnableFeatureWithParameters(
      features::kBtm, {{"triggering_action", "bounce"}});
  SetNow(base::Time::FromSecondsSinceUnixEpoch(2));
  ASSERT_FALSE(Are3PcsGenerallyEnabled());

  // Record a bounce.
  GURL url("https://example.com");
  base::Time bounce = Now();
  RecordBounce(url.spec(), "https://initial.com", "https://final.com", bounce,
               false, base::DoNothing());
  WaitOnStorage(GetService());
  EXPECT_TRUE(GetBtmState(GetService(), url).has_value());

  // Set the current time to just after the bounce happened and simulate firing
  // the BTM timer.
  AdvanceTimeTo(bounce + tiny_delta);
  FireBtmTimer();
  task_environment_.RunUntilIdle();

  // Verify a removal task was not posted to the BrowsingDataRemover(Delegate).
  delegate_.VerifyAndClearExpectations();

  auto filter_builder = BrowsingDataFilterBuilder::Create(
      BrowsingDataFilterBuilder::Mode::kDelete);
  filter_builder->AddRegisterableDomain(GetSiteForBtm(url));
  filter_builder->SetCookiePartitionKeyCollection(
      net::CookiePartitionKeyCollection());
  delegate_.ExpectCall(
      base::Time::Min(), base::Time::Max(),
      (ContentBrowserClient::kDefaultBtmRemoveMask &
       ~BrowsingDataRemover::DATA_TYPE_PRIVACY_SANDBOX) |
          BrowsingDataRemover::DATA_TYPE_AVOID_CLOSING_CONNECTIONS,
      BrowsingDataRemover::ORIGIN_TYPE_UNPROTECTED_WEB |
          BrowsingDataRemover::ORIGIN_TYPE_PROTECTED_WEB,
      filter_builder.get());
  // We don't test the filter builder for partitioned cookies here because it's
  // messy. The browser tests ensure that it behaves as expected.
  delegate_.ExpectCallDontCareAboutFilterBuilder(
      base::Time::Min(), base::Time::Max(),
      BrowsingDataRemover::DATA_TYPE_COOKIES,
      BrowsingDataRemover::ORIGIN_TYPE_UNPROTECTED_WEB |
          BrowsingDataRemover::ORIGIN_TYPE_PROTECTED_WEB);

  // Perform immediate enforcement of deletion, without regard for grace period
  // and verify `url` is returned the `DeletedSitesCallback`.
  base::RunLoop run_loop;
  base::OnceCallback<void(const std::vector<std::string>& sites)> callback =
      base::BindLambdaForTesting(
          [&](const std::vector<std::string>& deleted_sites) {
            EXPECT_THAT(deleted_sites,
                        testing::UnorderedElementsAre(GetSiteForBtm(url)));
            run_loop.Quit();
          });
  GetService()->DeleteEligibleSitesImmediately(std::move(callback));
  task_environment_.RunUntilIdle();
  run_loop.Run();

  // Verify that a removal task was posted to the BrowsingDataRemover(Delegate)
  // for 'url'.
  delegate_.VerifyAndClearExpectations();
}

// A test class that verifies BtmService state deletion metrics collection
// behavior.
class BtmServiceHistogramTest : public BtmServiceStateRemovalTest {
 public:
  BtmServiceHistogramTest() = default;

  const base::HistogramTester& histograms() const { return histogram_tester_; }

 protected:
  const std::string kBlock3PC = "Block3PC";
  const std::string kUmaHistogramDeletionPrefix = "Privacy.DIPS.Deletion.";
  const std::string kServerRedirectsDelayHist =
      "Privacy.DIPS.ServerBounceDelay";
  const std::string kServerRedirectsChainDelayHist =
      "Privacy.DIPS.ServerBounceChainDelay";
  const std::string kServerRedirectsStatusCodePrefix =
      "Privacy.DIPS.BounceStatusCode.";
  const std::string kNoCache = "NoCache";
  const std::string kCached = "Cached";

  base::HistogramTester histogram_tester_;
};

TEST_F(BtmServiceHistogramTest, DeletionLatency) {
  base::test::ScopedFeatureList feature_list;
  feature_list.InitAndEnableFeatureWithParameters(
      features::kBtm, {{"triggering_action", "bounce"}});

  // Verify the histogram starts empty
  histograms().ExpectTotalCount("Privacy.DIPS.DeletionLatency2", 0);

  // Record a bounce.
  GURL url("https://example.com");
  base::Time bounce = base::Time::FromSecondsSinceUnixEpoch(2);
  RecordBounce(url.spec(), "https://initial.com", "https://final.com", bounce,
               false, base::DoNothing());
  WaitOnStorage(GetService());

  // Set the current time to just after the bounce happened.
  AdvanceTimeTo(bounce + tiny_delta);
  FireBtmTimer();
  task_environment_.RunUntilIdle();

  // Verify deletion latency metrics were NOT emitted and the BTM entry was NOT
  // removed.
  histograms().ExpectTotalCount("Privacy.DIPS.DeletionLatency2", 0);
  EXPECT_TRUE(GetBtmState(GetService(), url).has_value());

  // Time-travel to after the grace period has ended for the bounce.
  AdvanceTimeTo(bounce + grace_period + tiny_delta);
  FireBtmTimer();
  task_environment_.RunUntilIdle();

  // Verify a deletion latency metric was emitted and the BTM entry was
  // removed.
  histograms().ExpectTotalCount("Privacy.DIPS.DeletionLatency2", 1);
  EXPECT_FALSE(GetBtmState(GetService(), url).has_value());
}

TEST_F(BtmServiceHistogramTest, Deletion_ExceptedAs1P) {
  base::test::ScopedFeatureList feature_list;
  feature_list.InitAndEnableFeatureWithParameters(
      features::kBtm, {{"triggering_action", "stateful_bounce"}});

  // Verify the histogram is initially empty.
  EXPECT_TRUE(histograms()
                  .GetTotalCountsForPrefix(kUmaHistogramDeletionPrefix)
                  .empty());

  // Record a bounce.
  GURL url("https://example.com");
  GURL excepted_1p_url("https://initial.com");
  browser_client_.AllowThirdPartyCookiesOnSite(excepted_1p_url);
  base::Time bounce_time = base::Time::FromSecondsSinceUnixEpoch(2);
  RecordBounce(url.spec(), excepted_1p_url.spec(), "https://final.com",
               bounce_time, true, base::DoNothing());
  WaitOnStorage(GetService());

  // Time-travel to after the grace period has ended for the bounce.
  AdvanceTimeTo(bounce_time + grace_period + tiny_delta);
  FireBtmTimer();
  task_environment_.RunUntilIdle();

  // Verify a deletion metric was emitted and the BTM entry was removed.
  base::HistogramTester::CountsMap expected_counts;
  expected_counts[kUmaHistogramDeletionPrefix + kBlock3PC] = 1;
  EXPECT_THAT(histograms().GetTotalCountsForPrefix(kUmaHistogramDeletionPrefix),
              testing::ContainerEq(expected_counts));
  histograms().ExpectUniqueSample(kUmaHistogramDeletionPrefix + kBlock3PC,
                                  BtmDeletionAction::kExcepted, 1);
  EXPECT_FALSE(GetBtmState(GetService(), url).has_value());
}

TEST_F(BtmServiceHistogramTest, Deletion_ExceptedAs3P) {
  base::test::ScopedFeatureList feature_list;
  feature_list.InitAndEnableFeatureWithParameters(
      features::kBtm, {{"triggering_action", "stateful_bounce"}});

  // Verify the histogram is initially empty.
  EXPECT_TRUE(histograms()
                  .GetTotalCountsForPrefix(kUmaHistogramDeletionPrefix)
                  .empty());

  // Record a bounce.
  GURL excepted_3p_url("https://example.com");
  browser_client_.GrantCookieAccessTo3pSite(excepted_3p_url);
  base::Time bounce_time = base::Time::FromSecondsSinceUnixEpoch(2);
  RecordBounce(excepted_3p_url.spec(), "https://initial.com",
               "https://final.com", bounce_time, true, base::DoNothing());
  WaitOnStorage(GetService());

  // Time-travel to after the grace period has ended for the bounce.
  AdvanceTimeTo(bounce_time + grace_period + tiny_delta);
  FireBtmTimer();
  task_environment_.RunUntilIdle();

  // Verify a deletion metric was emitted and the BTM entry was removed.
  base::HistogramTester::CountsMap expected_counts;
  expected_counts[kUmaHistogramDeletionPrefix + kBlock3PC] = 1;
  EXPECT_THAT(histograms().GetTotalCountsForPrefix(kUmaHistogramDeletionPrefix),
              testing::ContainerEq(expected_counts));
  histograms().ExpectUniqueSample(kUmaHistogramDeletionPrefix + kBlock3PC,
                                  BtmDeletionAction::kExcepted, 1);
  EXPECT_FALSE(GetBtmState(GetService(), excepted_3p_url).has_value());
}

TEST_F(BtmServiceHistogramTest, DISABLED_Deletion_Enforced) {
  base::test::ScopedFeatureList feature_list;
  feature_list.InitAndEnableFeatureWithParameters(
      features::kBtm, {{"triggering_action", "stateful_bounce"}});

  // Verify the histogram is initially empty.
  EXPECT_TRUE(histograms()
                  .GetTotalCountsForPrefix(kUmaHistogramDeletionPrefix)
                  .empty());

  // Record a bounce.
  GURL url("https://example.com");
  base::Time bounce_time = base::Time::FromSecondsSinceUnixEpoch(2);
  RecordBounce(url.spec(), "https://initial.com", "https://final.com",
               bounce_time, true, base::DoNothing());
  WaitOnStorage(GetService());

  // Time-travel to after the grace period has ended for the bounce.
  AdvanceTimeTo(bounce_time + grace_period + tiny_delta);
  FireBtmTimer();
  task_environment_.RunUntilIdle();

  // Verify a deletion metric was emitted and the BTM entry was not removed.
  base::HistogramTester::CountsMap expected_counts;
  expected_counts[kUmaHistogramDeletionPrefix + kBlock3PC] = 1;
  EXPECT_THAT(histograms().GetTotalCountsForPrefix(kUmaHistogramDeletionPrefix),
              testing::ContainerEq(expected_counts));
  histograms().ExpectUniqueSample(kUmaHistogramDeletionPrefix + kBlock3PC,
                                  BtmDeletionAction::kEnforced, 1);
  EXPECT_TRUE(GetBtmState(GetService(), url).has_value());
}

MATCHER_P(HasSourceId, id, "") {
  *result_listener << "where the source id is " << arg.source_id;
  return arg.source_id == id;
}

MATCHER_P(HasMetrics, matcher, "") {
  return ExplainMatchResult(matcher, arg.metrics, result_listener);
}

using BtmServiceUkmTest = BtmServiceTest;

TEST_F(BtmServiceUkmTest, BothChainBeginAndChainEnd) {
  ukm::TestAutoSetUkmRecorder ukm_recorder;
  TestBrowserContext profile;
  BtmServiceImpl* service = BtmServiceImpl::Get(&profile);

  GURL initial_url = GURL("http://a.test/");
  ukm::SourceId initial_source_id = ukm::AssignNewSourceId();
  GURL redirect_url1 = GURL("http://b.test/");
  ukm::SourceId redirect_source_id1 = ukm::AssignNewSourceId();
  GURL redirect_url2 = GURL("http://c.test/first");
  ukm::SourceId redirect_source_id2 = ukm::AssignNewSourceId();
  GURL final_url = GURL("http://c.test/second");
  ukm::SourceId final_source_id = ukm::AssignNewSourceId();

  BtmRedirectChainObserver observer(service, final_url);
  std::vector<BtmRedirectPtr> redirects;
  redirects.push_back(
      BtmRedirect::CreateForServer(redirect_url1, redirect_source_id1,
                                   /*access_type=*/BtmDataAccessType::kNone,
                                   /*time=*/base::Time::Now(),
                                   /*was_response_cached=*/false,
                                   /*response_code=*/net::HTTP_FOUND,
                                   /*server_bounce_delay=*/base::TimeDelta()));
  redirects.push_back(
      BtmRedirect::CreateForServer(redirect_url2, redirect_source_id2,
                                   /*access_type=*/BtmDataAccessType::kNone,
                                   /*time=*/base::Time::Now(),
                                   /*was_response_cached=*/false,
                                   /*response_code=*/net::HTTP_FOUND,
                                   /*server_bounce_delay=*/base::TimeDelta()));
  BtmRedirectChainPtr chain = std::make_unique<BtmRedirectChain>(
      initial_url, initial_source_id, final_url, final_source_id,
      /*length=*/2, /*is_partial_chain=*/false,
      /*are_3pcs_generally_enabled=*/false);
  const int32_t chain_id = chain->chain_id;
  btm::Populate3PcExceptions(&profile, /*web_contents=*/nullptr, initial_url,
                             final_url, redirects);
  service->HandleRedirectChain(std::move(redirects), std::move(chain),
                               base::DoNothing());
  observer.Wait();

  EXPECT_THAT(ukm_recorder.GetEntries("BTM.ChainBegin",
                                      {"ChainId", "InitialAndFinalSitesSame"}),
              ElementsAre(AllOf(HasSourceId(initial_source_id),
                                HasMetrics(ElementsAre(
                                    Pair("ChainId", chain_id),
                                    Pair("InitialAndFinalSitesSame", 0))))));

  EXPECT_THAT(
      ukm_recorder.GetEntries("BTM.Redirect",
                              {"ChainId", "InitialAndFinalSitesSame"}),
      ElementsAre(
          AllOf(HasSourceId(redirect_source_id1),
                HasMetrics(ElementsAre(Pair("ChainId", chain_id),
                                       Pair("InitialAndFinalSitesSame", 0)))),
          AllOf(HasSourceId(redirect_source_id2),
                HasMetrics(ElementsAre(Pair("ChainId", chain_id),
                                       Pair("InitialAndFinalSitesSame", 0))))));

  EXPECT_THAT(ukm_recorder.GetEntries("BTM.ChainEnd",
                                      {"ChainId", "InitialAndFinalSitesSame"}),
              ElementsAre(AllOf(HasSourceId(final_source_id),
                                HasMetrics(ElementsAre(
                                    Pair("ChainId", chain_id),
                                    Pair("InitialAndFinalSitesSame", 0))))));
}

TEST_F(BtmServiceUkmTest, InitialAndFinalSitesSame_True) {
  ukm::TestAutoSetUkmRecorder ukm_recorder;
  TestBrowserContext profile;
  BtmServiceImpl* service = BtmServiceImpl::Get(&profile);

  GURL initial_url = GURL("http://a.test/");
  ukm::SourceId initial_source_id = ukm::AssignNewSourceId();
  GURL redirect_url = GURL("http://b.test/");
  ukm::SourceId redirect_source_id = ukm::AssignNewSourceId();
  GURL final_url = GURL("http://a.test/different-path");
  ukm::SourceId final_source_id = ukm::AssignNewSourceId();

  BtmRedirectChainObserver observer(service, final_url);
  std::vector<BtmRedirectPtr> redirects;
  redirects.push_back(
      BtmRedirect::CreateForServer(redirect_url, redirect_source_id,
                                   /*access_type=*/BtmDataAccessType::kNone,
                                   /*time=*/base::Time::Now(),
                                   /*was_response_cached=*/false,
                                   /*response_code=*/net::HTTP_FOUND,
                                   /*server_bounce_delay=*/base::TimeDelta()));
  BtmRedirectChainPtr chain = std::make_unique<BtmRedirectChain>(
      initial_url, initial_source_id, final_url, final_source_id,
      /*length=*/1, /*is_partial_chain=*/false,
      /*are_3pcs_generally_enabled=*/false);
  btm::Populate3PcExceptions(&profile, /*web_contents=*/nullptr,
                             chain->initial_url, chain->final_url, redirects);
  service->HandleRedirectChain(std::move(redirects), std::move(chain),
                               base::DoNothing());
  observer.Wait();

  EXPECT_THAT(
      ukm_recorder.GetEntries("BTM.ChainBegin", {"InitialAndFinalSitesSame"}),
      ElementsAre(
          AllOf(HasSourceId(initial_source_id),
                HasMetrics(ElementsAre(Pair("InitialAndFinalSitesSame", 1))))));

  EXPECT_THAT(
      ukm_recorder.GetEntries("BTM.Redirect", {"InitialAndFinalSitesSame"}),
      ElementsAre(
          AllOf(HasSourceId(redirect_source_id),
                HasMetrics(ElementsAre(Pair("InitialAndFinalSitesSame", 1))))));

  EXPECT_THAT(
      ukm_recorder.GetEntries("BTM.ChainEnd", {"InitialAndFinalSitesSame"}),
      ElementsAre(
          AllOf(HasSourceId(final_source_id),
                HasMetrics(ElementsAre(Pair("InitialAndFinalSitesSame", 1))))));
}

TEST_F(BtmServiceUkmTest, DontReportEmptyChainsAtAll) {
  ukm::TestAutoSetUkmRecorder ukm_recorder;
  TestBrowserContext profile;
  BtmServiceImpl* service = BtmServiceImpl::Get(&profile);

  GURL initial_url = GURL("http://a.test/");
  ukm::SourceId initial_source_id = ukm::AssignNewSourceId();
  GURL final_url = GURL("http://b.test/");
  ukm::SourceId final_source_id = ukm::AssignNewSourceId();

  BtmRedirectChainObserver observer(service, final_url);
  BtmRedirectChainPtr chain = std::make_unique<BtmRedirectChain>(
      initial_url, initial_source_id, final_url, final_source_id,
      /*length=*/0, /*is_partial_chain=*/false,
      /*are_3pcs_generally_enabled*/ false);

  service->HandleRedirectChain({}, std::move(chain), base::DoNothing());
  observer.Wait();

  EXPECT_THAT(ukm_recorder.GetEntries("BTM.ChainBegin", {}), IsEmpty());
  EXPECT_THAT(ukm_recorder.GetEntries("BTM.Redirect", {}), IsEmpty());
  EXPECT_THAT(ukm_recorder.GetEntries("BTM.ChainEnd", {}), IsEmpty());
}

TEST_F(BtmServiceUkmTest, DontReportChainBeginIfInvalidSourceId) {
  ukm::TestAutoSetUkmRecorder ukm_recorder;
  TestBrowserContext profile;
  BtmServiceImpl* service = BtmServiceImpl::Get(&profile);

  GURL redirect_url = GURL("http://b.test/");
  ukm::SourceId redirect_source_id = ukm::AssignNewSourceId();
  GURL final_url = GURL("http://c.test/");
  ukm::SourceId final_source_id = ukm::AssignNewSourceId();

  BtmRedirectChainObserver observer(service, final_url);
  std::vector<BtmRedirectPtr> redirects;
  redirects.push_back(
      BtmRedirect::CreateForServer(redirect_url, redirect_source_id,
                                   /*access_type=*/BtmDataAccessType::kNone,
                                   /*time=*/base::Time::Now(),
                                   /*was_response_cached=*/false,
                                   /*response_code=*/net::HTTP_FOUND,
                                   /*server_bounce_delay=*/base::TimeDelta()));
  BtmRedirectChainPtr chain = std::make_unique<BtmRedirectChain>(
      GURL(), ukm::kInvalidSourceId, final_url, final_source_id,
      /*length=*/1, /*is_partial_chain=*/false,
      /*are_3pcs_generally_enabled=*/false);
  btm::Populate3PcExceptions(&profile, /*web_contents=*/nullptr,
                             chain->initial_url, chain->final_url, redirects);
  service->HandleRedirectChain(std::move(redirects), std::move(chain),
                               base::DoNothing());
  observer.Wait();

  EXPECT_THAT(ukm_recorder.GetEntries("BTM.ChainBegin", {}), IsEmpty());

  EXPECT_THAT(ukm_recorder.GetEntries("BTM.Redirect", {}),
              ElementsAre(AllOf(HasSourceId(redirect_source_id))));

  EXPECT_THAT(ukm_recorder.GetEntries("BTM.ChainEnd", {}),
              ElementsAre(AllOf(HasSourceId(final_source_id))));
}

TEST_F(BtmServiceUkmTest, DontReportChainEndIfInvalidSourceId) {
  ukm::TestAutoSetUkmRecorder ukm_recorder;
  TestBrowserContext profile;
  BtmServiceImpl* service = BtmServiceImpl::Get(&profile);

  GURL initial_url = GURL("http://a.test/");
  ukm::SourceId initial_source_id = ukm::AssignNewSourceId();
  GURL redirect_url = GURL("http://b.test/");
  ukm::SourceId redirect_source_id = ukm::AssignNewSourceId();

  BtmRedirectChainObserver observer(service, GURL());
  std::vector<BtmRedirectPtr> redirects;
  redirects.push_back(
      BtmRedirect::CreateForServer(redirect_url, redirect_source_id,
                                   /*access_type=*/BtmDataAccessType::kNone,
                                   /*time=*/base::Time::Now(),
                                   /*was_response_cached=*/false,
                                   /*response_code=*/net::HTTP_FOUND,
                                   /*server_bounce_delay=*/base::TimeDelta()));
  BtmRedirectChainPtr chain = std::make_unique<BtmRedirectChain>(
      initial_url, initial_source_id, GURL(), ukm::kInvalidSourceId,
      /*length=*/1, /*is_partial_chain=*/false,
      /*are_3pcs_generally_enabled=*/false);
  btm::Populate3PcExceptions(&profile, /*web_contents=*/nullptr,
                             chain->initial_url, chain->final_url, redirects);
  service->HandleRedirectChain(std::move(redirects), std::move(chain),
                               base::DoNothing());
  observer.Wait();

  EXPECT_THAT(ukm_recorder.GetEntries("BTM.ChainBegin", {}),
              ElementsAre(AllOf(HasSourceId(initial_source_id))));

  EXPECT_THAT(ukm_recorder.GetEntries("BTM.Redirect", {}),
              ElementsAre(AllOf(HasSourceId(redirect_source_id))));

  EXPECT_THAT(ukm_recorder.GetEntries("BTM.ChainEnd", {}), IsEmpty());
}

TEST_F(BtmServiceUkmTest, DontReportChainIfTpcsEnabled) {
  ukm::TestAutoSetUkmRecorder ukm_recorder;
  TestBrowserContext profile;
  BtmServiceImpl* service = BtmServiceImpl::Get(&profile);

  GURL initial_url = GURL("http://a.test/");
  ukm::SourceId initial_source_id = ukm::AssignNewSourceId();
  GURL redirect_url = GURL("http://b.test/");
  ukm::SourceId redirect_source_id = ukm::AssignNewSourceId();
  GURL final_url = GURL("http://c.test/");
  ukm::SourceId final_source_id = ukm::AssignNewSourceId();

  BtmRedirectChainObserver observer(service, final_url);
  std::vector<BtmRedirectPtr> redirects;
  redirects.push_back(
      BtmRedirect::CreateForServer(redirect_url, redirect_source_id,
                                   /*access_type=*/BtmDataAccessType::kNone,
                                   /*time=*/base::Time::Now(),
                                   /*was_response_cached=*/false,
                                   /*response_code=*/net::HTTP_FOUND,
                                   /*server_bounce_delay=*/base::TimeDelta()));
  BtmRedirectChainPtr chain = std::make_unique<BtmRedirectChain>(
      initial_url, initial_source_id, final_url, final_source_id,
      redirects.size(), /*is_partial_chain=*/false,
      /*are_3pcs_generally_enabled=*/true);
  btm::Populate3PcExceptions(&profile, /*web_contents=*/nullptr, initial_url,
                             final_url, redirects);
  service->HandleRedirectChain(std::move(redirects), std::move(chain),
                               base::DoNothing());
  observer.Wait();

  // There should be no BTM chain UKMs, as processing gets short-circuited when
  // third-party cookies are enabled.
  EXPECT_THAT(ukm_recorder.GetEntries("BTM.ChainBegin", {"ChainId"}),
              IsEmpty());
  EXPECT_THAT(ukm_recorder.GetEntries("BTM.Redirect", {"ChainId"}), IsEmpty());
  EXPECT_THAT(ukm_recorder.GetEntries("BTM.ChainEnd", {"ChainId"}), IsEmpty());
}

}  // namespace content
