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

#include "chrome/browser/ui/webui/settings/site_settings_handler.h"

#include <algorithm>
#include <array>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <tuple>
#include <utility>
#include <vector>

#include "base/barrier_closure.h"
#include "base/byte_size.h"
#include "base/check_deref.h"
#include "base/command_line.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/json/json_reader.h"
#include "base/json/values_util.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/raw_ref.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/metrics/user_action_tester.h"
#include "base/test/simple_test_clock.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "base/test/values_test_util.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/bluetooth/bluetooth_chooser_context_factory.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/browsing_data/chrome_browsing_data_model_delegate.h"
#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
#include "chrome/browser/engagement/site_engagement_service_factory.h"
#include "chrome/browser/extensions/test_extension_system.h"
#include "chrome/browser/file_system_access/chrome_file_system_access_permission_context.h"
#include "chrome/browser/file_system_access/file_system_access_permission_context_factory.h"
#include "chrome/browser/global_features.h"
#include "chrome/browser/hid/hid_chooser_context.h"
#include "chrome/browser/hid/hid_chooser_context_factory.h"
#include "chrome/browser/history/history_service_factory.h"
#include "chrome/browser/infobars/browser_infobar_manager.h"
#include "chrome/browser/infobars/infobar_features.h"
#include "chrome/browser/infobars/infobar_spec.h"
#include "chrome/browser/permissions/permission_actions_history_factory.h"
#include "chrome/browser/permissions/permission_decision_auto_blocker_factory.h"
#include "chrome/browser/permissions/system/mock_platform_handle.h"
#include "chrome/browser/permissions/system/system_permission_settings.h"
#include "chrome/browser/prefs/browser_prefs.h"
#include "chrome/browser/privacy_sandbox/mock_privacy_sandbox_service.h"
#include "chrome/browser/privacy_sandbox/privacy_sandbox_service_factory.h"
#include "chrome/browser/serial/serial_chooser_context.h"
#include "chrome/browser/serial/serial_chooser_context_factory.h"
#include "chrome/browser/tab_list/mock_tab_list_interface.h"
#include "chrome/browser/tab_list/tab_list_interface.h"
#include "chrome/browser/ui/browser_window/public/browser_collection_observer.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/browser_window/public/global_browser_collection.h"
#include "chrome/browser/ui/browser_window/test/mock_browser_window_interface.h"
#include "chrome/browser/ui/safety_hub/safety_hub_test_util.h"
#include "chrome/browser/ui/views/chrome_layout_provider.h"
#include "chrome/browser/ui/web_applications/test/isolated_web_app_test_utils.h"
#include "chrome/browser/ui/webui/settings/site_settings_helper.h"
#include "chrome/browser/usb/usb_chooser_context.h"
#include "chrome/browser/usb/usb_chooser_context_factory.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_url_info.h"
#include "chrome/browser/web_applications/isolated_web_apps/test/isolated_web_app_builder.h"
#include "chrome/browser/web_applications/test/web_app_install_test_utils.h"
#include "chrome/browser/web_applications/test/web_app_test_utils.h"
#include "chrome/browser/web_applications/web_app_helpers.h"
#include "chrome/browser/web_applications/web_app_registry_update.h"
#include "chrome/browser/web_applications/web_app_sync_bridge.h"
#include "chrome/common/pref_names.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile.h"
#include "chrome/test/base/testing_profile_manager.h"
#include "components/browsing_data/content/browsing_data_model.h"
#include "components/browsing_data/content/browsing_data_model_test_util.h"
#include "components/browsing_data/content/fake_browsing_data_model.h"
#include "components/browsing_data/content/mock_cookie_helper.h"
#include "components/browsing_data/content/mock_local_storage_helper.h"
#include "components/client_hints/common/client_hints.h"
#include "components/content_settings/core/browser/content_settings_uma_util.h"
#include "components/content_settings/core/browser/cookie_settings.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "components/content_settings/core/common/content_settings.h"
#include "components/content_settings/core/common/content_settings_pattern.h"
#include "components/content_settings/core/common/content_settings_types.h"
#include "components/content_settings/core/common/pref_names.h"
#include "components/history/core/browser/history_service.h"
#include "components/infobars/content/content_infobar_manager.h"
#include "components/infobars/core/infobar.h"
#include "components/infobars/core/infobar_delegate.h"
#include "components/permissions/contexts/bluetooth_chooser_context.h"
#include "components/permissions/features.h"
#include "components/permissions/object_permission_context_base.h"
#include "components/permissions/permission_actions_history.h"
#include "components/permissions/permission_decision_auto_blocker.h"
#include "components/permissions/permission_uma_util.h"
#include "components/permissions/permission_util.h"
#include "components/permissions/test/object_permission_context_base_mock_permission_observer.h"
#include "components/permissions/test/permission_test_util.h"
#include "components/prefs/testing_pref_service.h"
#include "components/privacy_sandbox/privacy_sandbox_features.h"
#include "components/safe_browsing/core/browser/safe_browsing_metrics_collector.h"
#include "components/services/app_service/public/cpp/app_registry_cache.h"
#include "components/services/app_service/public/cpp/app_types.h"
#include "components/services/app_service/public/cpp/app_update.h"
#include "components/site_engagement/content/site_engagement_score.h"
#include "components/strings/grit/components_strings.h"
#include "components/sync_preferences/testing_pref_service_syncable.h"
#include "components/tabs/public/mock_tab_interface.h"
#include "components/tabs/public/tab_interface.h"
#include "components/ukm/test_ukm_recorder.h"
#include "components/vector_icons/vector_icons.h"
#include "components/webapps/common/web_app_id.h"
#include "content/public/browser/host_zoom_map.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/browser/web_ui_data_source.h"
#include "content/public/common/buildflags.h"
#include "content/public/common/content_features.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/navigation_simulator.h"
#include "content/public/test/test_renderer_host.h"
#include "content/public/test/test_web_ui.h"
#include "content/public/test/web_contents_tester.h"
#include "device/bluetooth/bluetooth_adapter_factory.h"
#include "device/bluetooth/test/mock_bluetooth_adapter.h"
#include "device/bluetooth/test/mock_bluetooth_device.h"
#include "extensions/browser/extension_registrar.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/common/extension_builder.h"
#include "google_apis/gaia/gaia_id.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "services/device/public/cpp/test/fake_hid_manager.h"
#include "services/device/public/cpp/test/fake_serial_port_manager.h"
#include "services/device/public/cpp/test/fake_usb_device_manager.h"
#include "services/device/public/mojom/hid.mojom.h"
#include "services/device/public/mojom/serial.mojom.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 "third_party/blink/public/mojom/bluetooth/web_bluetooth.mojom.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/text/bytes_formatting.h"
#include "ui/base/unowned_user_data/scoped_unowned_user_data.h"
#include "ui/webui/webui_allowlist.h"
#include "url/gurl.h"

#if BUILDFLAG(IS_CHROMEOS)
#include "chrome/browser/ash/login/users/fake_chrome_user_manager.h"
#include "chrome/browser/smart_card/smart_card_permission_context.h"
#include "chrome/browser/smart_card/smart_card_permission_context_factory.h"
#include "components/account_id/account_id.h"
#include "components/user_manager/scoped_user_manager.h"
#endif

#if BUILDFLAG(ENABLE_PLUGINS)
#include "chrome/browser/plugins/chrome_plugin_service_filter.h"
#endif

namespace {

using ::base::test::ParseJson;
using ::base::test::RunClosure;
using ::base::test::TestFuture;
using ::testing::_;
using ::testing::NiceMock;
using ::testing::Return;
using ::testing::UnorderedElementsAre;

using GroupingKey = settings::SiteSettingsHandler::GroupingKey;
using PermissionStatus = blink::mojom::PermissionStatus;

constexpr char kCallbackId[] = "test-callback-id";
constexpr char kSetting[] = "setting";
constexpr char kSource[] = "source";
constexpr char kExtensionName[] = "Test Extension";
constexpr char kTestUserEmail[] = "user@example.com";

const struct PatternContentTypeTestCase {
  struct {
    const char* const pattern;
    const char* const content_type;
  } arguments;
  struct {
    const bool validity;
    const char* const reason;
  } expected;
} kPatternsAndContentTypeTestCases[]{
    {{"https://google.com", "cookies"}, {true, ""}},
    {{";", "cookies"}, {true, ""}},
    {{"^", "cookies"}, {false, "Not a valid web address"}},
    {{"*", "cookies"}, {false, "Not a valid web address"}},
    {{"chrome://test", "popups"}, {false, "Not a valid web address"}},
    {{"chrome-untrusted://test", "popups"}, {false, "Not a valid web address"}},
    {{"devtools://devtools", "popups"}, {false, "Not a valid web address"}},
    {{"chrome-search://search", "popups"}, {false, "Not a valid web address"}},
    {{"http://google.com", "location"}, {false, "Origin must be secure"}},
    {{"http://127.0.0.1", "location"}, {true, ""}},  // Localhost is secure.
    {{"http://[::1]", "location"}, {true, ""}}};

struct EmbeddingStorageAccessException {
  const std::string embedding_origin;
  const std::string embedding_display_name;
  const bool incognito;
  const bool embargoed = false;
  const int lifetime_in_days = 0;
};

// Matchers to make verifying GroupingKey contents easier.
MATCHER_P(IsEtldPlus1, etld_plus1, "") {
  return arg == std::string("etld:") + etld_plus1;
}

MATCHER_P(IsOrigin, origin, "") {
  return arg == std::string("origin:") + origin.spec();
}

// Converts |etld_plus1| into an HTTPS SchemefulSite.
net::SchemefulSite ConvertEtldToSchemefulSite(const std::string etld_plus1) {
  return net::SchemefulSite(GURL(std::string(url::kHttpsScheme) +
                                 url::kStandardSchemeSeparator + etld_plus1 +
                                 "/"));
}

// Validates that the list of sites are aligned with the related website sets
// mapping.
void ValidateSitesWithRws(
    const base::ListValue& storage_and_cookie_list,
    base::flat_map<net::SchemefulSite, net::SchemefulSite>&
        related_website_sets) {
  for (const base::Value& site_group_value : storage_and_cookie_list) {
    const base::DictValue& site_group = site_group_value.GetDict();
    GroupingKey grouping_key = GroupingKey::Deserialize(
        CHECK_DEREF(site_group.FindString("groupingKey")));
    if (!grouping_key.GetEtldPlusOne().has_value()) {
      return;
    }
    std::string etld_plus1 = *grouping_key.GetEtldPlusOne();
    auto schemeful_site = ConvertEtldToSchemefulSite(etld_plus1);

    if (related_website_sets.count(schemeful_site)) {
      // Ensure that the `rwsOwner` is set correctly and aligned with
      // |related_website_sets| mapping of site group owners.
      std::string owner_etldplus1 =
          related_website_sets[schemeful_site].GetURL().GetHost();
      ASSERT_EQ(owner_etldplus1, *site_group.FindString("rwsOwner"));
      if (owner_etldplus1 == "google.com") {
        ASSERT_EQ(2, *site_group.FindInt("rwsNumMembers"));
        ASSERT_EQ(false, *site_group.FindBool("rwsEnterpriseManaged"));
      } else if (owner_etldplus1 == "example.com") {
        ASSERT_EQ(1, *site_group.FindInt("rwsNumMembers"));
        ASSERT_EQ(true, *site_group.FindBool("rwsEnterpriseManaged"));
      }
    } else {
      // The site is not part of a RWS therefore doesn't have `rwsOwner` or
      // `rwsNumMembers` set. `FindString` and `FindInt` should return null.
      ASSERT_FALSE(site_group.FindString("rwsOwner"));
      ASSERT_FALSE(site_group.FindInt("rwsNumMembers"));
      ASSERT_FALSE(site_group.FindBool("rwsEnterpriseManaged"));
    }
  }
}

apps::AppPtr MakeApp(const std::string& app_id,
                     apps::AppType app_type,
                     const std::string& publisher_id,
                     apps::Readiness readiness,
                     apps::InstallReason install_reason) {
  apps::AppPtr app = std::make_unique<apps::App>(app_type, app_id);
  app->publisher_id = publisher_id;
  app->readiness = readiness;
  app->install_reason = install_reason;
  return app;
}

void RegisterWebApp(Profile* profile, apps::AppPtr app) {
  std::vector<apps::AppPtr> deltas;
  deltas.push_back(std::move(app));
  apps::AppServiceProxyFactory::GetForProfile(profile)->OnApps(
      std::move(deltas), apps::AppType::kWeb,
      /*should_notify_initialized=*/true);
}

std::unique_ptr<net::CanonicalCookie> CreateCookieKey(
    const GURL& url,
    const std::string& cookie_line,
    std::optional<net::CookiePartitionKey> cookie_partition_key =
        std::nullopt) {
  return net::CanonicalCookie::CreateForTesting(
      url, cookie_line, base::Time::Now(), net::CookieSourceType::kOther,
      std::nullopt /* server_time */, cookie_partition_key);
}

void RemoveModelEntries(
    std::vector<browsing_data_model_test_util::BrowsingDataEntry>&
        model_entries,
    const std::vector<browsing_data_model_test_util::BrowsingDataEntry>&
        entries_to_remove) {
  for (const auto& entry : entries_to_remove) {
    std::erase(model_entries, entry);
  }
}

struct TestModel {
  const raw_ref<FakeBrowsingDataModel> browsing_data_model;
};

}  // namespace

namespace settings {

// Helper class for setting ContentSettings via different sources.
class ContentSettingSourceSetter {
 public:
  ContentSettingSourceSetter(TestingProfile* profile,
                             ContentSettingsType content_type)
      : prefs_(profile->GetTestingPrefService()), content_type_(content_type) {}
  ContentSettingSourceSetter(const ContentSettingSourceSetter&) = delete;
  ContentSettingSourceSetter& operator=(const ContentSettingSourceSetter&) =
      delete;

  void SetPolicyDefault(ContentSetting setting) {
    prefs_->SetManagedPref(GetPrefNameForDefaultPermissionSetting(),
                           std::make_unique<base::Value>(setting));
  }

  const char* GetPrefNameForDefaultPermissionSetting() {
    switch (content_type_) {
      case ContentSettingsType::NOTIFICATIONS:
        return prefs::kManagedDefaultNotificationsSetting;
      default:
        // Add support as needed.
        NOTREACHED();
    }
  }

 private:
  raw_ptr<sync_preferences::TestingPrefServiceSyncable> prefs_;
  ContentSettingsType content_type_;
};

class SiteSettingsHandlerBaseTest : public testing::Test {
 public:
  // Defer any initialization and setup to SetUp(). This is done so that
  // subclasses can configure feature flags in their constructor, before the
  // test environment is fully established.
  SiteSettingsHandlerBaseTest() = default;

  void SetUp() override {
    profile_manager_ =
        TestingBrowserProcess::GetGlobal()->SetUpGlobalFeaturesForTesting(
            /*profile_manager=*/true);
    profile_ = profile_manager_->CreateTestingProfile(
        kTestUserEmail, {TestingProfile::TestingFactory{
                            HistoryServiceFactory::GetInstance(),
                            HistoryServiceFactory::GetDefaultFactory()}});
    EXPECT_TRUE(profile_);

#if BUILDFLAG(IS_CHROMEOS)
    SetUpUserManager(profile_.get());
#endif


    mock_privacy_sandbox_service_ = static_cast<MockPrivacySandboxService*>(
        PrivacySandboxServiceFactory::GetInstance()->SetTestingFactoryAndUse(
            profile(), base::BindRepeating(&BuildMockPrivacySandboxService)));

    profile()->SetPermissionControllerDelegate(
        permissions::GetPermissionControllerDelegate(profile()));

    safety_hub_test_util::CreateNotificationPermissionsReviewService(profile());

    SetUpIsolatedWebApp();

    handler_ = std::make_unique<SiteSettingsHandler>(profile());
    handler()->set_web_ui(web_ui());
    handler()->AllowJavascript();
    // AllowJavascript() adds a callback to create leveldb_env::ChromiumEnv
    // which reads the FeatureList. Wait for the callback to be finished so that
    // we won't destruct |feature_list_| before the callback is executed.
    base::RunLoop().RunUntilIdle();
    web_ui()->ClearTrackedCalls();
  }

  void TearDown() override {
    handler_.reset();
    if (profile_) {
      auto* partition = profile_->GetDefaultStoragePartition();
      if (partition) {
        partition->WaitForDeletionTasksForTesting();
      }
    }

    // `handler_` must be reset before the profile is destroyed (via
    // `TearDownGlobalFeaturesForTesting()`) because it holds a pointer to the
    // profile and may access profile-owned services during destruction.
    handler_.reset();

#if BUILDFLAG(IS_CHROMEOS)
    scoped_user_manager_.reset();
#endif  // BUILDFLAG(IS_CHROMEOS)
    mock_privacy_sandbox_service_ = nullptr;
    incognito_profile_ = nullptr;
    profile_ = nullptr;
    profile_manager_ = nullptr;
    TestingBrowserProcess::GetGlobal()->TearDownGlobalFeaturesForTesting();
  }

#if BUILDFLAG(IS_CHROMEOS)
  void SetUpUserManager(TestingProfile* profile) {
    // On ChromeOS a user account is needed in order to check whether the user
    // account is affiliated with the device owner for the purposes of applying
    // enterprise policy.
    constexpr GaiaId::Literal kTestUserGaiaId("1111111111");
    auto fake_user_manager = std::make_unique<ash::FakeChromeUserManager>();
    auto* fake_user_manager_ptr = fake_user_manager.get();
    scoped_user_manager_ = std::make_unique<user_manager::ScopedUserManager>(
        std::move(fake_user_manager));

    auto account_id =
        AccountId::FromUserEmailGaiaId(kTestUserEmail, kTestUserGaiaId);
    fake_user_manager_ptr->AddUserWithAffiliation(account_id,
                                                  /*is_affiliated=*/true);
    fake_user_manager_ptr->LoginUser(account_id);
  }
#endif  // BUILDFLAG(IS_CHROMEOS)

  TestingProfileManager* profile_manager() { return profile_manager_; }
  TestingProfile* profile() { return profile_.get(); }
  Profile* incognito_profile() { return incognito_profile_; }
  content::TestWebUI* web_ui() { return &web_ui_; }
  SiteSettingsHandler* handler() { return handler_.get(); }
  MockPrivacySandboxService* mock_privacy_sandbox_service() {
    return mock_privacy_sandbox_service_.get();
  }

  void ValidateBlockAutoplay(bool expected_value, bool expected_enabled) {
    const content::TestWebUI::CallData& data = *web_ui()->call_data().back();
    EXPECT_EQ("cr.webUIListenerCallback", data.function_name());

    ASSERT_TRUE(data.arg1()->is_string());
    EXPECT_EQ("onBlockAutoplayStatusChanged", data.arg1()->GetString());

    const base::Value* event_data = data.arg2();
    ASSERT_TRUE(event_data->is_dict());

    std::optional<bool> enabled = event_data->GetDict().FindBool("enabled");
    ASSERT_TRUE(enabled.has_value());
    EXPECT_EQ(expected_enabled, *enabled);

    const base::DictValue* pref_data = event_data->GetDict().FindDict("pref");
    ASSERT_TRUE(pref_data);

    std::optional<bool> value = pref_data->FindBool("value");
    ASSERT_TRUE(value.has_value());
    EXPECT_EQ(expected_value, *value);
  }

  void SetSoundContentSettingDefault(ContentSetting value) {
    HostContentSettingsMap* content_settings =
        HostContentSettingsMapFactory::GetForProfile(profile());
    content_settings->SetDefaultContentSetting(ContentSettingsType::SOUND,
                                               value);
  }

  void ValidateDefault(const ContentSetting expected_setting,
                       const std::string expected_source,
                       size_t expected_total_calls) {
    EXPECT_EQ(expected_total_calls, web_ui()->call_data().size());

    const content::TestWebUI::CallData& data = *web_ui()->call_data().back();
    EXPECT_EQ("cr.webUIResponse", data.function_name());

    ASSERT_TRUE(data.arg1()->is_string());
    EXPECT_EQ(kCallbackId, data.arg1()->GetString());

    ASSERT_TRUE(data.arg2()->is_bool());
    ASSERT_TRUE(data.arg2()->GetBool());

    const base::DictValue& default_value = data.arg3()->GetDict();
    const std::string* setting = default_value.FindString(kSetting);
    ASSERT_TRUE(setting);
    EXPECT_EQ(*setting,
              content_settings::ContentSettingToString(expected_setting));
    const std::string* source = default_value.FindString(kSource);
    if (source) {
      EXPECT_EQ(*source, expected_source);
    } else {
      EXPECT_TRUE(expected_source.empty());
    }
  }

  void ValidateOrigin(const std::string& expected_origin,
                      const std::string& expected_embedding,
                      const std::string& expected_display_name,
                      const ContentSetting expected_setting,
                      const site_settings::SiteSettingSource expected_source,
                      size_t expected_total_calls) {
    EXPECT_EQ(expected_total_calls, web_ui()->call_data().size());

    const content::TestWebUI::CallData& data = *web_ui()->call_data().back();
    EXPECT_EQ("cr.webUIResponse", data.function_name());

    ASSERT_TRUE(data.arg1()->is_string());
    EXPECT_EQ(kCallbackId, data.arg1()->GetString());
    ASSERT_TRUE(data.arg2()->is_bool());
    ASSERT_TRUE(data.arg2()->GetBool());

    ASSERT_TRUE(data.arg3()->is_list());
    EXPECT_EQ(1U, data.arg3()->GetList().size());

    const base::DictValue& exception = data.arg3()->GetList()[0].GetDict();

    const std::string* origin = exception.FindString(site_settings::kOrigin);
    ASSERT_TRUE(origin);
    ASSERT_EQ(expected_origin, *origin);

    const std::string* display_name =
        exception.FindString(site_settings::kDisplayName);
    ASSERT_TRUE(display_name);
    ASSERT_EQ(expected_display_name, *display_name);

    const std::string* embedding_origin =
        exception.FindString(site_settings::kEmbeddingOrigin);
    ASSERT_TRUE(embedding_origin);
    ASSERT_EQ(expected_embedding, *embedding_origin);

    const std::string* setting = exception.FindString(site_settings::kSetting);
    ASSERT_TRUE(setting);
    ASSERT_EQ(content_settings::ContentSettingToString(expected_setting),
              *setting);

    const std::string* source = exception.FindString(site_settings::kSource);
    ASSERT_TRUE(source);
    ASSERT_EQ(site_settings::SiteSettingSourceToString(expected_source),
              *source);
  }

  void SetContentSettingCustomScope(
      std::string primary_pattern,
      std::string secondary_pattern,
      ContentSettingsType content_setting_type,
      ContentSetting content_setting,
      size_t expected_total_calls = 1U,
      bool is_incognito = false,
      base::TimeDelta lifetime = base::TimeDelta(),
      bool is_auto_granted = false) {
    HostContentSettingsMap* map = HostContentSettingsMapFactory::GetForProfile(
        is_incognito ? incognito_profile() : profile());

    content_settings::ContentSettingConstraints constraints;
    constraints.set_lifetime(lifetime);
    if (is_auto_granted) {
      constraints.set_session_model(
          content_settings::mojom::SessionModel::DURABLE);
      constraints.set_decided_by_related_website_sets(true);
    }

    map->SetContentSettingCustomScope(
        ContentSettingsPattern::FromString(primary_pattern),
        secondary_pattern.empty()
            ? ContentSettingsPattern::Wildcard()
            : ContentSettingsPattern::FromString(secondary_pattern),
        content_setting_type, content_setting, constraints);
    EXPECT_EQ(expected_total_calls, web_ui()->call_data().size());
    ASSERT_EQ("contentSettingSitePermissionChanged",
              web_ui()->call_data().back()->arg1()->GetString());
  }

  void ValidateStorageAccessList(size_t expected_total_calls,
                                 size_t expected_num_groups) {
    EXPECT_EQ(expected_total_calls, web_ui()->call_data().size());

    const content::TestWebUI::CallData& data = *web_ui()->call_data().back();
    EXPECT_EQ("cr.webUIResponse", data.function_name());

    ASSERT_TRUE(data.arg1()->is_string());
    EXPECT_EQ(kCallbackId, data.arg1()->GetString());
    ASSERT_TRUE(data.arg2()->is_bool());
    ASSERT_TRUE(data.arg2()->GetBool());

    ASSERT_TRUE(data.arg3()->is_list());
    EXPECT_EQ(expected_num_groups, data.arg3()->GetList().size());
  }

  void ValidateStorageAccessException(
      const std::string& expected_origin,
      const std::string& expected_display_name,
      const ContentSetting expected_setting,
      const std::vector<EmbeddingStorageAccessException>
          expected_embedding_exceptions,
      size_t index = 0) {
    const content::TestWebUI::CallData& data = *web_ui()->call_data().back();

    const base::DictValue& exception = data.arg3()->GetList()[index].GetDict();

    const auto* origin = exception.FindString(site_settings::kOrigin);
    ASSERT_TRUE(origin);
    ASSERT_EQ(expected_origin, *origin);

    const auto* display_name =
        exception.FindString(site_settings::kDisplayName);
    ASSERT_TRUE(display_name);
    ASSERT_EQ(expected_display_name, *display_name);

    // Simple description and incognito should only be present for static
    // exceptions.
    const auto* description = exception.FindString(site_settings::kDescription);
    ASSERT_FALSE(description);
    std::optional<bool> incognito =
        exception.FindBool(site_settings::kIncognito);
    ASSERT_FALSE(incognito.has_value());

    const auto expected_close_description = l10n_util::GetPluralStringFUTF8(
        IDS_DEL_SITE_SETTINGS_COUNTER, expected_embedding_exceptions.size());
    const auto* close_description =
        exception.FindString(site_settings::kCloseDescription);
    ASSERT_TRUE(close_description);
    ASSERT_EQ(expected_close_description, *close_description);

    const auto expected_open_description = l10n_util::GetStringUTF8(
        (expected_setting == ContentSetting::CONTENT_SETTING_ALLOW)
            ? IDS_SETTINGS_STORAGE_ACCESS_ALLOWED_SITE_LABEL
            : IDS_SETTINGS_STORAGE_ACCESS_BLOCKED_SITE_LABEL);
    const auto* open_description =
        exception.FindString(site_settings::kOpenDescription);
    ASSERT_TRUE(open_description);
    ASSERT_EQ(expected_open_description, *open_description);

    const auto* setting = exception.FindString(site_settings::kSetting);
    ASSERT_TRUE(setting);
    ASSERT_EQ(content_settings::ContentSettingToString(expected_setting),
              *setting);

    const auto* exceptions_list =
        exception.FindList(site_settings::kExceptions);
    ASSERT_TRUE(exceptions_list);
    ASSERT_EQ(expected_embedding_exceptions.size(), exceptions_list->size());

    for (size_t i = 0; i < expected_embedding_exceptions.size(); i++) {
      ValidateStorageAccessEmbeddingException(expected_embedding_exceptions[i],
                                              (*exceptions_list)[i].GetDict());
    }
  }

  void ValidateStorageAccessEmbeddingException(
      EmbeddingStorageAccessException expected_embedding_exception,
      const base::DictValue& embedding_exception) {
    const auto* embedding_origin =
        embedding_exception.FindString(site_settings::kEmbeddingOrigin);
    ASSERT_TRUE(embedding_origin);
    ASSERT_EQ(expected_embedding_exception.embedding_origin, *embedding_origin);

    const auto* embedding_display_name =
        embedding_exception.FindString(site_settings::kEmbeddingDisplayName);
    ASSERT_TRUE(embedding_display_name);
    ASSERT_EQ(expected_embedding_exception.embedding_display_name,
              *embedding_display_name);

    std::optional<bool> incognito =
        embedding_exception.FindBool(site_settings::kIncognito);
    ASSERT_TRUE(incognito.has_value());
    EXPECT_EQ(expected_embedding_exception.incognito, *incognito);

    const std::string* description =
        embedding_exception.FindString(site_settings::kDescription);

    if (expected_embedding_exception.lifetime_in_days == 0 &&
        !expected_embedding_exception.embargoed) {
      ASSERT_FALSE(description);
      return;
    }

    std::string expected_description =
        expected_embedding_exception.embargoed
            ? l10n_util::GetStringUTF8(
                  IDS_PAGE_INFO_PERMISSION_AUTOMATICALLY_BLOCKED)
            : l10n_util::GetPluralStringFUTF8(
                  IDS_SETTINGS_EXPIRES_AFTER_TIME_LABEL,
                  expected_embedding_exception.lifetime_in_days);
    ASSERT_TRUE(description);
    ASSERT_EQ(expected_description, *description);
  }

  void ValidateStaticStorageAccessException(
      const std::string& expected_origin,
      const std::string& expected_display_name,
      const ContentSetting expected_setting,
      bool expected_incognito,
      size_t index = 0) {
    const content::TestWebUI::CallData& data = *web_ui()->call_data().back();

    const base::DictValue& exception = data.arg3()->GetList()[index].GetDict();

    const auto* origin = exception.FindString(site_settings::kOrigin);
    ASSERT_TRUE(origin);
    ASSERT_EQ(expected_origin, *origin);

    const auto* display_name =
        exception.FindString(site_settings::kDisplayName);
    ASSERT_TRUE(display_name);
    ASSERT_EQ(expected_display_name, *display_name);

    // Close and open description should only be present for non-static
    // exceptions.
    const auto* close_description =
        exception.FindString(site_settings::kCloseDescription);
    ASSERT_FALSE(close_description);
    const auto* open_description =
        exception.FindString(site_settings::kOpenDescription);
    ASSERT_FALSE(open_description);

    std::string expected_description = l10n_util::GetStringUTF8(
        IDS_PAGE_INFO_PERMISSION_AUTOMATICALLY_BLOCKED);
    const auto* description = exception.FindString(site_settings::kDescription);
    ASSERT_TRUE(description);
    ASSERT_EQ(expected_description, *description);

    const auto* setting = exception.FindString(site_settings::kSetting);
    ASSERT_TRUE(setting);
    ASSERT_EQ(content_settings::ContentSettingToString(expected_setting),
              *setting);

    std::optional<bool> incognito =
        exception.FindBool(site_settings::kIncognito);
    ASSERT_TRUE(incognito.has_value());
    EXPECT_EQ(expected_incognito, *incognito);

    const auto* exceptions_list =
        exception.FindList(site_settings::kExceptions);
    ASSERT_TRUE(exceptions_list);
    ASSERT_EQ(0U, exceptions_list->size());
  }

  void ValidateNoOrigin(size_t expected_total_calls) {
    EXPECT_EQ(expected_total_calls, web_ui()->call_data().size());

    const content::TestWebUI::CallData& data = *web_ui()->call_data().back();
    EXPECT_EQ("cr.webUIResponse", data.function_name());

    ASSERT_TRUE(data.arg1()->is_string());
    EXPECT_EQ(kCallbackId, data.arg1()->GetString());

    ASSERT_TRUE(data.arg2()->is_bool());
    ASSERT_TRUE(data.arg2()->GetBool());

    const base::Value& exceptions = *data.arg3();
    ASSERT_TRUE(exceptions.is_list());
    EXPECT_TRUE(exceptions.GetList().empty());
  }

  void ValidatePattern(bool expected_validity,
                       size_t expected_total_calls,
                       std::string expected_reason) {
    EXPECT_EQ(expected_total_calls, web_ui()->call_data().size());

    const content::TestWebUI::CallData& data = *web_ui()->call_data().back();
    EXPECT_EQ("cr.webUIResponse", data.function_name());

    ASSERT_TRUE(data.arg1()->is_string());
    EXPECT_EQ(kCallbackId, data.arg1()->GetString());

    ASSERT_TRUE(data.arg2()->is_bool());
    ASSERT_TRUE(data.arg2()->GetBool());

    const base::Value* result = data.arg3();
    ASSERT_TRUE(result->is_dict());

    std::optional<bool> valid = result->GetDict().FindBool("isValid");
    ASSERT_TRUE(valid.has_value());
    EXPECT_EQ(expected_validity, *valid);

    const std::string* reason = result->GetDict().FindString("reason");
    ASSERT_TRUE(reason);
    EXPECT_EQ(expected_reason, *reason);
  }

  void ValidateIncognitoExists(bool expected_incognito,
                               size_t expected_total_calls) {
    EXPECT_EQ(expected_total_calls, web_ui()->call_data().size());

    const content::TestWebUI::CallData& data = *web_ui()->call_data().back();
    EXPECT_EQ("cr.webUIListenerCallback", data.function_name());

    ASSERT_TRUE(data.arg1()->is_string());
    EXPECT_EQ("onIncognitoStatusChanged", data.arg1()->GetString());

    ASSERT_TRUE(data.arg2()->is_bool());
    EXPECT_EQ(expected_incognito, data.arg2()->GetBool());
  }

  struct ZoomLevel {
    std::string host_or_spec;
    std::string display_name;
    std::string zoom;
  };

  void ValidateZoom(const std::vector<ZoomLevel>& zoom_levels,
                    size_t expected_total_calls) {
    const size_t zoom_changed_count = std::ranges::count_if(
        web_ui()->call_data(),
        [](const std::unique_ptr<content::TestWebUI::CallData>& call_data_ptr) {
          return call_data_ptr && call_data_ptr->arg1() &&
                 *call_data_ptr->arg1() == "onZoomLevelsChanged";
        });
    EXPECT_EQ(expected_total_calls, zoom_changed_count);

    const content::TestWebUI::CallData& data = *web_ui()->call_data().back();
    EXPECT_EQ("cr.webUIListenerCallback", data.function_name());

    ASSERT_TRUE(data.arg1()->is_string());
    EXPECT_EQ("onZoomLevelsChanged", data.arg1()->GetString());

    ASSERT_TRUE(data.arg2()->is_list());
    const base::ListValue& exceptions = data.arg2()->GetList();
    ASSERT_EQ(zoom_levels.size(), exceptions.size());
    for (size_t i = 0; i < zoom_levels.size(); i++) {
      const ZoomLevel& zoom_level = zoom_levels[i];
      const base::DictValue& exception = exceptions[i].GetDict();

      const std::string* host_or_spec = exception.FindString("hostOrSpec");
      ASSERT_TRUE(host_or_spec);
      ASSERT_EQ(zoom_level.host_or_spec, *host_or_spec);

      const std::string* display_name = exception.FindString("displayName");
      ASSERT_TRUE(display_name);
      ASSERT_EQ(zoom_level.display_name, *display_name);

      const std::string* zoom = exception.FindString("zoom");
      ASSERT_TRUE(zoom);
      ASSERT_EQ(zoom_level.zoom, *zoom);
    }
  }

  void ValidateCookieSettingUpdate(const std::string expected_string,
                                   const int expected_call_index) {
    const content::TestWebUI::CallData& data =
        *web_ui()->call_data()[expected_call_index];

    ASSERT_EQ("cr.webUIListenerCallback", data.function_name());
    ASSERT_EQ("cookieSettingDescriptionChanged", data.arg1()->GetString());
    ASSERT_EQ(expected_string, data.arg2()->GetString());
  }

  void ValidateUsageInfo(const std::string& expected_usage_origin,
                         const std::string& expected_usage_string,
                         const std::string& expected_cookie_string,
                         const std::string& expected_rws_member_count_string,
                         const bool expected_rws_policy) {
    const content::TestWebUI::CallData& data = *web_ui()->call_data().back();
    EXPECT_EQ("cr.webUIListenerCallback", data.function_name());

    ASSERT_TRUE(data.arg_nth(0)->is_string());
    EXPECT_EQ("usage-total-changed", data.arg_nth(0)->GetString());

    ASSERT_TRUE(data.arg_nth(1)->is_string());
    EXPECT_EQ(expected_usage_origin, data.arg_nth(1)->GetString());

    ASSERT_TRUE(data.arg_nth(2)->is_string());
    EXPECT_EQ(expected_usage_string, data.arg_nth(2)->GetString());

    ASSERT_TRUE(data.arg_nth(3)->is_string());
    EXPECT_EQ(expected_cookie_string, data.arg_nth(3)->GetString());

    ASSERT_TRUE(data.arg_nth(4)->is_string());
    EXPECT_EQ(expected_rws_member_count_string, data.arg_nth(4)->GetString());

    ASSERT_TRUE(data.arg_nth(5)->is_bool());
    EXPECT_EQ(expected_rws_policy, data.arg_nth(5)->GetBool());
  }

  void CreateIncognitoProfile() {
    incognito_profile_ = profile_->GetOffTheRecordProfile(
        Profile::OTRProfileID::PrimaryID(), /*create_if_needed=*/true);
  }

  virtual void DestroyIncognitoProfile() {
    if (incognito_profile_) {
      profile_->DestroyOffTheRecordProfile(incognito_profile_);
      incognito_profile_ = nullptr;
    }
  }

  void SetupModel(base::OnceCallback<void(const TestModel& model)> setup) {
    auto fake_browsing_data_model = std::make_unique<FakeBrowsingDataModel>(
        ChromeBrowsingDataModelDelegate::CreateForProfile(profile()));

    std::move(setup).Run({ToRawRef(*fake_browsing_data_model)});

    handler()->SetModelForTesting(std::move(fake_browsing_data_model));
  }

  void SetupModel() {
    SetupModel(base::BindLambdaForTesting([this](const TestModel& model) {
      std::vector<browsing_data_model_test_util::BrowsingDataEntry>
          browsing_data_model_entries = {
              kGoogleUnpartitionedEntry,
              kExampleUnpartitionedEntry,
              kGoogleOnExampleEntry,
              kExampleOnGoogleSecureEntry,
              kExampleOnGoogleInsecureEntry,
              kExampleOnGoogleAuEntry,
              kExampleLocalStorage,
              kHttpExampleCookie,
              kHttpsWwwExampleCookie,
              kPartitionedHttpsWwwExampleOnGoogleAuCookie,
              kPartitionedHttpsWwwExampleOnGoogleCookie,
              kHttpAbcExampleCookie,
              kHttpGoogleCookieA,
              kHttpGoogleCookieB,
              kHttpGoogleAuCookie,
              kPartitionedHttpsGoogleAu1PCookie,
              kPartitionedHttpsWwwAnotherExampleOnGoogleAuCookie,
              kUngroupedHttpCookie,
          };

      for (const auto& entry : browsing_data_model_entries) {
        model.browsing_data_model->AddBrowsingData(
            entry.data_key, *(entry.data_details.storage_types.begin()),
            entry.data_details.storage_size, entry.data_details.cookie_count);
      }
    }));
  }

  void SetupModelWithIsolatedWebAppData(
      const std::vector<std::pair<std::string, int64_t>>& iwa_url_and_usage) {
    SetupModel(base::BindLambdaForTesting([&](const TestModel& model) {
      for (const auto& url_and_usage : iwa_url_and_usage) {
        model.browsing_data_model->AddBrowsingData(
            url::Origin::Create(GURL(url_and_usage.first)),
            static_cast<BrowsingDataModel::StorageType>(
                ChromeBrowsingDataModelDelegate::StorageType::kIsolatedWebApp),
            url_and_usage.second);
      }
    }));
  }

  base::ListValue GetOnStorageFetchedSentList() {
    handler()->ClearAllSitesMapForTesting();

    auto get_all_sites_args = base::ListValue().Append(kCallbackId);
    handler()->HandleGetAllSites(get_all_sites_args);
    handler()->ServicePendingRequests();

    const content::TestWebUI::CallData& data = *web_ui()->call_data().back();
    return data.arg2()->GetList().Clone();
  }

  void SetupDefaultRelatedWebsiteSets(MockPrivacySandboxService* mock_service) {
    EXPECT_CALL(*mock_service, GetRelatedWebsiteSetOwner(_))
        .WillRepeatedly(
            [&](const GURL& url) -> std::optional<net::SchemefulSite> {
              auto related_website_sets = GetTestRelatedWebsiteSets();
              if (related_website_sets.count(net::SchemefulSite(url))) {
                return related_website_sets[net::SchemefulSite(url)];
              }

              return std::nullopt;
            });
  }

  base::flat_map<net::SchemefulSite, net::SchemefulSite>
  GetTestRelatedWebsiteSets() {
    base::flat_map<net::SchemefulSite, net::SchemefulSite>
        related_website_sets = {{ConvertEtldToSchemefulSite("google.com"),
                                 ConvertEtldToSchemefulSite("google.com")},
                                {ConvertEtldToSchemefulSite("google.com.au"),
                                 ConvertEtldToSchemefulSite("google.com")},
                                {ConvertEtldToSchemefulSite("example.com"),
                                 ConvertEtldToSchemefulSite("example.com")},
                                {ConvertEtldToSchemefulSite("unrelated.com"),
                                 ConvertEtldToSchemefulSite("unrelated.com")}};

    return related_website_sets;
  }

  scoped_refptr<const extensions::Extension> LoadExtension(
      const std::string& extension_name) {
    auto extension = extensions::ExtensionBuilder()
                         .SetManifest(base::DictValue()
                                          .Set("name", kExtensionName)
                                          .Set("version", "1.0.0")
                                          .Set("manifest_version", 3))
                         .Build();

    extensions::TestExtensionSystem* extension_system =
        static_cast<extensions::TestExtensionSystem*>(
            extensions::ExtensionSystem::Get(profile()));
    extension_system->CreateExtensionService(
        base::CommandLine::ForCurrentProcess(), base::FilePath(),
        /*autoupdate_enabled=*/false);
    extensions::ExtensionRegistrar::Get(profile())->AddExtension(
        extension.get());
    return extension;
  }

  void UnloadExtension(std::string extension_id) {
    extensions::ExtensionRegistrar::Get(profile())->RemoveExtension(
        extension_id, extensions::UnloadedExtensionReason::DISABLE);
  }

  void ValidateCallbacksForNotificationPermission(int index) {
    // When a notification permission is set or reset, there are two consecutive
    // callbacks. The first one is to notify content setting observers, and
    // the second one is to update safety check notification permission review.
    ASSERT_EQ("contentSettingSitePermissionChanged",
              web_ui()->call_data()[index]->arg1()->GetString());
    ASSERT_EQ("notification-permission-review-list-maybe-changed",
              web_ui()->call_data()[index + 1]->arg1()->GetString());
  }

  // Content setting group name for the relevant ContentSettingsType.
  const std::string_view kNotifications =
      site_settings::ContentSettingsTypeToGroupName(
          ContentSettingsType::NOTIFICATIONS);
  const std::string_view kCookies =
      site_settings::ContentSettingsTypeToGroupName(
          ContentSettingsType::COOKIES);
  const std::string_view kStorageAccess =
      site_settings::ContentSettingsTypeToGroupName(
          ContentSettingsType::STORAGE_ACCESS);
  const std::string_view kGeolocation =
      site_settings::ContentSettingsTypeToGroupName(
          ContentSettingsType::GEOLOCATION);

  const ContentSettingsType kPermissionNotifications =
      ContentSettingsType::NOTIFICATIONS;
  const ContentSettingsType kPermissionStorageAccess =
      ContentSettingsType::STORAGE_ACCESS;

  // The number of listeners that are expected to fire when notification content
  // setting is changed.
  const size_t kNumberNotificationsContentSettingListeners = 2;

  // The number of listeners that are expected to fire when cookies content
  // setting is changed.
  const size_t kNumberCookiesContentSettingListeners = 1;

  // Browsing data model constants. Here, instead of in the anon namespace, to
  // avoid static GURL creation.
  const browsing_data_model_test_util::BrowsingDataEntry
      kGoogleUnpartitionedEntry{
          "www.google.com",
          url::Origin::Create(GURL("https://www.google.com")),
          {{BrowsingDataModel::StorageType::kTrustTokens}, 50000000000, 0}};
  const browsing_data_model_test_util::BrowsingDataEntry
      kExampleUnpartitionedEntry{
          "www.example.com",
          url::Origin::Create(GURL("https://www.example.com")),
          {{BrowsingDataModel::StorageType::kTrustTokens}, 100, 0}};
  const browsing_data_model_test_util::BrowsingDataEntry kGoogleOnExampleEntry{
      "google.com",
      blink::StorageKey::Create(
          url::Origin::Create(GURL("https://google.com/")),
          net::SchemefulSite(
              url::Origin::Create(GURL("https://www.example.com/"))),
          blink::mojom::AncestorChainBit::kCrossSite,
          /*third_party_partitioning_allowed=*/true),
      {{BrowsingDataModel::StorageType::kQuotaStorage}, 100, 0}};
  const browsing_data_model_test_util::BrowsingDataEntry
      kExampleOnGoogleSecureEntry{
          "www.example.com",
          blink::StorageKey::Create(
              url::Origin::Create(GURL("https://www.example.com/")),
              net::SchemefulSite(
                  url::Origin::Create(GURL("https://www.google.com/"))),
              blink::mojom::AncestorChainBit::kCrossSite,
              /*third_party_partitioning_allowed=*/true),
          {{BrowsingDataModel::StorageType::kQuotaStorage}, 100, 0}};
  const browsing_data_model_test_util::BrowsingDataEntry
      kExampleOnGoogleInsecureEntry{
          "www.example.com",
          blink::StorageKey::Create(
              url::Origin::Create(GURL("https://www.example.com/")),
              net::SchemefulSite(
                  url::Origin::Create(GURL("http://www.google.com/"))),
              blink::mojom::AncestorChainBit::kCrossSite,
              /*third_party_partitioning_allowed=*/true),
          {{BrowsingDataModel::StorageType::kQuotaStorage}, 100, 0}};

  const browsing_data_model_test_util::BrowsingDataEntry
      kExampleOnGoogleAuEntry{
          "www.example.com",
          blink::StorageKey::Create(
              url::Origin::Create(GURL("https://www.example.com/")),
              net::SchemefulSite(
                  url::Origin::Create(GURL("https://www.google.com.au/"))),
              blink::mojom::AncestorChainBit::kCrossSite,
              /*third_party_partitioning_allowed=*/true),
          {{BrowsingDataModel::StorageType::kQuotaStorage}, 400, 0}};

  const browsing_data_model_test_util::BrowsingDataEntry kHttpExampleCookie{
      "example.com",
      *(CreateCookieKey(GURL("http://example.com"), "A=1")),
      {{BrowsingDataModel::StorageType::kCookie},
       /*storage_size=*/0,
       /*cookie_count=*/1}};

  const browsing_data_model_test_util::BrowsingDataEntry kExampleLocalStorage{
      "www.example.com",
      blink::StorageKey::CreateFromStringForTesting("https://www.example.com/"),
      {{BrowsingDataModel::StorageType::kLocalStorage},
       /*storage_size=*/2,
       /*cookie_count=*/0}};

  const browsing_data_model_test_util::BrowsingDataEntry kHttpsWwwExampleCookie{
      "www.example.com",
      *(CreateCookieKey(GURL("https://www.example.com"), "B=1")),
      {{BrowsingDataModel::StorageType::kCookie},
       /*storage_size=*/0,
       /*cookie_count=*/1}};

  const browsing_data_model_test_util::BrowsingDataEntry
      kPartitionedHttpsWwwExampleOnGoogleAuCookie{
          "www.example.com",
          *(CreateCookieKey(GURL("https://www.example.com"),
                            "__Host-A=1; Path=/; Partitioned; Secure;",
                            net::CookiePartitionKey::FromURLForTesting(
                                GURL("https://google.com.au")))),
          {{BrowsingDataModel::StorageType::kCookie},
           /*storage_size=*/0,
           /*cookie_count=*/1}};

  const browsing_data_model_test_util::BrowsingDataEntry
      kPartitionedHttpsWwwExampleOnGoogleCookie{
          "www.example.com",
          *(CreateCookieKey(GURL("https://www.example.com"),
                            "__Host-A=1; Path=/; Partitioned; Secure;",
                            net::CookiePartitionKey::FromURLForTesting(
                                GURL("https://google.com")))),
          {{BrowsingDataModel::StorageType::kCookie},
           /*storage_size=*/0,
           /*cookie_count=*/1}};

  const browsing_data_model_test_util::BrowsingDataEntry kHttpAbcExampleCookie{
      "abc.example.com",
      *(CreateCookieKey(GURL("http://abc.example.com"), "C=1")),
      {{BrowsingDataModel::StorageType::kCookie},
       /*storage_size=*/0,
       /*cookie_count=*/1}};

  const browsing_data_model_test_util::BrowsingDataEntry kHttpGoogleCookieA{
      "google.com",
      *(CreateCookieKey(GURL("http://google.com"), "A=1")),
      {{BrowsingDataModel::StorageType::kCookie},
       /*storage_size=*/0,
       /*cookie_count=*/1}};

  const browsing_data_model_test_util::BrowsingDataEntry kHttpGoogleCookieB{
      "google.com",
      *(CreateCookieKey(GURL("http://google.com"), "B=1")),
      {{BrowsingDataModel::StorageType::kCookie},
       /*storage_size=*/0,
       /*cookie_count=*/1}};

  const browsing_data_model_test_util::BrowsingDataEntry kHttpGoogleAuCookie{
      "google.com.au",
      *(CreateCookieKey(GURL("http://google.com.au"), "A=1")),
      {{BrowsingDataModel::StorageType::kCookie},
       /*storage_size=*/0,
       /*cookie_count=*/1}};

  const browsing_data_model_test_util::BrowsingDataEntry
      kPartitionedHttpsGoogleAu1PCookie{
          "google.com.au",
          *(CreateCookieKey(GURL("https://google.com.au"),
                            "__Host-A=1; Path=/; Partitioned; Secure;",
                            net::CookiePartitionKey::FromURLForTesting(
                                GURL("https://google.com.au")))),
          {{BrowsingDataModel::StorageType::kCookie},
           /*storage_size=*/0,
           /*cookie_count=*/1}};

  const browsing_data_model_test_util::BrowsingDataEntry
      kPartitionedHttpsWwwAnotherExampleOnGoogleAuCookie{
          "www.another-example.com",
          *(CreateCookieKey(GURL("https://www.another-example.com"),
                            "__Host-A=1; Path=/; Partitioned; Secure;",
                            net::CookiePartitionKey::FromURLForTesting(
                                GURL("https://google.com.au")))),
          {{BrowsingDataModel::StorageType::kCookie},
           /*storage_size=*/0,
           /*cookie_count=*/1}};

  const browsing_data_model_test_util::BrowsingDataEntry kUngroupedHttpCookie{
      "ungrouped.com",
      *(CreateCookieKey(GURL("http://ungrouped.com"), "A=1")),
      {{BrowsingDataModel::StorageType::kCookie},
       /*storage_size=*/0,
       /*cookie_count=*/1}};

 protected:
  virtual void SetUpIsolatedWebApp() {}

 private:
  content::BrowserTaskEnvironment task_environment_{
      base::test::TaskEnvironment::TimeSource::MOCK_TIME};
  raw_ptr<TestingProfileManager> profile_manager_ = nullptr;
  raw_ptr<TestingProfile> profile_ = nullptr;
  raw_ptr<Profile, DanglingUntriaged> incognito_profile_ = nullptr;
  content::TestWebUI web_ui_;
  std::unique_ptr<SiteSettingsHandler> handler_;
#if BUILDFLAG(IS_CHROMEOS)
  std::unique_ptr<user_manager::ScopedUserManager> scoped_user_manager_;
#endif
  raw_ptr<MockPrivacySandboxService> mock_privacy_sandbox_service_;
};

class SiteSettingsHandlerSchemeTest : public SiteSettingsHandlerBaseTest,
                                      public testing::WithParamInterface<bool> {
 protected:
  bool IsHttps() { return GetParam(); }
};

// True if testing for handle clear unpartitioned usage with
// HTTPS scheme URL. When set to true, the tests use HTTPS scheme as origin.
// When set to false, the tests use HTTP scheme as origin.
INSTANTIATE_TEST_SUITE_P(All, SiteSettingsHandlerSchemeTest, testing::Bool());

TEST_P(SiteSettingsHandlerSchemeTest, StorageAccessExceptions_Description_All) {
  const std::string kOrigin("google.com");
  const std::string kEmbeddingOrigin("example.com");

  const ContentSetting content_setting =
      IsHttps() ? CONTENT_SETTING_ALLOW : CONTENT_SETTING_BLOCK;

  SetContentSettingCustomScope(kOrigin, kEmbeddingOrigin,
                               kPermissionStorageAccess, content_setting);

  base::ListValue get_exception_list_args;
  get_exception_list_args.Append(kCallbackId);
  get_exception_list_args.Append(
      content_settings::ContentSettingToString(content_setting));
  handler()->HandleGetStorageAccessExceptionList(get_exception_list_args);

  // Verify that the grouped exception is correct.
  ValidateStorageAccessList(/*expected_total_calls=*/2U,
                            /*expected_num_groups=*/1U);

  ValidateStorageAccessException(
      kOrigin, kOrigin, content_setting,
      {{kEmbeddingOrigin, kEmbeddingOrigin, /*incognito=*/false}});
}

TEST_P(SiteSettingsHandlerSchemeTest, HandleClearUnpartitionedUsage) {
  SetupModel();
  std::vector<browsing_data_model_test_util::BrowsingDataEntry>
      expected_browsing_data_model_entries = {
          kGoogleUnpartitionedEntry,
          kExampleUnpartitionedEntry,
          kGoogleOnExampleEntry,
          kExampleOnGoogleSecureEntry,
          kExampleOnGoogleInsecureEntry,
          kExampleOnGoogleAuEntry,
          kExampleLocalStorage,
          kHttpExampleCookie,
          kHttpsWwwExampleCookie,
          kPartitionedHttpsWwwExampleOnGoogleAuCookie,
          kPartitionedHttpsWwwExampleOnGoogleCookie,
          kHttpAbcExampleCookie,
          kHttpGoogleCookieA,
          kHttpGoogleCookieB,
          kHttpGoogleAuCookie,
          kPartitionedHttpsGoogleAu1PCookie,
          kPartitionedHttpsWwwAnotherExampleOnGoogleAuCookie,
          kUngroupedHttpCookie,
      };
  browsing_data_model_test_util::ValidateBrowsingDataEntries(
      handler()->GetBrowsingDataModelForTesting(),
      expected_browsing_data_model_entries);

  base::ListValue args;
  args.Append(IsHttps() ? "https://www.example.com/"
                        : "http://www.example.com/");
  handler()->HandleClearUnpartitionedUsage(args);

  // Confirm that only the unpartitioned items for example.com have been
  // cleared.
  RemoveModelEntries(expected_browsing_data_model_entries,
                     {
                         kExampleUnpartitionedEntry,
                         kExampleLocalStorage,
                         kHttpsWwwExampleCookie,
                     });
  browsing_data_model_test_util::ValidateBrowsingDataEntries(
      handler()->GetBrowsingDataModelForTesting(),
      expected_browsing_data_model_entries);

  args = base::ListValue();
  args.Append("https://google.com.au/");
  handler()->HandleClearUnpartitionedUsage(args);

  // First-party partitioned storage should be cleared.
  RemoveModelEntries(expected_browsing_data_model_entries,
                     {
                         kHttpGoogleAuCookie,
                         kPartitionedHttpsGoogleAu1PCookie,
                     });
  browsing_data_model_test_util::ValidateBrowsingDataEntries(
      handler()->GetBrowsingDataModelForTesting(),
      expected_browsing_data_model_entries);

  args = base::ListValue();
  args.Append("https://www.google.com/");
  handler()->HandleClearUnpartitionedUsage(args);

  // The unpartitioned Google and Example entries should be removed.
  RemoveModelEntries(expected_browsing_data_model_entries,
                     {
                         kGoogleUnpartitionedEntry,
                     });
  browsing_data_model_test_util::ValidateBrowsingDataEntries(
      handler()->GetBrowsingDataModelForTesting(),
      expected_browsing_data_model_entries);
}

class SiteSettingsHandlerTest : public SiteSettingsHandlerBaseTest {
#if BUILDFLAG(IS_CHROMEOS)
 private:
  base::test::ScopedFeatureList feature_list_{blink::features::kSmartCard};
#endif  // BUILDFLAG(IS_CHROMEOS)
};

TEST_F(SiteSettingsHandlerTest, GetAndSetDefault) {
  // Test the JS -> C++ -> JS callback path for getting and setting defaults.
  base::ListValue get_args;
  get_args.Append(kCallbackId);
  get_args.Append(kNotifications);
  handler()->HandleGetDefaultValueForContentType(get_args);
  ValidateDefault(CONTENT_SETTING_ASK, "", 1U);

  // Set the default to 'Blocked'.
  base::ListValue set_args;
  set_args.Append(kNotifications);
  set_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_BLOCK));
  handler()->HandleSetDefaultValueForContentType(set_args);

  EXPECT_EQ(2U, web_ui()->call_data().size());

  // Verify that the default has been set to 'Blocked'.
  handler()->HandleGetDefaultValueForContentType(get_args);
  ValidateDefault(CONTENT_SETTING_BLOCK, "", 3U);
}

TEST_F(SiteSettingsHandlerTest, GetEnforcedDefault) {
  ContentSettingSourceSetter source_setter(profile(),
                                           ContentSettingsType::NOTIFICATIONS);
  source_setter.SetPolicyDefault(CONTENT_SETTING_ALLOW);

  base::ListValue get_args;
  get_args.Append(kCallbackId);
  get_args.Append(kNotifications);
  handler()->HandleGetDefaultValueForContentType(get_args);
  ValidateDefault(CONTENT_SETTING_ALLOW, "policy", 1U);
}

// Flaky on CrOS and Linux. https://crbug.com/41440409
TEST_F(SiteSettingsHandlerTest, GetAllSites) {
  SetupModel();

  base::ListValue get_all_sites_args;
  get_all_sites_args.Append(kCallbackId);

  // Test all sites is empty when there are no preferences.
  handler()->HandleGetAllSites(get_all_sites_args);
  EXPECT_EQ(1U, web_ui()->call_data().size());

  {
    const content::TestWebUI::CallData& data = *web_ui()->call_data().back();
    EXPECT_EQ("cr.webUIResponse", data.function_name());
    EXPECT_EQ(kCallbackId, data.arg1()->GetString());
    ASSERT_TRUE(data.arg2()->GetBool());

    const base::ListValue& site_groups = data.arg3()->GetList();
    EXPECT_TRUE(site_groups.empty());
  }

  // Add a couple of exceptions and check they appear in all sites.
  HostContentSettingsMap* map =
      HostContentSettingsMapFactory::GetForProfile(profile());
  const GURL url1("http://example.com");
  const GURL url2("https://other.example.com");
  map->SetContentSettingDefaultScope(
      url1, url1, ContentSettingsType::NOTIFICATIONS, CONTENT_SETTING_BLOCK);
  map->SetContentSettingDefaultScope(
      url2, url2, ContentSettingsType::NOTIFICATIONS, CONTENT_SETTING_ALLOW);
  handler()->HandleGetAllSites(get_all_sites_args);

  {
    const content::TestWebUI::CallData& data = *web_ui()->call_data().back();
    EXPECT_EQ("cr.webUIResponse", data.function_name());
    EXPECT_EQ(kCallbackId, data.arg1()->GetString());
    ASSERT_TRUE(data.arg2()->GetBool());

    const base::ListValue& site_groups = data.arg3()->GetList();
    EXPECT_EQ(1UL, site_groups.size());
    for (const base::Value& site_group_val : site_groups) {
      const base::DictValue& site_group = site_group_val.GetDict();
      const base::ListValue& origin_list =
          CHECK_DEREF(site_group.FindList("origins"));
      EXPECT_THAT(CHECK_DEREF(site_group.FindString("groupingKey")),
                  IsEtldPlus1("example.com"));
      EXPECT_EQ(2UL, origin_list.size());
      const base::DictValue& first_origin = origin_list[0].GetDict();
      const base::DictValue& second_origin = origin_list[1].GetDict();
      EXPECT_EQ(url1.spec(), CHECK_DEREF(first_origin.FindString("origin")));
      EXPECT_EQ(0, first_origin.FindDouble("engagement"));
      EXPECT_EQ(url2.spec(), CHECK_DEREF(second_origin.FindString("origin")));
      EXPECT_EQ(0, second_origin.FindDouble("engagement"));
    }
  }

  // Add an additional exception belonging to a different eTLD+1.
  const GURL url3("https://example2.net");
  map->SetContentSettingDefaultScope(
      url3, url3, ContentSettingsType::NOTIFICATIONS, CONTENT_SETTING_BLOCK);
  handler()->HandleGetAllSites(get_all_sites_args);

  {
    const content::TestWebUI::CallData& data = *web_ui()->call_data().back();
    EXPECT_EQ("cr.webUIResponse", data.function_name());

    EXPECT_EQ(kCallbackId, data.arg1()->GetString());
    ASSERT_TRUE(data.arg2()->GetBool());

    const base::ListValue& site_groups = data.arg3()->GetList();
    EXPECT_EQ(2UL, site_groups.size());
    for (const base::Value& site_group_val : site_groups) {
      const base::DictValue& site_group = site_group_val.GetDict();
      const std::string& grouping_key_string =
          CHECK_DEREF(site_group.FindString("groupingKey"));
      auto grouping_key = GroupingKey::Deserialize(grouping_key_string);
      const base::ListValue& origin_list =
          CHECK_DEREF(site_group.FindList("origins"));
      if (grouping_key.GetEtldPlusOne() == "example2.net") {
        EXPECT_EQ(1UL, origin_list.size());
        const base::DictValue& first_origin = origin_list[0].GetDict();
        EXPECT_EQ(url3.spec(), CHECK_DEREF(first_origin.FindString("origin")));
      } else {
        EXPECT_THAT(grouping_key_string, IsEtldPlus1("example.com"));
      }
    }
  }

  // Test embargoed settings also appear.
  permissions::PermissionDecisionAutoBlocker* auto_blocker =
      PermissionDecisionAutoBlockerFactory::GetForProfile(profile());
  base::SimpleTestClock clock;
  clock.SetNow(base::Time::Now());
  auto_blocker->SetClockForTesting(&clock);
  const GURL url4("https://example2.co.uk");
  for (int i = 0; i < 3; ++i) {
    auto_blocker->RecordDismissAndEmbargo(
        url4, ContentSettingsType::NOTIFICATIONS, false);
  }
  EXPECT_EQ(
      PermissionStatus::DENIED,
      auto_blocker->GetEmbargoResult(url4, ContentSettingsType::NOTIFICATIONS)
          ->status);
  handler()->HandleGetAllSites(get_all_sites_args);

  {
    const content::TestWebUI::CallData& data = *web_ui()->call_data().back();
    EXPECT_EQ("cr.webUIResponse", data.function_name());
    EXPECT_EQ(kCallbackId, data.arg1()->GetString());
    ASSERT_TRUE(data.arg2()->GetBool());

    EXPECT_EQ(3UL, data.arg3()->GetList().size());
  }

  // Check |url4| disappears from the list when its embargo expires.
  clock.Advance(base::Days(8));
  handler()->HandleGetAllSites(get_all_sites_args);

  {
    const content::TestWebUI::CallData& data = *web_ui()->call_data().back();
    EXPECT_EQ("cr.webUIResponse", data.function_name());
    EXPECT_EQ(kCallbackId, data.arg1()->GetString());
    ASSERT_TRUE(data.arg2()->GetBool());

    const base::ListValue& site_groups = data.arg3()->GetList();
    EXPECT_EQ(2UL, site_groups.size());
    EXPECT_THAT(CHECK_DEREF(site_groups[0].GetDict().FindString("groupingKey")),
                IsEtldPlus1("example.com"));
    EXPECT_THAT(CHECK_DEREF(site_groups[1].GetDict().FindString("groupingKey")),
                IsEtldPlus1("example2.net"));
  }

  // Add an expired embargo setting to an existing eTLD+1 group and make sure it
  // still appears.
  for (int i = 0; i < 3; ++i) {
    auto_blocker->RecordDismissAndEmbargo(
        url3, ContentSettingsType::NOTIFICATIONS, false);
  }
  EXPECT_EQ(
      PermissionStatus::DENIED,
      auto_blocker->GetEmbargoResult(url3, ContentSettingsType::NOTIFICATIONS)
          ->status);
  clock.Advance(base::Days(8));
  EXPECT_FALSE(
      auto_blocker->GetEmbargoResult(url3, ContentSettingsType::NOTIFICATIONS)
          .has_value());

  handler()->HandleGetAllSites(get_all_sites_args);
  {
    const content::TestWebUI::CallData& data = *web_ui()->call_data().back();
    EXPECT_EQ("cr.webUIResponse", data.function_name());
    EXPECT_EQ(kCallbackId, data.arg1()->GetString());
    ASSERT_TRUE(data.arg2()->GetBool());

    const base::ListValue& site_groups = data.arg3()->GetList();
    EXPECT_EQ(2UL, site_groups.size());
    EXPECT_THAT(CHECK_DEREF(site_groups[0].GetDict().FindString("groupingKey")),
                IsEtldPlus1("example.com"));
    EXPECT_THAT(CHECK_DEREF(site_groups[1].GetDict().FindString("groupingKey")),
                IsEtldPlus1("example2.net"));
  }

  // Add an expired embargo to a new eTLD+1 and make sure it doesn't appear.
  const GURL url5("http://test.example5.com");
  for (int i = 0; i < 3; ++i) {
    auto_blocker->RecordDismissAndEmbargo(
        url5, ContentSettingsType::NOTIFICATIONS, false);
  }
  EXPECT_EQ(
      PermissionStatus::DENIED,
      auto_blocker->GetEmbargoResult(url5, ContentSettingsType::NOTIFICATIONS)
          ->status);
  clock.Advance(base::Days(8));
  EXPECT_FALSE(
      auto_blocker->GetEmbargoResult(url5, ContentSettingsType::NOTIFICATIONS)
          .has_value());

  handler()->HandleGetAllSites(get_all_sites_args);
  {
    const content::TestWebUI::CallData& data = *web_ui()->call_data().back();
    EXPECT_EQ("cr.webUIResponse", data.function_name());
    EXPECT_EQ(kCallbackId, data.arg1()->GetString());
    ASSERT_TRUE(data.arg2()->GetBool());

    const base::ListValue& site_groups = data.arg3()->GetList();
    EXPECT_EQ(2UL, site_groups.size());
    EXPECT_THAT(CHECK_DEREF(site_groups[0].GetDict().FindString("groupingKey")),
                IsEtldPlus1("example.com"));
    EXPECT_THAT(CHECK_DEREF(site_groups[1].GetDict().FindString("groupingKey")),
                IsEtldPlus1("example2.net"));
  }

  // Same extension url from different content setting types shows only one
  // extension site group.
  auto extension = LoadExtension(kExtensionName);
  map->SetContentSettingDefaultScope(extension->url(), extension->url(),
                                     ContentSettingsType::NOTIFICATIONS,
                                     CONTENT_SETTING_BLOCK);
  map->SetContentSettingDefaultScope(extension->url(), extension->url(),
                                     ContentSettingsType::GEOLOCATION,
                                     CONTENT_SETTING_BLOCK);
  handler()->HandleGetAllSites(get_all_sites_args);
  {
    const content::TestWebUI::CallData& data = *web_ui()->call_data().back();
    EXPECT_EQ("cr.webUIResponse", data.function_name());
    EXPECT_EQ(kCallbackId, data.arg1()->GetString());
    ASSERT_TRUE(data.arg2()->GetBool());

    const base::ListValue& site_groups = data.arg3()->GetList();
    EXPECT_EQ(3UL, site_groups.size());
    // Extension groupingKey will be its origin with the pattern
    // "chrome-extension://<extension_id>" so it is before other site groups in
    // the list.
    EXPECT_THAT(CHECK_DEREF(site_groups[0].GetDict().FindString("groupingKey")),
                IsOrigin(extension->url()));
    EXPECT_EQ(nullptr, site_groups[0].GetDict().FindString("etldPlus1"));
    EXPECT_THAT(CHECK_DEREF(site_groups[1].GetDict().FindString("groupingKey")),
                IsEtldPlus1("example.com"));
    EXPECT_EQ("example.com",
              CHECK_DEREF(site_groups[1].GetDict().FindString("etldPlus1")));
    EXPECT_THAT(CHECK_DEREF(site_groups[2].GetDict().FindString("groupingKey")),
                IsEtldPlus1("example2.net"));
    EXPECT_EQ("example2.net",
              CHECK_DEREF(site_groups[2].GetDict().FindString("etldPlus1")));
  }

  // Each call to HandleGetAllSites() above added a callback to the profile's
  // browsing_data::LocalStorageHelper, so make sure these aren't stuck waiting
  // to run at the end of the test.
  base::RunLoop run_loop;
  run_loop.RunUntilIdle();
}

TEST_F(SiteSettingsHandlerTest, GetAllSitesIncludesStorage) {
  SetupModel();

  base::ListValue get_all_sites_args;
  get_all_sites_args.Append(kCallbackId);

  // Test all sites is empty when there are no preferences.
  handler()->HandleGetAllSites(get_all_sites_args);
  EXPECT_EQ(1U, web_ui()->call_data().size());

  // Add a couple of double-keyed exceptions and check that they appear for the
  // primary (embedded) site using a representative URL.
  HostContentSettingsMap* map =
      HostContentSettingsMapFactory::GetForProfile(profile());

  const GURL embedded_url("https://altostrat.com");
  const GURL embedding_url1("https://examplepetstore.com");
  const GURL embedding_url2("https://cymbalgroup.com");
  map->SetContentSettingDefaultScope(embedded_url, embedding_url1,
                                     ContentSettingsType::STORAGE_ACCESS,
                                     CONTENT_SETTING_ALLOW);
  map->SetContentSettingDefaultScope(embedded_url, embedding_url2,
                                     ContentSettingsType::STORAGE_ACCESS,
                                     CONTENT_SETTING_ALLOW);
  handler()->HandleGetAllSites(get_all_sites_args);

  {
    const content::TestWebUI::CallData& data = *web_ui()->call_data().back();
    EXPECT_EQ("cr.webUIResponse", data.function_name());
    EXPECT_EQ(kCallbackId, data.arg1()->GetString());
    ASSERT_TRUE(data.arg2()->GetBool());
    const base::ListValue& site_groups = data.arg3()->GetList();
    EXPECT_EQ(1UL, site_groups.size());
    for (const base::Value& site_group_val : site_groups) {
      const base::DictValue& site_group = site_group_val.GetDict();
      const base::ListValue& origin_list =
          CHECK_DEREF(site_group.FindList("origins"));
      EXPECT_THAT(CHECK_DEREF(site_group.FindString("groupingKey")),
                  IsEtldPlus1("altostrat.com"));
      EXPECT_EQ(1UL, origin_list.size());
    }
  }

  // Now remove the double-keyed exceptions using SetOriginPermissions and
  // verify that the list is empty.
  base::ListValue reset_args;
  reset_args.Append(embedded_url.spec());
  reset_args.Append(std::move(kStorageAccess));
  reset_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_DEFAULT));

  handler()->HandleSetOriginPermissions(reset_args);

  handler()->HandleGetAllSites(get_all_sites_args);

  {
    const content::TestWebUI::CallData& data = *web_ui()->call_data().back();
    EXPECT_EQ("cr.webUIResponse", data.function_name());
    EXPECT_EQ(kCallbackId, data.arg1()->GetString());
    ASSERT_TRUE(data.arg2()->GetBool());
    const base::ListValue& site_groups = data.arg3()->GetList();
    EXPECT_EQ(0UL, site_groups.size());
  }

  // Each call to HandleGetAllSites() above added a callback to the profile's
  // browsing_data::LocalStorageHelper, so make sure these aren't stuck waiting
  // to run at the end of the test.
  base::RunLoop().RunUntilIdle();
}

TEST_F(SiteSettingsHandlerTest, Cookies) {
  base::ListValue get_all_sites_args;
  get_all_sites_args.Append(kCallbackId);

  // Tests that a cookie eTLD+1 origin, which should use a placeholder in
  // AllSitesMap, returns the correct origin in GetAllSites.
  // This corresponds to case 1 in InsertOriginIntoGroup.
  {
    SetupModel(base::BindLambdaForTesting([](const TestModel& model) {
      model.browsing_data_model->AddBrowsingData(
          *(CreateCookieKey(GURL("http://c1.com"), "A=1")),
          BrowsingDataModel::StorageType::kCookie,
          /*storage_size=*/0,
          /*cookie_count=*/1);
    }));

    base::ListValue site_groups = GetOnStorageFetchedSentList();

    ASSERT_EQ(1UL, site_groups.size());
    const base::DictValue& first_group = site_groups[0].GetDict();
    EXPECT_THAT(CHECK_DEREF(first_group.FindString("groupingKey")),
                IsEtldPlus1("c1.com"));
    EXPECT_EQ(1, *first_group.FindInt("numCookies"));
    const base::ListValue& first_group_origins =
        CHECK_DEREF(first_group.FindList("origins"));
    ASSERT_EQ(1UL, first_group_origins.size());
    EXPECT_EQ(
        "http://c1.com/",
        CHECK_DEREF(first_group_origins[0].GetDict().FindString("origin")));
    EXPECT_EQ(1, first_group_origins[0].GetDict().FindInt("numCookies"));
  }

  // Tests that multiple cookie eTLD+1 origins result in a single origin being
  // returned in GetAllSites.
  // This corresponds to case 2 in InsertOriginIntoGroup.
  {
    SetupModel(base::BindLambdaForTesting([](const TestModel& model) {
      model.browsing_data_model->AddBrowsingData(
          *(CreateCookieKey(GURL("https://c2.com"), "A=1")),
          BrowsingDataModel::StorageType::kCookie,
          /*storage_size=*/0,
          /*cookie_count=*/1);
      model.browsing_data_model->AddBrowsingData(
          *(CreateCookieKey(GURL("https://c2.com"), "B=1")),
          BrowsingDataModel::StorageType::kCookie,
          /*storage_size=*/0,
          /*cookie_count=*/1);
    }));

    base::ListValue site_groups = GetOnStorageFetchedSentList();

    ASSERT_EQ(1UL, site_groups.size());
    const base::DictValue& first_group = site_groups[0].GetDict();
    EXPECT_THAT(CHECK_DEREF(first_group.FindString("groupingKey")),
                IsEtldPlus1("c2.com"));
    EXPECT_EQ(2, *first_group.FindInt("numCookies"));
    const base::ListValue& first_group_origins =
        CHECK_DEREF(first_group.FindList("origins"));
    ASSERT_EQ(1UL, first_group_origins.size());
    EXPECT_EQ(
        "http://c2.com/",
        CHECK_DEREF(first_group_origins[0].GetDict().FindString("origin")));
    EXPECT_EQ(2, first_group_origins[0].GetDict().FindInt("numCookies"));
  }

  // Tests that an HTTP cookie origin will reuse an equivalent HTTPS origin if
  // one exists.
  // This corresponds to case 3 in InsertOriginIntoGroup.
  {
    SetupModel(base::BindLambdaForTesting([](const TestModel& model) {
      model.browsing_data_model->AddBrowsingData(
          url::Origin::Create(GURL("https://w.c3.com")),
          BrowsingDataModel::StorageType::kTrustTokens, 50);
      model.browsing_data_model->AddBrowsingData(
          *(CreateCookieKey(GURL("http://w.c3.com"), "A=1")),
          BrowsingDataModel::StorageType::kCookie,
          /*storage_size=*/0,
          /*cookie_count=*/1);
    }));

    base::ListValue site_groups = GetOnStorageFetchedSentList();

    ASSERT_EQ(1UL, site_groups.size());
    const base::DictValue& first_group = site_groups[0].GetDict();
    EXPECT_THAT(CHECK_DEREF(first_group.FindString("groupingKey")),
                IsEtldPlus1("c3.com"));
    EXPECT_EQ(1, *first_group.FindInt("numCookies"));
    const base::ListValue& first_group_origins =
        CHECK_DEREF(first_group.FindList("origins"));
    ASSERT_EQ(1UL, first_group_origins.size());
    EXPECT_EQ(
        "https://w.c3.com/",
        CHECK_DEREF(first_group_origins[0].GetDict().FindString("origin")));
    EXPECT_EQ(1, first_group_origins[0].GetDict().FindInt("numCookies"));
  }

  // Tests that placeholder cookie eTLD+1 origins get removed from AllSitesMap
  // when a more specific origin is added later.
  {
    SetupModel(base::BindLambdaForTesting([](const TestModel& model) {
      model.browsing_data_model->AddBrowsingData(
          *(CreateCookieKey(GURL("https://c4.com"), "B=1")),
          BrowsingDataModel::StorageType::kCookie,
          /*storage_size=*/0,
          /*cookie_count=*/1);
      model.browsing_data_model->AddBrowsingData(
          *(CreateCookieKey(GURL("https://w.c4.com"), "A=1")),
          BrowsingDataModel::StorageType::kCookie,
          /*storage_size=*/0,
          /*cookie_count=*/1);
    }));

    base::ListValue site_groups = GetOnStorageFetchedSentList();

    ASSERT_EQ(1UL, site_groups.size());
    const base::DictValue& first_group = site_groups[0].GetDict();
    EXPECT_THAT(CHECK_DEREF(first_group.FindString("groupingKey")),
                IsEtldPlus1("c4.com"));
    EXPECT_EQ(2, *first_group.FindInt("numCookies"));
    const base::ListValue& first_group_origins =
        CHECK_DEREF(first_group.FindList("origins"));
    ASSERT_EQ(1UL, first_group_origins.size());
    EXPECT_EQ(
        "https://w.c4.com/",
        CHECK_DEREF(first_group_origins[0].GetDict().FindString("origin")));
    EXPECT_EQ(1, first_group_origins[0].GetDict().FindInt("numCookies"));
  }
}

TEST_F(SiteSettingsHandlerTest, GetRecentSitePermissions) {
  // Constants used only in this test.
  std::string kAllowed =
      content_settings::ContentSettingToString(CONTENT_SETTING_ALLOW);
  std::string kBlocked = content_settings::ContentSettingToString(
      ContentSetting::CONTENT_SETTING_BLOCK);
  std::string kEmbargo =
      SiteSettingSourceToString(site_settings::SiteSettingSource::kEmbargo);
  std::string kPreference =
      SiteSettingSourceToString(site_settings::SiteSettingSource::kPreference);

  base::ListValue get_recent_permissions_args;
  get_recent_permissions_args.Append(kCallbackId);
  get_recent_permissions_args.Append(3);

  // Configure prefs and auto blocker with a controllable clock.
  base::SimpleTestClock clock;
  clock.SetNow(base::Time::Now());
  HostContentSettingsMap* map =
      HostContentSettingsMapFactory::GetForProfile(profile());
  map->SetClockForTesting(&clock);
  permissions::PermissionDecisionAutoBlocker* auto_blocker =
      PermissionDecisionAutoBlockerFactory::GetForProfile(profile());
  auto_blocker->SetClockForTesting(&clock);
  clock.Advance(base::Hours(1));

  // Test recent permissions is empty when there are no preferences.
  handler()->HandleGetRecentSitePermissions(get_recent_permissions_args);
  EXPECT_EQ(1U, web_ui()->call_data().size());

  {
    const content::TestWebUI::CallData& data = *web_ui()->call_data().back();
    EXPECT_EQ("cr.webUIResponse", data.function_name());
    EXPECT_EQ(kCallbackId, data.arg1()->GetString());
    ASSERT_TRUE(data.arg2()->GetBool());

    const base::ListValue& recent_permissions = data.arg3()->GetList();
    EXPECT_TRUE(recent_permissions.empty());
  }

  // Add numerous permissions from different sources and confirm that the recent
  // permissions are correctly transformed for usage by JS.
  const GURL url1("https://example.com");
  const GURL url2("http://example.com");
  for (int i = 0; i < 3; ++i) {
    auto_blocker->RecordDismissAndEmbargo(
        url1, ContentSettingsType::NOTIFICATIONS, false);
  }

  clock.Advance(base::Hours(2));
  clock.Advance(base::Hours(1));
  CreateIncognitoProfile();
  HostContentSettingsMap* incognito_map =
      HostContentSettingsMapFactory::GetForProfile(incognito_profile());
  incognito_map->SetClockForTesting(&clock);

  clock.Advance(base::Hours(1));
  permissions::PermissionDecisionAutoBlocker* incognito_auto_blocker =
      PermissionDecisionAutoBlockerFactory::GetForProfile(incognito_profile());
  incognito_auto_blocker->SetClockForTesting(&clock);
  for (int i = 0; i < 3; ++i) {
    incognito_auto_blocker->RecordDismissAndEmbargo(
        url1, ContentSettingsType::NOTIFICATIONS, false);
  }

  handler()->HandleGetRecentSitePermissions(get_recent_permissions_args);
  {
    const content::TestWebUI::CallData& data = *web_ui()->call_data().back();
    EXPECT_EQ("cr.webUIResponse", data.function_name());
    EXPECT_EQ(kCallbackId, data.arg1()->GetString());
    ASSERT_TRUE(data.arg2()->GetBool());

    const base::ListValue& recent_permissions = data.arg3()->GetList();
    EXPECT_EQ(2UL, recent_permissions.size());
    const base::DictValue& first_permission = recent_permissions[0].GetDict();
    const base::DictValue& second_permission = recent_permissions[1].GetDict();

    EXPECT_EQ(url1.spec(), CHECK_DEREF(second_permission.FindString("origin")));
    EXPECT_EQ(url1.spec(), CHECK_DEREF(first_permission.FindString("origin")));

    EXPECT_TRUE(first_permission.FindBool("incognito").value_or(false));
    EXPECT_FALSE(second_permission.FindBool("incognito").value_or(true));

    const base::ListValue& incognito_url1_permissions =
        CHECK_DEREF(first_permission.FindList("recentPermissions"));
    const base::ListValue& url1_permissions =
        CHECK_DEREF(second_permission.FindList("recentPermissions"));

    EXPECT_EQ(1UL, incognito_url1_permissions.size());
    const base::DictValue& first_incognito_permission =
        incognito_url1_permissions[0].GetDict();

    EXPECT_EQ(kNotifications,
              CHECK_DEREF(first_incognito_permission.FindString("type")));
    EXPECT_EQ(kBlocked,
              CHECK_DEREF(first_incognito_permission.FindString("setting")));
    EXPECT_EQ(kEmbargo,
              CHECK_DEREF(first_incognito_permission.FindString("source")));

    const base::DictValue& first_url_permission = url1_permissions[0].GetDict();
    EXPECT_EQ(kNotifications,
              CHECK_DEREF(first_url_permission.FindString("type")));
    EXPECT_EQ(kBlocked,
              CHECK_DEREF(first_url_permission.FindString("setting")));
    EXPECT_EQ(kEmbargo, CHECK_DEREF(first_url_permission.FindString("source")));
  }
}

TEST_F(SiteSettingsHandlerTest, OnStorageFetched) {
  SetupModel();

  handler()->ClearAllSitesMapForTesting();
  handler()->OnStorageFetched();

  const content::TestWebUI::CallData& data = *web_ui()->call_data().back();
  EXPECT_EQ("cr.webUIListenerCallback", data.function_name());

  ASSERT_TRUE(data.arg1()->is_string());
  EXPECT_EQ("onStorageListFetched", data.arg1()->GetString());

  ASSERT_TRUE(data.arg2()->is_list());
  const base::ListValue& storage_and_cookie_list = data.arg2()->GetList();
  EXPECT_EQ(4U, storage_and_cookie_list.size());

  {
    const base::Value& site_group_val = storage_and_cookie_list[0];
    ASSERT_TRUE(site_group_val.is_dict());
    const base::DictValue& site_group = site_group_val.GetDict();

    ASSERT_THAT(CHECK_DEREF(site_group.FindString("groupingKey")),
                IsEtldPlus1("example.com"));

    EXPECT_EQ(3, site_group.FindDouble("numCookies"));

    const base::ListValue* origin_list = site_group.FindList("origins");
    ASSERT_TRUE(origin_list);
    // There will be 3 origins in this case. Cookie node with url
    // http://www.example.com/ will be treat as https://www.example.com/ because
    // this url existed in the storage nodes.
    EXPECT_EQ(3U, origin_list->size());

    const base::DictValue& origin_info_0 = (*origin_list)[0].GetDict();

    EXPECT_EQ("http://abc.example.com/",
              CHECK_DEREF(origin_info_0.FindString("origin")));
    EXPECT_EQ(0, origin_info_0.FindDouble("engagement"));
    EXPECT_EQ(0, origin_info_0.FindDouble("usage"));
    EXPECT_EQ(1, origin_info_0.FindDouble("numCookies"));
    EXPECT_FALSE(origin_info_0.FindBool("isPartitioned").value_or(false));

    const base::DictValue& origin_info_1 = (*origin_list)[1].GetDict();

    EXPECT_EQ("https://google.com/",
              CHECK_DEREF(origin_info_1.FindString("origin")));
    EXPECT_EQ(0, origin_info_1.FindDouble("engagement"));
    EXPECT_EQ(100, origin_info_1.FindDouble("usage"));
    EXPECT_EQ(0, origin_info_1.FindDouble("numCookies"));
    EXPECT_TRUE(origin_info_1.FindBool("isPartitioned").value_or(false));

    const base::DictValue& origin_info_2 = (*origin_list)[2].GetDict();

    // Even though in the cookies the scheme is http, it still stored as https
    // because there is https data stored.
    EXPECT_EQ("https://www.example.com/",
              CHECK_DEREF(origin_info_2.FindString("origin")));
    EXPECT_EQ(0, origin_info_2.FindDouble("engagement"));
    // Usage is reported per displayed row. This unpartitioned row only includes
    // www.example.com's unpartitioned storage:
    // kExampleUnpartitionedEntry (100 B) + kExampleLocalStorage (2 B) = 102 B.
    // Partitioned storage for the same origin is reported separately below.
    EXPECT_EQ(102, origin_info_2.FindDouble("usage"));
    EXPECT_EQ(1, origin_info_2.FindDouble("numCookies"));
    EXPECT_FALSE(origin_info_2.FindBool("isPartitioned").value_or(false));
  }

  {
    const base::DictValue& site_group = storage_and_cookie_list[1].GetDict();

    ASSERT_THAT(CHECK_DEREF(site_group.FindString("groupingKey")),
                IsEtldPlus1("google.com"));

    EXPECT_EQ(3, site_group.FindDouble("numCookies"));

    const base::ListValue* origin_list = site_group.FindList("origins");
    ASSERT_TRUE(origin_list);

    EXPECT_EQ(2U, origin_list->size());

    const base::DictValue& partitioned_origin_info =
        (*origin_list)[0].GetDict();

    EXPECT_EQ("https://www.example.com/",
              CHECK_DEREF(partitioned_origin_info.FindString("origin")));
    EXPECT_EQ(0, partitioned_origin_info.FindDouble("engagement"));
    // This partitioned row only includes www.example.com's storage under
    // google.com: kExampleOnGoogleSecureEntry (100 B) +
    // kExampleOnGoogleInsecureEntry (100 B) = 200 B.
    EXPECT_EQ(200, partitioned_origin_info.FindDouble("usage"));
    EXPECT_EQ(1, partitioned_origin_info.FindDouble("numCookies"));
    EXPECT_TRUE(
        partitioned_origin_info.FindBool("isPartitioned").value_or(false));

    const base::DictValue& unpartitioned_origin_info =
        (*origin_list)[1].GetDict();

    EXPECT_EQ("https://www.google.com/",
              CHECK_DEREF(unpartitioned_origin_info.FindString("origin")));
    EXPECT_EQ(0, unpartitioned_origin_info.FindDouble("engagement"));
    EXPECT_EQ(50000000000, unpartitioned_origin_info.FindDouble("usage"));
    EXPECT_EQ(0, unpartitioned_origin_info.FindDouble("numCookies"));
    EXPECT_FALSE(
        unpartitioned_origin_info.FindBool("isPartitioned").value_or(true));
  }

  {
    const base::Value& site_group_val = storage_and_cookie_list[2];
    ASSERT_TRUE(site_group_val.is_dict());
    const base::DictValue& site_group = site_group_val.GetDict();

    ASSERT_THAT(CHECK_DEREF(site_group.FindString("groupingKey")),
                IsEtldPlus1("google.com.au"));

    EXPECT_EQ(4, site_group.FindDouble("numCookies"));

    const base::ListValue* origin_list = site_group.FindList("origins");
    ASSERT_TRUE(origin_list);

    // The unpartitioned cookie set for google.com.au should be associated with
    // the eTLD+1, and thus won't have an origin entry as other origin entries
    // exist for the unpartitioned storage. The partitioned cookie for
    // google.com.au, partitioned by google.com.au should have also created an
    // entry.
    EXPECT_EQ(3U, origin_list->size());

    const base::DictValue& partitioned_origin_one_info =
        (*origin_list)[0].GetDict();

    EXPECT_EQ("https://google.com.au/",
              CHECK_DEREF(partitioned_origin_one_info.FindString("origin")));
    EXPECT_EQ(0, partitioned_origin_one_info.FindDouble("engagement"));
    EXPECT_EQ(0, partitioned_origin_one_info.FindDouble("usage"));
    EXPECT_EQ(1, partitioned_origin_one_info.FindDouble("numCookies"));
    EXPECT_TRUE(
        partitioned_origin_one_info.FindBool("isPartitioned").value_or(false));

    const base::DictValue& partitioned_origin_two_info =
        (*origin_list)[1].GetDict();
    EXPECT_EQ("https://www.another-example.com/",
              CHECK_DEREF(partitioned_origin_two_info.FindString("origin")));
    EXPECT_EQ(0, partitioned_origin_two_info.FindDouble("engagement"));
    EXPECT_EQ(0, partitioned_origin_two_info.FindDouble("usage"));
    EXPECT_EQ(1, partitioned_origin_two_info.FindDouble("numCookies"));
    EXPECT_TRUE(
        partitioned_origin_two_info.FindBool("isPartitioned").value_or(false));

    const base::DictValue& partitioned_origin_three_info =
        (*origin_list)[2].GetDict();

    EXPECT_EQ("https://www.example.com/",
              CHECK_DEREF(partitioned_origin_three_info.FindString("origin")));
    EXPECT_EQ(0, partitioned_origin_three_info.FindDouble("engagement"));
    // This partitioned row only includes www.example.com's storage under
    // google.com.au: kExampleOnGoogleAuEntry (400 B).
    EXPECT_EQ(400, partitioned_origin_three_info.FindDouble("usage"));
    EXPECT_EQ(1, partitioned_origin_three_info.FindDouble("numCookies"));
    EXPECT_TRUE(partitioned_origin_three_info.FindBool("isPartitioned")
                    .value_or(false));
  }

  {
    const base::Value& site_group_val = storage_and_cookie_list[3];
    ASSERT_TRUE(site_group_val.is_dict());
    const base::DictValue& site_group = site_group_val.GetDict();

    ASSERT_THAT(CHECK_DEREF(site_group.FindString("groupingKey")),
                IsEtldPlus1("ungrouped.com"));

    EXPECT_EQ(1, site_group.FindDouble("numCookies"));

    const base::ListValue* origin_list = site_group.FindList("origins");
    ASSERT_TRUE(origin_list);
    EXPECT_EQ(1U, origin_list->size());

    const base::DictValue& origin_info = (*origin_list)[0].GetDict();

    EXPECT_EQ("http://ungrouped.com/",
              CHECK_DEREF(origin_info.FindString("origin")));
    EXPECT_EQ(0, origin_info.FindDouble("engagement"));
    EXPECT_EQ(0, origin_info.FindDouble("usage"));
    EXPECT_EQ(1, origin_info.FindDouble("numCookies"));
  }
}

TEST_F(SiteSettingsHandlerTest, InstalledApps) {
  GURL start_url("http://abc.example.com/path");
  RegisterWebApp(profile(),
                 MakeApp(web_app::GenerateAppId(
                             /*manifest_id_path=*/std::nullopt, start_url),
                         apps::AppType::kWeb, start_url.spec(),
                         apps::Readiness::kReady, apps::InstallReason::kSync));

  SetupModel();

  base::ListValue storage_and_cookie_list = GetOnStorageFetchedSentList();
  EXPECT_EQ(4U, storage_and_cookie_list.size());

  {
    const base::Value& site_group_val = storage_and_cookie_list[0];
    ASSERT_TRUE(site_group_val.is_dict());
    const base::DictValue& site_group = site_group_val.GetDict();

    ASSERT_THAT(CHECK_DEREF(site_group.FindString("groupingKey")),
                IsEtldPlus1("example.com"));

    ASSERT_TRUE(site_group.FindBool("hasInstalledPWA").value_or(false));

    const base::ListValue* origin_list = site_group.FindList("origins");
    ASSERT_TRUE(origin_list);

    const base::DictValue& origin_info = (*origin_list)[0].GetDict();

    EXPECT_EQ("http://abc.example.com/",
              CHECK_DEREF(origin_info.FindString("origin")));
    EXPECT_TRUE(origin_info.FindBool("isInstalled").value_or(false));
  }

  // Verify that installed booleans are false for other siteGroups/origins
  {
    const base::Value& site_group_val = storage_and_cookie_list[1];
    ASSERT_TRUE(site_group_val.is_dict());
    const base::DictValue& site_group = site_group_val.GetDict();

    ASSERT_THAT(CHECK_DEREF(site_group.FindString("groupingKey")),
                IsEtldPlus1("google.com"));
    EXPECT_FALSE(site_group.FindBool("hasInstalledPWA").value_or(true));

    const base::ListValue* origin_list = site_group.FindList("origins");
    ASSERT_TRUE(origin_list);

    for (const auto& origin_info : *origin_list) {
      ASSERT_TRUE(origin_info.is_dict());
      EXPECT_FALSE(
          origin_info.GetDict().FindBool("isInstalled").value_or(true));
    }
  }
}

TEST_F(SiteSettingsHandlerTest, IncognitoExceptions) {
  constexpr char kOriginToBlock[] = "https://www.blocked.com:443";

  auto validate_exception = [&kOriginToBlock](const base::Value& exception) {
    ASSERT_TRUE(exception.is_dict());

    ASSERT_TRUE(exception.GetDict().FindString(site_settings::kOrigin));
    ASSERT_EQ(kOriginToBlock,
              *exception.GetDict().FindString(site_settings::kOrigin));
  };

  CreateIncognitoProfile();

  {
    SetContentSettingCustomScope(kOriginToBlock, std::string(),
                                 kPermissionNotifications,
                                 CONTENT_SETTING_BLOCK, 2U,
                                 /*is_incognito=*/true);

    base::ListValue get_exception_list_args;
    get_exception_list_args.Append(kCallbackId);
    get_exception_list_args.Append(kNotifications);
    handler()->HandleGetExceptionList(get_exception_list_args);

    const content::TestWebUI::CallData& data = *web_ui()->call_data().back();

    ASSERT_TRUE(data.arg3()->is_list());
    const base::ListValue& exceptions = data.arg3()->GetList();
    ASSERT_EQ(1U, exceptions.size());

    validate_exception(exceptions[0]);
  }

  {
    SetContentSettingCustomScope(kOriginToBlock, std::string(),
                                 kPermissionNotifications,
                                 CONTENT_SETTING_BLOCK, 5U,
                                 /*is_incognito=*/false);

    base::ListValue get_exception_list_args;
    get_exception_list_args.Append(kCallbackId);
    get_exception_list_args.Append(kNotifications);
    handler()->HandleGetExceptionList(get_exception_list_args);

    const content::TestWebUI::CallData& data = *web_ui()->call_data().back();

    ASSERT_TRUE(data.arg3()->is_list());
    const base::ListValue& exceptions = data.arg3()->GetList();
    ASSERT_EQ(2U, exceptions.size());

    validate_exception(exceptions[0]);
    validate_exception(exceptions[1]);
  }

  DestroyIncognitoProfile();
}

TEST_F(SiteSettingsHandlerTest, ResetCategoryPermissionForEmbargoedOrigins) {
  constexpr char kOriginToBlock[] = "https://www.blocked.com:443";
  constexpr char kOriginToEmbargo[] = "https://embargoed.co.uk";

  // Add and test 1 blocked origin
  {
    base::ListValue set_args;
    set_args.Append(kOriginToBlock);  // Primary pattern.
    set_args.Append(std::string());   // Secondary pattern.
    set_args.Append(kNotifications);
    set_args.Append(
        content_settings::ContentSettingToString(CONTENT_SETTING_BLOCK));
    set_args.Append(false);  // Incognito.

    handler()->HandleSetCategoryPermissionForPattern(set_args);
    ASSERT_EQ(2U, web_ui()->call_data().size());
    // When HandleSetCategoryPermissionForPattern is called for a notification
    // permission, there are two callbacks that make call_data size increase
    // by 2 instead of 1.
    ValidateCallbacksForNotificationPermission(0);
  }

  // Add and test 1 embargoed origin.
  {
    auto* auto_blocker =
        PermissionDecisionAutoBlockerFactory::GetForProfile(profile());
    for (size_t i = 0; i < 3; ++i) {
      auto_blocker->RecordDismissAndEmbargo(GURL(kOriginToEmbargo),
                                            kPermissionNotifications, false);
    }
    // Check that origin is under embargo.
    EXPECT_EQ(
        PermissionStatus::DENIED,
        auto_blocker
            ->GetEmbargoResult(GURL(kOriginToEmbargo), kPermissionNotifications)
            ->status);
  }

  // Check there are 2 blocked origins.
  {
    base::ListValue exceptions;
    site_settings::GetExceptionsForContentType(
        kPermissionNotifications, profile(), web_ui(),
        /*incognito=*/false, &exceptions);

    // The size should be 2, 1st is blocked origin, 2nd is embargoed origin.
    ASSERT_EQ(2U, exceptions.size());
  }

  {
    // Reset blocked origin.
    base::ListValue reset_args;
    reset_args.Append(kOriginToBlock);
    reset_args.Append(std::string());
    reset_args.Append(kNotifications);
    reset_args.Append(false);  // Incognito.
    handler()->HandleResetCategoryPermissionForPattern(reset_args);

    // Check there is 1 blocked origin.
    base::ListValue exceptions;
    site_settings::GetExceptionsForContentType(
        kPermissionNotifications, profile(), web_ui(),
        /*incognito=*/false, &exceptions);
    ASSERT_EQ(1U, exceptions.size());
  }

  {
    // Reset embargoed origin.
    base::ListValue reset_args;
    reset_args.Append(kOriginToEmbargo);
    reset_args.Append(std::string());
    reset_args.Append(kNotifications);
    reset_args.Append(false);  // Incognito.
    handler()->HandleResetCategoryPermissionForPattern(reset_args);

    // Check that there are no blocked or embargoed origins.
    base::ListValue exceptions;
    site_settings::GetExceptionsForContentType(
        kPermissionNotifications, profile(), web_ui(),
        /*incognito=*/false, &exceptions);
    ASSERT_TRUE(exceptions.empty());
  }
}

TEST_F(SiteSettingsHandlerTest, ClearHeuristicData) {
  base::test::ScopedFeatureList features;
  features.InitWithFeatures(
      {blink::features::kGeolocationElement,
       permissions::features::kPermissionHeuristicAutoGrant},
      {});
  constexpr char kOrigin[] = "https://www.example.com:443";
  auto* permission_actions_history =
      PermissionActionsHistoryFactory::GetForProfile(profile());

  {
    // Grant a temporary permission to create heuristic data.
    permission_actions_history->RecordTemporaryGrant(
        GURL(kOrigin), ContentSettingsType::GEOLOCATION);

    // Verify that heuristic data exists.
    EXPECT_EQ(1, permission_actions_history->GetTemporaryGrantCountForTesting(
                     GURL(kOrigin), ContentSettingsType::GEOLOCATION));

    // Reset permission for the origin.
    base::ListValue reset_args;
    reset_args.Append(kOrigin);
    reset_args.Append(std::string());
    reset_args.Append(kGeolocation);
    reset_args.Append(false);  // Incognito.
    handler()->HandleResetCategoryPermissionForPattern(reset_args);

    // Heuristic data for the given origin should be cleared.
    EXPECT_EQ(0, permission_actions_history->GetTemporaryGrantCountForTesting(
                     GURL(kOrigin), ContentSettingsType::GEOLOCATION));
  }

  const auto kContentSettings = std::to_array<ContentSetting>({
      CONTENT_SETTING_BLOCK,
      CONTENT_SETTING_ASK,
  });
  for (const ContentSetting content_setting : kContentSettings) {
    // Grant a temporary permission to create heuristic data.
    permission_actions_history->RecordTemporaryGrant(
        GURL(kOrigin), ContentSettingsType::GEOLOCATION);

    // Verify that heuristic data exists.
    EXPECT_EQ(1, permission_actions_history->GetTemporaryGrantCountForTesting(
                     GURL(kOrigin), ContentSettingsType::GEOLOCATION));

    base::ListValue reset_args;
    reset_args.Append(kOrigin);
    reset_args.Append(kGeolocation);
    reset_args.Append(
        content_settings::ContentSettingToString(content_setting));
    handler()->HandleSetOriginPermissions(reset_args);

    // Heuristic data for the given origin should be cleared.
    EXPECT_EQ(0, permission_actions_history->GetTemporaryGrantCountForTesting(
                     GURL(kOrigin), ContentSettingsType::GEOLOCATION));
  }
}

TEST_F(SiteSettingsHandlerTest, ResetCategoryPermissionForInvalidOrigins) {
  constexpr char kInvalidOrigin[] = "example.com";
  auto url = GURL(kInvalidOrigin);
  EXPECT_FALSE(url.is_valid());
  EXPECT_TRUE(url.is_empty());

  base::ListValue set_args;
  set_args.Append(kInvalidOrigin);  // Primary pattern.
  set_args.Append(std::string());   // Secondary pattern.
  set_args.Append(kNotifications);
  set_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_BLOCK));
  set_args.Append(false);  // Incognito.

  handler()->HandleSetCategoryPermissionForPattern(set_args);
  ASSERT_EQ(2U, web_ui()->call_data().size());
  // When HandleSetCategoryPermissionForPattern is called for a notification
  // permission, there are two callbacks that make call_data size increase
  // by 2 instead of 1.
  ValidateCallbacksForNotificationPermission(0);

  // Reset blocked origin.
  base::ListValue reset_args;
  reset_args.Append(kInvalidOrigin);
  reset_args.Append(std::string());
  reset_args.Append(kNotifications);
  reset_args.Append(false);  // Incognito.
  // Check that this method is not crashing for an invalid origin.
  handler()->HandleResetCategoryPermissionForPattern(reset_args);
}

TEST_F(SiteSettingsHandlerTest, SetCategory_GetException_ResetCategory) {
  const std::string google("https://www.google.com:443");
  {
    // Test the JS -> C++ -> JS callback path for configuring origins, by
    // setting Google.com to blocked.
    base::ListValue set_args;
    set_args.Append(google);         // Primary pattern.
    set_args.Append(std::string());  // Secondary pattern.
    set_args.Append(kNotifications);
    set_args.Append(
        content_settings::ContentSettingToString(CONTENT_SETTING_BLOCK));
    set_args.Append(false);  // Incognito.
    handler()->HandleSetCategoryPermissionForPattern(set_args);
    EXPECT_EQ(2U, web_ui()->call_data().size());
    // When HandleSetCategoryPermissionForPattern is called for a notification
    // permission, there are two callbacks that make call_data size increase
    // by 2 instead of 1.
    ValidateCallbacksForNotificationPermission(0);
  }

  base::ListValue get_exception_list_args;
  get_exception_list_args.Append(kCallbackId);
  get_exception_list_args.Append(kNotifications);
  handler()->HandleGetExceptionList(get_exception_list_args);
  ValidateOrigin(google, "", google, CONTENT_SETTING_BLOCK,
                 site_settings::SiteSettingSource::kPreference, 3U);

  {
    // Reset things back to how they were.
    base::ListValue reset_args;
    reset_args.Append(google);
    reset_args.Append(std::string());
    reset_args.Append(kNotifications);
    reset_args.Append(false);  // Incognito.
    handler()->HandleResetCategoryPermissionForPattern(reset_args);
    EXPECT_EQ(5U, web_ui()->call_data().size());
    // When HandleResetCategoryPermissionForPattern is called for a notification
    // permission, there are two callbacks that make call_data size increase
    // by 2 instead of 1.
    ValidateCallbacksForNotificationPermission(3);
  }

  // Verify the reset was successful.
  handler()->HandleGetExceptionList(get_exception_list_args);
  ValidateNoOrigin(6U);
}

TEST_F(SiteSettingsHandlerTest, NotificationPermissionRevokeUkm) {
  const std::string google("https://www.google.com");
  ukm::TestAutoSetUkmRecorder ukm_recorder;
  {
    base::ListValue set_notification_origin_args;
    set_notification_origin_args.Append(google);
    set_notification_origin_args.Append("");
    set_notification_origin_args.Append(kNotifications);
    set_notification_origin_args.Append(
        content_settings::ContentSettingToString(CONTENT_SETTING_ALLOW));
    set_notification_origin_args.Append(false /* incognito */);
    handler()->HandleSetCategoryPermissionForPattern(
        set_notification_origin_args);
  }

  {
    base::ListValue set_notification_origin_args;
    set_notification_origin_args.Append(google);
    set_notification_origin_args.Append("");
    set_notification_origin_args.Append(kNotifications);
    set_notification_origin_args.Append(
        content_settings::ContentSettingToString(CONTENT_SETTING_BLOCK));
    set_notification_origin_args.Append(false /* incognito */);
    handler()->HandleSetCategoryPermissionForPattern(
        set_notification_origin_args);
  }

  auto entries = ukm_recorder.GetEntriesByName("Permission");
  EXPECT_EQ(1u, entries.size());
  auto* entry = entries.front().get();

  ukm_recorder.ExpectEntrySourceHasUrl(entry, GURL(google));
  EXPECT_EQ(
      *ukm_recorder.GetEntryMetric(entry, "Source"),
      static_cast<int64_t>(permissions::PermissionSourceUI::SITE_SETTINGS));

  EXPECT_EQ(*ukm_recorder.GetEntryMetric(entry, "PermissionType"),
            content_settings_uma_util::ContentSettingTypeToHistogramValue(
                ContentSettingsType::NOTIFICATIONS));
  EXPECT_EQ(*ukm_recorder.GetEntryMetric(entry, "Action"),
            static_cast<int64_t>(permissions::PermissionAction::REVOKED));
}

// TODO(crbug.com/40688152): Test flakes on TSAN and ASAN.
#if defined(THREAD_SANITIZER) || defined(ADDRESS_SANITIZER)
#define MAYBE_DefaultSettingSource DISABLED_DefaultSettingSource
#else
#define MAYBE_DefaultSettingSource DefaultSettingSource
#endif
TEST_F(SiteSettingsHandlerTest, MAYBE_DefaultSettingSource) {
  // Use a non-default port to verify the display name does not strip this
  // off.
  const std::string google("https://www.google.com:183");
  const std::string expected_display_name("www.google.com:183");

  ContentSettingSourceSetter source_setter(profile(),
                                           ContentSettingsType::NOTIFICATIONS);

  base::ListValue get_origin_permissions_args;
  get_origin_permissions_args.Append(kCallbackId);
  get_origin_permissions_args.Append(google);
  base::ListValue category_list;
  category_list.Append(kNotifications);
  get_origin_permissions_args.Append(std::move(category_list));

  // Test Chrome built-in defaults are marked as default.
  handler()->HandleGetOriginPermissions(get_origin_permissions_args);
  ValidateOrigin(google, google, expected_display_name, CONTENT_SETTING_ASK,
                 site_settings::SiteSettingSource::kDefault, 1U);

  base::ListValue default_value_args;
  default_value_args.Append(kNotifications);
  default_value_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_BLOCK));
  handler()->HandleSetDefaultValueForContentType(default_value_args);
  // A user-set global default should also show up as default.
  handler()->HandleGetOriginPermissions(get_origin_permissions_args);
  ValidateOrigin(google, google, expected_display_name, CONTENT_SETTING_BLOCK,
                 site_settings::SiteSettingSource::kDefault, 3U);

  base::ListValue set_notification_pattern_args;
  set_notification_pattern_args.Append("[*.]google.com");
  set_notification_pattern_args.Append("");
  set_notification_pattern_args.Append(kNotifications);
  set_notification_pattern_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_ALLOW));
  set_notification_pattern_args.Append(false);
  handler()->HandleSetCategoryPermissionForPattern(
      set_notification_pattern_args);
  ASSERT_EQ(5U, web_ui()->call_data().size());
  // When HandleSetCategoryPermissionForPattern is called for a notification
  // permission, there are two callbacks that make call_data size increase
  // by 2 instead of 1.
  ValidateCallbacksForNotificationPermission(3);
  // A user-set pattern should not show up as default.
  handler()->HandleGetOriginPermissions(get_origin_permissions_args);
  ValidateOrigin(google, google, expected_display_name, CONTENT_SETTING_ALLOW,
                 site_settings::SiteSettingSource::kPreference, 6U);

  base::ListValue set_notification_origin_args;
  set_notification_origin_args.Append(google);
  set_notification_origin_args.Append("");
  set_notification_origin_args.Append(kNotifications);
  set_notification_origin_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_BLOCK));
  set_notification_origin_args.Append(false);
  handler()->HandleSetCategoryPermissionForPattern(
      set_notification_origin_args);
  ASSERT_EQ(8U, web_ui()->call_data().size());
  // When HandleSetCategoryPermissionForPattern is called for a notification
  // permission, there are two callbacks that make call_data size increase
  // by 2 instead of 1.
  ValidateCallbacksForNotificationPermission(6);
  // A user-set per-origin permission should not show up as default.
  handler()->HandleGetOriginPermissions(get_origin_permissions_args);
  ValidateOrigin(google, google, expected_display_name, CONTENT_SETTING_BLOCK,
                 site_settings::SiteSettingSource::kPreference, 9U);

  // Enterprise-policy set defaults should not show up as default.
  source_setter.SetPolicyDefault(CONTENT_SETTING_ALLOW);
  handler()->HandleGetOriginPermissions(get_origin_permissions_args);
  ValidateOrigin(google, google, expected_display_name, CONTENT_SETTING_ALLOW,
                 site_settings::SiteSettingSource::kPolicy, 10U);
}

TEST_F(SiteSettingsHandlerTest, GetAndSetOriginPermissions) {
  const std::string origin_with_port("https://www.example.com:443");
  // The display name won't show the port if it's default for that scheme.
  const std::string origin("www.example.com");
  base::ListValue get_args;
  get_args.Append(kCallbackId);
  get_args.Append(origin_with_port);
  {
    base::ListValue category_list;
    category_list.Append(kNotifications);
    get_args.Append(std::move(category_list));
  }
  handler()->HandleGetOriginPermissions(get_args);
  ValidateOrigin(origin_with_port, origin_with_port, origin,
                 CONTENT_SETTING_ASK,
                 site_settings::SiteSettingSource::kDefault, 1U);

  // Block notifications.
  base::ListValue set_args;
  set_args.Append(origin_with_port);
  set_args.Append(kNotifications);
  set_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_BLOCK));
  handler()->HandleSetOriginPermissions(set_args);
  EXPECT_EQ(2U, web_ui()->call_data().size());

  // Reset things back to how they were.
  base::ListValue reset_args;
  reset_args.Append(origin_with_port);
  reset_args.Append(std::move(kNotifications));
  reset_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_DEFAULT));

  handler()->HandleSetOriginPermissions(reset_args);
  EXPECT_EQ(3U, web_ui()->call_data().size());

  // Verify the reset was successful.
  handler()->HandleGetOriginPermissions(get_args);
  ValidateOrigin(origin_with_port, origin_with_port, origin,
                 CONTENT_SETTING_ASK,
                 site_settings::SiteSettingSource::kDefault, 4U);
}

TEST_F(SiteSettingsHandlerTest, SetOriginPermissionsForStorageAccess) {
  const GURL origin_a_with_port("https://www.example.com:443");
  const GURL origin_b_with_port("https://www.examplepetstore.com:443");
  // The display name won't show the port if it's default for that scheme.
  const std::string origin_a_display_name("example.com");
  const std::string origin_b_display_name("examplepetstore.com");
  // The resulting pattern will be broadened to the scheme+eTLD+1 level.
  const std::string origin_a_pattern("https://[*.]example.com");
  const std::string origin_b_pattern("https://[*.]examplepetstore.com");

  HostContentSettingsMap* map =
      HostContentSettingsMapFactory::GetForProfile(profile());

  // Allow storage access for this origin/embedding pair, in both directions.
  // This is an unlikely scenario in practice but we want users to be able to
  // clear an embedding relationship between two sites from either site.
  map->SetContentSettingDefaultScope(origin_a_with_port, origin_b_with_port,
                                     ContentSettingsType::STORAGE_ACCESS,
                                     CONTENT_SETTING_ALLOW);
  map->SetContentSettingDefaultScope(origin_b_with_port, origin_a_with_port,
                                     ContentSettingsType::STORAGE_ACCESS,
                                     CONTENT_SETTING_ALLOW);

  base::ListValue get_exception_list_args;
  get_exception_list_args.Append(kCallbackId);
  get_exception_list_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_ALLOW));
  handler()->HandleGetStorageAccessExceptionList(get_exception_list_args);

  // Verify that the group exception was created correctly.
  ValidateStorageAccessList(/*expected_total_calls=*/3U,
                            /*expected_num_groups=*/2U);

  ValidateStorageAccessException(
      origin_a_pattern, origin_a_display_name, CONTENT_SETTING_ALLOW,
      {{origin_b_pattern, origin_b_display_name, /*incognito=*/false}},
      /*index=*/0U);

  ValidateStorageAccessException(
      origin_b_pattern, origin_b_display_name, CONTENT_SETTING_ALLOW,
      {{origin_a_pattern, origin_a_display_name, /*incognito=*/false}},
      /*index=*/1U);

  // Reset things to default for STORAGE_ACCESS on origin_a.
  base::ListValue reset_args;
  reset_args.Append(origin_a_with_port.spec());
  reset_args.Append(std::move(kStorageAccess));
  reset_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_DEFAULT));
  handler()->HandleSetOriginPermissions(reset_args);
  EXPECT_EQ(5U, web_ui()->call_data().size());

  // Verify that there are no storage access exceptions after resetting.
  handler()->HandleGetStorageAccessExceptionList(get_exception_list_args);
  ValidateStorageAccessList(/*expected_total_calls=*/6U,
                            /*expected_num_groups=*/0U);
}

TEST_F(SiteSettingsHandlerTest, GetAndSetForInvalidURLs) {
  const std::string origin("arbitrary string");
  EXPECT_FALSE(GURL(origin).is_valid());
  base::ListValue get_args;
  get_args.Append(kCallbackId);
  get_args.Append(origin);
  {
    base::ListValue category_list;
    category_list.Append(kNotifications);
    get_args.Append(std::move(category_list));
  }
  handler()->HandleGetOriginPermissions(get_args);
  // Verify that it'll return CONTENT_SETTING_BLOCK as |origin| is not a secure
  // context, a requirement for notifications. Note that the display string
  // will be blank since it's an invalid URL.
  ValidateOrigin(origin, origin, "", CONTENT_SETTING_BLOCK,
                 site_settings::SiteSettingSource::kInsecureOrigin, 1U);

  // Make sure setting a permission on an invalid origin doesn't crash.
  base::ListValue set_args;
  set_args.Append(origin);
  {
    base::ListValue category_list;
    category_list.Append(kNotifications);
    set_args.Append(std::move(category_list));
  }
  set_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_ALLOW));
  handler()->HandleSetOriginPermissions(set_args);

  // Also make sure the content setting for |origin| wasn't actually changed.
  handler()->HandleGetOriginPermissions(get_args);
  ValidateOrigin(origin, origin, "", CONTENT_SETTING_BLOCK,
                 site_settings::SiteSettingSource::kInsecureOrigin, 2U);
}

TEST_F(SiteSettingsHandlerTest, SetCategoryPermissionForPattern) {
  const std::string kOrigin = "https://www.example.com:443";

  base::ListValue set_args;
  set_args.Append(kOrigin);        // Primary pattern.
  set_args.Append(std::string());  // Secondary pattern.
  set_args.Append(kNotifications);
  set_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_BLOCK));
  set_args.Append(false);  // Incognito.

  handler()->HandleSetCategoryPermissionForPattern(set_args);
  EXPECT_EQ(kNumberNotificationsContentSettingListeners,
            web_ui()->call_data().size());

  HostContentSettingsMap* map =
      HostContentSettingsMapFactory::GetForProfile(profile());

  ASSERT_EQ(CONTENT_SETTING_BLOCK,
            map->GetContentSetting(GURL(kOrigin), GURL(std::string()),
                                   kPermissionNotifications));
}

TEST_F(SiteSettingsHandlerTest, SetCategoryPermissionForPattern_WildCard) {
  const std::string kWildcardOrigin = "[*.]example.com";
  const std::string kRealOrigin = "https://www.example.com";

  base::ListValue set_args;
  set_args.Append(kWildcardOrigin);  // Primary pattern.
  set_args.Append(std::string());    // Secondary pattern.
  set_args.Append(kNotifications);
  set_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_BLOCK));
  set_args.Append(false);  // Incognito.

  handler()->HandleSetCategoryPermissionForPattern(set_args);
  EXPECT_EQ(kNumberNotificationsContentSettingListeners,
            web_ui()->call_data().size());

  HostContentSettingsMap* map =
      HostContentSettingsMapFactory::GetForProfile(profile());

  ASSERT_EQ(CONTENT_SETTING_BLOCK,
            map->GetContentSetting(GURL(kRealOrigin), GURL(std::string()),
                                   kPermissionNotifications));
}

TEST_F(SiteSettingsHandlerTest,
       SetCategoryPermissionForPattern_SecondaryPattern) {
  const std::string kOrigin = "https://www.example.com:443";
  const std::string kSecondary = "https://www.secondary.com:443";

  base::ListValue set_args;
  set_args.Append(kOrigin);     // Primary pattern.
  set_args.Append(kSecondary);  // Secondary pattern.
  set_args.Append(site_settings::ContentSettingsTypeToGroupName(
      ContentSettingsType::STORAGE_ACCESS));
  set_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_BLOCK));
  set_args.Append(false);  // Incognito.

  handler()->HandleSetCategoryPermissionForPattern(set_args);
  EXPECT_EQ(1U, web_ui()->call_data().size());

  HostContentSettingsMap* map =
      HostContentSettingsMapFactory::GetForProfile(profile());

  ASSERT_EQ(CONTENT_SETTING_BLOCK,
            map->GetContentSetting(GURL(kOrigin), GURL(kSecondary),
                                   kPermissionStorageAccess));
}

TEST_F(SiteSettingsHandlerTest, SetCategoryPermissionForPattern_Incognito) {
  const std::string kOrigin = "https://www.example.com:443";
  CreateIncognitoProfile();

  HostContentSettingsMap* incognito_map =
      HostContentSettingsMapFactory::GetForProfile(incognito_profile());
  HostContentSettingsMap* map =
      HostContentSettingsMapFactory::GetForProfile(profile());

  {
    base::ListValue set_args;
    set_args.Append(kOrigin);        // Primary pattern.
    set_args.Append(std::string());  // Secondary pattern.
    set_args.Append(kNotifications);
    set_args.Append(
        content_settings::ContentSettingToString(CONTENT_SETTING_BLOCK));
    set_args.Append(true);  // Incognito.

    handler()->HandleSetCategoryPermissionForPattern(set_args);
    EXPECT_EQ(3U, web_ui()->call_data().size());

    ASSERT_EQ(CONTENT_SETTING_BLOCK, incognito_map->GetContentSetting(
                                         GURL(kOrigin), GURL(std::string()),
                                         kPermissionNotifications));
    // Shouldn't change the setting for the normal profile.
    ASSERT_EQ(CONTENT_SETTING_ASK,
              map->GetContentSetting(GURL(kOrigin), GURL(std::string()),
                                     kPermissionNotifications));
  }

  {
    base::ListValue set_args;
    set_args.Append(kOrigin);        // Primary pattern.
    set_args.Append(std::string());  // Secondary pattern.
    set_args.Append(kNotifications);
    set_args.Append(
        content_settings::ContentSettingToString(CONTENT_SETTING_ALLOW));
    set_args.Append(false);  // Incognito.

    handler()->HandleSetCategoryPermissionForPattern(set_args);
    EXPECT_EQ(6U, web_ui()->call_data().size());

    ASSERT_EQ(CONTENT_SETTING_ALLOW,
              map->GetContentSetting(GURL(kOrigin), GURL(std::string()),
                                     kPermissionNotifications));
    // Shouldn't change the setting for the incognito profile.
    ASSERT_EQ(CONTENT_SETTING_BLOCK, incognito_map->GetContentSetting(
                                         GURL(kOrigin), GURL(std::string()),
                                         kPermissionNotifications));
  }

  DestroyIncognitoProfile();
}

TEST_F(SiteSettingsHandlerTest,
       SetCategoryPermissionForPattern_ExceptionHelpers) {
  ContentSettingsPattern pattern =
      ContentSettingsPattern::FromString("[*.]google.com");
  base::DictValue exception = site_settings::GetExceptionForPage(
      ContentSettingsType::NOTIFICATIONS, /*profile=*/nullptr, pattern,
      ContentSettingsPattern::Wildcard(), pattern.ToString(),
      CONTENT_SETTING_BLOCK, site_settings::SiteSettingSource::kPreference,
      /*expiration=*/base::Time::Now(), /*incognito=*/false);

  CHECK(exception.FindString(site_settings::kOrigin));
  CHECK(exception.FindString(site_settings::kDisplayName));
  CHECK(exception.FindString(site_settings::kEmbeddingOrigin));
  CHECK(exception.FindString(site_settings::kSetting));
  // Notifications should not have a description.
  CHECK(!exception.FindString(site_settings::kDescription));
  CHECK(exception.FindBool(site_settings::kIncognito).has_value());

  base::ListValue args;
  args.Append(*exception.FindString(site_settings::kOrigin));
  args.Append(*exception.FindString(site_settings::kEmbeddingOrigin));
  args.Append(kNotifications);  // Chosen arbitrarily.
  args.Append(*exception.FindString(site_settings::kSetting));
  args.Append(*exception.FindBool(site_settings::kIncognito));

  // We don't need to check the results. This is just to make sure it doesn't
  // crash on the input.
  handler()->HandleSetCategoryPermissionForPattern(args);

  scoped_refptr<const extensions::Extension> extension;
  extension = extensions::ExtensionBuilder()
                  .SetManifest(base::DictValue()
                                   .Set("name", kExtensionName)
                                   .Set("version", "1.0.0")
                                   .Set("manifest_version", 2))
                  .SetID("ahfgeienlihckogmohjhadlkjgocpleb")
                  .Build();

  base::ListValue exceptions;
  site_settings::AddExceptionForHostedApp("[*.]google.com", *extension.get(),
                                          &exceptions);

  const base::Value& dictionary_value = exceptions[0];
  CHECK(dictionary_value.is_dict());
  const base::DictValue& dictionary = dictionary_value.GetDict();
  CHECK(dictionary.FindString(site_settings::kOrigin));
  CHECK(dictionary.FindString(site_settings::kDisplayName));
  CHECK(dictionary.FindString(site_settings::kEmbeddingOrigin));
  CHECK(dictionary.FindString(site_settings::kSetting));
  CHECK(dictionary.FindBool(site_settings::kIncognito).has_value());

  // Again, don't need to check the results.
  handler()->HandleSetCategoryPermissionForPattern(args);
}

TEST_F(SiteSettingsHandlerTest, SetCategoryPermissionForPattern_SessionOnly) {
  const std::string kGoogleWithPort("https://www.google.com:443");
  base::ListValue set_args;
  set_args.Append(kGoogleWithPort);  // Primary pattern.
  set_args.Append(std::string());    // Secondary pattern.
  set_args.Append(kCookies);
  set_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_SESSION_ONLY));
  set_args.Append(false);  // Incognito.
  handler()->HandleSetCategoryPermissionForPattern(set_args);

  EXPECT_EQ(kNumberCookiesContentSettingListeners,
            web_ui()->call_data().size());
}

TEST_F(SiteSettingsHandlerTest, ExtensionDisplayName) {
  // When the extension is loaded, displayName is the extension's name and id.
  auto extension = LoadExtension(kExtensionName);
  auto extension_url = extension->url().spec();
  {
    base::ListValue get_origin_permissions_args;
    get_origin_permissions_args.Append(kCallbackId);
    get_origin_permissions_args.Append(extension_url);
    {
      base::ListValue category_list;
      category_list.Append(kNotifications);
      get_origin_permissions_args.Append(std::move(category_list));
    }
    handler()->HandleGetOriginPermissions(get_origin_permissions_args);
    std::string expected_display_name =
        base::StringPrintf("Test Extension (ID: %s)", extension->id().c_str());
    ValidateOrigin(extension_url, extension_url, expected_display_name,
                   CONTENT_SETTING_ASK,
                   site_settings::SiteSettingSource::kDefault, 1U);
  }

  // When the extension is unloaded, the displayName is the extension's origin.
  UnloadExtension(extension->id());
  {
    base::ListValue get_origin_permissions_args;
    get_origin_permissions_args.Append(kCallbackId);
    get_origin_permissions_args.Append(extension_url);
    {
      base::ListValue category_list;
      category_list.Append(kNotifications);
      get_origin_permissions_args.Append(std::move(category_list));
    }
    handler()->HandleGetOriginPermissions(get_origin_permissions_args);
    ValidateOrigin(
        extension_url, extension_url,
        base::StringPrintf("chrome-extension://%s", extension->id().c_str()),
        CONTENT_SETTING_ASK, site_settings::SiteSettingSource::kDefault, 2U);
  }
}

TEST_F(SiteSettingsHandlerTest, PatternsAndContentType) {
  unsigned counter = 1;
  for (const auto& test_case : kPatternsAndContentTypeTestCases) {
    base::ListValue args;
    args.Append(kCallbackId);
    args.Append(test_case.arguments.pattern);
    args.Append(test_case.arguments.content_type);
    handler()->HandleIsPatternValidForType(args);
    ValidatePattern(test_case.expected.validity, counter,
                    test_case.expected.reason);
    ++counter;
  }
}

TEST_F(SiteSettingsHandlerTest, Incognito) {
  base::ListValue args;
  handler()->HandleUpdateIncognitoStatus(args);
  ValidateIncognitoExists(false, 1U);

  CreateIncognitoProfile();
  ValidateIncognitoExists(true, 2U);

  DestroyIncognitoProfile();
  ValidateIncognitoExists(false, 3U);
}

TEST_F(SiteSettingsHandlerTest, ZoomLevels) {
  std::string http_host("www.google.com");
  std::string error_host("chromewebdata");
  std::string data_url("data:text/plain;base64,SGVsbG8sIFdvcmxkIQ==");
  double zoom_level = 1.1;

  content::HostZoomMap* host_zoom_map =
      content::HostZoomMap::GetDefaultForBrowserContext(profile());
  host_zoom_map->SetZoomLevelForHost(http_host, zoom_level);
  host_zoom_map->SetZoomLevelForHost(error_host, zoom_level);
  host_zoom_map->SetZoomLevelForHost(data_url, zoom_level);
  ValidateZoom({{error_host, "(Chrome error pages)", "122%"},
                {data_url, data_url, "122%"},
                {http_host, http_host, "122%"}},
               3U);

  base::ListValue args;
  handler()->HandleFetchZoomLevels(args);
  ValidateZoom({{error_host, "(Chrome error pages)", "122%"},
                {data_url, data_url, "122%"},
                {http_host, http_host, "122%"}},
               4U);

  args.Append(http_host);
  handler()->HandleRemoveZoomLevel(args);
  args.front() = base::Value(error_host);
  handler()->HandleRemoveZoomLevel(args);
  args.front() = base::Value(data_url);
  handler()->HandleRemoveZoomLevel(args);
  ValidateZoom({}, 7U);

  double default_level = host_zoom_map->GetDefaultZoomLevel();
  double level = host_zoom_map->GetZoomLevelForHostAndScheme("http", http_host);
  EXPECT_EQ(default_level, level);
}

TEST_F(SiteSettingsHandlerTest, TemporaryCookieExceptions) {
  // Set a temporary exception directly, instead of relying on any helpers that
  // have duration configurable via feature parameters.
  constexpr int kExpirationDurationInDays = 100;

  content_settings::ContentSettingConstraints constraints;
  constraints.set_lifetime(base::Days(kExpirationDurationInDays));
  constraints.set_session_model(content_settings::mojom::SessionModel::DURABLE);

  HostContentSettingsMap* host_content_settings_map =
      HostContentSettingsMapFactory::GetForProfile(profile());

  host_content_settings_map->SetContentSettingCustomScope(
      ContentSettingsPattern::Wildcard(),
      ContentSettingsPattern::FromURL(GURL("https://example.com")),
      ContentSettingsType::COOKIES, ContentSetting::CONTENT_SETTING_ALLOW,
      constraints);

  base::ListValue get_exception_list_args;
  get_exception_list_args.Append(kCallbackId);
  get_exception_list_args.Append(kCookies);

  handler()->HandleGetExceptionList(get_exception_list_args);

  const content::TestWebUI::CallData& data = *web_ui()->call_data().back();
  const base::ListValue& exception_list = data.arg3()->GetList();
  EXPECT_EQ(1UL, exception_list.size());

  // Mirror the logic in the helper to avoid flakes on time edges.
  auto time_diff = (base::Time::Now() + base::Days(kExpirationDurationInDays))
                       .LocalMidnight() -
                   base::Time::Now().LocalMidnight();

  EXPECT_EQ(l10n_util::GetPluralStringFUTF8(
                IDS_SETTINGS_EXPIRES_AFTER_TIME_LABEL, time_diff.InDays()),
            CHECK_DEREF(exception_list[0].GetDict().FindString("description")));
}

class SiteSettingsHandlerIsolatedWebAppTest
    : public SiteSettingsHandlerBaseTest {
 protected:
  static constexpr char kAppName[] = "IWA Name";
  static constexpr char kSubAppName[] = "Sub App";

  void SetUpIsolatedWebApp() override {
    web_app::test::AwaitStartWebAppProviderAndSubsystems(profile());
    iwa_url_info_ = InstallIsolatedWebApp(kAppName);
  }

  const base::ListValue& CallHandleGetOriginPermissions(
      const std::string& url,
      base::ListValue category_list) {
    base::ListValue args;
    args.Append(kCallbackId);
    args.Append(url);
    args.Append(std::move(category_list));
    handler()->HandleGetOriginPermissions(args);

    const content::TestWebUI::CallData& data = *web_ui()->call_data().back();
    EXPECT_EQ("cr.webUIResponse", data.function_name());
    return data.arg3()->GetList();
  }

  const base::DictValue& CallHandleGetSubAppsPermissionExplanation(
      const std::string& url) {
    base::ListValue args;
    args.Append(kCallbackId);
    args.Append(url);
    handler()->HandleGetSubAppsPermissionExplanation(args);

    const content::TestWebUI::CallData& data = *web_ui()->call_data().back();
    EXPECT_EQ("cr.webUIResponse", data.function_name());
    return data.arg3()->GetDict();
  }

  void InstallSubApp(const GURL& url, const std::string& name) {
    auto sub_app =
        web_app::test::CreateWebApp(url, web_app::WebAppManagement::kSubApp);
    sub_app->SetName(name);
    sub_app->SetParentAppId(iwa_url_info_->app_id());

    auto* provider = web_app::WebAppProvider::GetForTest(profile());
    {
      web_app::ScopedRegistryUpdate update =
          provider->sync_bridge_unsafe().BeginUpdate();
      update->CreateApp(std::move(sub_app));
    }
  }

 protected:
  std::optional<web_app::IsolatedWebAppUrlInfo> iwa_url_info_;

  web_app::IsolatedWebAppUrlInfo InstallIsolatedWebApp(
      const std::string& name) {
    const std::unique_ptr<web_app::ScopedBundledIsolatedWebApp> bundle =
        web_app::IsolatedWebAppBuilder(web_app::ManifestBuilder().SetName(name))
            .BuildBundle();
    bundle->FakeInstallPageState(profile());
    bundle->TrustSigningKey();
    return bundle->InstallChecked(profile());
  }

  content::HostZoomMap* GetIwaHostZoomMap(
      const web_app::IsolatedWebAppUrlInfo& url_info) {
    content::StoragePartition* iwa_partition = profile()->GetStoragePartition(
        url_info.storage_partition_config(profile()));
    return content::HostZoomMap::GetForStoragePartition(iwa_partition);
  }

 private:
  data_decoder::test::InProcessDataDecoder in_process_data_decoder_;
};

TEST_F(SiteSettingsHandlerIsolatedWebAppTest, GetOriginPermissionsSubApp) {
  GURL sub_app_url = iwa_url_info_->origin().GetURL().Resolve("/sub-app/");
  InstallSubApp(sub_app_url, std::string(kSubAppName));

  base::ListValue category_list;
  category_list.Append(site_settings::ContentSettingsTypeToGroupName(
      ContentSettingsType::NOTIFICATIONS));

  const base::ListValue& permissions = CallHandleGetOriginPermissions(
      sub_app_url.spec(), std::move(category_list));
  ASSERT_EQ(1U, permissions.size());

  const base::DictValue& permission = permissions[0].GetDict();
  // Append " (ID: <id>)" to the name as per HandleGetOriginPermissions
  std::string expected_name = l10n_util::GetStringFUTF8(
      IDS_SETTINGS_EXTENSION_OR_APP_DISPLAY_NAME,
      base::UTF8ToUTF16(std::string_view(kSubAppName)),
      base::UTF8ToUTF16(sub_app_url.host()));
  EXPECT_EQ(expected_name, *permission.FindString("displayName"));
}

TEST_F(SiteSettingsHandlerIsolatedWebAppTest,
       GetSubAppsPermissionExplanation_NeitherParentNorSubApp) {
  const base::DictValue& result = CallHandleGetSubAppsPermissionExplanation(
      iwa_url_info_->origin().GetURL().spec());
  EXPECT_FALSE(*result.FindBool("isSubApp"));
  EXPECT_FALSE(*result.FindBool("hasSubApps"));
}

TEST_F(SiteSettingsHandlerIsolatedWebAppTest,
       GetSubAppsPermissionExplanation_SubApp) {
  GURL sub_app_url = iwa_url_info_->origin().GetURL().Resolve("/sub-app/");
  InstallSubApp(sub_app_url, std::string(kSubAppName));

  const base::DictValue& result =
      CallHandleGetSubAppsPermissionExplanation(sub_app_url.spec());
  EXPECT_TRUE(*result.FindBool("isSubApp"));
  EXPECT_FALSE(*result.FindBool("hasSubApps"));
  EXPECT_EQ(kSubAppName, *result.FindString("appName"));
  EXPECT_EQ(kAppName, *result.FindString("parentAppName"));
  EXPECT_EQ(iwa_url_info_->origin().GetURL().spec(),
            *result.FindString("parentAppOrigin"));
}

TEST_F(SiteSettingsHandlerIsolatedWebAppTest,
       GetSubAppsPermissionExplanation_ParentApp) {
  GURL sub_app_url = iwa_url_info_->origin().GetURL().Resolve("/sub-app/");
  InstallSubApp(sub_app_url, std::string(kSubAppName));

  const base::DictValue& result = CallHandleGetSubAppsPermissionExplanation(
      iwa_url_info_->origin().GetURL().spec());
  EXPECT_FALSE(*result.FindBool("isSubApp"));
  EXPECT_TRUE(*result.FindBool("hasSubApps"));
  EXPECT_EQ(kAppName, *result.FindString("appName"));
}

TEST_F(SiteSettingsHandlerIsolatedWebAppTest, AllSitesDisplaysAppName) {
  GURL https_url("https://" + iwa_url_info_->origin().host());
  GURL iwa_origin_url = iwa_url_info_->origin().GetURL();

  SetupModelWithIsolatedWebAppData({{iwa_url_info_->origin().Serialize(), 50}});
  HostContentSettingsMap* map =
      HostContentSettingsMapFactory::GetForProfile(profile());
  map->SetContentSettingDefaultScope(iwa_origin_url, iwa_origin_url,
                                     ContentSettingsType::NOTIFICATIONS,
                                     CONTENT_SETTING_BLOCK);
  map->SetContentSettingDefaultScope(https_url, https_url,
                                     ContentSettingsType::NOTIFICATIONS,
                                     CONTENT_SETTING_BLOCK);

  base::ListValue site_groups = GetOnStorageFetchedSentList();

  ASSERT_EQ(site_groups.size(), 2u);
  const base::DictValue& group1 = site_groups[0].GetDict();
  const base::DictValue& origin1 =
      CHECK_DEREF(group1.FindList("origins"))[0].GetDict();
  EXPECT_THAT(CHECK_DEREF(group1.FindString("groupingKey")),
              IsOrigin(iwa_origin_url));
  EXPECT_EQ(group1.FindString("etldPlus1"), nullptr);
  EXPECT_EQ(CHECK_DEREF(group1.FindString("displayName")), kAppName);
  EXPECT_EQ(CHECK_DEREF(origin1.FindString("origin")), iwa_origin_url);
  EXPECT_EQ(origin1.FindDouble("usage").value(), 50.0);

  const base::DictValue& group2 = site_groups[1].GetDict();
  const base::DictValue& origin2 =
      CHECK_DEREF(group2.FindList("origins"))[0].GetDict();
  EXPECT_THAT(CHECK_DEREF(group2.FindString("groupingKey")),
              IsEtldPlus1(iwa_url_info_->origin().host()));
  EXPECT_EQ(CHECK_DEREF(group2.FindString("etldPlus1")),
            iwa_url_info_->origin().host());
  EXPECT_EQ(CHECK_DEREF(group2.FindString("displayName")),
            iwa_url_info_->origin().host());
  EXPECT_EQ(CHECK_DEREF(origin2.FindString("origin")), https_url);
  EXPECT_EQ(origin2.FindDouble("usage").value(), 0.0);
}

TEST_F(SiteSettingsHandlerIsolatedWebAppTest, ZoomLevel) {
  content::HostZoomMap* iwa_host_zoom_map = GetIwaHostZoomMap(*iwa_url_info_);

  std::string host_or_spec = iwa_url_info_->origin().Serialize();
  iwa_host_zoom_map->SetZoomLevelForHost(iwa_url_info_->origin().host(), 1.1);
  ValidateZoom({{host_or_spec, kAppName, "122%"}}, 1U);

  base::ListValue args;
  handler()->HandleFetchZoomLevels(args);
  ValidateZoom({{host_or_spec, kAppName, "122%"}}, 2U);

  args.Append(host_or_spec);
  handler()->HandleRemoveZoomLevel(args);
  ValidateZoom({}, 3U);

  double default_level = iwa_host_zoom_map->GetDefaultZoomLevel();
  double level = iwa_host_zoom_map->GetZoomLevelForHostAndScheme(
      "isolated-app", iwa_url_info_->origin().host());
  EXPECT_EQ(default_level, level);
}

TEST_F(SiteSettingsHandlerIsolatedWebAppTest, ZoomLevelsSortedByAppName) {
  GetIwaHostZoomMap(*iwa_url_info_)
      ->SetZoomLevelForHost(iwa_url_info_->origin().host(), 1.1);

  // Install 3 more IWAs.
  web_app::IsolatedWebAppUrlInfo iwa3_url_info =
      InstallIsolatedWebApp("IWA Name 3");
  GetIwaHostZoomMap(iwa3_url_info)
      ->SetZoomLevelForHost(iwa3_url_info.origin().host(), 1.1);

  web_app::IsolatedWebAppUrlInfo iwa2_url_info =
      InstallIsolatedWebApp("IWA Name 2");
  GetIwaHostZoomMap(iwa2_url_info)
      ->SetZoomLevelForHost(iwa2_url_info.origin().host(), 1.1);

  // Don't set a zoom for this app to make sure it's not in the list.
  web_app::IsolatedWebAppUrlInfo iwa4_url = InstallIsolatedWebApp("IWA Name 4");

  base::ListValue args;
  handler()->HandleFetchZoomLevels(args);

  ValidateZoom({{iwa_url_info_->origin().Serialize(), kAppName, "122%"},
                {iwa2_url_info.origin().Serialize(), "IWA Name 2", "122%"},
                {iwa3_url_info.origin().Serialize(), "IWA Name 3", "122%"}},
               2U);
}

namespace {

class FakeTab : public tabs::MockTabInterface {
 public:
  FakeTab(BrowserWindowInterface* window,
          std::unique_ptr<content::WebContents> web_contents)
      : window_(window), web_contents_(std::move(web_contents)) {
    infobars::ContentInfoBarManager::CreateForWebContents(web_contents_.get());
    ON_CALL(*this, GetBrowserWindowInterface())
        .WillByDefault(testing::Return(window_));
    ON_CALL(*this, GetContents())
        .WillByDefault(testing::Return(web_contents_.get()));
    ON_CALL(*this, GetWeakPtr())
        .WillByDefault(testing::Invoke(this, &FakeTab::GetWeakPtrImpl));
  }
  ~FakeTab() override = default;

  base::WeakPtr<tabs::TabInterface> GetWeakPtrImpl() {
    return weak_factory_.GetWeakPtr();
  }

  content::WebContents* web_contents() { return web_contents_.get(); }
  ui::UnownedUserDataHost& GetUnownedUserDataHost() override {
    return user_data_host_;
  }
  const ui::UnownedUserDataHost& GetUnownedUserDataHost() const override {
    return user_data_host_;
  }

 private:
  raw_ptr<BrowserWindowInterface> window_;
  std::unique_ptr<content::WebContents> web_contents_;
  ui::UnownedUserDataHost user_data_host_;
  base::WeakPtrFactory<FakeTab> weak_factory_{this};
};

class FakeBrowserWindow {
 public:
  explicit FakeBrowserWindow(Profile* profile) : profile_(profile) {
    ON_CALL(window_, GetProfile()).WillByDefault(testing::Return(profile_));
    ON_CALL(tab_list_, GetAllTabs()).WillByDefault([this]() {
      std::vector<tabs::TabInterface*> result;
      result.reserve(tabs_.size());
      for (const auto& tab : tabs_) {
        result.push_back(tab.get());
      }
      return result;
    });
#if !BUILDFLAG(IS_ANDROID)
    static_cast<BrowserCollectionObserver*>(
        GlobalBrowserCollection::GetInstance()->GetPlatformDelegate())
        ->OnBrowserCreated(&window_);
#endif
  }

  ~FakeBrowserWindow() {
#if !BUILDFLAG(IS_ANDROID)
    static_cast<BrowserCollectionObserver*>(
        GlobalBrowserCollection::GetInstance()->GetPlatformDelegate())
        ->OnBrowserClosed(&window_);
#endif
    CloseAllTabs();
  }

  MockBrowserWindowInterface* window() { return &window_; }

  FakeTab* AddTab(const GURL& url) {
    std::unique_ptr<content::WebContents> web_contents =
        content::WebContentsTester::CreateTestWebContents(profile_, nullptr);
    content::NavigationSimulator::NavigateAndCommitFromBrowser(
        web_contents.get(), url);
    auto tab = std::make_unique<FakeTab>(&window_, std::move(web_contents));
    FakeTab* tab_ptr = tab.get();
    tabs_.push_back(std::move(tab));
    return tab_ptr;
  }

  FakeTab* InsertTabAt(size_t index, const GURL& url) {
    std::unique_ptr<content::WebContents> web_contents =
        content::WebContentsTester::CreateTestWebContents(profile_, nullptr);
    content::NavigationSimulator::NavigateAndCommitFromBrowser(
        web_contents.get(), url);
    auto tab = std::make_unique<FakeTab>(&window_, std::move(web_contents));
    FakeTab* tab_ptr = tab.get();
    if (index >= tabs_.size()) {
      tabs_.push_back(std::move(tab));
    } else {
      tabs_.insert(tabs_.begin() + index, std::move(tab));
    }
    return tab_ptr;
  }

  FakeTab* GetTab(size_t index) {
    CHECK_LT(index, tabs_.size());
    return tabs_[index].get();
  }

  size_t tab_count() const { return tabs_.size(); }

  void CloseAllTabs() { tabs_.clear(); }

 private:
  raw_ptr<Profile> profile_;
  NiceMock<MockBrowserWindowInterface> window_;
  NiceMock<MockTabListInterface> tab_list_;
  ui::ScopedUnownedUserData<TabListInterface> tab_list_registration_{
      window_.GetUnownedUserDataHost(), tab_list_};
  std::vector<std::unique_ptr<FakeTab>> tabs_;
};

}  // namespace

class SiteSettingsHandlerInfobarTest
    : public SiteSettingsHandlerBaseTest,
      public testing::WithParamInterface<bool> {
 public:
  SiteSettingsHandlerInfobarTest() = default;
  SiteSettingsHandlerInfobarTest(const SiteSettingsHandlerInfobarTest&) =
      delete;
  SiteSettingsHandlerInfobarTest& operator=(
      const SiteSettingsHandlerInfobarTest&) = delete;
  void SetUp() override {
    layout_provider_ = ChromeLayoutProvider::CreateLayoutProvider();

    if (GetParam()) {
      feature_list_.InitAndEnableFeatureWithParameters(
          infobars::kCentralizedInfoBarFramework,
          {{infobars::kMigratedPageInfo.name, "true"}});
    } else {
      feature_list_.InitAndDisableFeature(
          infobars::kCentralizedInfoBarFramework);
    }

    SiteSettingsHandlerBaseTest::SetUp();

    if (infobars::IsInfoBarMigrated(
            infobars::InfoBarDelegate::PAGE_INFO_INFOBAR_DELEGATE)) {
      auto* browser_infobar_manager =
          infobars::BrowserInfoBarManager::From(g_browser_process);
      if (browser_infobar_manager) {
        auto spec = infobars::InfoBarSpec::Builder(
                        infobars::InfoBarDelegate::PAGE_INFO_INFOBAR_DELEGATE)
                        .SetMessageText(u"Test")
                        .SetIcon(vector_icons::kSettingsIcon)
                        .SetScope(infobars::InfoBarScope::kTab)
                        .Build();
        browser_infobar_manager->Register(std::move(spec));
      }
    }

    browser1_ = std::make_unique<FakeBrowserWindow>(profile());
    browser2_ = std::make_unique<FakeBrowserWindow>(profile());

    // Creates the second profile used by this test.
    profile2_ = profile_manager()->CreateTestingProfile(
        "testing_profile2@test", nullptr, std::u16string(), 0,
        TestingProfile::TestingFactories());

    browser3_ = std::make_unique<FakeBrowserWindow>(profile2_);

    extensions::TestExtensionSystem* extension_system =
        static_cast<extensions::TestExtensionSystem*>(
            extensions::ExtensionSystem::Get(profile()));
    extension_system->CreateExtensionService(
        base::CommandLine::ForCurrentProcess(), base::FilePath(), false);
  }

  void TearDown() override {
    browser1_.reset();
    browser2_.reset();
    browser3_.reset();
    profile2_ = nullptr;
    layout_provider_.reset();

    SiteSettingsHandlerBaseTest::TearDown();
  }

  infobars::ContentInfoBarManager* GetInfoBarManagerForTab(
      FakeBrowserWindow* browser,
      size_t tab_index,
      GURL* tab_url) {
    content::WebContents* web_contents =
        browser->GetTab(tab_index)->web_contents();
    if (tab_url) {
      *tab_url = web_contents->GetLastCommittedURL();
    }
    return infobars::ContentInfoBarManager::FromWebContents(web_contents);
  }

  FakeBrowserWindow* browser1() { return browser1_.get(); }
  FakeBrowserWindow* browser2() { return browser2_.get(); }
  FakeBrowserWindow* browser3() { return browser3_.get(); }

  const std::string_view kNotifications =
      site_settings::ContentSettingsTypeToGroupName(
          ContentSettingsType::NOTIFICATIONS);

 private:
  std::unique_ptr<views::LayoutProvider> layout_provider_;
  content::RenderViewHostTestEnabler rvh_test_enabler_;
  base::test::ScopedFeatureList feature_list_;
  std::unique_ptr<FakeBrowserWindow> browser1_;
  std::unique_ptr<FakeBrowserWindow> browser2_;
  raw_ptr<TestingProfile> profile2_ = nullptr;
  std::unique_ptr<FakeBrowserWindow> browser3_;
};

TEST_P(SiteSettingsHandlerInfobarTest, SettingPermissionsTriggersInfobar) {
  // Note all GURLs starting with 'origin' below belong to the same origin.
  //               _____  _______________  ________  ________  ___________
  //   Window 1:  / foo \' origin_anchor \' chrome \' origin \' extension \
  // -------------       -----------------------------------------------------
  std::string origin_anchor_string =
      "https://www.example.com/with/path/blah#heading";
  const GURL foo("http://foo");
  const GURL origin_anchor(origin_anchor_string);
  const GURL chrome("chrome://about");
  const GURL origin("https://www.example.com/");
  const GURL extension(
      "chrome-extension://fooooooooooooooooooooooooooooooo/bar.html");

  // Make sure |extension|'s extension ID exists before navigating to it. This
  // fixes a test timeout that occurs with --enable-browser-side-navigation on.
  scoped_refptr<const extensions::Extension> test_extension =
      extensions::ExtensionBuilder("Test")
          .SetID("fooooooooooooooooooooooooooooooo")
          .Build();
  extensions::ExtensionRegistrar::Get(profile())->AddExtension(
      test_extension.get());

  //               __________  ______________  ___________________  _______
  //   Window 2:  / insecure '/ origin_query \' example_subdomain \' about \
  // -------------------------                --------------------------------
  const GURL insecure("http://www.example.com/");
  const GURL origin_query("https://www.example.com/?param=value");
  const GURL example_subdomain("https://subdomain.example.com/");
  const GURL about(url::kAboutBlankURL);

  // Set up. Note InsertTabAt(0, ...) adds tab at index 0, so add them in
  // reverse order.
  browser1()->InsertTabAt(0, extension);
  browser1()->InsertTabAt(0, origin);
  browser1()->InsertTabAt(0, chrome);
  browser1()->InsertTabAt(0, origin_anchor);
  browser1()->InsertTabAt(0, foo);
  for (size_t i = 0; i < browser1()->tab_count(); ++i) {
    EXPECT_EQ(
        0u, GetInfoBarManagerForTab(browser1(), i, nullptr)->infobars().size());
  }

  browser2()->InsertTabAt(0, about);
  browser2()->InsertTabAt(0, example_subdomain);
  browser2()->InsertTabAt(0, origin_query);
  browser2()->InsertTabAt(0, insecure);
  for (size_t i = 0; i < browser2()->tab_count(); ++i) {
    EXPECT_EQ(
        0u, GetInfoBarManagerForTab(browser2(), i, nullptr)->infobars().size());
  }

  // Block notifications.
  base::ListValue set_args;
  set_args.Append(origin_anchor_string);
  set_args.Append(kNotifications);
  set_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_BLOCK));
  handler()->HandleSetOriginPermissions(set_args);

  // Make sure all tabs belonging to the same origin as |origin_anchor| have an
  // infobar shown.
  GURL tab_url;
  for (size_t i = 0; i < browser1()->tab_count(); ++i) {
    if (i == /*origin_anchor=*/1 || i == /*origin=*/3) {
      EXPECT_EQ(
          1u,
          GetInfoBarManagerForTab(browser1(), i, &tab_url)->infobars().size());
      EXPECT_TRUE(url::IsSameOriginWith(origin, tab_url));
    } else {
      EXPECT_EQ(
          0u,
          GetInfoBarManagerForTab(browser1(), i, &tab_url)->infobars().size());
      EXPECT_FALSE(url::IsSameOriginWith(origin, tab_url));
    }
  }
  for (size_t i = 0; i < browser2()->tab_count(); ++i) {
    if (i == /*origin_query=*/1) {
      EXPECT_EQ(
          1u,
          GetInfoBarManagerForTab(browser2(), i, &tab_url)->infobars().size());
      EXPECT_TRUE(url::IsSameOriginWith(origin, tab_url));
    } else {
      EXPECT_EQ(
          0u,
          GetInfoBarManagerForTab(browser2(), i, &tab_url)->infobars().size());
      EXPECT_FALSE(url::IsSameOriginWith(origin, tab_url));
    }
  }

  // Navigate the |foo| tab to the same origin as |origin_anchor|, and the
  // |origin_query| tab to a different origin.
  const GURL origin_path("https://www.example.com/path/to/page.html");
  content::WebContents* foo_contents =
      browser1()->GetTab(/*index=*/0)->web_contents();
  content::NavigationSimulator::NavigateAndCommitFromBrowser(foo_contents,
                                                             origin_path);

  const GURL example_without_www("https://example.com/");
  content::WebContents* origin_query_contents =
      browser2()->GetTab(/*index=*/1)->web_contents();
  content::NavigationSimulator::NavigateAndCommitFromBrowser(
      origin_query_contents, example_without_www);

  // Reset all permissions.
  base::ListValue reset_args;
  reset_args.Append(origin_anchor_string);
  base::ListValue category_list;
  category_list.Append(kNotifications);
  reset_args.Append(std::move(category_list));
  reset_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_DEFAULT));
  handler()->HandleSetOriginPermissions(reset_args);

  // Check the same tabs (plus the tab navigated to |origin_path|) still have
  // infobars showing.
  for (size_t i = 0; i < browser1()->tab_count(); ++i) {
    if (i == /*origin_path=*/0 || i == /*origin_anchor=*/1 ||
        i == /*origin=*/3) {
      EXPECT_EQ(
          1u,
          GetInfoBarManagerForTab(browser1(), i, &tab_url)->infobars().size());
      EXPECT_TRUE(url::IsSameOriginWith(origin, tab_url));
    } else {
      EXPECT_EQ(
          0u,
          GetInfoBarManagerForTab(browser1(), i, &tab_url)->infobars().size());
      EXPECT_FALSE(url::IsSameOriginWith(origin, tab_url));
    }
  }
  // The infobar on the original |origin_query| tab (which has now been
  // navigated to |example_without_www|) should disappear.
  for (size_t i = 0; i < browser2()->tab_count(); ++i) {
    EXPECT_EQ(
        0u,
        GetInfoBarManagerForTab(browser2(), i, &tab_url)->infobars().size());
    EXPECT_FALSE(url::IsSameOriginWith(origin, tab_url));
  }

  // Make sure it's the correct infobar that's being shown.
  EXPECT_EQ(infobars::InfoBarDelegate::PAGE_INFO_INFOBAR_DELEGATE,
            GetInfoBarManagerForTab(browser1(), /*tab_index=*/0, &tab_url)
                ->infobars()[0]
                ->delegate()
                ->GetIdentifier());
  EXPECT_TRUE(url::IsSameOriginWith(origin, tab_url));
}

TEST_P(SiteSettingsHandlerInfobarTest,
       SettingPermissionsDoesNotTriggerInfobarOnDifferentProfile) {
  // Note all GURLs starting with 'origin' below belong to the same origin.
  //               _______________
  //   Window 1:  / origin_anchor \
  // -------------       -----------------------------------------------------
  const GURL origin("https://www.example.com/");
  std::string origin_anchor_string =
      "https://www.example.com/with/path/blah#heading";
  const GURL origin_anchor(origin_anchor_string);

  //   Different
  //   Profile (2) ______________
  //   Window 3:  / origin_query \
  // -------------------------------------------------------------------------
  const GURL origin_query("https://www.example.com/?param=value");

  // Set up. No info bars.
  browser1()->AddTab(origin_anchor);
  EXPECT_EQ(0u,
            GetInfoBarManagerForTab(browser1(), 0, nullptr)->infobars().size());

  browser3()->AddTab(origin_query);
  EXPECT_EQ(0u,
            GetInfoBarManagerForTab(browser3(), 0, nullptr)->infobars().size());

  // Block notifications.
  base::ListValue set_args;
  set_args.Append(origin_anchor_string);
  set_args.Append(kNotifications);
  set_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_BLOCK));
  handler()->HandleSetOriginPermissions(set_args);

  // Make sure all tabs within the same profile belonging to the same origin
  // as `origin_anchor` have an infobar shown.
  GURL tab_url;
  EXPECT_EQ(
      1u, GetInfoBarManagerForTab(browser1(), 0, &tab_url)->infobars().size());
  EXPECT_TRUE(url::IsSameOriginWith(origin, tab_url));

  // Make sure all tabs with the same origin as `origin_anchor` that don't
  // belong to the same profile don't have an infobar shown
  EXPECT_EQ(
      0u, GetInfoBarManagerForTab(browser3(), 0, &tab_url)->infobars().size());
  EXPECT_TRUE(url::IsSameOriginWith(origin, tab_url));
}

INSTANTIATE_TEST_SUITE_P(All,
                         SiteSettingsHandlerInfobarTest,
                         testing::Bool(),
                         [](const testing::TestParamInfo<bool>& info) {
                           return info.param ? "Migrated" : "Legacy";
                         });

TEST_F(SiteSettingsHandlerTest, BlockAutoplay_SendOnRequest) {
  base::ListValue args;
  handler()->HandleFetchBlockAutoplayStatus(args);

  // Check that we are checked and enabled.
  ValidateBlockAutoplay(true, true);
}

TEST_F(SiteSettingsHandlerTest, BlockAutoplay_SoundSettingUpdate) {
  SetSoundContentSettingDefault(CONTENT_SETTING_BLOCK);
  base::RunLoop().RunUntilIdle();

  // Check that we are not checked or enabled.
  ValidateBlockAutoplay(false, false);

  SetSoundContentSettingDefault(CONTENT_SETTING_ALLOW);
  base::RunLoop().RunUntilIdle();

  // Check that we are checked and enabled.
  ValidateBlockAutoplay(true, true);
}

TEST_F(SiteSettingsHandlerTest, BlockAutoplay_PrefUpdate) {
  profile()->GetPrefs()->SetBoolean(prefs::kBlockAutoplayEnabled, false);
  base::RunLoop().RunUntilIdle();

  // Check that we are not checked but are enabled.
  ValidateBlockAutoplay(false, true);

  profile()->GetPrefs()->SetBoolean(prefs::kBlockAutoplayEnabled, true);
  base::RunLoop().RunUntilIdle();

  // Check that we are checked and enabled.
  ValidateBlockAutoplay(true, true);
}

TEST_F(SiteSettingsHandlerTest, BlockAutoplay_Update) {
  EXPECT_TRUE(profile()->GetPrefs()->GetBoolean(prefs::kBlockAutoplayEnabled));

  base::ListValue data;
  data.Append(false);

  handler()->HandleSetBlockAutoplayEnabled(data);
  EXPECT_FALSE(profile()->GetPrefs()->GetBoolean(prefs::kBlockAutoplayEnabled));
}

TEST_F(SiteSettingsHandlerTest, ExcludeWebUISchemesInLists) {
  SetupModel();
  const ContentSettingsType content_settings_type =
      ContentSettingsType::NOTIFICATIONS;
  // Register WebUIAllowlist auto-granted permissions.
  const auto kWebUIOrigins = std::to_array<url::Origin>({
      url::Origin::Create(GURL("chrome://test")),
      url::Origin::Create(GURL("chrome-untrusted://test")),
      url::Origin::Create(GURL("devtools://devtools")),
  });

  WebUIAllowlist* allowlist = WebUIAllowlist::GetOrCreate(profile());
  for (const url::Origin& origin : kWebUIOrigins) {
    allowlist->RegisterAutoGrantedPermission(origin, content_settings_type);
  }

  // Verify the auto-granted permissions are registered, and they are indeed
  // provided by WebUIAllowlist.
  HostContentSettingsMap* map =
      HostContentSettingsMapFactory::GetForProfile(profile());
  content_settings::SettingInfo info;
  base::Value value = map->GetWebsiteSetting(kWebUIOrigins[0].GetURL(),
                                             kWebUIOrigins[0].GetURL(),
                                             content_settings_type, &info);
  EXPECT_EQ(CONTENT_SETTING_ALLOW, value.GetInt());
  EXPECT_EQ(content_settings::SettingSource::kAllowList, info.source);

  // Register an ordinary website permission.
  const GURL kWebUrl = GURL("https://example.com");
  map->SetContentSettingDefaultScope(kWebUrl, kWebUrl, content_settings_type,
                                     CONTENT_SETTING_ALLOW);
  EXPECT_EQ(CONTENT_SETTING_ALLOW,
            map->GetContentSetting(kWebUrl, kWebUrl, content_settings_type));

  // GetAllSites() only returns website exceptions.
  {
    base::ListValue get_all_sites_args;
    get_all_sites_args.Append(kCallbackId);

    handler()->HandleGetAllSites(get_all_sites_args);

    const content::TestWebUI::CallData& data = *web_ui()->call_data().back();
    const base::ListValue& site_groups = data.arg3()->GetList();
    EXPECT_EQ(1UL, site_groups.size());
    const base::DictValue& first_site_group = site_groups[0].GetDict();

    EXPECT_THAT(CHECK_DEREF(first_site_group.FindString("groupingKey")),
                IsEtldPlus1("example.com"));
    const base::ListValue& origin_list =
        CHECK_DEREF(first_site_group.FindList("origins"));
    EXPECT_EQ(1UL, origin_list.size());
    EXPECT_EQ(kWebUrl.spec(),
              CHECK_DEREF(origin_list[0].GetDict().FindString("origin")));
  }

  // GetExceptionList() only returns website exceptions.
  {
    base::ListValue get_exception_list_args;
    get_exception_list_args.Append(kCallbackId);
    get_exception_list_args.Append(kNotifications);

    handler()->HandleGetExceptionList(get_exception_list_args);

    const content::TestWebUI::CallData& data = *web_ui()->call_data().back();
    const base::ListValue& exception_list = data.arg3()->GetList();
    EXPECT_EQ(1UL, exception_list.size());
    EXPECT_EQ("https://example.com:443",
              CHECK_DEREF(exception_list[0].GetDict().FindString("origin")));
  }

  // GetRecentSitePermissions() only returns website exceptions.
  {
    base::ListValue get_recent_permissions_args;
    get_recent_permissions_args.Append(kCallbackId);
    get_recent_permissions_args.Append(3);

    handler()->HandleGetRecentSitePermissions(get_recent_permissions_args);

    const content::TestWebUI::CallData& data = *web_ui()->call_data().back();
    const base::ListValue& recent_permission_list = data.arg3()->GetList();
    EXPECT_EQ(1UL, recent_permission_list.size());
    EXPECT_EQ(
        kWebUrl.spec(),
        CHECK_DEREF(recent_permission_list[0].GetDict().FindString("origin")));
  }
}

// GetOriginPermissions() returns the allowlisted exception. We explicitly
// return this, so developers can easily test things (e.g. by navigating to
// chrome://settings/content/siteDetails?site=chrome://example).
TEST_F(SiteSettingsHandlerTest, IncludeWebUISchemesInGetOriginPermissions) {
  const ContentSettingsType content_settings_type =
      ContentSettingsType::NOTIFICATIONS;

  // Register WebUIAllowlist auto-granted permissions.
  const url::Origin kWebUIOrigins[] = {
      url::Origin::Create(GURL("chrome://test")),
      url::Origin::Create(GURL("chrome-untrusted://test")),
      url::Origin::Create(GURL("devtools://devtools")),
  };

  WebUIAllowlist* allowlist = WebUIAllowlist::GetOrCreate(profile());
  for (const url::Origin& origin : kWebUIOrigins) {
    allowlist->RegisterAutoGrantedPermission(origin, content_settings_type);
  }

  for (const url::Origin& origin : kWebUIOrigins) {
    base::ListValue get_origin_permissions_args;
    get_origin_permissions_args.Append(kCallbackId);
    get_origin_permissions_args.Append(origin.GetURL().spec());
    base::ListValue category_list;
    category_list.Append(kNotifications);
    get_origin_permissions_args.Append(std::move(category_list));

    handler()->HandleGetOriginPermissions(get_origin_permissions_args);
    const content::TestWebUI::CallData& data = *web_ui()->call_data().back();
    const base::ListValue& exception_list = data.arg3()->GetList();
    EXPECT_EQ(1UL, exception_list.size());
    const base::DictValue& first_exception = exception_list[0].GetDict();

    EXPECT_EQ(origin.GetURL().spec(),
              CHECK_DEREF(first_exception.FindString("origin")));
    EXPECT_EQ("allowlist", CHECK_DEREF(first_exception.FindString("source")));
  }
}

TEST_F(SiteSettingsHandlerTest, StorageAccessExceptions_DiffPatterns) {
  const std::string kOrigin("https://[*.]google.com:443");
  const std::string kDisplayName("google.com");

  const std::string kOrigin2("https://[*.]google2.com:443");
  const std::string kDisplayName2("google2.com");

  const std::string kEmbeddingOrigin("https://[*.]example.com:443");
  const std::string kEmbeddingDisplayName("example.com");

  const std::string kEmbeddingOrigin2("https://[*.]example2.com:443");
  const std::string kEmbeddingDisplayName2("example2.com");

  SetContentSettingCustomScope(kOrigin, kEmbeddingOrigin,
                               kPermissionStorageAccess, CONTENT_SETTING_BLOCK);
  SetContentSettingCustomScope(kOrigin2, kEmbeddingOrigin2,
                               kPermissionStorageAccess, CONTENT_SETTING_BLOCK,
                               /*expected_total_calls=*/2U);

  base::ListValue get_exception_list_args;
  get_exception_list_args.Append(kCallbackId);
  get_exception_list_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_BLOCK));
  handler()->HandleGetStorageAccessExceptionList(get_exception_list_args);

  // Verify that the exception list is correct.
  ValidateStorageAccessList(/*expected_total_calls=*/3U,
                            /*expected_num_groups=*/2U);

  // Verify that the first group exception is correct.
  ValidateStorageAccessException(
      kOrigin, kDisplayName, CONTENT_SETTING_BLOCK,
      {{kEmbeddingOrigin, kEmbeddingDisplayName, /*incognito=*/false}},
      /*index=*/0U);

  // Verify that the second group exception is correct.
  ValidateStorageAccessException(
      kOrigin2, kDisplayName2, CONTENT_SETTING_BLOCK,
      {{kEmbeddingOrigin2, kEmbeddingDisplayName2, /*incognito=*/false}},
      /*index=*/1U);
}

TEST_F(SiteSettingsHandlerTest, StorageAccessExceptions_SamePrimaryPattern) {
  const std::string kOrigin("https://[*.]google.com:443");
  const std::string kDisplayName("google.com");

  const std::string kEmbeddingOrigin("https://[*.]example.com:443");
  const std::string kEmbeddingDisplayName("example.com");

  const std::string kEmbeddingOrigin2("https://[*.]example2.com:443");
  const std::string kEmbeddingDisplayName2("example2.com");

  SetContentSettingCustomScope(kOrigin, kEmbeddingOrigin,
                               kPermissionStorageAccess, CONTENT_SETTING_BLOCK);
  SetContentSettingCustomScope(kOrigin, kEmbeddingOrigin2,
                               kPermissionStorageAccess, CONTENT_SETTING_BLOCK,
                               /*expected_total_calls=*/2U);

  base::ListValue get_exception_list_args;
  get_exception_list_args.Append(kCallbackId);
  get_exception_list_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_BLOCK));
  handler()->HandleGetStorageAccessExceptionList(get_exception_list_args);

  // Verify that the group exception is correct.
  ValidateStorageAccessList(/*expected_total_calls=*/3U,
                            /*expected_num_groups=*/1U);

  ValidateStorageAccessException(
      kOrigin, kDisplayName, CONTENT_SETTING_BLOCK,
      {{kEmbeddingOrigin2, kEmbeddingDisplayName2, /*incognito=*/false},
       {kEmbeddingOrigin, kEmbeddingDisplayName, /*incognito=*/false}},
      /*index=*/0U);
}

TEST_F(SiteSettingsHandlerTest, StorageAccessExceptions_DiffType) {
  const std::string kOrigin("https://[*.]google.com:443");
  const std::string kEmbeddingOrigin("https://[*.]example.com:443");

  SetContentSettingCustomScope(kOrigin, kEmbeddingOrigin,
                               kPermissionStorageAccess, CONTENT_SETTING_BLOCK);

  base::ListValue get_exception_list_args;
  get_exception_list_args.Append(kCallbackId);
  get_exception_list_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_ALLOW));
  handler()->HandleGetStorageAccessExceptionList(get_exception_list_args);

  // Verify that no exception is returned.
  ValidateNoOrigin(2U);
}

TEST_F(SiteSettingsHandlerTest, StorageAccessExceptions_AutoGranted) {
  const std::string kOrigin("https://[*.]google.com:443");
  const std::string kEmbeddingOrigin("https://[*.]example.com:443");

  SetContentSettingCustomScope(
      kOrigin, kEmbeddingOrigin, kPermissionStorageAccess,
      CONTENT_SETTING_BLOCK, /*expected_total_calls=*/1U,
      /*is_incognito=*/false, /*lifetime=*/base::TimeDelta(),
      /*is_auto_granted=*/true);

  base::ListValue get_exception_list_args;
  get_exception_list_args.Append(kCallbackId);
  get_exception_list_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_BLOCK));
  handler()->HandleGetStorageAccessExceptionList(get_exception_list_args);

  // Verify that no exception is returned since auto granted exceptions should
  // not be returned.
  ValidateNoOrigin(2U);
}

TEST_F(SiteSettingsHandlerTest, StorageAccessExceptions_Incognito) {
  const std::string kOrigin("https://[*.]google.com:443");
  const std::string kDisplayName("google.com");

  const std::string kEmbeddingOrigin("https://[*.]example.com:443");
  const std::string kEmbeddingDisplayName("example.com");

  CreateIncognitoProfile();
  ValidateIncognitoExists(true, 1U);

  SetContentSettingCustomScope(kOrigin, kEmbeddingOrigin,
                               kPermissionStorageAccess, CONTENT_SETTING_BLOCK,
                               /*expected_total_calls=*/2U,
                               /*is_incognito=*/true);

  base::ListValue get_exception_list_args;
  get_exception_list_args.Append(kCallbackId);
  get_exception_list_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_BLOCK));
  handler()->HandleGetStorageAccessExceptionList(get_exception_list_args);

  // Verify that the group exception is correct.
  ValidateStorageAccessList(/*expected_total_calls=*/3U,
                            /*expected_num_groups=*/1U);
  ValidateStorageAccessException(
      kOrigin, kDisplayName, CONTENT_SETTING_BLOCK,
      {{kEmbeddingOrigin, kEmbeddingDisplayName, /*incognito=*/true}});
}

TEST_F(SiteSettingsHandlerTest, StorageAccessExceptions_NormalAndIncognito) {
  const std::string kOrigin("https://[*.]google.com:443");
  const std::string kDisplayName("google.com");

  const std::string kOrigin2("https://[*.]google2.com:443");
  const std::string kDisplayName2("google2.com");

  const std::string kEmbeddingOrigin("https://[*.]example.com:443");
  const std::string kEmbeddingDisplayName("example.com");

  const std::string kEmbeddingOrigin2("https://[*.]example2.com:443");
  const std::string kEmbeddingDisplayName2("example2.com");

  SetContentSettingCustomScope(kOrigin, kEmbeddingOrigin,
                               kPermissionStorageAccess, CONTENT_SETTING_BLOCK);

  CreateIncognitoProfile();
  ValidateIncognitoExists(true, 2U);

  SetContentSettingCustomScope(kOrigin2, kEmbeddingOrigin2,
                               kPermissionStorageAccess, CONTENT_SETTING_BLOCK,
                               /*expected_total_calls=*/3U,
                               /*is_incognito=*/true);

  base::ListValue get_exception_list_args;
  get_exception_list_args.Append(kCallbackId);
  get_exception_list_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_BLOCK));
  handler()->HandleGetStorageAccessExceptionList(get_exception_list_args);

  ValidateStorageAccessList(/*expected_total_calls=*/4U,
                            /*expected_num_groups=*/2U);

  // Verify that group exception for non-incognito is correct.
  ValidateStorageAccessException(
      kOrigin, kDisplayName, CONTENT_SETTING_BLOCK,
      {{kEmbeddingOrigin, kEmbeddingDisplayName, /*incognito=*/false}},
      /*index=*/0U);

  // Verify that group exception for incognito is correct.
  ValidateStorageAccessException(
      kOrigin2, kDisplayName2, CONTENT_SETTING_BLOCK,
      {{kEmbeddingOrigin2, kEmbeddingDisplayName2, /*incognito=*/true}},
      /*index=*/1U);

  DestroyIncognitoProfile();
  handler()->HandleGetStorageAccessExceptionList(get_exception_list_args);

  // Check that the incognito exception gets deleted if the incognito profile is
  // destroyed.
  ValidateStorageAccessList(/*expected_total_calls=*/6U,
                            /*expected_num_groups=*/1U);
  ValidateStorageAccessException(
      kOrigin, kDisplayName, CONTENT_SETTING_BLOCK,
      {{kEmbeddingOrigin, kEmbeddingDisplayName, /*incognito=*/false}},
      /*index=*/0U);
}

TEST_F(SiteSettingsHandlerTest,
       StorageAccessExceptions_NormalAndIncognito_SamePatterns) {
  const std::string kOrigin("https://[*.]google.com:443");
  const std::string kDisplayName("google.com");

  const std::string kEmbeddingOrigin("https://[*.]example.com:443");
  const std::string kEmbeddingDisplayName("example.com");

  SetContentSettingCustomScope(kOrigin, kEmbeddingOrigin,
                               kPermissionStorageAccess, CONTENT_SETTING_BLOCK);

  CreateIncognitoProfile();
  ValidateIncognitoExists(true, 2U);

  SetContentSettingCustomScope(kOrigin, kEmbeddingOrigin,
                               kPermissionStorageAccess, CONTENT_SETTING_BLOCK,
                               /*expected_total_calls=*/3U,
                               /*is_incognito=*/true);

  base::ListValue get_exception_list_args;
  get_exception_list_args.Append(kCallbackId);
  get_exception_list_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_BLOCK));
  handler()->HandleGetStorageAccessExceptionList(get_exception_list_args);

  // Verify that group exception for non-incognito and incognito is correct.
  ValidateStorageAccessList(/*expected_total_calls=*/4U,
                            /*expected_num_groups=*/1U);
  ValidateStorageAccessException(
      kOrigin, kDisplayName, CONTENT_SETTING_BLOCK,
      {{kEmbeddingOrigin, kEmbeddingDisplayName, /*incognito=*/false},
       {kEmbeddingOrigin, kEmbeddingDisplayName, /*incognito=*/true}});
}

TEST_F(SiteSettingsHandlerTest, StorageAccessExceptions_Extension) {
  auto extension = LoadExtension(kExtensionName);
  auto extension_url = extension->url().spec();

  const std::string kEmbeddingOrigin("https://[*.]example.com:443");
  const std::string kEmbeddingDisplayName("example.com");

  SetContentSettingCustomScope(extension_url, kEmbeddingOrigin,
                               kPermissionStorageAccess, CONTENT_SETTING_BLOCK);

  base::ListValue get_exception_list_args;
  get_exception_list_args.Append(kCallbackId);
  get_exception_list_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_BLOCK));
  handler()->HandleGetStorageAccessExceptionList(get_exception_list_args);

  // Verify that the grouped exception is correct.
  ValidateStorageAccessList(/*expected_total_calls=*/2U,
                            /*expected_num_groups=*/1U);
  ValidateStorageAccessException(
      extension_url, kExtensionName, CONTENT_SETTING_BLOCK,
      {{kEmbeddingOrigin, kEmbeddingDisplayName, /*incognito=*/false}});

  // When the extension is unloaded, the display name should be the plain url.
  UnloadExtension(extension->id());
  // Display name does not have a trailing '/'.
  const auto extensionDisplayName =
      extension_url.substr(0, extension_url.length() - 1);

  handler()->HandleGetStorageAccessExceptionList(get_exception_list_args);
  ValidateStorageAccessList(/*expected_total_calls=*/3U,
                            /*expected_num_groups=*/1U);

  ValidateStorageAccessException(
      extension_url, extensionDisplayName, CONTENT_SETTING_BLOCK,
      {{kEmbeddingOrigin, kEmbeddingDisplayName, /*incognito=*/false}});
}

TEST_F(SiteSettingsHandlerTest, StorageAccessExceptions_Description_Embargoed) {
  const std::string kOrigin("https://google.com:443");
  const std::string kDisplayName("google.com");

  // Set an embargoed setting.
  permissions::PermissionDecisionAutoBlocker* auto_blocker =
      PermissionDecisionAutoBlockerFactory::GetForProfile(profile());
  base::SimpleTestClock clock;
  clock.SetNow(base::Time::Now());
  auto_blocker->SetClockForTesting(&clock);
  for (int i = 0; i < 3; ++i) {
    auto_blocker->RecordDismissAndEmbargo(GURL(kOrigin),
                                          kPermissionStorageAccess, false);
  }
  EXPECT_EQ(
      PermissionStatus::DENIED,
      auto_blocker->GetEmbargoResult(GURL(kOrigin), kPermissionStorageAccess)
          ->status);

  base::ListValue get_exception_list_args;
  get_exception_list_args.Append(kCallbackId);
  get_exception_list_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_BLOCK));
  handler()->HandleGetStorageAccessExceptionList(get_exception_list_args);

  // Verify that group exception with an embargoed is correct.
  ValidateStorageAccessList(/*expected_total_calls=*/1U,
                            /*expected_num_groups=*/1U);
  ValidateStaticStorageAccessException(kOrigin, kDisplayName,
                                       CONTENT_SETTING_BLOCK,
                                       /*expected_incognito=*/false);
}

TEST_F(SiteSettingsHandlerTest,
       StorageAccessExceptions_Description_EmbargoedTwoProfiles) {
  const std::string kOrigin("https://google.com:443");
  const std::string kDisplayName("google.com");

  // Set an embargoed setting for regular and incognito profile
  base::SimpleTestClock clock;
  clock.SetNow(base::Time::Now());

  permissions::PermissionDecisionAutoBlocker* auto_blocker =
      PermissionDecisionAutoBlockerFactory::GetForProfile(profile());
  auto_blocker->SetClockForTesting(&clock);
  for (int i = 0; i < 3; ++i) {
    auto_blocker->RecordDismissAndEmbargo(GURL(kOrigin),
                                          kPermissionStorageAccess, false);
  }
  EXPECT_EQ(
      PermissionStatus::DENIED,
      auto_blocker->GetEmbargoResult(GURL(kOrigin), kPermissionStorageAccess)
          ->status);

  CreateIncognitoProfile();
  permissions::PermissionDecisionAutoBlocker* auto_blocker_incognito =
      PermissionDecisionAutoBlockerFactory::GetForProfile(incognito_profile());
  auto_blocker_incognito->SetClockForTesting(&clock);
  for (int i = 0; i < 3; ++i) {
    auto_blocker_incognito->RecordDismissAndEmbargo(
        GURL(kOrigin), kPermissionStorageAccess, false);
  }
  EXPECT_EQ(PermissionStatus::DENIED,
            auto_blocker_incognito
                ->GetEmbargoResult(GURL(kOrigin), kPermissionStorageAccess)
                ->status);

  base::ListValue get_exception_list_args;
  get_exception_list_args.Append(kCallbackId);
  get_exception_list_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_BLOCK));
  handler()->HandleGetStorageAccessExceptionList(get_exception_list_args);

  // Verify that group exception with an embargoed origin in two profiles is
  // correct.
  ValidateStorageAccessList(/*expected_total_calls=*/2U,
                            /*expected_num_groups=*/1U);
  ValidateStorageAccessException(kOrigin, kDisplayName, CONTENT_SETTING_BLOCK,
                                 {{std::string(), "*", /*incognito=*/false,
                                   /*embargoed=*/true, 0},
                                  {std::string(), "*", /*incognito=*/true,
                                   /*embargoed=*/true, 0}});
}

typedef std::pair<std::string_view, std::string_view> OriginStringParams;
class StorageAccessSiteSettingsHandlerTest
    : public SiteSettingsHandlerBaseTest,
      public testing::WithParamInterface<
          std::tuple<OriginStringParams, OriginStringParams>> {};

// Several pairs <origin, displayName> to test on both the embedded and
// embedding.
constexpr OriginStringParams kOrigins[] = {
    {"192.168.0.1", "192.168.0.1"},
    {"google.com", "google.com"},
    {"google.com:443", "google.com"},
    {"docs.google.com:443", "docs.google.com"},
    {"https://[*.]google.com", "google.com"},
    {"https://[*.]docs.example.com:443", "docs.example.com"},
    {"chrome-extension://peoadpeiejnhkmpaakpnompolbglelel/",
     "chrome-extension://peoadpeiejnhkmpaakpnompolbglelel"}};

INSTANTIATE_TEST_SUITE_P(All,
                         StorageAccessSiteSettingsHandlerTest,
                         testing::Combine(testing::ValuesIn(kOrigins),
                                          testing::ValuesIn(kOrigins)));

TEST_P(StorageAccessSiteSettingsHandlerTest, StorageAccessExceptions_Origins) {
  OriginStringParams embedded = std::get<0>(GetParam());
  const std::string kOrigin(std::get<0>(embedded));
  const std::string kDisplayName(std::get<1>(embedded));

  OriginStringParams embedding = std::get<1>(GetParam());
  const std::string kEmbeddingOrigin(std::get<0>(embedding));
  const std::string kEmbeddingDisplayName(std::get<1>(embedding));

  SetContentSettingCustomScope(kOrigin, kEmbeddingOrigin,
                               kPermissionStorageAccess, CONTENT_SETTING_BLOCK);

  base::ListValue get_exception_list_args;
  get_exception_list_args.Append(kCallbackId);
  get_exception_list_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_BLOCK));
  handler()->HandleGetStorageAccessExceptionList(get_exception_list_args);

  // Verify that the grouped exception is correct.
  ValidateStorageAccessList(/*expected_total_calls=*/2U,
                            /*expected_num_groups=*/1U);

  ValidateStorageAccessException(
      kOrigin, kDisplayName, CONTENT_SETTING_BLOCK,
      {{kEmbeddingOrigin, kEmbeddingDisplayName, /*incognito=*/false}});
}

class StorageAccessSiteSettingsHandlerLifetimeTest
    : public SiteSettingsHandlerBaseTest,
      public testing::WithParamInterface<int> {};

// Range for the lifetime in days of a Storage Access permission.
INSTANTIATE_TEST_SUITE_P(All,
                         StorageAccessSiteSettingsHandlerLifetimeTest,
                         testing::Range(0, 3));

TEST_P(StorageAccessSiteSettingsHandlerLifetimeTest,
       StorageAccessExceptions_Description) {
  const std::string kOrigin("https://[*.]google.com:443");
  const std::string kDisplayName("google.com");

  const std::string kEmbeddingOrigin("https://[*.]example.com:443");
  const std::string kEmbeddingDisplayName("example.com");

  const int kLifetimeInDays = GetParam();

  SetContentSettingCustomScope(kOrigin, kEmbeddingOrigin,
                               kPermissionStorageAccess, CONTENT_SETTING_BLOCK,
                               /*expected_total_calls=*/1U,
                               /*is_incognito=*/false,
                               base::Days(kLifetimeInDays));

  base::ListValue get_exception_list_args;
  get_exception_list_args.Append(kCallbackId);
  get_exception_list_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_BLOCK));
  handler()->HandleGetStorageAccessExceptionList(get_exception_list_args);

  // Verify that group exception description with expiration is correct.
  ValidateStorageAccessList(/*expected_total_calls=*/2U,
                            /*expected_num_groups=*/1U);
  ValidateStorageAccessException(
      kOrigin, kDisplayName, CONTENT_SETTING_BLOCK,
      {{kEmbeddingOrigin, kEmbeddingDisplayName, /*incognito=*/false,
        /*embargoed=*/false, kLifetimeInDays}});
}

class PersistentPermissionsSiteSettingsHandlerTest
    : public SiteSettingsHandlerBaseTest {
 public:
  PersistentPermissionsSiteSettingsHandlerTest() {
    feature_list_.InitAndEnableFeature(
        features::kFileSystemAccessPersistentPermissions);
  }

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

// GetFileSystemGrants() returns the allowed grants for a given origin
// based on the File System Access persistent permissions policy.
TEST_F(PersistentPermissionsSiteSettingsHandlerTest,
       HandleGetFileSystemGrants) {
  ChromeFileSystemAccessPermissionContext* context =
      FileSystemAccessPermissionContextFactory::GetForProfile(profile());

  auto kTestOrigin1 = url::Origin::Create(GURL("https://www.a.com"));
  auto kTestOrigin2 = url::Origin::Create(GURL("https://www.b.com"));

  const base::FilePath kTestPath = base::FilePath(FILE_PATH_LITERAL("/a/b"));
  const base::FilePath kTestPath2 = base::FilePath(FILE_PATH_LITERAL("/c/d"));
  const base::FilePath kTestPath3 = base::FilePath(FILE_PATH_LITERAL("/e/"));
  const base::FilePath kTestPath4 =
      base::FilePath(FILE_PATH_LITERAL("/f/g/h/"));

  context->SetOriginHasExtendedPermissionForTesting(kTestOrigin1);
  context->SetOriginHasExtendedPermissionForTesting(kTestOrigin2);

  // Populate the `grants` object with permissions.
  auto file_read_grant = context->GetExtendedReadPermissionGrantForTesting(
      kTestOrigin1, content::PathInfo(kTestPath),
      ChromeFileSystemAccessPermissionContext::HandleType::kFile);
  auto file_write_grant = context->GetExtendedWritePermissionGrantForTesting(
      kTestOrigin2, content::PathInfo(kTestPath2),
      ChromeFileSystemAccessPermissionContext::HandleType::kFile);
  auto directory_read_grant = context->GetExtendedReadPermissionGrantForTesting(
      kTestOrigin1, content::PathInfo(kTestPath3),
      ChromeFileSystemAccessPermissionContext::HandleType::kDirectory);
  auto directory_write_grant =
      context->GetExtendedWritePermissionGrantForTesting(
          kTestOrigin2, content::PathInfo(kTestPath4),
          ChromeFileSystemAccessPermissionContext::HandleType::kDirectory);

  auto kTestOrigin1Grants =
      context->ConvertObjectsToGrants(context->GetGrantedObjects(kTestOrigin1));
  auto kTestOrigin2Grants =
      context->ConvertObjectsToGrants(context->GetGrantedObjects(kTestOrigin2));
  EXPECT_EQ(kTestOrigin1Grants.file_read_grants.size(), 1UL);
  EXPECT_EQ(kTestOrigin2Grants.file_read_grants.size(), 0UL);
  EXPECT_EQ(kTestOrigin1Grants.file_write_grants.size(), 0UL);
  EXPECT_EQ(kTestOrigin2Grants.file_write_grants.size(), 1UL);
  EXPECT_EQ(kTestOrigin1Grants.directory_read_grants.size(), 1UL);
  EXPECT_EQ(kTestOrigin2Grants.directory_read_grants.size(), 0UL);
  EXPECT_EQ(kTestOrigin1Grants.directory_write_grants.size(), 0UL);
  EXPECT_EQ(kTestOrigin2Grants.directory_write_grants.size(), 1UL);

  base::ListValue get_file_system_permissions_args;
  get_file_system_permissions_args.Append(kCallbackId);

  handler()->HandleGetFileSystemGrants(get_file_system_permissions_args);
  const content::TestWebUI::CallData& data = *web_ui()->call_data().back();
  const base::ListValue& grants = data.arg3()->GetList();

  EXPECT_EQ(grants.size(), 2UL);
  const base::DictValue& first_grant = grants[0].GetDict();
  const base::DictValue& second_grant = grants[1].GetDict();
  EXPECT_EQ(CHECK_DEREF(first_grant.FindString(site_settings::kOrigin)),
            "https://www.a.com/");
  EXPECT_EQ(CHECK_DEREF(second_grant.FindString(site_settings::kOrigin)),
            "https://www.b.com/");

  const base::ListValue* kTestOrigin1ViewGrants =
      first_grant.FindList(site_settings::kFileSystemViewGrants);
  const base::ListValue* kTestOrigin1EditGrants =
      first_grant.FindList(site_settings::kFileSystemEditGrants);

  const base::ListValue* kTestOrigin2ViewGrants =
      second_grant.FindList(site_settings::kFileSystemViewGrants);
  const base::ListValue* kTestOrigin2EditGrants =
      second_grant.FindList(site_settings::kFileSystemEditGrants);

  // Checks that the grants for test origins are populated as expected.
  EXPECT_TRUE(CHECK_DEREF(kTestOrigin1ViewGrants)[0]
                  .GetDict()
                  .FindBool(site_settings::kFileSystemIsDirectory)
                  .value_or(false));
  EXPECT_EQ(
      CHECK_DEREF(CHECK_DEREF(kTestOrigin1ViewGrants)[1].GetDict().FindString(
          site_settings::kFileSystemFilePath)),
      "/a/b");
  ASSERT_TRUE(kTestOrigin1EditGrants != nullptr);
  EXPECT_TRUE(kTestOrigin1EditGrants->empty());

  // In the case of kTestOrigin2, check that when an origin has an
  // associated 'write' grant, that the grant is only recorded in the
  // respective write grants list, and is not recorded in the origin's
  // read grants list.
  ASSERT_TRUE(kTestOrigin2ViewGrants != nullptr);
  EXPECT_TRUE(kTestOrigin2ViewGrants->empty());
  EXPECT_EQ(
      CHECK_DEREF(CHECK_DEREF(kTestOrigin2EditGrants)[0].GetDict().FindString(
          site_settings::kDisplayName)),
      "/f/g/h/");
  EXPECT_FALSE(CHECK_DEREF(kTestOrigin2EditGrants)[1]
                   .GetDict()
                   .FindBool(site_settings::kFileSystemIsDirectory)
                   .value_or(true));
}

// RevokeGrant() revokes a single File System Access permission grant,
// for a given origin and file path.
TEST_F(PersistentPermissionsSiteSettingsHandlerTest,
       HandleRevokeFileSystemGrant) {
  ChromeFileSystemAccessPermissionContext* context =
      FileSystemAccessPermissionContextFactory::GetForProfile(profile());

  auto kTestOrigin1 = url::Origin::Create(GURL("https://www.a.com"));
  auto kTestOrigin2 = url::Origin::Create(GURL("https://www.b.com"));

  const base::FilePath kTestPath = base::FilePath(FILE_PATH_LITERAL("/a/b"));
  const base::FilePath kTestPath2 = base::FilePath(FILE_PATH_LITERAL("/c/d/"));
  const base::FilePath kTestPath3 = base::FilePath(FILE_PATH_LITERAL("/e/"));
  const base::FilePath kTestPath4 =
      base::FilePath(FILE_PATH_LITERAL("/f/g/h/"));

  context->SetOriginHasExtendedPermissionForTesting(kTestOrigin1);
  context->SetOriginHasExtendedPermissionForTesting(kTestOrigin2);

  // Populate the `grants` object with permissions.
  auto file_read_grant = context->GetExtendedReadPermissionGrantForTesting(
      kTestOrigin1, content::PathInfo(kTestPath),
      ChromeFileSystemAccessPermissionContext::HandleType::kFile);
  auto directory_read_grant = context->GetExtendedReadPermissionGrantForTesting(
      kTestOrigin1, content::PathInfo(kTestPath2),
      ChromeFileSystemAccessPermissionContext::HandleType::kDirectory);
  auto directory_write_grant =
      context->GetExtendedWritePermissionGrantForTesting(
          kTestOrigin2, content::PathInfo(kTestPath3),
          ChromeFileSystemAccessPermissionContext::HandleType::kDirectory);
  auto second_directory_write_grant =
      context->GetExtendedWritePermissionGrantForTesting(
          kTestOrigin2, content::PathInfo(kTestPath4),
          ChromeFileSystemAccessPermissionContext::HandleType::kDirectory);

  base::ListValue revoke_origin1_grant_permissions_args;
  revoke_origin1_grant_permissions_args.Append("https://www.a.com");
  revoke_origin1_grant_permissions_args.Append("/a/b");

  base::ListValue get_file_system_grants_permissions_args;
  get_file_system_grants_permissions_args.Append(kCallbackId);

  handler()->HandleRevokeFileSystemGrant(revoke_origin1_grant_permissions_args);
  handler()->HandleGetFileSystemGrants(get_file_system_grants_permissions_args);
  const content::TestWebUI::CallData& data = *web_ui()->call_data().back();
  const base::ListValue& grants = data.arg3()->GetList();

  // After revoking the `file_read_grant` for kTestOrigin1, only one view grant
  // should remain when retrieving the file system grants for kTestOrigin1.
  EXPECT_EQ(grants[0]
                .GetDict()
                .FindList(site_settings::kFileSystemViewGrants)
                ->size(),
            1UL);

  // Revoking a single grant from an origin with multiple grants in a given
  // grants list only revokes the grant with the given file path.
  // In this case, for kTestOrigin2, only the directory write grant for
  // kTestPath2 is revoked, and the directory write grant with kTestPath4
  // remains in the `directory_write_grants` list.
  base::ListValue revoke_origin2_grant_permissions_args;
  revoke_origin2_grant_permissions_args.Append("https://www.b.com");
  revoke_origin2_grant_permissions_args.Append("/e/");

  handler()->HandleRevokeFileSystemGrant(revoke_origin2_grant_permissions_args);
  handler()->HandleGetFileSystemGrants(get_file_system_grants_permissions_args);
  const content::TestWebUI::CallData& updated_data =
      *web_ui()->call_data().back();
  const base::ListValue& updated_grants = updated_data.arg3()->GetList();

  EXPECT_EQ(updated_grants[1]
                .GetDict()
                .FindList(site_settings::kFileSystemEditGrants)
                ->size(),
            1UL);
  EXPECT_EQ(CHECK_DEREF(CHECK_DEREF(updated_grants[1].GetDict().FindList(
                site_settings::kFileSystemEditGrants))[0]
                            .GetDict()
                            .FindString(site_settings::kFileSystemFilePath)),
            "/f/g/h/");
}

// RevokeGrants() revokes all File System Access permission grants,
// for a given origin.
TEST_F(PersistentPermissionsSiteSettingsHandlerTest,
       HandleRevokeFileSystemGrants) {
  ChromeFileSystemAccessPermissionContext* context =
      FileSystemAccessPermissionContextFactory::GetForProfile(profile());

  auto kTestOrigin1 = url::Origin::Create(GURL("https://www.a.com"));
  auto kTestOrigin2 = url::Origin::Create(GURL("https://www.b.com"));

  const base::FilePath kTestPath = base::FilePath(FILE_PATH_LITERAL("/a/b"));
  const base::FilePath kTestPath2 = base::FilePath(FILE_PATH_LITERAL("/c/d"));
  const base::FilePath kTestPath3 = base::FilePath(FILE_PATH_LITERAL("/e/"));
  const base::FilePath kTestPath4 =
      base::FilePath(FILE_PATH_LITERAL("/f/g/h/"));

  context->SetOriginHasExtendedPermissionForTesting(kTestOrigin1);
  context->SetOriginHasExtendedPermissionForTesting(kTestOrigin2);

  // Populate the `grants` object with permissions.
  auto file_read_grant = context->GetExtendedReadPermissionGrantForTesting(
      kTestOrigin1, content::PathInfo(kTestPath),
      ChromeFileSystemAccessPermissionContext::HandleType::kFile);
  auto file_write_grant = context->GetExtendedWritePermissionGrantForTesting(
      kTestOrigin2, content::PathInfo(kTestPath2),
      ChromeFileSystemAccessPermissionContext::HandleType::kFile);
  auto directory_read_grant = context->GetExtendedReadPermissionGrantForTesting(
      kTestOrigin1, content::PathInfo(kTestPath3),
      ChromeFileSystemAccessPermissionContext::HandleType::kDirectory);
  auto directory_write_grant =
      context->GetExtendedWritePermissionGrantForTesting(
          kTestOrigin2, content::PathInfo(kTestPath4),
          ChromeFileSystemAccessPermissionContext::HandleType::kDirectory);

  base::ListValue get_file_system_grants_permissions_args;
  get_file_system_grants_permissions_args.Append(kCallbackId);

  handler()->HandleGetFileSystemGrants(get_file_system_grants_permissions_args);
  const content::TestWebUI::CallData& data = *web_ui()->call_data().back();
  const base::ListValue& grants = data.arg3()->GetList();

  // The number of entries in grants is equal to the number of origins with
  // permission grants, before revoking grants for a given origin.
  EXPECT_EQ(grants.size(), 2UL);

  base::ListValue revoke_origin1_grants_permissions_args;
  revoke_origin1_grants_permissions_args.Append("https://www.a.com");

  handler()->HandleRevokeFileSystemGrants(
      revoke_origin1_grants_permissions_args);
  handler()->HandleGetFileSystemGrants(get_file_system_grants_permissions_args);
  const content::TestWebUI::CallData& updated_data =
      *web_ui()->call_data().back();
  const base::ListValue& updated_grants = updated_data.arg3()->GetList();

  // All grants are revoked for kTestOrigin1, and the grants for kTestOrigin2
  // are unaffected.
  EXPECT_EQ(updated_grants.size(), 1UL);
  EXPECT_EQ(updated_grants[0]
                .GetDict()
                .FindList(site_settings::kFileSystemEditGrants)
                ->size(),
            2UL);

  // Expect that the WebUIListenerCallback was triggered.
  EXPECT_EQ(web_ui()->call_data()[0]->function_name(),
            "cr.webUIListenerCallback");

  EXPECT_EQ(updated_data.arg1()->GetString(), kCallbackId);
}

namespace {

std::vector<std::string> GetExceptionDisplayNames(
    const base::ListValue& exceptions) {
  std::vector<std::string> display_names;
  for (const base::Value& exception : exceptions) {
    const std::string* display_name =
        exception.GetDict().FindString(site_settings::kDisplayName);
    if (display_name) {
      display_names.push_back(*display_name);
    }
  }
  return display_names;
}

}  // namespace

class SiteSettingsHandlerChooserExceptionTest
    : public SiteSettingsHandlerBaseTest {
 protected:
  const GURL kAndroidUrl{"https://android.com"};
  const GURL kChromiumUrl{"https://chromium.org"};
  const GURL kGoogleUrl{"https://google.com"};
  const GURL kWebUIUrl{"chrome://test"};

  void SetUp() override {
    SiteSettingsHandlerBaseTest::SetUp();
    SetUpChooserContext();
    SetUpPolicyGrantedPermissions();

    // Add the observer for permission changes.
    GetChooserContext(profile())->AddObserver(&observer_);
  }

  void TearDown() override {
    GetChooserContext(profile())->RemoveObserver(&observer_);
    SiteSettingsHandlerBaseTest::TearDown();
  }

  void DestroyIncognitoProfile() override {
    GetChooserContext(incognito_profile())->RemoveObserver(&observer_);
    SiteSettingsHandlerBaseTest::DestroyIncognitoProfile();
  }

  // Call SiteSettingsHandler::HandleGetChooserExceptionList for |chooser_type|
  // and return the exception list received by the WebUI.
  void ValidateChooserExceptionList(const std::string& chooser_type,
                                    size_t expected_total_calls) {
    base::ListValue args;
    args.Append(kCallbackId);
    args.Append(chooser_type);

    handler()->HandleGetChooserExceptionList(args);

    EXPECT_EQ(web_ui()->call_data().size(), expected_total_calls);

    const content::TestWebUI::CallData& data = *web_ui()->call_data().back();
    EXPECT_EQ("cr.webUIResponse", data.function_name());

    ASSERT_TRUE(data.arg1());
    ASSERT_TRUE(data.arg1()->is_string());
    EXPECT_EQ(data.arg1()->GetString(), kCallbackId);

    ASSERT_TRUE(data.arg2());
    ASSERT_TRUE(data.arg2()->is_bool());
    EXPECT_TRUE(data.arg2()->GetBool());

    ASSERT_TRUE(data.arg3());
    ASSERT_TRUE(data.arg3()->is_list());
  }

  const base::ListValue& GetChooserExceptionListFromWebUiCallData(
      const std::string& chooser_type,
      size_t expected_total_calls) {
    ValidateChooserExceptionList(chooser_type, expected_total_calls);
    return web_ui()->call_data().back()->arg3()->GetList();
  }

  // Iterate through the exception's sites array and return true if a site
  // exception matches |requesting_origin| and |embedding_origin|.
  bool ChooserExceptionContainsSiteException(const base::DictValue& exception,
                                             std::string_view origin) {
    const base::ListValue* sites = exception.FindList(site_settings::kSites);
    if (!sites) {
      return false;
    }

    for (const auto& site : *sites) {
      const std::string* exception_origin =
          site.GetDict().FindString(site_settings::kOrigin);
      if (!exception_origin) {
        continue;
      }
      if (*exception_origin == origin) {
        return true;
      }
    }
    return false;
  }

  // Iterate through the |exception_list| array and return true if there is a
  // chooser exception with |display_name| that contains a site exception for
  // |origin|.
  bool ChooserExceptionContainsSiteException(const base::ListValue& exceptions,
                                             std::string_view display_name,
                                             std::string_view origin) {
    for (const auto& exception : exceptions) {
      const std::string* exception_display_name =
          exception.GetDict().FindString(site_settings::kDisplayName);
      if (!exception_display_name) {
        continue;
      }

      if (*exception_display_name == display_name) {
        return ChooserExceptionContainsSiteException(exception.GetDict(),
                                                     origin);
      }
    }
    return false;
  }

  void TestHandleGetChooserExceptionList() {
    AddPersistentDevice();
    AddEphemeralDevice();
    AddUserGrantedDevice();
    base::RunLoop().RunUntilIdle();

    SetUpUserGrantedPermissions();

    base::RunLoop().RunUntilIdle();
    web_ui()->ClearTrackedCalls();

    const std::string group_name(
        site_settings::ContentSettingsTypeToGroupName(content_type()));

    const base::ListValue& exceptions =
        GetChooserExceptionListFromWebUiCallData(group_name,
                                                 /*expected_total_calls=*/1u);

    // There are 9 granted permissions:
    // 1. Persistent permission for persistent-device on kChromiumOrigin
    // 2. Persistent permission for persistent-device on kGoogleOrigin
    // 3. Persistent permission for persistent-device on kWebUIOrigin
    // 4. Persistent permission for user-granted-device on kAndroidOrigin
    // 5. Ephemeral permission for ephemeral-device on kAndroidOrigin
    // 6. Policy-granted permission for any device on kGoogleOrigin
    // 7. Policy-granted permission for vendor 18D1 on kAndroidOrigin
    // 8. Policy-granted permission for vendor 18D2 on kAndroidOrigin
    // 9. Policy-granted permission for device 18D1:162E on kChromiumOrigin
    //
    // Permission 3 is ignored by GetChooserExceptionListFromProfile because its
    // origin has a WebUI scheme (chrome://).
    //
    // Some of the user-granted permissions are redundant due to policy-granted
    // permissions. Permission 1 is redundant due to permission 9; 2 is
    // redundant due to 6; 5 is redundant due to 8. UsbChooserContext omits
    // redundant items but other APIs do not. This causes
    // GetChooserExceptionListForProfile to return a different number of
    // exceptions depending on the API.
    //
    // UsbChooserContext also detects when a policy-granted permission refers to
    // the same origin and device IDs as a user-granted permission. When
    // deduplicating redundant exceptions, the user-granted exception display
    // name is used because it contains the device name.
    //
    // TODO(crbug.com/40247735): Update SerialChooserContext and
    // HidChooserContext to deduplicate redundant exceptions.
    switch (content_type()) {
      case ContentSettingsType::BLUETOOTH_CHOOSER_DATA:
        // BluetoothChooserContext creates a different permission object for
        // each (device,origin) pair, so persistent-device shows up for each of
        // kChromiumOrigin and kGoogleOrigin.
        //
        // TODO(crbug.com/40667219): No policy-granted exceptions are
        // included because Web Bluetooth does not support granting device
        // permissions by policy.
        EXPECT_THAT(
            GetExceptionDisplayNames(exceptions),
            UnorderedElementsAre("persistent-device", "persistent-device",
                                 "ephemeral-device", "user-granted-device"));
        break;
      case ContentSettingsType::HID_CHOOSER_DATA:
      case ContentSettingsType::SERIAL_CHOOSER_DATA:
        EXPECT_THAT(
            GetExceptionDisplayNames(exceptions),
            UnorderedElementsAre("persistent-device", "user-granted-device",
                                 "ephemeral-device", GetAllDevicesDisplayName(),
                                 GetDevicesFromGoogleDisplayName(),
                                 GetDevicesFromVendor18D2DisplayName(),
                                 GetUnknownProductDisplayName()));
        break;
      case ContentSettingsType::USB_CHOOSER_DATA:
        EXPECT_THAT(
            GetExceptionDisplayNames(exceptions),
            UnorderedElementsAre("persistent-device", "user-granted-device",
                                 GetAllDevicesDisplayName(),
                                 GetDevicesFromGoogleDisplayName(),
                                 GetDevicesFromVendor18D2DisplayName()));
        break;
      default:
        NOTREACHED();
    }

    // Don't include WebUI schemes.
    const std::string kWebUIOriginStr =
        kWebUIUrl.DeprecatedGetOriginAsURL().spec();
    EXPECT_FALSE(ChooserExceptionContainsSiteException(
        exceptions, "persistent-device", kWebUIOriginStr));
  }

  void TestHandleGetChooserExceptionListForOffTheRecord() {
    SetUpOffTheRecordChooserContext();
    AddPersistentDevice();
    AddEphemeralDevice();
    AddUserGrantedDevice();
    AddOffTheRecordDevice();
    base::RunLoop().RunUntilIdle();

    SetUpUserGrantedPermissions();

    base::RunLoop().RunUntilIdle();
    web_ui()->ClearTrackedCalls();

    const std::string group_name(
        site_settings::ContentSettingsTypeToGroupName(content_type()));

    // The objects returned by GetChooserExceptionListFromProfile should also
    // include the incognito permissions.
    {
      const base::ListValue& exceptions =
          GetChooserExceptionListFromWebUiCallData(group_name,
                                                   /*expected_total_calls=*/1u);
      switch (content_type()) {
        case ContentSettingsType::BLUETOOTH_CHOOSER_DATA:
          EXPECT_THAT(
              GetExceptionDisplayNames(exceptions),
              UnorderedElementsAre("persistent-device", "persistent-device",
                                   "ephemeral-device", "user-granted-device",
                                   "off-the-record-device"));
          break;
        case ContentSettingsType::HID_CHOOSER_DATA:
        case ContentSettingsType::SERIAL_CHOOSER_DATA:
          EXPECT_THAT(
              GetExceptionDisplayNames(exceptions),
              UnorderedElementsAre(
                  "persistent-device", "ephemeral-device",
                  "user-granted-device", "off-the-record-device",
                  GetAllDevicesDisplayName(), GetDevicesFromGoogleDisplayName(),
                  GetDevicesFromVendor18D2DisplayName(),
                  GetUnknownProductDisplayName()));
          break;
        case ContentSettingsType::USB_CHOOSER_DATA:
          EXPECT_THAT(GetExceptionDisplayNames(exceptions),
                      UnorderedElementsAre(
                          "persistent-device", "user-granted-device",
                          "off-the-record-device", GetAllDevicesDisplayName(),
                          GetDevicesFromGoogleDisplayName(),
                          GetDevicesFromVendor18D2DisplayName(),
                          GetUnknownProductDisplayName()));
          break;
        default:
          NOTREACHED();
      }
    }

    // Destroy the off the record profile and check that the objects returned do
    // not include incognito permissions anymore. The destruction of the profile
    // causes the "onIncognitoStatusChanged" WebUIListener callback to fire.
    DestroyIncognitoProfile();
    EXPECT_EQ(web_ui()->call_data().size(), 2u);

    {
      const base::ListValue& exceptions =
          GetChooserExceptionListFromWebUiCallData(group_name,
                                                   /*expected_total_calls=*/3u);
      switch (content_type()) {
        case ContentSettingsType::BLUETOOTH_CHOOSER_DATA:
          EXPECT_THAT(
              GetExceptionDisplayNames(exceptions),
              UnorderedElementsAre("persistent-device", "persistent-device",
                                   "ephemeral-device", "user-granted-device"));
          break;
        case ContentSettingsType::HID_CHOOSER_DATA:
        case ContentSettingsType::SERIAL_CHOOSER_DATA:
          EXPECT_THAT(GetExceptionDisplayNames(exceptions),
                      UnorderedElementsAre(
                          "persistent-device", "ephemeral-device",
                          "user-granted-device", GetAllDevicesDisplayName(),
                          GetDevicesFromGoogleDisplayName(),
                          GetDevicesFromVendor18D2DisplayName(),
                          GetUnknownProductDisplayName()));
          break;
        case ContentSettingsType::USB_CHOOSER_DATA:
          EXPECT_THAT(
              GetExceptionDisplayNames(exceptions),
              UnorderedElementsAre("persistent-device", "user-granted-device",
                                   GetAllDevicesDisplayName(),
                                   GetDevicesFromGoogleDisplayName(),
                                   GetDevicesFromVendor18D2DisplayName()));
          break;
        default:
          NOTREACHED();
      }
    }
  }

  void TestHandleResetChooserExceptionForSite() {
    AddPersistentDevice();
    AddEphemeralDevice();
    AddUserGrantedDevice();
    base::RunLoop().RunUntilIdle();

    SetUpUserGrantedPermissions();

    base::RunLoop().RunUntilIdle();
    web_ui()->ClearTrackedCalls();

    const std::string group_name(
        site_settings::ContentSettingsTypeToGroupName(content_type()));
    const auto kAndroidOrigin = url::Origin::Create(kAndroidUrl);
    const auto kChromiumOrigin = url::Origin::Create(kChromiumUrl);
    const auto kGoogleOrigin = url::Origin::Create(kGoogleUrl);
    const std::string kAndroidOriginStr =
        kAndroidUrl.DeprecatedGetOriginAsURL().spec();
    const std::string kChromiumOriginStr =
        kChromiumUrl.DeprecatedGetOriginAsURL().spec();
    const std::string kGoogleOriginStr =
        kGoogleUrl.DeprecatedGetOriginAsURL().spec();

    {
      const base::ListValue& exceptions =
          GetChooserExceptionListFromWebUiCallData(group_name,
                                                   /*expected_total_calls=*/1u);
      switch (content_type()) {
        case ContentSettingsType::BLUETOOTH_CHOOSER_DATA:
          EXPECT_THAT(
              GetExceptionDisplayNames(exceptions),
              UnorderedElementsAre("persistent-device", "persistent-device",
                                   "ephemeral-device", "user-granted-device"));
          break;
        case ContentSettingsType::HID_CHOOSER_DATA:
        case ContentSettingsType::SERIAL_CHOOSER_DATA:
          EXPECT_THAT(GetExceptionDisplayNames(exceptions),
                      UnorderedElementsAre(
                          "persistent-device", "ephemeral-device",
                          "user-granted-device", GetAllDevicesDisplayName(),
                          GetDevicesFromGoogleDisplayName(),
                          GetDevicesFromVendor18D2DisplayName(),
                          GetUnknownProductDisplayName()));
          break;
        case ContentSettingsType::USB_CHOOSER_DATA:
          EXPECT_THAT(
              GetExceptionDisplayNames(exceptions),
              UnorderedElementsAre("persistent-device", "user-granted-device",
                                   GetAllDevicesDisplayName(),
                                   GetDevicesFromGoogleDisplayName(),
                                   GetDevicesFromVendor18D2DisplayName()));
          break;
        default:
          NOTREACHED();
      }
    }

    // User granted USB permissions for devices also containing policy
    // permissions should be able to be reset without removing the chooser
    // exception object from the list.
    base::ListValue args;
    args.Append(group_name);
    args.Append(kGoogleOriginStr);
    args.Append(GetPersistentDeviceValueForOrigin(kGoogleOrigin));

    EXPECT_CALL(observer_,
                OnObjectPermissionChanged({guard_type()}, content_type()));
    EXPECT_CALL(observer_, OnPermissionRevoked(kGoogleOrigin));
    handler()->HandleResetChooserExceptionForSite(args);
    GetChooserContext(profile())->FlushScheduledSaveSettingsCalls();

    // The HandleResetChooserExceptionForSite() method should have also caused
    // the WebUIListenerCallbacks for contentSettingSitePermissionChanged and
    // contentSettingChooserPermissionChanged to fire.
    EXPECT_EQ(web_ui()->call_data().size(), 3u);
    {
      // The exception list size should not have been reduced since there is
      // still a policy granted permission for "persistent-device".
      const base::ListValue& exceptions =
          GetChooserExceptionListFromWebUiCallData(group_name,
                                                   /*expected_total_calls=*/4u);
      switch (content_type()) {
        case ContentSettingsType::BLUETOOTH_CHOOSER_DATA:
          EXPECT_THAT(
              GetExceptionDisplayNames(exceptions),
              UnorderedElementsAre("persistent-device", "ephemeral-device",
                                   "user-granted-device"));
          break;
        case ContentSettingsType::HID_CHOOSER_DATA:
        case ContentSettingsType::SERIAL_CHOOSER_DATA:
          EXPECT_THAT(GetExceptionDisplayNames(exceptions),
                      UnorderedElementsAre(
                          "persistent-device", "ephemeral-device",
                          "user-granted-device", GetAllDevicesDisplayName(),
                          GetDevicesFromGoogleDisplayName(),
                          GetDevicesFromVendor18D2DisplayName(),
                          GetUnknownProductDisplayName()));
          break;
        case ContentSettingsType::USB_CHOOSER_DATA:
          EXPECT_THAT(
              GetExceptionDisplayNames(exceptions),
              UnorderedElementsAre("persistent-device", "user-granted-device",
                                   GetAllDevicesDisplayName(),
                                   GetDevicesFromGoogleDisplayName(),
                                   GetDevicesFromVendor18D2DisplayName()));
          break;
        default:
          NOTREACHED();
      }

      // Ensure that the sites list does not contain the URLs of the removed
      // permission.
      EXPECT_FALSE(ChooserExceptionContainsSiteException(
          exceptions, "persistent-device", kGoogleOriginStr));

      // User granted exceptions that are also granted by policy are only
      // displayed through the policy granted site exception, so ensure that the
      // policy exception is present under "persistent-device".
      if (content_type() != ContentSettingsType::BLUETOOTH_CHOOSER_DATA) {
        EXPECT_TRUE(ChooserExceptionContainsSiteException(
            exceptions, "persistent-device", kChromiumOriginStr));
      }
    }

    // Try revoking the user-granted permission for persistent-device. There is
    // also a policy-granted permission for the same device.
    args.clear();
    args.Append(group_name);
    args.Append(kChromiumOriginStr);
    args.Append(GetPersistentDeviceValueForOrigin(kChromiumOrigin));

    EXPECT_CALL(observer_,
                OnObjectPermissionChanged({guard_type()}, content_type()));
    EXPECT_CALL(observer_, OnPermissionRevoked(kChromiumOrigin));
    handler()->HandleResetChooserExceptionForSite(args);
    GetChooserContext(profile())->FlushScheduledSaveSettingsCalls();

    // The HandleResetChooserExceptionForSite() method should have also caused
    // the WebUIListenerCallbacks for contentSettingSitePermissionChanged and
    // contentSettingChooserPermissionChanged to fire.
    EXPECT_EQ(web_ui()->call_data().size(), 6u);
    {
      const base::ListValue& exceptions =
          GetChooserExceptionListFromWebUiCallData(group_name,
                                                   /*expected_total_calls=*/7u);
      switch (content_type()) {
        case ContentSettingsType::BLUETOOTH_CHOOSER_DATA:
          EXPECT_THAT(
              GetExceptionDisplayNames(exceptions),
              UnorderedElementsAre("ephemeral-device", "user-granted-device"));
          break;
        case ContentSettingsType::HID_CHOOSER_DATA:
        case ContentSettingsType::SERIAL_CHOOSER_DATA:
          EXPECT_THAT(
              GetExceptionDisplayNames(exceptions),
              UnorderedElementsAre("ephemeral-device", "user-granted-device",
                                   GetAllDevicesDisplayName(),
                                   GetDevicesFromGoogleDisplayName(),
                                   GetDevicesFromVendor18D2DisplayName(),
                                   GetUnknownProductDisplayName()));
          break;
        case ContentSettingsType::USB_CHOOSER_DATA:
          EXPECT_THAT(GetExceptionDisplayNames(exceptions),
                      UnorderedElementsAre(
                          "user-granted-device", GetAllDevicesDisplayName(),
                          GetDevicesFromGoogleDisplayName(),
                          GetDevicesFromVendor18D2DisplayName(),
                          GetUnknownProductDisplayName()));
          break;
        default:
          NOTREACHED();
      }

      // Ensure that the sites list still displays a site exception entry for an
      // origin of kGoogleOriginStr.  Since now the device has had its
      // permission revoked, the policy-provided object will not be able to
      // deduce the name "persistent-device" from the connected device. As such
      // we check that the policy is still active by looking for the genericly
      // constructed name.
      if (content_type() != ContentSettingsType::BLUETOOTH_CHOOSER_DATA) {
        EXPECT_TRUE(ChooserExceptionContainsSiteException(
            exceptions, GetUnknownProductDisplayName(), kChromiumOriginStr));
        EXPECT_FALSE(ChooserExceptionContainsSiteException(
            exceptions, "persistent-device", kGoogleOriginStr));
      }

      // Ensure the exception for user-granted-device on kAndroidOrigin is
      // present since we will try to revoke it.
      EXPECT_TRUE(ChooserExceptionContainsSiteException(
          exceptions, "user-granted-device", kAndroidOriginStr));
    }

    // User granted USB permissions that are not covered by policy should be
    // able to be reset and the chooser exception entry should be removed from
    // the list when the exception only has one site exception granted to it.
    args.clear();
    args.Append(group_name);
    args.Append(kAndroidOriginStr);
    args.Append(GetUserGrantedDeviceValueForOrigin(kAndroidOrigin));

    EXPECT_CALL(observer_,
                OnObjectPermissionChanged({guard_type()}, content_type()));
    EXPECT_CALL(observer_, OnPermissionRevoked(kAndroidOrigin));
    handler()->HandleResetChooserExceptionForSite(args);
    GetChooserContext(profile())->FlushScheduledSaveSettingsCalls();

    // The HandleResetChooserExceptionForSite() method should have also caused
    // the WebUIListenerCallbacks for contentSettingSitePermissionChanged and
    // contentSettingChooserPermissionChanged to fire.
    EXPECT_EQ(web_ui()->call_data().size(), 9u);
    {
      const base::ListValue& exceptions =
          GetChooserExceptionListFromWebUiCallData(
              group_name, /*expected_total_calls=*/10u);
      switch (content_type()) {
        case ContentSettingsType::BLUETOOTH_CHOOSER_DATA:
          EXPECT_THAT(GetExceptionDisplayNames(exceptions),
                      UnorderedElementsAre("ephemeral-device"));
          break;
        case ContentSettingsType::HID_CHOOSER_DATA:
        case ContentSettingsType::SERIAL_CHOOSER_DATA:
          EXPECT_THAT(GetExceptionDisplayNames(exceptions),
                      UnorderedElementsAre(
                          "ephemeral-device", GetAllDevicesDisplayName(),
                          GetDevicesFromGoogleDisplayName(),
                          GetDevicesFromVendor18D2DisplayName(),
                          GetUnknownProductDisplayName()));
          break;
        case ContentSettingsType::USB_CHOOSER_DATA:
          EXPECT_THAT(
              GetExceptionDisplayNames(exceptions),
              UnorderedElementsAre(GetAllDevicesDisplayName(),
                                   GetDevicesFromGoogleDisplayName(),
                                   GetDevicesFromVendor18D2DisplayName(),
                                   GetUnknownProductDisplayName()));
          break;
        default:
          NOTREACHED();
      }
      EXPECT_FALSE(ChooserExceptionContainsSiteException(
          exceptions, "user-granted-device", kAndroidOriginStr));
    }
  }

  void TestHandleSetOriginPermissions() {
    constexpr std::string_view kYoutubeOriginStr = "https://youtube.com/";
    const GURL kYoutubeUrl{kYoutubeOriginStr};
    const auto kYoutubeOrigin = url::Origin::Create(kYoutubeUrl);

    // Grant permissions for user-granted-device on `kYoutubeOrigin`.
    AddUserGrantedDevice();
    SetUpUserGrantedPermissionForOrigin(kYoutubeOrigin);

    base::RunLoop().RunUntilIdle();
    web_ui()->ClearTrackedCalls();

    const std::string group_name(
        site_settings::ContentSettingsTypeToGroupName(content_type()));

    {
      const base::ListValue& exceptions =
          GetChooserExceptionListFromWebUiCallData(group_name,
                                                   /*expected_total_calls=*/1u);
      switch (content_type()) {
        case ContentSettingsType::BLUETOOTH_CHOOSER_DATA:
          EXPECT_THAT(GetExceptionDisplayNames(exceptions),
                      UnorderedElementsAre("user-granted-device"));
          break;
        case ContentSettingsType::HID_CHOOSER_DATA:
        case ContentSettingsType::SERIAL_CHOOSER_DATA:
        case ContentSettingsType::USB_CHOOSER_DATA:
          EXPECT_THAT(GetExceptionDisplayNames(exceptions),
                      UnorderedElementsAre(
                          "user-granted-device", GetAllDevicesDisplayName(),
                          GetDevicesFromGoogleDisplayName(),
                          GetDevicesFromVendor18D2DisplayName(),
                          GetUnknownProductDisplayName()));
          break;
        default:
          NOTREACHED();
      }
      EXPECT_TRUE(ChooserExceptionContainsSiteException(
          exceptions, "user-granted-device", kYoutubeOriginStr));
    }

    // Clear data for kYoutubeOrigin. The permission should be revoked.
    base::ListValue args;
    args.Append(kYoutubeOriginStr);
    args.Append(base::Value());
    args.Append(
        content_settings::ContentSettingToString(CONTENT_SETTING_DEFAULT));

    EXPECT_CALL(observer_,
                OnObjectPermissionChanged({guard_type()}, content_type()));
    EXPECT_CALL(observer_, OnPermissionRevoked(kYoutubeOrigin));
    handler()->HandleSetOriginPermissions(args);
    GetChooserContext(profile())->FlushScheduledSaveSettingsCalls();

    // HandleSetOriginPermissions caused WebUIListenerCallbacks:
    // * contentSettingsChooserPermissionChanged once
    // * contentSettingsSitePermissionChanged once
    // * contentSettingsSitePermissionChanged again for `content_type()`
    EXPECT_EQ(3u, web_ui()->call_data().size());
    {
      const base::ListValue& exceptions =
          GetChooserExceptionListFromWebUiCallData(group_name,
                                                   /*expected_total_calls=*/4u);
      switch (content_type()) {
        case ContentSettingsType::BLUETOOTH_CHOOSER_DATA:
          EXPECT_TRUE(exceptions.empty());
          break;
        case ContentSettingsType::HID_CHOOSER_DATA:
        case ContentSettingsType::SERIAL_CHOOSER_DATA:
        case ContentSettingsType::USB_CHOOSER_DATA:
          EXPECT_THAT(
              GetExceptionDisplayNames(exceptions),
              UnorderedElementsAre(GetAllDevicesDisplayName(),
                                   GetDevicesFromGoogleDisplayName(),
                                   GetDevicesFromVendor18D2DisplayName(),
                                   GetUnknownProductDisplayName()));
          break;
        default:
          NOTREACHED();
      }
      EXPECT_FALSE(ChooserExceptionContainsSiteException(
          exceptions, "user-granted-device", kYoutubeOriginStr));
    }
  }

  void TestHandleSetOriginPermissionsPolicyOnly() {
    const auto kGoogleOrigin = url::Origin::Create(kGoogleUrl);
    const std::string kGoogleOriginStr =
        kGoogleUrl.DeprecatedGetOriginAsURL().spec();

    base::RunLoop().RunUntilIdle();
    web_ui()->ClearTrackedCalls();

    const std::string group_name(
        site_settings::ContentSettingsTypeToGroupName(content_type()));

    {
      const base::ListValue& exceptions =
          GetChooserExceptionListFromWebUiCallData(group_name,
                                                   /*expected_total_calls=*/1u);
      switch (content_type()) {
        case ContentSettingsType::BLUETOOTH_CHOOSER_DATA:
          EXPECT_TRUE(exceptions.empty());
          break;
        case ContentSettingsType::HID_CHOOSER_DATA:
        case ContentSettingsType::SERIAL_CHOOSER_DATA:
        case ContentSettingsType::USB_CHOOSER_DATA:
          EXPECT_THAT(
              GetExceptionDisplayNames(exceptions),
              UnorderedElementsAre(GetAllDevicesDisplayName(),
                                   GetDevicesFromGoogleDisplayName(),
                                   GetDevicesFromVendor18D2DisplayName(),
                                   GetUnknownProductDisplayName()));
          break;
        default:
          NOTREACHED();
      }
    }

    // Clear data for kGoogleOrigin.
    base::ListValue args;
    args.Append(kGoogleOriginStr);
    args.Append(base::Value());
    args.Append(
        content_settings::ContentSettingToString(CONTENT_SETTING_DEFAULT));

    EXPECT_CALL(observer_, OnObjectPermissionChanged).Times(0);
    EXPECT_CALL(observer_, OnPermissionRevoked).Times(0);
    handler()->HandleSetOriginPermissions(args);
    GetChooserContext(profile())->FlushScheduledSaveSettingsCalls();

    EXPECT_EQ(1u, web_ui()->call_data().size());
    {
      const base::ListValue& exceptions =
          GetChooserExceptionListFromWebUiCallData(group_name,
                                                   /*expected_total_calls=*/2u);
      switch (content_type()) {
        case ContentSettingsType::BLUETOOTH_CHOOSER_DATA:
          EXPECT_TRUE(exceptions.empty());
          break;
        case ContentSettingsType::HID_CHOOSER_DATA:
        case ContentSettingsType::SERIAL_CHOOSER_DATA:
        case ContentSettingsType::USB_CHOOSER_DATA:
          EXPECT_THAT(
              GetExceptionDisplayNames(exceptions),
              UnorderedElementsAre(GetAllDevicesDisplayName(),
                                   GetDevicesFromGoogleDisplayName(),
                                   GetDevicesFromVendor18D2DisplayName(),
                                   GetUnknownProductDisplayName()));
          break;
        default:
          NOTREACHED();
      }
    }
  }

  // Returns the ContentSettingsType for the chooser data (device permissions).
  virtual ContentSettingsType content_type() = 0;

  // Returns the ContentSettingsType for the guard permission.
  virtual ContentSettingsType guard_type() = 0;

  // Returns the chooser context for `profile`.
  virtual permissions::ObjectPermissionContextBase* GetChooserContext(
      Profile* profile) = 0;

  // Sets up the chooser context.
  virtual void SetUpChooserContext() = 0;

  // Creates an incognito profile and sets up the chooser context for that
  // profile. Tests must call DestroyIncognitoProfile before exiting.
  virtual void SetUpOffTheRecordChooserContext() {}

  // Grants permissions used by tests. There are four devices that are
  // granted user permissions. Two (persistent-device and ephemeral-device) are
  // covered by different policy permissions, while the third
  // (user-granted-device) is not covered by policy at all. If the
  // off-the-record-device is present, a user-granted permission is granted for
  // the incognito profile.
  virtual void SetUpUserGrantedPermissions() = 0;

  // Configures policies to automatically grant device permissions.
  virtual void SetUpPolicyGrantedPermissions() {}

  // Grants device permissions for user-granted-device on `origin`.
  virtual void SetUpUserGrantedPermissionForOrigin(
      const url::Origin& origin) = 0;

  // Create and add a device eligible for persistent permissions. The device
  // name is "persistent-device".
  virtual void AddPersistentDevice() = 0;

  // Create and add a device ineligible for persistent permissions. The device
  // name is "ephemeral-device".
  virtual void AddEphemeralDevice() = 0;

  // Create and add a device. The permission policies added in
  // `SetUpPolicyGrantedPermissions` should not grant permissions for this
  // device except for policies which affect all devices. The device name is
  // "user-granted-device".
  virtual void AddUserGrantedDevice() = 0;

  // Create and add a device. Tests should only grant permissions for this
  // device using the off the record profile. The device name is
  // "off-the-record-device".
  virtual void AddOffTheRecordDevice() = 0;

  // Returns the permission object representing persistent-device granted to
  // `origin`.
  virtual base::Value GetPersistentDeviceValueForOrigin(
      const url::Origin& origin) = 0;

  // Returns the permission object representing user-granted-device granted to
  // `origin`.
  virtual base::Value GetUserGrantedDeviceValueForOrigin(
      const url::Origin& origin) = 0;

  // Returns the display name for a chooser exception that allows an origin to
  // access any device.
  virtual std::string GetAllDevicesDisplayName() { return {}; }

  // Returns the display name for a chooser exception that allows an origin to
  // access any device with the Google vendor ID.
  virtual std::string GetDevicesFromGoogleDisplayName() { return {}; }

  // Returns the display name for a chooser exception that allows an origin to
  // access any device from vendor 0x18D2.
  virtual std::string GetDevicesFromVendor18D2DisplayName() { return {}; }

  // Returns the display name for a chooser exception that allows an origin to
  // access a specific device by its vendor and product IDs.
  virtual std::string GetUnknownProductDisplayName() { return {}; }

  permissions::MockPermissionObserver observer_;
};

class SiteSettingsHandlerBluetoothTest
    : public SiteSettingsHandlerChooserExceptionTest {
 protected:
  SiteSettingsHandlerBluetoothTest()
      : SiteSettingsHandlerChooserExceptionTest() {
    feature_list_.InitAndEnableFeature(
        features::kWebBluetoothNewPermissionsBackend);
  }

  permissions::ObjectPermissionContextBase* GetChooserContext(
      Profile* profile) override {
    return BluetoothChooserContextFactory::GetForProfile(profile);
  }

  void SetUpChooserContext() override {
    adapter_ = base::MakeRefCounted<NiceMock<device::MockBluetoothAdapter>>();
    EXPECT_CALL(*adapter_, IsPresent).WillRepeatedly(Return(true));
    EXPECT_CALL(*adapter_, IsPowered).WillRepeatedly(Return(true));
    device::BluetoothAdapterFactory::SetAdapterForTesting(adapter_);
    base::RunLoop().RunUntilIdle();
  }

  void SetUpOffTheRecordChooserContext() override {
    CreateIncognitoProfile();
    GetChooserContext(incognito_profile())->AddObserver(&observer_);
  }

  void AddPersistentDevice() override {
    persistent_device_ =
        std::make_unique<NiceMock<device::MockBluetoothDevice>>(
            adapter_.get(), /*bluetooth_class=*/0, /*name=*/"persistent-device",
            /*address=*/"1", /*paired=*/false, /*connected=*/false);
  }

  void AddEphemeralDevice() override {
    ephemeral_device_ = std::make_unique<NiceMock<device::MockBluetoothDevice>>(
        adapter_.get(), /*bluetooth_class=*/0, /*name=*/"ephemeral-device",
        /*address=*/"2", /*paired=*/false, /*connected=*/false);
  }

  void AddUserGrantedDevice() override {
    user_granted_device_ =
        std::make_unique<NiceMock<device::MockBluetoothDevice>>(
            adapter_.get(), /*bluetooth_class=*/0,
            /*name=*/"user-granted-device", /*address=*/"3", /*paired=*/false,
            /*connected=*/false);
  }

  void AddOffTheRecordDevice() override {
    off_the_record_device_ =
        std::make_unique<NiceMock<device::MockBluetoothDevice>>(
            adapter_.get(), /*bluetooth_class=*/0,
            /*name=*/"off-the-record-device", /*address=*/"4", /*paired=*/false,
            /*connected=*/false);
  }

  void SetUpUserGrantedPermissions() override {
    const auto kAndroidOrigin = url::Origin::Create(kAndroidUrl);
    const auto kChromiumOrigin = url::Origin::Create(kChromiumUrl);
    const auto kGoogleOrigin = url::Origin::Create(kGoogleUrl);
    const auto kWebUIOrigin = url::Origin::Create(kWebUIUrl);

    auto options = blink::mojom::WebBluetoothRequestDeviceOptions::New();
    options->accept_all_devices = true;
    {
      base::RunLoop loop;
      auto barrier_closure = base::BarrierClosure(5, loop.QuitClosure());
      auto* bluetooth_chooser_context =
          BluetoothChooserContextFactory::GetForProfile(profile());
      EXPECT_CALL(observer_, OnObjectPermissionChanged(
                                 {ContentSettingsType::BLUETOOTH_GUARD},
                                 ContentSettingsType::BLUETOOTH_CHOOSER_DATA))
          .Times(5)
          .WillRepeatedly(RunClosure(barrier_closure));
      bluetooth_chooser_context->GrantServiceAccessPermission(
          kChromiumOrigin, persistent_device_.get(), options.get());
      bluetooth_chooser_context->GrantServiceAccessPermission(
          kGoogleOrigin, persistent_device_.get(), options.get());
      bluetooth_chooser_context->GrantServiceAccessPermission(
          kWebUIOrigin, persistent_device_.get(), options.get());
      bluetooth_chooser_context->GrantServiceAccessPermission(
          kAndroidOrigin, ephemeral_device_.get(), options.get());
      bluetooth_chooser_context->GrantServiceAccessPermission(
          kAndroidOrigin, user_granted_device_.get(), options.get());
      loop.Run();
    }

    if (off_the_record_device_) {
      base::RunLoop loop;
      EXPECT_CALL(observer_, OnObjectPermissionChanged(
                                 {ContentSettingsType::BLUETOOTH_GUARD},
                                 ContentSettingsType::BLUETOOTH_CHOOSER_DATA))
          .WillOnce(RunClosure(loop.QuitClosure()));
      BluetoothChooserContextFactory::GetForProfile(incognito_profile())
          ->GrantServiceAccessPermission(
              kChromiumOrigin, off_the_record_device_.get(), options.get());
      loop.Run();
    }
  }

  void SetUpUserGrantedPermissionForOrigin(const url::Origin& origin) override {
    auto* bluetooth_chooser_context =
        BluetoothChooserContextFactory::GetForProfile(profile());
    base::RunLoop loop;
    EXPECT_CALL(observer_, OnObjectPermissionChanged(
                               {ContentSettingsType::BLUETOOTH_GUARD},
                               ContentSettingsType::BLUETOOTH_CHOOSER_DATA))
        .WillOnce(RunClosure(loop.QuitClosure()));
    auto options = blink::mojom::WebBluetoothRequestDeviceOptions::New();
    options->accept_all_devices = true;
    bluetooth_chooser_context->GrantServiceAccessPermission(
        origin, user_granted_device_.get(), options.get());
    loop.Run();
  }

  base::Value GetPersistentDeviceValueForOrigin(
      const url::Origin& origin) override {
    auto options = blink::mojom::WebBluetoothRequestDeviceOptions::New();
    options->accept_all_devices = true;
    auto device_id =
        BluetoothChooserContextFactory::GetForProfile(profile())
            ->GetWebBluetoothDeviceId(origin, persistent_device_->GetAddress());
    return base::Value(permissions::BluetoothChooserContext::DeviceInfoToValue(
        persistent_device_.get(), options.get(), device_id));
  }

  base::Value GetUserGrantedDeviceValueForOrigin(
      const url::Origin& origin) override {
    auto options = blink::mojom::WebBluetoothRequestDeviceOptions::New();
    options->accept_all_devices = true;
    auto device_id = BluetoothChooserContextFactory::GetForProfile(profile())
                         ->GetWebBluetoothDeviceId(
                             origin, user_granted_device_->GetAddress());
    return base::Value(permissions::BluetoothChooserContext::DeviceInfoToValue(
        user_granted_device_.get(), options.get(), device_id));
  }

  ContentSettingsType content_type() override {
    return ContentSettingsType::BLUETOOTH_CHOOSER_DATA;
  }

  ContentSettingsType guard_type() override {
    return ContentSettingsType::BLUETOOTH_GUARD;
  }

  base::test::ScopedFeatureList feature_list_;
  scoped_refptr<NiceMock<device::MockBluetoothAdapter>> adapter_;
  std::unique_ptr<NiceMock<device::MockBluetoothDevice>> ephemeral_device_;
  std::unique_ptr<NiceMock<device::MockBluetoothDevice>> off_the_record_device_;
  std::unique_ptr<NiceMock<device::MockBluetoothDevice>> persistent_device_;
  std::unique_ptr<NiceMock<device::MockBluetoothDevice>> user_granted_device_;
};

TEST_F(SiteSettingsHandlerBluetoothTest, HandleGetChooserExceptionList) {
  TestHandleGetChooserExceptionList();
}

TEST_F(SiteSettingsHandlerBluetoothTest,
       HandleGetChooserExceptionListForOffTheRecord) {
  TestHandleGetChooserExceptionListForOffTheRecord();
}

TEST_F(SiteSettingsHandlerBluetoothTest, HandleResetChooserExceptionForSite) {
  TestHandleResetChooserExceptionForSite();
}

TEST_F(SiteSettingsHandlerBluetoothTest, HandleSetOriginPermissions) {
  TestHandleSetOriginPermissions();
}

TEST_F(SiteSettingsHandlerBluetoothTest, HandleSetOriginPermissionsPolicyOnly) {
  TestHandleSetOriginPermissionsPolicyOnly();
}

class SiteSettingsHandlerHidTest
    : public SiteSettingsHandlerChooserExceptionTest {
 protected:
  void SetUpChooserContext() override {
    mojo::PendingRemote<device::mojom::HidManager> hid_manager;
    hid_manager_.AddReceiver(hid_manager.InitWithNewPipeAndPassReceiver());
    HidChooserContext* hid_chooser_context =
        HidChooserContextFactory::GetForProfile(profile());
    TestFuture<std::vector<device::mojom::HidDeviceInfoPtr>> get_devices_future;
    hid_chooser_context->SetHidManagerForTesting(
        std::move(hid_manager), get_devices_future.GetCallback());
    EXPECT_TRUE(get_devices_future.Wait());
  }

  void SetUpPolicyGrantedPermissions() override {
    auto* local_state = TestingBrowserProcess::GetGlobal()->local_state();
    ASSERT_TRUE(local_state);
    local_state->Set(prefs::kManagedWebHidAllowDevicesForUrls, ParseJson(R"(
        [
          {
            "devices": [{ "vendor_id": 6353, "product_id": 5678 }],
            "urls": ["https://chromium.org"]
          }, {
            "devices": [{ "vendor_id": 6353 }],
            "urls": ["https://android.com"]
          }, {
            "devices": [{ "vendor_id": 6354 }],
            "urls": ["https://android.com"]
          }
        ])"));
    local_state->Set(prefs::kManagedWebHidAllowAllDevicesForUrls,
                     ParseJson(R"([ "https://google.com" ])"));
  }

  void SetUpOffTheRecordChooserContext() override {
    CreateIncognitoProfile();
    mojo::PendingRemote<device::mojom::HidManager> hid_manager;
    hid_manager_.AddReceiver(hid_manager.InitWithNewPipeAndPassReceiver());
    HidChooserContext* hid_chooser_context =
        HidChooserContextFactory::GetForProfile(incognito_profile());
    TestFuture<std::vector<device::mojom::HidDeviceInfoPtr>> get_devices_future;
    hid_chooser_context->SetHidManagerForTesting(
        std::move(hid_manager), get_devices_future.GetCallback());
    EXPECT_TRUE(get_devices_future.Wait());
    GetChooserContext(incognito_profile())->AddObserver(&observer_);
  }

  void AddPersistentDevice() override {
    persistent_device_ = hid_manager_.CreateAndAddDevice(
        /*physical_device_id=*/"1", /*vendor_id=*/6353, /*product_id=*/5678,
        /*product_name=*/"persistent-device", /*serial_number=*/"123ABC",
        device::mojom::HidBusType::kHIDBusTypeUSB);
  }

  void AddEphemeralDevice() override {
    ephemeral_device_ = hid_manager_.CreateAndAddDevice(
        /*physical_device_id=*/"2", /*vendor_id=*/6354, /*product_id=*/0,
        /*product_name=*/"ephemeral-device", /*serial_number=*/"",
        device::mojom::HidBusType::kHIDBusTypeUSB);
  }

  void AddUserGrantedDevice() override {
    user_granted_device_ = hid_manager_.CreateAndAddDevice(
        /*physical_device_id=*/"3", /*vendor_id=*/6355, /*product_id=*/0,
        /*product_name=*/"user-granted-device", /*serial_number=*/"789XYZ",
        device::mojom::HidBusType::kHIDBusTypeUSB);
  }

  void AddOffTheRecordDevice() override {
    off_the_record_device_ = hid_manager_.CreateAndAddDevice(
        /*physical_device_id=*/"4", /*vendor_id=*/6353,
        /*product_id=*/8765, /*product_name=*/"off-the-record-device",
        /*serial_number=*/"A9B8C7", device::mojom::HidBusType::kHIDBusTypeUSB);
  }

  void SetUpUserGrantedPermissions() override {
    const auto kAndroidOrigin = url::Origin::Create(kAndroidUrl);
    const auto kChromiumOrigin = url::Origin::Create(kChromiumUrl);
    const auto kGoogleOrigin = url::Origin::Create(kGoogleUrl);
    const auto kWebUIOrigin = url::Origin::Create(kWebUIUrl);

    // Add the user granted permissions for testing.
    // These two persistent device permissions should be lumped together
    // with the policy permissions, since they apply to the same device and
    // URL.
    {
      base::RunLoop loop;
      auto barrier_closure = base::BarrierClosure(5, loop.QuitClosure());
      EXPECT_CALL(observer_, OnObjectPermissionChanged(
                                 {ContentSettingsType::HID_GUARD},
                                 ContentSettingsType::HID_CHOOSER_DATA))
          .Times(5)
          .WillRepeatedly(RunClosure(barrier_closure));
      auto* hid_chooser_context =
          HidChooserContextFactory::GetForProfile(profile());
      hid_chooser_context->GrantDevicePermission(kChromiumOrigin,
                                                 *persistent_device_);
      hid_chooser_context->GrantDevicePermission(kGoogleOrigin,
                                                 *persistent_device_);
      hid_chooser_context->GrantDevicePermission(kWebUIOrigin,
                                                 *persistent_device_);
      hid_chooser_context->GrantDevicePermission(kAndroidOrigin,
                                                 *ephemeral_device_);
      hid_chooser_context->GrantDevicePermission(kAndroidOrigin,
                                                 *user_granted_device_);
      loop.Run();
    }

    if (off_the_record_device_) {
      base::RunLoop loop;
      EXPECT_CALL(observer_, OnObjectPermissionChanged(
                                 {ContentSettingsType::HID_GUARD},
                                 ContentSettingsType::HID_CHOOSER_DATA))
          .WillOnce(RunClosure(loop.QuitClosure()));
      HidChooserContextFactory::GetForProfile(incognito_profile())
          ->GrantDevicePermission(kChromiumOrigin, *off_the_record_device_);
      loop.Run();
    }
  }

  void SetUpUserGrantedPermissionForOrigin(const url::Origin& origin) override {
    auto* hid_chooser_context =
        HidChooserContextFactory::GetForProfile(profile());
    base::RunLoop loop;
    EXPECT_CALL(observer_, OnObjectPermissionChanged(
                               {ContentSettingsType::HID_GUARD},
                               ContentSettingsType::HID_CHOOSER_DATA))
        .WillOnce(RunClosure(loop.QuitClosure()));
    hid_chooser_context->GrantDevicePermission(origin, *user_granted_device_);
    loop.Run();
  }

  base::Value GetPersistentDeviceValueForOrigin(
      const url::Origin& origin) override {
    return base::Value(
        HidChooserContext::DeviceInfoToValue(*persistent_device_));
  }

  base::Value GetUserGrantedDeviceValueForOrigin(
      const url::Origin& origin) override {
    return base::Value(
        HidChooserContext::DeviceInfoToValue(*user_granted_device_));
  }

  std::string GetAllDevicesDisplayName() override { return "Any HID device"; }

  std::string GetDevicesFromGoogleDisplayName() override {
    return "HID devices from vendor 18D1";
  }

  std::string GetDevicesFromVendor18D2DisplayName() override {
    return "HID devices from vendor 18D2";
  }

  std::string GetUnknownProductDisplayName() override {
    return "HID device (18D1:162E)";
  }

  permissions::ObjectPermissionContextBase* GetChooserContext(
      Profile* profile) override {
    return HidChooserContextFactory::GetForProfile(profile);
  }

  ContentSettingsType content_type() override {
    return ContentSettingsType::HID_CHOOSER_DATA;
  }

  ContentSettingsType guard_type() override {
    return ContentSettingsType::HID_GUARD;
  }

  device::FakeHidManager hid_manager_;
  device::mojom::HidDeviceInfoPtr ephemeral_device_;
  device::mojom::HidDeviceInfoPtr off_the_record_device_;
  device::mojom::HidDeviceInfoPtr persistent_device_;
  device::mojom::HidDeviceInfoPtr user_granted_device_;
};

TEST_F(SiteSettingsHandlerHidTest, HandleGetChooserExceptionList) {
  TestHandleGetChooserExceptionList();
}

TEST_F(SiteSettingsHandlerHidTest,
       HandleGetChooserExceptionListForOffTheRecord) {
  TestHandleGetChooserExceptionListForOffTheRecord();
}

TEST_F(SiteSettingsHandlerHidTest, HandleResetChooserExceptionForSite) {
  TestHandleResetChooserExceptionForSite();
}

TEST_F(SiteSettingsHandlerHidTest, HandleSetOriginPermissions) {
  TestHandleSetOriginPermissions();
}

TEST_F(SiteSettingsHandlerHidTest, HandleSetOriginPermissionsPolicyOnly) {
  TestHandleSetOriginPermissionsPolicyOnly();
}

class SiteSettingsHandlerSerialTest
    : public SiteSettingsHandlerChooserExceptionTest {
 protected:
  void SetUpChooserContext() override {
    mojo::PendingRemote<device::mojom::SerialPortManager> serial_port_manager;
    serial_port_manager_.AddReceiver(
        serial_port_manager.InitWithNewPipeAndPassReceiver());
    SerialChooserContext* serial_chooser_context =
        SerialChooserContextFactory::GetForProfile(profile());
    serial_chooser_context->SetPortManagerForTesting(
        std::move(serial_port_manager));
    base::RunLoop().RunUntilIdle();
  }

  void SetUpPolicyGrantedPermissions() override {
    auto* local_state = TestingBrowserProcess::GetGlobal()->local_state();
    ASSERT_TRUE(local_state);
    local_state->Set(prefs::kManagedSerialAllowUsbDevicesForUrls, ParseJson(R"(
        [
          {
            "devices": [{ "vendor_id": 6353, "product_id": 5678 }],
            "urls": ["https://chromium.org"]
          }, {
            "devices": [{ "vendor_id": 6353 }],
            "urls": ["https://android.com"]
          }, {
            "devices": [{ "vendor_id": 6354 }],
            "urls": ["https://android.com"]
          }
        ])"));
    local_state->Set(prefs::kManagedSerialAllowAllPortsForUrls,
                     ParseJson(R"([ "https://google.com" ])"));
  }

  void SetUpOffTheRecordChooserContext() override {
    CreateIncognitoProfile();
    mojo::PendingRemote<device::mojom::SerialPortManager> serial_port_manager;
    serial_port_manager_.AddReceiver(
        serial_port_manager.InitWithNewPipeAndPassReceiver());
    SerialChooserContext* serial_chooser_context =
        SerialChooserContextFactory::GetForProfile(incognito_profile());
    serial_chooser_context->SetPortManagerForTesting(
        std::move(serial_port_manager));
    base::RunLoop().RunUntilIdle();
    GetChooserContext(incognito_profile())->AddObserver(&observer_);
  }

  void AddPersistentDevice() override {
    persistent_port_ = device::mojom::SerialPortInfo::New();
    persistent_port_->token = base::UnguessableToken::Create();
    persistent_port_->display_name = "persistent-device";
#if BUILDFLAG(IS_WIN)
    persistent_port_->device_instance_id = "1";
#else
    persistent_port_->has_vendor_id = true;
    persistent_port_->vendor_id = 6353;
    persistent_port_->has_product_id = true;
    persistent_port_->product_id = 5678;
    persistent_port_->serial_number = "123ABC";
#if BUILDFLAG(IS_MAC)
    persistent_port_->usb_driver_name = "AppleUSBCDC";
#endif
#endif  // BUILDFLAG(IS_WIN)
    serial_port_manager_.AddPort(persistent_port_.Clone());
  }

  void AddEphemeralDevice() override {
    ephemeral_port_ = device::mojom::SerialPortInfo::New();
    ephemeral_port_->token = base::UnguessableToken::Create();
    ephemeral_port_->display_name = "ephemeral-device";
#if BUILDFLAG(IS_WIN)
    ephemeral_port_->device_instance_id = "2";
#else
    ephemeral_port_->has_vendor_id = true;
    ephemeral_port_->vendor_id = 6354;
    ephemeral_port_->has_product_id = true;
    ephemeral_port_->product_id = 0;
#if BUILDFLAG(IS_MAC)
    ephemeral_port_->usb_driver_name = "AppleUSBCDC";
#endif
#endif  // BUILDFLAG(IS_WIN)
    serial_port_manager_.AddPort(ephemeral_port_.Clone());
  }

  void AddUserGrantedDevice() override {
    user_granted_port_ = device::mojom::SerialPortInfo::New();
    user_granted_port_->token = base::UnguessableToken::Create();
    user_granted_port_->display_name = "user-granted-device";
#if BUILDFLAG(IS_WIN)
    user_granted_port_->device_instance_id = "3";
#else
    user_granted_port_->has_vendor_id = true;
    user_granted_port_->vendor_id = 6355;
    user_granted_port_->has_product_id = true;
    user_granted_port_->product_id = 0;
    user_granted_port_->serial_number = "789XYZ";
#if BUILDFLAG(IS_MAC)
    user_granted_port_->usb_driver_name = "AppleUSBCDC";
#endif
#endif  // BUILDFLAG(IS_WIN)
    serial_port_manager_.AddPort(user_granted_port_.Clone());
  }

  void AddOffTheRecordDevice() override {
    off_the_record_port_ = device::mojom::SerialPortInfo::New();
    off_the_record_port_->token = base::UnguessableToken::Create();
    off_the_record_port_->display_name = "off-the-record-device";
#if BUILDFLAG(IS_WIN)
    off_the_record_port_->device_instance_id = "4";
#else
    off_the_record_port_->has_vendor_id = true;
    off_the_record_port_->vendor_id = 6353;
    off_the_record_port_->has_product_id = true;
    off_the_record_port_->product_id = 8765;
    off_the_record_port_->serial_number = "A9B8C7";
#if BUILDFLAG(IS_MAC)
    off_the_record_port_->usb_driver_name = "AppleUSBCDC";
#endif
#endif  // BUILDFLAG(IS_WIN)
    serial_port_manager_.AddPort(off_the_record_port_.Clone());
  }

  void SetUpUserGrantedPermissions() override {
    const auto kAndroidOrigin = url::Origin::Create(kAndroidUrl);
    const auto kChromiumOrigin = url::Origin::Create(kChromiumUrl);
    const auto kGoogleOrigin = url::Origin::Create(kGoogleUrl);
    const auto kWebUIOrigin = url::Origin::Create(kWebUIUrl);

    // Add the user granted permissions for testing.
    // These two persistent device permissions should be lumped together
    // with the policy permissions, since they apply to the same device and
    // URL.
    {
      base::RunLoop loop;
      auto barrier_closure = base::BarrierClosure(5, loop.QuitClosure());
      EXPECT_CALL(observer_, OnObjectPermissionChanged(
                                 {ContentSettingsType::SERIAL_GUARD},
                                 ContentSettingsType::SERIAL_CHOOSER_DATA))
          .Times(5)
          .WillRepeatedly(RunClosure(barrier_closure));
      auto* serial_chooser_context =
          SerialChooserContextFactory::GetForProfile(profile());
      serial_chooser_context->GrantPortPermission(kChromiumOrigin,
                                                  *persistent_port_);
      serial_chooser_context->GrantPortPermission(kGoogleOrigin,
                                                  *persistent_port_);
      serial_chooser_context->GrantPortPermission(kWebUIOrigin,
                                                  *persistent_port_);
      serial_chooser_context->GrantPortPermission(kAndroidOrigin,
                                                  *ephemeral_port_);
      serial_chooser_context->GrantPortPermission(kAndroidOrigin,
                                                  *user_granted_port_);
      loop.Run();
    }

    if (off_the_record_port_) {
      base::RunLoop loop;
      EXPECT_CALL(observer_, OnObjectPermissionChanged(
                                 {ContentSettingsType::SERIAL_GUARD},
                                 ContentSettingsType::SERIAL_CHOOSER_DATA))
          .WillOnce(RunClosure(loop.QuitClosure()));
      SerialChooserContextFactory::GetForProfile(incognito_profile())
          ->GrantPortPermission(kChromiumOrigin, *off_the_record_port_);
      loop.Run();
    }
  }

  void SetUpUserGrantedPermissionForOrigin(const url::Origin& origin) override {
    auto* serial_chooser_context =
        SerialChooserContextFactory::GetForProfile(profile());
    base::RunLoop loop;
    EXPECT_CALL(observer_, OnObjectPermissionChanged(
                               {ContentSettingsType::SERIAL_GUARD},
                               ContentSettingsType::SERIAL_CHOOSER_DATA))
        .WillOnce(RunClosure(loop.QuitClosure()));
    serial_chooser_context->GrantPortPermission(origin, *user_granted_port_);
    loop.Run();
  }

  base::Value GetPersistentDeviceValueForOrigin(
      const url::Origin& origin) override {
    return base::Value(
        SerialChooserContext::PortInfoToValue(*persistent_port_));
  }

  base::Value GetUserGrantedDeviceValueForOrigin(
      const url::Origin& origin) override {
    return base::Value(
        SerialChooserContext::PortInfoToValue(*user_granted_port_));
  }

  std::string GetAllDevicesDisplayName() override { return "Any serial port"; }

  std::string GetDevicesFromGoogleDisplayName() override {
    return "USB devices from Google Inc.";
  }

  std::string GetDevicesFromVendor18D2DisplayName() override {
    return "USB devices from vendor 18D2";
  }

  std::string GetUnknownProductDisplayName() override {
    return "USB device from Google Inc. (product 162E)";
  }

  permissions::ObjectPermissionContextBase* GetChooserContext(
      Profile* profile) override {
    return SerialChooserContextFactory::GetForProfile(profile);
  }

  ContentSettingsType content_type() override {
    return ContentSettingsType::SERIAL_CHOOSER_DATA;
  }

  ContentSettingsType guard_type() override {
    return ContentSettingsType::SERIAL_GUARD;
  }

  device::FakeSerialPortManager serial_port_manager_;
  device::mojom::SerialPortInfoPtr ephemeral_port_;
  device::mojom::SerialPortInfoPtr off_the_record_port_;
  device::mojom::SerialPortInfoPtr persistent_port_;
  device::mojom::SerialPortInfoPtr user_granted_port_;
};

TEST_F(SiteSettingsHandlerSerialTest, HandleGetChooserExceptionList) {
  TestHandleGetChooserExceptionList();
}

TEST_F(SiteSettingsHandlerSerialTest,
       HandleGetChooserExceptionListForOffTheRecord) {
  TestHandleGetChooserExceptionListForOffTheRecord();
}

TEST_F(SiteSettingsHandlerSerialTest, HandleResetChooserExceptionForSite) {
  TestHandleResetChooserExceptionForSite();
}

TEST_F(SiteSettingsHandlerSerialTest, HandleSetOriginPermissions) {
  TestHandleSetOriginPermissions();
}

TEST_F(SiteSettingsHandlerSerialTest, HandleSetOriginPermissionsPolicyOnly) {
  TestHandleSetOriginPermissionsPolicyOnly();
}

class SiteSettingsHandlerUsbTest
    : public SiteSettingsHandlerChooserExceptionTest {
 protected:
  void SetUpChooserContext() override {
    mojo::PendingRemote<device::mojom::UsbDeviceManager> device_manager;
    usb_device_manager_.AddReceiver(
        device_manager.InitWithNewPipeAndPassReceiver());
    UsbChooserContext* usb_chooser_context =
        UsbChooserContextFactory::GetForProfile(profile());
    usb_chooser_context->SetDeviceManagerForTesting(std::move(device_manager));
    TestFuture<std::vector<device::mojom::UsbDeviceInfoPtr>> get_devices_future;
    usb_chooser_context->GetDevices(get_devices_future.GetCallback());
    EXPECT_TRUE(get_devices_future.Wait());
  }

  void SetUpPolicyGrantedPermissions() override {
    profile()->GetPrefs()->Set(prefs::kManagedWebUsbAllowDevicesForUrls,
                               ParseJson(R"(
        [
          {
            "devices": [{ "vendor_id": 6353, "product_id": 5678 }],
            "urls": ["https://chromium.org"]
          }, {
            "devices": [{ "vendor_id": 6353 }],
            "urls": ["https://google.com,https://android.com"]
          }, {
            "devices": [{ "vendor_id": 6354 }],
            "urls": ["https://android.com,"]
          }, {
            "devices": [{}],
            "urls": ["https://google.com,https://google.com"]
          }
        ])"));
  }

  void SetUpOffTheRecordChooserContext() override {
    CreateIncognitoProfile();
    mojo::PendingRemote<device::mojom::UsbDeviceManager> device_manager;
    usb_device_manager_.AddReceiver(
        device_manager.InitWithNewPipeAndPassReceiver());
    UsbChooserContext* usb_chooser_context =
        UsbChooserContextFactory::GetForProfile(incognito_profile());
    usb_chooser_context->SetDeviceManagerForTesting(std::move(device_manager));
    TestFuture<std::vector<device::mojom::UsbDeviceInfoPtr>> get_devices_future;
    usb_chooser_context->GetDevices(get_devices_future.GetCallback());
    EXPECT_TRUE(get_devices_future.Wait());
    GetChooserContext(incognito_profile())->AddObserver(&observer_);
  }

  void AddPersistentDevice() override {
    persistent_device_ = usb_device_manager_.CreateAndAddDevice(
        /*vendor_id=*/6353, /*product_id=*/5678,
        /*manufacturer_string=*/"Google",
        /*product_string=*/"persistent-device", /*serial_number=*/"123ABC");
  }

  void AddEphemeralDevice() override {
    ephemeral_device_ = usb_device_manager_.CreateAndAddDevice(
        /*vendor_id=*/6354, /*product_id=*/0,
        /*manufacturer_string=*/"Google",
        /*product_string=*/"ephemeral-device", /*serial_number=*/"");
  }

  void AddUserGrantedDevice() override {
    user_granted_device_ = usb_device_manager_.CreateAndAddDevice(
        /*vendor_id=*/6355, /*product_id=*/0,
        /*manufacturer_string=*/"Google",
        /*product_string=*/"user-granted-device",
        /*serial_number=*/"789XYZ");
  }

  void AddOffTheRecordDevice() override {
    off_the_record_device_ = usb_device_manager_.CreateAndAddDevice(
        /*vendor_id=*/6353, /*product_id=*/8765,
        /*manufacturer_string=*/"Google",
        /*product_string=*/"off-the-record-device",
        /*serial_number=*/"A9B8C7");
  }

  void SetUpUserGrantedPermissions() override {
    const auto kAndroidOrigin = url::Origin::Create(kAndroidUrl);
    const auto kChromiumOrigin = url::Origin::Create(kChromiumUrl);
    const auto kGoogleOrigin = url::Origin::Create(kGoogleUrl);
    const auto kWebUIOrigin = url::Origin::Create(kWebUIUrl);

    // Add the user granted permissions for testing.
    // These two persistent device permissions should be lumped together
    // with the policy permissions, since they apply to the same device and
    // URL.
    {
      base::RunLoop loop;
      auto barrier_closure = base::BarrierClosure(5, loop.QuitClosure());
      EXPECT_CALL(observer_, OnObjectPermissionChanged(
                                 {ContentSettingsType::USB_GUARD},
                                 ContentSettingsType::USB_CHOOSER_DATA))
          .Times(5)
          .WillRepeatedly(RunClosure(barrier_closure));
      auto* usb_chooser_context =
          UsbChooserContextFactory::GetForProfile(profile());
      usb_chooser_context->GrantDevicePermission(kChromiumOrigin,
                                                 *persistent_device_);
      usb_chooser_context->GrantDevicePermission(kGoogleOrigin,
                                                 *persistent_device_);
      usb_chooser_context->GrantDevicePermission(kWebUIOrigin,
                                                 *persistent_device_);
      usb_chooser_context->GrantDevicePermission(kAndroidOrigin,
                                                 *ephemeral_device_);
      usb_chooser_context->GrantDevicePermission(kAndroidOrigin,
                                                 *user_granted_device_);
      loop.Run();
    }

    if (off_the_record_device_) {
      base::RunLoop loop;
      EXPECT_CALL(observer_, OnObjectPermissionChanged(
                                 {ContentSettingsType::USB_GUARD},
                                 ContentSettingsType::USB_CHOOSER_DATA))
          .WillOnce(RunClosure(loop.QuitClosure()));
      UsbChooserContextFactory::GetForProfile(incognito_profile())
          ->GrantDevicePermission(kChromiumOrigin, *off_the_record_device_);
      loop.Run();
    }
  }

  void SetUpUserGrantedPermissionForOrigin(const url::Origin& origin) override {
    auto* usb_chooser_context =
        UsbChooserContextFactory::GetForProfile(profile());
    base::RunLoop loop;
    EXPECT_CALL(observer_, OnObjectPermissionChanged(
                               {ContentSettingsType::USB_GUARD},
                               ContentSettingsType::USB_CHOOSER_DATA))
        .WillOnce(RunClosure(loop.QuitClosure()));
    usb_chooser_context->GrantDevicePermission(origin, *user_granted_device_);
    loop.Run();
  }

  base::Value GetPersistentDeviceValueForOrigin(
      const url::Origin& origin) override {
    return base::Value(
        UsbChooserContext::DeviceInfoToValue(*persistent_device_));
  }

  base::Value GetUserGrantedDeviceValueForOrigin(
      const url::Origin& origin) override {
    return base::Value(
        UsbChooserContext::DeviceInfoToValue(*user_granted_device_));
  }

  std::string GetAllDevicesDisplayName() override {
    return "Devices from any vendor";
  }

  std::string GetDevicesFromGoogleDisplayName() override {
    return "Devices from Google Inc.";
  }

  std::string GetDevicesFromVendor18D2DisplayName() override {
    return "Devices from vendor 0x18D2";
  }

  std::string GetUnknownProductDisplayName() override {
    return "Unknown product 0x162E from Google Inc.";
  }

  permissions::ObjectPermissionContextBase* GetChooserContext(
      Profile* profile) override {
    return UsbChooserContextFactory::GetForProfile(profile);
  }

  ContentSettingsType content_type() override {
    return ContentSettingsType::USB_CHOOSER_DATA;
  }

  ContentSettingsType guard_type() override {
    return ContentSettingsType::USB_GUARD;
  }

  device::FakeUsbDeviceManager usb_device_manager_;
  device::mojom::UsbDeviceInfoPtr ephemeral_device_;
  device::mojom::UsbDeviceInfoPtr off_the_record_device_;
  device::mojom::UsbDeviceInfoPtr persistent_device_;
  device::mojom::UsbDeviceInfoPtr user_granted_device_;
};

TEST_F(SiteSettingsHandlerUsbTest, HandleGetChooserExceptionList) {
  TestHandleGetChooserExceptionList();
}

TEST_F(SiteSettingsHandlerUsbTest,
       HandleGetChooserExceptionListForOffTheRecord) {
  TestHandleGetChooserExceptionListForOffTheRecord();
}

TEST_F(SiteSettingsHandlerUsbTest, HandleResetChooserExceptionForSite) {
  TestHandleResetChooserExceptionForSite();
}

TEST_F(SiteSettingsHandlerUsbTest, HandleSetOriginPermissions) {
  TestHandleSetOriginPermissions();
}

TEST_F(SiteSettingsHandlerUsbTest, HandleSetOriginPermissionsPolicyOnly) {
  TestHandleSetOriginPermissionsPolicyOnly();
}

TEST_F(SiteSettingsHandlerTest, HandleClearSiteGroupDataAndCookies) {
  SetupModel();
  std::vector<browsing_data_model_test_util::BrowsingDataEntry>
      expected_browsing_data_model_entries = {
          kGoogleUnpartitionedEntry,
          kExampleUnpartitionedEntry,
          kGoogleOnExampleEntry,
          kExampleOnGoogleSecureEntry,
          kExampleOnGoogleInsecureEntry,
          kExampleOnGoogleAuEntry,
          kExampleLocalStorage,
          kHttpExampleCookie,
          kHttpsWwwExampleCookie,
          kPartitionedHttpsWwwExampleOnGoogleAuCookie,
          kPartitionedHttpsWwwExampleOnGoogleCookie,
          kHttpAbcExampleCookie,
          kHttpGoogleCookieA,
          kHttpGoogleCookieB,
          kHttpGoogleAuCookie,
          kPartitionedHttpsGoogleAu1PCookie,
          kPartitionedHttpsWwwAnotherExampleOnGoogleAuCookie,
          kUngroupedHttpCookie,
      };
  browsing_data_model_test_util::ValidateBrowsingDataEntries(
      handler()->GetBrowsingDataModelForTesting(),
      expected_browsing_data_model_entries);

  auto verify_site_group = [](const base::Value& site_group,
                              std::string expected_etld_plus1) {
    ASSERT_TRUE(site_group.is_dict());
    ASSERT_THAT(CHECK_DEREF(site_group.GetDict().FindString("groupingKey")),
                IsEtldPlus1(expected_etld_plus1));
  };

  base::ListValue storage_and_cookie_list = GetOnStorageFetchedSentList();
  EXPECT_EQ(4U, storage_and_cookie_list.size());
  verify_site_group(storage_and_cookie_list[0], "example.com");
  verify_site_group(storage_and_cookie_list[1], "google.com");
  verify_site_group(storage_and_cookie_list[2], "google.com.au");
  verify_site_group(storage_and_cookie_list[3], "ungrouped.com");

  base::ListValue args;
  args.Append(GroupingKey::CreateFromEtldPlus1("example.com").Serialize());
  handler()->HandleClearSiteGroupDataAndCookies(args);

  // Items partitioned on example.com, as well as unpartitioned example.com
  // storage should be removed.
  RemoveModelEntries(expected_browsing_data_model_entries,
                     {
                         kExampleUnpartitionedEntry,
                         kExampleLocalStorage,
                         kHttpExampleCookie,
                         kHttpsWwwExampleCookie,
                         kHttpAbcExampleCookie,
                         kGoogleOnExampleEntry,
                     });
  browsing_data_model_test_util::ValidateBrowsingDataEntries(
      handler()->GetBrowsingDataModelForTesting(),
      expected_browsing_data_model_entries);

  storage_and_cookie_list = GetOnStorageFetchedSentList();
  EXPECT_EQ(3U, storage_and_cookie_list.size());
  verify_site_group(storage_and_cookie_list[0], "google.com");
  verify_site_group(storage_and_cookie_list[1], "google.com.au");
  verify_site_group(storage_and_cookie_list[2], "ungrouped.com");

  args.clear();
  args.Append(GroupingKey::CreateFromEtldPlus1("google.com").Serialize());

  handler()->HandleClearSiteGroupDataAndCookies(args);
  RemoveModelEntries(expected_browsing_data_model_entries,
                     {
                         kGoogleUnpartitionedEntry,
                         kExampleOnGoogleSecureEntry,
                         kExampleOnGoogleInsecureEntry,
                         kHttpGoogleCookieA,
                         kHttpGoogleCookieB,
                         kPartitionedHttpsWwwExampleOnGoogleCookie,
                     });
  browsing_data_model_test_util::ValidateBrowsingDataEntries(
      handler()->GetBrowsingDataModelForTesting(),
      expected_browsing_data_model_entries);

  storage_and_cookie_list = GetOnStorageFetchedSentList();
  EXPECT_EQ(2U, storage_and_cookie_list.size());
  verify_site_group(storage_and_cookie_list[0], "google.com.au");
  verify_site_group(storage_and_cookie_list[1], "ungrouped.com");

  args.clear();
  args.Append(GroupingKey::CreateFromEtldPlus1("google.com.au").Serialize());

  handler()->HandleClearSiteGroupDataAndCookies(args);
  // No nodes representing storage partitioned on google.com.au should be
  // present.
  RemoveModelEntries(expected_browsing_data_model_entries,
                     {
                         kExampleOnGoogleAuEntry,
                         kPartitionedHttpsWwwExampleOnGoogleAuCookie,
                         kHttpGoogleAuCookie,
                         kPartitionedHttpsGoogleAu1PCookie,
                         kPartitionedHttpsWwwAnotherExampleOnGoogleAuCookie,
                     });
  browsing_data_model_test_util::ValidateBrowsingDataEntries(
      handler()->GetBrowsingDataModelForTesting(),
      expected_browsing_data_model_entries);

  storage_and_cookie_list = GetOnStorageFetchedSentList();
  EXPECT_EQ(1U, storage_and_cookie_list.size());
  verify_site_group(storage_and_cookie_list[0], "ungrouped.com");

  args.clear();
  args.Append(GroupingKey::CreateFromEtldPlus1("ungrouped.com").Serialize());

  handler()->HandleClearSiteGroupDataAndCookies(args);

  storage_and_cookie_list = GetOnStorageFetchedSentList();
  EXPECT_EQ(0U, storage_and_cookie_list.size());
}

#if BUILDFLAG(IS_WIN)
TEST_F(SiteSettingsHandlerTest, ClearSiteSpecificMediaLicenses) {
  SetupModel();
  PrefService* user_prefs = profile()->GetPrefs();

  // In the beginning, there should be nothing stored in the origin data.
  ASSERT_EQ(0u, user_prefs->GetDict(prefs::kMediaCdmOriginData).size());

  auto entry_google = base::DictValue().Set(
      "https://www.google.com/",
      base::UnguessableTokenToValue(base::UnguessableToken::Create()));

  base::DictValue entry_example;
  entry_example.Set(
      "https://www.example.com/",
      base::UnguessableTokenToValue(base::UnguessableToken::Create()));

  {
    ScopedDictPrefUpdate update(user_prefs, prefs::kMediaCdmOriginData);

    base::DictValue& dict = update.Get();
    dict.Set("https://www.google.com/", std::move(entry_google));
    dict.Set("https://www.example.com/", std::move(entry_example));
  }
  // The code above adds origin data for both google and example.com
  EXPECT_EQ(2u, user_prefs->GetDict(prefs::kMediaCdmOriginData).size());

  base::ListValue args;
  args = base::ListValue();
  args.Append("https://www.google.com/");
  handler()->HandleClearUnpartitionedUsage(args);

  // The code clears the origin data for just google.com, so there should still
  // be the origin data for example.com left.
  EXPECT_EQ(1u, user_prefs->GetDict(prefs::kMediaCdmOriginData).size());
  EXPECT_TRUE(user_prefs->GetDict(prefs::kMediaCdmOriginData)
                  .contains("https://www.example.com/"));
}
#endif  // BUILDFLAG(IS_WIN)

TEST_F(SiteSettingsHandlerTest, ClearClientHints) {
  // Confirm that when the user clears unpartitioned storage, or the eTLD+1
  // group, client hints are also cleared.
  SetupModel();
  handler()->OnStorageFetched();

  auto hosts = std::to_array<GURL>({
      GURL("https://example.com/"),
      GURL("https://www.example.com"),
      GURL("https://google.com/"),
      GURL("https://www.google.com/"),
  });

  HostContentSettingsMap* host_content_settings_map =
      HostContentSettingsMapFactory::GetForProfile(profile());

  // Add setting for the two hosts host[0], host[1].
  base::Value client_hint_platform_version(14);
  base::Value client_hint_bitness(16);

  base::ListValue client_hints_list;
  client_hints_list.Append(std::move(client_hint_platform_version));
  client_hints_list.Append(std::move(client_hint_bitness));

  base::DictValue client_hints_dictionary;
  client_hints_dictionary.Set(client_hints::kClientHintsSettingKey,
                              std::move(client_hints_list));

  // Add setting for the hosts.
  for (const auto& host : hosts) {
    host_content_settings_map->SetWebsiteSettingDefaultScope(
        host, GURL(), ContentSettingsType::CLIENT_HINTS,
        base::Value(client_hints_dictionary.Clone()));
  }

  // Clear at the eTLD+1 level and ensure affected origins are cleared.
  base::ListValue args;
  args.Append(GroupingKey::CreateFromEtldPlus1("example.com").Serialize());
  handler()->HandleClearSiteGroupDataAndCookies(args);
  ContentSettingsForOneType client_hints_settings =
      host_content_settings_map->GetSettingsForOneType(
          ContentSettingsType::CLIENT_HINTS);
  EXPECT_EQ(2U, client_hints_settings.size());

  EXPECT_EQ(ContentSettingsPattern::FromURLNoWildcard(hosts[3]),
            client_hints_settings.at(0).primary_pattern);
  EXPECT_EQ(ContentSettingsPattern::Wildcard(),
            client_hints_settings.at(0).secondary_pattern);
  EXPECT_EQ(client_hints_dictionary, client_hints_settings.at(0).setting_value);

  EXPECT_EQ(ContentSettingsPattern::FromURLNoWildcard(hosts[2]),
            client_hints_settings.at(1).primary_pattern);
  EXPECT_EQ(ContentSettingsPattern::Wildcard(),
            client_hints_settings.at(1).secondary_pattern);
  EXPECT_EQ(client_hints_dictionary, client_hints_settings.at(1).setting_value);

  // Clear unpartitioned usage data, which should only affect the specific
  // origin.
  args.clear();
  args.Append("https://google.com/");
  handler()->HandleClearUnpartitionedUsage(args);

  // Validate the client hint has been cleared.
  client_hints_settings = host_content_settings_map->GetSettingsForOneType(
      ContentSettingsType::CLIENT_HINTS);
  EXPECT_EQ(1U, client_hints_settings.size());

  // www.google.com should be the only remaining entry.
  EXPECT_EQ(ContentSettingsPattern::FromURLNoWildcard(hosts[3]),
            client_hints_settings.at(0).primary_pattern);
  EXPECT_EQ(ContentSettingsPattern::Wildcard(),
            client_hints_settings.at(0).secondary_pattern);
  EXPECT_EQ(client_hints_dictionary, client_hints_settings.at(0).setting_value);

  // Clear unpartitioned usage data through HTTPS scheme, make sure https site
  // client hints have been cleared when the specific origin HTTPS scheme
  // exist.
  args.clear();
  args.Append("http://www.google.com/");
  handler()->HandleClearUnpartitionedUsage(args);

  // Validate the client hint has been cleared.
  client_hints_settings = host_content_settings_map->GetSettingsForOneType(
      ContentSettingsType::CLIENT_HINTS);
  EXPECT_EQ(0U, client_hints_settings.size());
}

TEST_F(SiteSettingsHandlerTest, ClearReducedAcceptLanguage) {
  // Confirm that when the user clears unpartitioned storage, or the eTLD+1
  // group, reduce accept language are also cleared.
  SetupModel();
  handler()->OnStorageFetched();

  auto hosts = std::to_array<GURL>({
      GURL("https://example.com/"),
      GURL("https://www.example.com"),
      GURL("https://google.com/"),
      GURL("https://www.google.com/"),
  });

  HostContentSettingsMap* host_content_settings_map =
      HostContentSettingsMapFactory::GetForProfile(profile());

  std::string language = "en-us";
  base::DictValue accept_language_dictionary;
  accept_language_dictionary.Set("reduce-accept-language", language);

  // Add setting for the hosts.
  for (const auto& host : hosts) {
    host_content_settings_map->SetWebsiteSettingDefaultScope(
        host, GURL(), ContentSettingsType::REDUCED_ACCEPT_LANGUAGE,
        base::Value(accept_language_dictionary.Clone()));
  }

  // Clear at the eTLD+1 level and ensure affected origins are cleared.
  base::ListValue args;
  args.Append(GroupingKey::CreateFromEtldPlus1("example.com").Serialize());
  handler()->HandleClearSiteGroupDataAndCookies(args);
  ContentSettingsForOneType accept_language_settings =
      host_content_settings_map->GetSettingsForOneType(
          ContentSettingsType::REDUCED_ACCEPT_LANGUAGE);
  EXPECT_EQ(2U, accept_language_settings.size());

  EXPECT_EQ(ContentSettingsPattern::FromURLNoWildcard(hosts[3]),
            accept_language_settings.at(0).primary_pattern);
  EXPECT_EQ(ContentSettingsPattern::Wildcard(),
            accept_language_settings.at(0).secondary_pattern);
  EXPECT_EQ(accept_language_dictionary,
            accept_language_settings.at(0).setting_value);

  EXPECT_EQ(ContentSettingsPattern::FromURLNoWildcard(hosts[2]),
            accept_language_settings.at(1).primary_pattern);
  EXPECT_EQ(ContentSettingsPattern::Wildcard(),
            accept_language_settings.at(1).secondary_pattern);
  EXPECT_EQ(accept_language_dictionary,
            accept_language_settings.at(1).setting_value);

  // Clear unpartitioned usage data, which should only affect the specific
  // origin.
  args.clear();
  args.Append("https://google.com/");
  handler()->HandleClearUnpartitionedUsage(args);

  // Validate the reduce accept language has been cleared.
  accept_language_settings = host_content_settings_map->GetSettingsForOneType(
      ContentSettingsType::REDUCED_ACCEPT_LANGUAGE);
  EXPECT_EQ(1U, accept_language_settings.size());

  // www.google.com should be the only remaining entry.
  EXPECT_EQ(ContentSettingsPattern::FromURLNoWildcard(hosts[3]),
            accept_language_settings.at(0).primary_pattern);
  EXPECT_EQ(ContentSettingsPattern::Wildcard(),
            accept_language_settings.at(0).secondary_pattern);
  EXPECT_EQ(accept_language_dictionary,
            accept_language_settings.at(0).setting_value);

  // Clear unpartitioned usage data through HTTPS scheme, make sure https site
  // reduced accept language have been cleared when the specific origin HTTPS
  // scheme exist.
  args.clear();
  args.Append("http://www.google.com/");
  handler()->HandleClearUnpartitionedUsage(args);

  // Validate the reduced accept language has been cleared.
  accept_language_settings = host_content_settings_map->GetSettingsForOneType(
      ContentSettingsType::REDUCED_ACCEPT_LANGUAGE);
  EXPECT_EQ(0U, accept_language_settings.size());
}

TEST_F(SiteSettingsHandlerTest, ClearPersistentStorage) {
  // Confirm that when the user clears persistent storage or the eTLD+1
  // group, persistent storage are also cleared.
  SetupModel();
  handler()->OnStorageFetched();

  auto hosts = std::to_array<GURL>({
      GURL("https://example.com/"),
      GURL("https://www.example.com"),
      GURL("https://google.com/"),
      GURL("https://www.google.com/"),
  });

  HostContentSettingsMap* host_content_settings_map =
      HostContentSettingsMapFactory::GetForProfile(profile());

  // Add setting for the hosts.
  for (const auto& host : hosts) {
    host_content_settings_map->SetContentSettingDefaultScope(
        host, GURL(), ContentSettingsType::PERSISTENT_STORAGE,
        ContentSetting::CONTENT_SETTING_ALLOW);
  }

  // Clear at the eTLD+1 level and ensure affected origins are cleared.
  base::ListValue args;
  args.Append(GroupingKey::CreateFromEtldPlus1("example.com").Serialize());
  handler()->HandleClearSiteGroupDataAndCookies(args);
  ContentSettingsForOneType settings =
      host_content_settings_map->GetSettingsForOneType(
          ContentSettingsType::PERSISTENT_STORAGE);

  // ContentSettingsType::PERSISTENT_STORAGE has a default settings type for the
  // wildcard '*' set to BLOCK. Here, we expect 2 but we put 3.
  EXPECT_EQ(3U, settings.size());

  EXPECT_EQ(ContentSettingsPattern::FromURLNoWildcard(hosts[3]),
            settings.at(0).primary_pattern);
  EXPECT_EQ(ContentSettingsPattern::Wildcard(),
            settings.at(0).secondary_pattern);
  EXPECT_EQ(ContentSetting::CONTENT_SETTING_ALLOW,
            settings.at(0).setting_value);

  EXPECT_EQ(ContentSettingsPattern::FromURLNoWildcard(hosts[2]),
            settings.at(1).primary_pattern);
  EXPECT_EQ(ContentSettingsPattern::Wildcard(),
            settings.at(1).secondary_pattern);
  EXPECT_EQ(ContentSetting::CONTENT_SETTING_ALLOW,
            settings.at(1).setting_value);

  // Clear unpartitioned usage data, which should only affect the specific
  // origin.
  args.clear();
  args.Append("https://google.com/");
  handler()->HandleClearUnpartitionedUsage(args);

  // Validate the reduce accept language has been cleared.
  settings = host_content_settings_map->GetSettingsForOneType(
      ContentSettingsType::PERSISTENT_STORAGE);

  // ContentSettingsType::PERSISTENT_STORAGE has a default settings type for the
  // wildcard '*' set to BLOCK. Here, we expect 1 but we put 2.
  EXPECT_EQ(2U, settings.size());

  // www.google.com should be the only remaining entry.
  EXPECT_EQ(ContentSettingsPattern::FromURLNoWildcard(hosts[3]),
            settings.at(0).primary_pattern);
  EXPECT_EQ(ContentSettingsPattern::Wildcard(),
            settings.at(0).secondary_pattern);
  EXPECT_EQ(ContentSetting::CONTENT_SETTING_ALLOW,
            settings.at(0).setting_value);

  // Clear unpartitioned usage data through HTTPS scheme, make sure https site
  // persistent storage have been cleared when the specific origin HTTPS
  // scheme exist.
  args.clear();
  args.Append("http://www.google.com/");
  handler()->HandleClearUnpartitionedUsage(args);

  // Validate the persistent storage has been cleared.
  settings = host_content_settings_map->GetSettingsForOneType(
      ContentSettingsType::PERSISTENT_STORAGE);

  // ContentSettingsType::PERSISTENT_STORAGE has a default settings type for the
  // wildcard '*' set to BLOCK. Therefore, when there's only one rule, it means
  // that there are none.
  EXPECT_EQ(1U, settings.size());
}

TEST_F(SiteSettingsHandlerTest, HandleClearPartitionedUsage) {
  // Confirm that removing unpartitioned storage correctly removes the
  // appropriate nodes.
  SetupModel();
  std::vector<browsing_data_model_test_util::BrowsingDataEntry>
      expected_browsing_data_model_entries = {
          kGoogleUnpartitionedEntry,
          kExampleUnpartitionedEntry,
          kGoogleOnExampleEntry,
          kExampleOnGoogleSecureEntry,
          kExampleOnGoogleInsecureEntry,
          kExampleOnGoogleAuEntry,
          kExampleLocalStorage,
          kHttpExampleCookie,
          kHttpsWwwExampleCookie,
          kPartitionedHttpsWwwExampleOnGoogleAuCookie,
          kPartitionedHttpsWwwExampleOnGoogleCookie,
          kHttpAbcExampleCookie,
          kHttpGoogleCookieA,
          kHttpGoogleCookieB,
          kHttpGoogleAuCookie,
          kPartitionedHttpsGoogleAu1PCookie,
          kPartitionedHttpsWwwAnotherExampleOnGoogleAuCookie,
          kUngroupedHttpCookie,
      };
  browsing_data_model_test_util::ValidateBrowsingDataEntries(
      handler()->GetBrowsingDataModelForTesting(),
      expected_browsing_data_model_entries);

  base::ListValue args;
  args.Append("https://www.example.com/");
  args.Append(GroupingKey::CreateFromEtldPlus1("google.com").Serialize());
  handler()->HandleClearPartitionedUsage(args);
  RemoveModelEntries(expected_browsing_data_model_entries,
                     {
                         kExampleOnGoogleSecureEntry,
                         kExampleOnGoogleInsecureEntry,
                         kPartitionedHttpsWwwExampleOnGoogleCookie,
                     });

  browsing_data_model_test_util::ValidateBrowsingDataEntries(
      handler()->GetBrowsingDataModelForTesting(),
      expected_browsing_data_model_entries);
}

TEST_F(SiteSettingsHandlerTest,
       HandleClearPartitionedUsageDoesNotAffectOtherPartitions) {
  // www.example.com has unpartitioned storage (102 B) and partitioned storage
  // under two top-level sites: google.com (200 B) and google.com.au (400 B).
  // Clearing its google.com.au partition must leave the unpartitioned storage,
  // the google.com partition, and other origins' google.com.au-partitioned
  // storage intact.
  SetupModel();
  std::vector<browsing_data_model_test_util::BrowsingDataEntry>
      expected_browsing_data_model_entries = {
          kGoogleUnpartitionedEntry,
          kExampleUnpartitionedEntry,
          kGoogleOnExampleEntry,
          kExampleOnGoogleSecureEntry,
          kExampleOnGoogleInsecureEntry,
          kExampleOnGoogleAuEntry,
          kExampleLocalStorage,
          kHttpExampleCookie,
          kHttpsWwwExampleCookie,
          kPartitionedHttpsWwwExampleOnGoogleAuCookie,
          kPartitionedHttpsWwwExampleOnGoogleCookie,
          kHttpAbcExampleCookie,
          kHttpGoogleCookieA,
          kHttpGoogleCookieB,
          kHttpGoogleAuCookie,
          kPartitionedHttpsGoogleAu1PCookie,
          kPartitionedHttpsWwwAnotherExampleOnGoogleAuCookie,
          kUngroupedHttpCookie,
      };
  browsing_data_model_test_util::ValidateBrowsingDataEntries(
      handler()->GetBrowsingDataModelForTesting(),
      expected_browsing_data_model_entries);

  auto find_origin_info = [](const base::ListValue& site_groups,
                             const std::string& etld_plus1,
                             const std::string& origin,
                             bool is_partitioned) -> const base::DictValue* {
    const std::string grouping_key =
        GroupingKey::CreateFromEtldPlus1(etld_plus1).Serialize();
    for (const base::Value& site_group_value : site_groups) {
      const base::DictValue& site_group = site_group_value.GetDict();
      if (CHECK_DEREF(site_group.FindString("groupingKey")) != grouping_key) {
        continue;
      }
      for (const base::Value& origin_value :
           CHECK_DEREF(site_group.FindList("origins"))) {
        const base::DictValue& origin_info = origin_value.GetDict();
        if (CHECK_DEREF(origin_info.FindString("origin")) == origin &&
            origin_info.FindBool("isPartitioned").value_or(false) ==
                is_partitioned) {
          return &origin_info;
        }
      }
    }
    return nullptr;
  };

  constexpr int kUnpartitionedExampleUsage = 102;
  constexpr int kExampleOnGoogleUsage = 200;
  constexpr int kExampleOnGoogleAuUsage = 400;
  constexpr int kOtherOriginOnGoogleAuUsage = 0;

  base::ListValue storage_and_cookie_list = GetOnStorageFetchedSentList();
  ASSERT_EQ(4U, storage_and_cookie_list.size());

  const base::DictValue* unpartitioned_example_before = find_origin_info(
      storage_and_cookie_list, "example.com", "https://www.example.com/",
      /*is_partitioned=*/false);
  ASSERT_TRUE(unpartitioned_example_before);
  EXPECT_EQ(kUnpartitionedExampleUsage,
            unpartitioned_example_before->FindDouble("usage"));

  const base::DictValue* example_on_google_before = find_origin_info(
      storage_and_cookie_list, "google.com", "https://www.example.com/",
      /*is_partitioned=*/true);
  ASSERT_TRUE(example_on_google_before);
  EXPECT_EQ(kExampleOnGoogleUsage,
            example_on_google_before->FindDouble("usage"));

  const base::DictValue* example_on_google_au_before = find_origin_info(
      storage_and_cookie_list, "google.com.au", "https://www.example.com/",
      /*is_partitioned=*/true);
  ASSERT_TRUE(example_on_google_au_before);
  EXPECT_EQ(kExampleOnGoogleAuUsage,
            example_on_google_au_before->FindDouble("usage"));

  const base::DictValue* other_origin_on_google_au_before = find_origin_info(
      storage_and_cookie_list, "google.com.au",
      "https://www.another-example.com/", /*is_partitioned=*/true);
  ASSERT_TRUE(other_origin_on_google_au_before);
  EXPECT_EQ(kOtherOriginOnGoogleAuUsage,
            other_origin_on_google_au_before->FindDouble("usage"));
  EXPECT_EQ(1, other_origin_on_google_au_before->FindDouble("numCookies"));

  // Clear www.example.com's storage partitioned under google.com.au only.
  base::ListValue args;
  args.Append("https://www.example.com/");
  args.Append(GroupingKey::CreateFromEtldPlus1("google.com.au").Serialize());
  handler()->HandleClearPartitionedUsage(args);

  // Only www.example.com's google.com.au partition is removed. Its google.com
  // partition, google.com.au's first-party storage, and
  // www.another-example.com's google.com.au partition all remain untouched.
  RemoveModelEntries(expected_browsing_data_model_entries,
                     {
                         kExampleOnGoogleAuEntry,
                         kPartitionedHttpsWwwExampleOnGoogleAuCookie,
                     });

  browsing_data_model_test_util::ValidateBrowsingDataEntries(
      handler()->GetBrowsingDataModelForTesting(),
      expected_browsing_data_model_entries);

  storage_and_cookie_list = GetOnStorageFetchedSentList();
  ASSERT_EQ(4U, storage_and_cookie_list.size());

  const base::DictValue* unpartitioned_example_after = find_origin_info(
      storage_and_cookie_list, "example.com", "https://www.example.com/",
      /*is_partitioned=*/false);
  ASSERT_TRUE(unpartitioned_example_after);
  EXPECT_EQ(kUnpartitionedExampleUsage,
            unpartitioned_example_after->FindDouble("usage"));

  const base::DictValue* example_on_google_after = find_origin_info(
      storage_and_cookie_list, "google.com", "https://www.example.com/",
      /*is_partitioned=*/true);
  ASSERT_TRUE(example_on_google_after);
  EXPECT_EQ(kExampleOnGoogleUsage,
            example_on_google_after->FindDouble("usage"));

  EXPECT_FALSE(find_origin_info(storage_and_cookie_list, "google.com.au",
                                "https://www.example.com/",
                                /*is_partitioned=*/true));

  const base::DictValue* other_origin_on_google_au_after = find_origin_info(
      storage_and_cookie_list, "google.com.au",
      "https://www.another-example.com/", /*is_partitioned=*/true);
  ASSERT_TRUE(other_origin_on_google_au_after);
  EXPECT_EQ(kOtherOriginOnGoogleAuUsage,
            other_origin_on_google_au_after->FindDouble("usage"));
  EXPECT_EQ(1, other_origin_on_google_au_after->FindDouble("numCookies"));
}

TEST_F(SiteSettingsHandlerTest, HandleGetRwsMembershipLabel) {
  base::ListValue args;
  args.Append("getRwsMembershipLabel");
  args.Append(5);
  args.Append("google.com");
  handler()->HandleGetRwsMembershipLabel(args);
  const content::TestWebUI::CallData& data = *web_ui()->call_data().back();

  EXPECT_EQ("cr.webUIResponse", data.function_name());
  EXPECT_EQ("getRwsMembershipLabel", data.arg1()->GetString());
  ASSERT_TRUE(data.arg2()->GetBool());
  EXPECT_EQ("5 sites in google.com's group", data.arg3()->GetString());
}

TEST_F(SiteSettingsHandlerTest, HandleGetFormattedBytes) {
  const double size = 120000000000;
  base::ListValue get_args;
  get_args.Append(kCallbackId);
  get_args.Append(size);
  handler()->HandleGetFormattedBytes(get_args);

  // Validate that this method can handle large data.
  const content::TestWebUI::CallData& data = *web_ui()->call_data().back();
  EXPECT_EQ("cr.webUIResponse", data.function_name());
  EXPECT_EQ(kCallbackId, data.arg1()->GetString());
  ASSERT_TRUE(data.arg2()->GetBool());
  EXPECT_EQ(base::UTF16ToUTF8(
                ui::FormatBytes(base::ByteSize(static_cast<uint64_t>(size)))),
            data.arg3()->GetString());
}

TEST_F(SiteSettingsHandlerTest, HandleGetUsageInfo) {
  SetupDefaultRelatedWebsiteSets(mock_privacy_sandbox_service());

  EXPECT_CALL(*mock_privacy_sandbox_service(),
              IsPartOfManagedRelatedWebsiteSet(_))
      .Times(1)
      .WillOnce(Return(false));
  EXPECT_CALL(*mock_privacy_sandbox_service(),
              IsPartOfManagedRelatedWebsiteSet(
                  ConvertEtldToSchemefulSite("example.com")))
      .Times(2)
      .WillRepeatedly(Return(true));

  // Confirm that usage info for an origin returns its total storage summed
  // across all partitions (both unpartitioned and partitioned). This is
  // intentionally different from OnStorageFetched, which reports each partition
  // separately; HandleFetchUsageTotal shows a single combined total per origin.
  SetupModel();

  EXPECT_EQ(18,
            std::distance(handler()->GetBrowsingDataModelForTesting()->begin(),
                          handler()->GetBrowsingDataModelForTesting()->end()));

  base::ListValue args;
  args.Append("http://www.example.com");
  handler()->HandleFetchUsageTotal(args);
  handler()->ServicePendingRequests();
  // www.example.com usage is summed across all partitions: 100 (unpartitioned
  // quota) + 2 (local storage) + 100 (partitioned on google.com, secure) + 100
  // (partitioned on google.com, insecure) + 400 (partitioned on google.com.au)
  // = 702 B.
  ValidateUsageInfo("http://www.example.com", "702 B", "1 cookie",
                    "1 site in example.com's group", true);

  args.clear();
  args.Append("http://example.com");
  handler()->HandleFetchUsageTotal(args);
  handler()->ServicePendingRequests();
  ValidateUsageInfo("http://example.com", "", "1 cookie",
                    "1 site in example.com's group", true);

  args.clear();
  args.Append("http://google.com");
  handler()->HandleFetchUsageTotal(args);
  handler()->ServicePendingRequests();
  ValidateUsageInfo("http://google.com", "100 B", "2 cookies",
                    "2 sites in google.com's group", false);

  args.clear();
  args.Append("http://ungrouped.com");
  handler()->HandleFetchUsageTotal(args);
  handler()->ServicePendingRequests();
  ValidateUsageInfo("http://ungrouped.com", "", "1 cookie", "", false);

  // Test that the argument URL formatting is preserved in the response because
  // the UI ignores responses with different URL strings.
  args.clear();
  args.Append("http://ungrouped.com//");
  handler()->HandleFetchUsageTotal(args);
  handler()->ServicePendingRequests();
  ValidateUsageInfo("http://ungrouped.com//", "", "1 cookie", "", false);
}

TEST_F(SiteSettingsHandlerTest, RelatedWebsiteSetsMembership) {
  SetupDefaultRelatedWebsiteSets(mock_privacy_sandbox_service());

  EXPECT_CALL(*mock_privacy_sandbox_service(),
              IsPartOfManagedRelatedWebsiteSet(_))
      .Times(2)
      .WillRepeatedly(Return(false));
  EXPECT_CALL(*mock_privacy_sandbox_service(),
              IsPartOfManagedRelatedWebsiteSet(
                  ConvertEtldToSchemefulSite("example.com")))
      .Times(1)
      .WillOnce(Return(true));

  SetupModel();

  handler()->ClearAllSitesMapForTesting();

  handler()->OnStorageFetched();
  const content::TestWebUI::CallData& data = *web_ui()->call_data().back();
  EXPECT_EQ("cr.webUIListenerCallback", data.function_name());

  ASSERT_TRUE(data.arg1()->is_string());
  EXPECT_EQ("onStorageListFetched", data.arg1()->GetString());

  ASSERT_TRUE(data.arg2()->is_list());
  const base::ListValue& storage_and_cookie_list = data.arg2()->GetList();
  EXPECT_EQ(4U, storage_and_cookie_list.size());

  auto related_website_sets = GetTestRelatedWebsiteSets();

  ValidateSitesWithRws(storage_and_cookie_list, related_website_sets);
}

TEST_F(SiteSettingsHandlerTest, IsolatedWebAppUsageInfo) {
  std::string iwa_url =
      "isolated-app://"
      "aerugqztij5biqquuk3mfwpsaibuegaqcitgfchwuosuofdjabzqaaic/";
  SetupModelWithIsolatedWebAppData({{iwa_url, 1000}});

  base::ListValue args;
  args.Append(iwa_url);
  handler()->HandleFetchUsageTotal(args);
  handler()->ServicePendingRequests();

  ValidateUsageInfo(
      /*expected_usage_origin=*/iwa_url, /*expected_usage_string=*/"1,000 B",
      /*expected_cookie_string=*/"",
      /*expected_rws_member_count_string=*/"", /*expected_rws_policy=*/false);
}

TEST_F(SiteSettingsHandlerTest, IsolatedWebAppClearSiteGroupDataAndCookies) {
  GURL iwa_url1(
      "isolated-app://"
      "abcdefztij5biqquuk3mfwpsaibuegaqcitgfchwuosuofdjabzqaaic/");
  GURL iwa_url2(
      "isolated-app://"
      "aerugqztij5biqquuk3mfwpsaibuegaqcitgfchwuosuofdjabzqaaic/");
  SetupModelWithIsolatedWebAppData(
      {{iwa_url1.spec(), 1000}, {iwa_url2.spec(), 2000}});

  auto verify_site_group = [](const base::Value& site_group,
                              const GURL& expected_origin,
                              int64_t expected_usage) {
    ASSERT_TRUE(site_group.is_dict());
    EXPECT_THAT(CHECK_DEREF(site_group.GetDict().FindString("groupingKey")),
                IsOrigin(expected_origin));
    ASSERT_EQ(1U, site_group.GetDict().FindList("origins")->size());
    const base::DictValue& origin_info =
        site_group.GetDict().FindList("origins")->front().GetDict();
    EXPECT_EQ(expected_usage, origin_info.FindDouble("usage").value());
  };

  base::ListValue all_sites_list = GetOnStorageFetchedSentList();
  EXPECT_EQ(2U, all_sites_list.size());
  verify_site_group(all_sites_list[0], iwa_url1, 1000);
  verify_site_group(all_sites_list[1], iwa_url2, 2000);

  base::ListValue args;
  args.Append(GroupingKey::Create(url::Origin::Create(iwa_url1)).Serialize());
  handler()->HandleClearSiteGroupDataAndCookies(args);

  all_sites_list = GetOnStorageFetchedSentList();
  EXPECT_EQ(1U, all_sites_list.size());
  verify_site_group(all_sites_list[0], iwa_url2, 2000);
}

TEST_F(SiteSettingsHandlerTest, IsolatedWebAppClearUnpartitionedUsage) {
  GURL iwa_url(
      "isolated-app://"
      "abcdefztij5biqquuk3mfwpsaibuegaqcitgfchwuosuofdjabzqaaic/");
  SetupModelWithIsolatedWebAppData({{iwa_url.spec(), 1000}});

  base::ListValue usage_args;
  usage_args.Append(iwa_url.spec());
  handler()->HandleFetchUsageTotal(usage_args);
  handler()->ServicePendingRequests();

  ValidateUsageInfo(
      /*expected_usage_origin=*/iwa_url.spec(),
      /*expected_usage_string=*/"1,000 B",
      /*expected_cookie_string=*/"",
      /*expected_rws_member_count_string=*/"", /*expected_rws_policy=*/false);

  base::ListValue clear_args;
  clear_args.Append(iwa_url.spec());
  handler()->HandleClearUnpartitionedUsage(clear_args);

  handler()->HandleFetchUsageTotal(usage_args);
  handler()->ServicePendingRequests();

  ValidateUsageInfo(
      /*expected_usage_origin=*/iwa_url.spec(),
      /*expected_usage_string=*/"",
      /*expected_cookie_string=*/"",
      /*expected_rws_member_count_string=*/"", /*expected_rws_policy=*/false);
}

TEST_F(SiteSettingsHandlerTest, SiteExceptionScopeTypeMetrics) {
  constexpr char kScopeTypeHistogram[] =
      "Privacy.SiteExceptionsAdded.ScopeType";
  constexpr char kContentSettingTypeHistogram[] =
      "Privacy.SiteExceptionsAdded.ContentSettingType";
  base::HistogramTester tester;
  tester.ExpectTotalCount(kScopeTypeHistogram, 0);

  {
    base::ListValue set_args;
    set_args.Append("https://www.blocked.com:443");  // Primary pattern.
    set_args.Append(std::string());                  // Secondary pattern.
    set_args.Append(kNotifications);
    set_args.Append(
        content_settings::ContentSettingToString(CONTENT_SETTING_BLOCK));
    set_args.Append(false);  // Incognito.

    handler()->HandleSetCategoryPermissionForPattern(set_args);
    ASSERT_EQ(2U, web_ui()->call_data().size());

    tester.ExpectBucketCount(kScopeTypeHistogram,
                             ContentSettingsPattern::Scope::kOriginScoped,
                             1 /* expected_count */);
    tester.ExpectBucketCount(
        kContentSettingTypeHistogram,
        content_settings_uma_util::ContentSettingTypeToHistogramValue(
            ContentSettingsType::NOTIFICATIONS),
        1 /* expected_count */);
  }

  {
    base::ListValue set_args;
    set_args.Append("https://[*.]blocked.com:443");  // Primary pattern.
    set_args.Append(std::string());                  // Secondary pattern.
    set_args.Append(kCookies);
    set_args.Append(
        content_settings::ContentSettingToString(CONTENT_SETTING_BLOCK));
    set_args.Append(false);  // Incognito.

    handler()->HandleSetCategoryPermissionForPattern(set_args);
    ASSERT_EQ(3U, web_ui()->call_data().size());

    tester.ExpectBucketCount(kScopeTypeHistogram,
                             ContentSettingsPattern::Scope::kWithDomainWildcard,
                             1 /* expected_count */);
    tester.ExpectBucketCount(
        kContentSettingTypeHistogram,
        content_settings_uma_util::ContentSettingTypeToHistogramValue(
            ContentSettingsType::COOKIES),
        1 /* expected_count */);
  }
}

#if BUILDFLAG(IS_CHROMEOS)
class SiteSettingsGlobalPermissionTest
    : public SiteSettingsHandlerBaseTest,
      public testing::WithParamInterface<std::tuple<bool, bool, bool>> {
 protected:
  bool CamBlocked() const { return std::get<0>(GetParam()); }
  bool MicBlocked() const { return std::get<1>(GetParam()); }
  bool GeoBlocked() const { return std::get<2>(GetParam()); }
};

TEST_P(SiteSettingsGlobalPermissionTest, GetSystemDeniedPermissions) {
  system_permission_settings::ScopedSettingsForTesting cam_settings(
      ContentSettingsType::MEDIASTREAM_CAMERA, CamBlocked());
  system_permission_settings::ScopedSettingsForTesting mic_settings(
      ContentSettingsType::MEDIASTREAM_MIC, MicBlocked());
  system_permission_settings::ScopedSettingsForTesting geo_settings(
      ContentSettingsType::GEOLOCATION, GeoBlocked());

  base::ListValue args;
  args.Append(kCallbackId);
  handler()->HandleGetSystemDeniedPermissions(args);
  EXPECT_LT(0u, CHECK_DEREF(web_ui()).call_data().size());
  const auto& call_data = *(CHECK_DEREF(web_ui()).call_data().back());
  EXPECT_EQ(3u, call_data.args().size());
  EXPECT_EQ(base::Value(kCallbackId), CHECK_DEREF(call_data.arg1()));
  EXPECT_EQ(base::Value(true), CHECK_DEREF(call_data.arg2()));

  base::ListValue expected_result;
  if (CamBlocked()) {
    expected_result.Append("media-stream-camera");
  }
  if (MicBlocked()) {
    expected_result.Append("media-stream-mic");
  }
  if (GeoBlocked()) {
    expected_result.Append("location");
  }

  EXPECT_EQ(expected_result, CHECK_DEREF(call_data.arg3()));
}

INSTANTIATE_TEST_SUITE_P(All,
                         SiteSettingsGlobalPermissionTest,
                         testing::Combine(testing::Bool(),
                                          testing::Bool(),
                                          testing::Bool()));

class SiteSettingsOpenSystemSettingsTest
    : public SiteSettingsHandlerBaseTest,
      public testing::WithParamInterface<ContentSettingsType> {
 public:
  SiteSettingsOpenSystemSettingsTest() {
    system_permission_settings::SetInstanceForTesting(&mock_platform_handle);
  }
  ~SiteSettingsOpenSystemSettingsTest() {
    system_permission_settings::SetInstanceForTesting(nullptr);
  }

  ContentSettingsType PermissionType() const { return GetParam(); }

  NiceMock<system_permission_settings::MockPlatformHandle> mock_platform_handle;
};

TEST_P(SiteSettingsOpenSystemSettingsTest, OpenSystemSettings) {
  base::Value permission_type(
      site_settings::ContentSettingsTypeToGroupName(PermissionType()));
  auto args = base::ListValue().Append(std::move(permission_type));
  EXPECT_CALL(mock_platform_handle, OpenSystemSettings(_, PermissionType()));
  handler()->HandleOpenSystemPermissionSettings(args);
}

INSTANTIATE_TEST_SUITE_P(
    All,
    SiteSettingsOpenSystemSettingsTest,
    testing::Values(ContentSettingsType::MEDIASTREAM_CAMERA,
                    ContentSettingsType::MEDIASTREAM_MIC,
                    ContentSettingsType::GEOLOCATION));
#endif

// Test suite for verifying that permissions granted through Site Settings
// surfaces are correctly marked as eligible for Safety Hub auto-revocation
// when the kSafetyHubUnusedPermissionRevocationForAllSurfaces flag is enabled.
//
// Only permissions of certain `ContentSettingType` are eligible. They are
// marked as such upon grant by initializing the `last_visited` timestamp from
// a default null value to the (coarsened) current time. Once initialized, the
// timestamp is updated on each navigation to the origin for which the
// permission was granted. Then permissions with a `last_visited` timestamp
// older than a certain threshold are eventually auto-revoked by Safety Hub.
class SiteSettingsHandlerUnusedPermissionRevocationForAllSurfacesTest
    : public SiteSettingsHandlerBaseTest {
 public:
  SiteSettingsHandlerUnusedPermissionRevocationForAllSurfacesTest() {
    feature_list_.InitAndEnableFeature(
        permissions::features::
            kSafetyHubUnusedPermissionRevocationForAllSurfaces);
  }

 protected:
  base::test::ScopedFeatureList feature_list_;
};

TEST_F(SiteSettingsHandlerUnusedPermissionRevocationForAllSurfacesTest,
       SetOriginPermissions_LastVisitedTracked) {
  const GURL primary_url("https://example.com");
  const GURL secondary_url;
  base::Time now = base::Time::Now();

  // Allow GEOLOCATION for an origin from Site Settings UI.
  base::ListValue reset_args;
  reset_args.Append(primary_url.spec());
  reset_args.Append(std::move(kGeolocation));
  reset_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_ALLOW));
  handler()->HandleSetOriginPermissions(reset_args);

  // Verify that 'last_visited` was recorded and lies within the past 7 days.
  //
  // The `last_visited` is coarsed by `GetCoarseVisitedTime` [1] due to privacy.
  // It rounds given timestamp down to the nearest multiple of 7 in the past.
  // [1] components/content_settings/core/browser/content_settings_utils.cc
  content_settings::SettingInfo info;
  HostContentSettingsMap* map =
      HostContentSettingsMapFactory::GetForProfile(profile());
  map->GetWebsiteSetting(primary_url, secondary_url,
                         ContentSettingsType::GEOLOCATION, &info);
  EXPECT_GE(info.metadata.last_visited(), now - base::Days(7));
  EXPECT_LE(info.metadata.last_visited(), now);
}

TEST_F(SiteSettingsHandlerUnusedPermissionRevocationForAllSurfacesTest,
       SetOriginPermissions_LastVisitedNotTracked_WrongValue) {
  const GURL primary_url("https://example.com");
  const GURL secondary_url;

  // Block GEOLOCATION for an origin from Site Settings UI.
  base::ListValue reset_args;
  reset_args.Append(primary_url.spec());
  reset_args.Append(std::move(kGeolocation));
  reset_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_BLOCK));
  handler()->HandleSetOriginPermissions(reset_args);

  // Verify that 'last_visited` is not recorded unless the value is ALLOW.
  content_settings::SettingInfo info;
  HostContentSettingsMap* map =
      HostContentSettingsMapFactory::GetForProfile(profile());
  map->GetWebsiteSetting(primary_url, secondary_url,
                         ContentSettingsType::GEOLOCATION, &info);
  EXPECT_EQ(base::Time(), info.metadata.last_visited());
}

TEST_F(SiteSettingsHandlerUnusedPermissionRevocationForAllSurfacesTest,
       SetOriginPermissions_LastVisitedNotTracked_WrongType) {
  const GURL primary_url("https://example.com");
  const GURL secondary_url;

  // Allow NOTIFICATIONS an origin from Site Settings UI.
  base::ListValue reset_args;
  reset_args.Append(primary_url.spec());
  reset_args.Append(std::move(kNotifications));
  reset_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_ALLOW));
  handler()->HandleSetOriginPermissions(reset_args);

  // Verify that 'last_visited` is not recorded for ineligible types
  // (e.g. NOTIFICATIONS).
  content_settings::SettingInfo info;
  HostContentSettingsMap* map =
      HostContentSettingsMapFactory::GetForProfile(profile());
  map->GetWebsiteSetting(primary_url, secondary_url,
                         ContentSettingsType::NOTIFICATIONS, &info);
  EXPECT_EQ(base::Time(), info.metadata.last_visited());
}

TEST_F(SiteSettingsHandlerUnusedPermissionRevocationForAllSurfacesTest,
       SetOriginPermissions_LastVisitedNotTracked_FeatureOff) {
  feature_list_.Reset();
  feature_list_.InitAndDisableFeature(
      permissions::features::
          kSafetyHubUnusedPermissionRevocationForAllSurfaces);

  const GURL primary_url("https://example.com");
  const GURL secondary_url;

  // Allow GEOLOCATION an origin from Site Settings UI.
  base::ListValue reset_args;
  reset_args.Append(primary_url.spec());
  reset_args.Append(std::move(kGeolocation));
  reset_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_ALLOW));
  handler()->HandleSetOriginPermissions(reset_args);

  // Verify that 'last_visited` is not recorded when the feature is off.
  content_settings::SettingInfo info;
  HostContentSettingsMap* map =
      HostContentSettingsMapFactory::GetForProfile(profile());
  map->GetWebsiteSetting(primary_url, secondary_url,
                         ContentSettingsType::GEOLOCATION, &info);
  EXPECT_EQ(base::Time(), info.metadata.last_visited());
}

TEST_F(SiteSettingsHandlerUnusedPermissionRevocationForAllSurfacesTest,
       SetCategoryPermissionForPattern_LastVisitedTracked) {
  constexpr char kOrigin[] = "https://www.google.com";
  const std::string primary_pattern(kOrigin);
  const std::string secondary_pattern;
  const GURL primary_url(kOrigin);
  const GURL secondary_url;
  base::Time now = base::Time::Now();

  // Allow GEOLOCATION for a pattern from Site Settings UI.
  base::ListValue set_args;
  set_args.Append(primary_pattern);
  set_args.Append(secondary_pattern);
  set_args.Append(kGeolocation);
  set_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_ALLOW));
  set_args.Append(false);  // Incognito.
  handler()->HandleSetCategoryPermissionForPattern(set_args);

  // Verify that 'last_visited` was recorded and lies within the past 7 days.
  //
  // The `last_visited` is coarsed by `GetCoarseVisitedTime` [1] due to privacy.
  // It rounds given timestamp down to the nearest multiple of 7 in the past.
  // [1] components/content_settings/core/browser/content_settings_utils.cc
  content_settings::SettingInfo info;
  HostContentSettingsMap* map =
      HostContentSettingsMapFactory::GetForProfile(profile());
  map->GetWebsiteSetting(primary_url, secondary_url,
                         ContentSettingsType::GEOLOCATION, &info);
  EXPECT_GE(info.metadata.last_visited(), now - base::Days(7));
  EXPECT_LE(info.metadata.last_visited(), now);
}

TEST_F(SiteSettingsHandlerUnusedPermissionRevocationForAllSurfacesTest,
       SetCategoryPermissionForPattern_LastVisitedTracked_WrongValue) {
  constexpr char kOrigin[] = "https://www.google.com";
  const std::string primary_pattern(kOrigin);
  const std::string secondary_pattern;
  const GURL primary_url(kOrigin);
  const GURL secondary_url;

  // Block GEOLOCATION for a pattern from Site Settings UI.
  base::ListValue set_args;
  set_args.Append(primary_pattern);
  set_args.Append(secondary_pattern);
  set_args.Append(kGeolocation);
  set_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_BLOCK));
  set_args.Append(false);  // Incognito.
  handler()->HandleSetCategoryPermissionForPattern(set_args);

  // Verify that 'last_visited` is not recorded unless the value is ALLOW.
  content_settings::SettingInfo info;
  HostContentSettingsMap* map =
      HostContentSettingsMapFactory::GetForProfile(profile());
  map->GetWebsiteSetting(primary_url, secondary_url,
                         ContentSettingsType::GEOLOCATION, &info);
  EXPECT_EQ(base::Time(), info.metadata.last_visited());
}

TEST_F(SiteSettingsHandlerUnusedPermissionRevocationForAllSurfacesTest,
       SetCategoryPermissionForPattern_LastVisitedTracked_WrongType) {
  constexpr char kOrigin[] = "https://www.google.com";
  const std::string primary_pattern(kOrigin);
  const std::string secondary_pattern;
  const GURL primary_url(kOrigin);
  const GURL secondary_url;

  // Allow NOTIFICATIONS for a pattern from Site Settings UI.
  base::ListValue set_args;
  set_args.Append(primary_pattern);
  set_args.Append(secondary_pattern);
  set_args.Append(kNotifications);
  set_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_ALLOW));
  set_args.Append(false);  // Incognito.
  handler()->HandleSetCategoryPermissionForPattern(set_args);

  // Verify that 'last_visited` is not recorded for ineligible types
  // (e.g. NOTIFICATIONS).
  content_settings::SettingInfo info;
  HostContentSettingsMap* map =
      HostContentSettingsMapFactory::GetForProfile(profile());
  map->GetWebsiteSetting(primary_url, secondary_url,
                         ContentSettingsType::NOTIFICATIONS, &info);
  EXPECT_EQ(base::Time(), info.metadata.last_visited());
}

TEST_F(SiteSettingsHandlerUnusedPermissionRevocationForAllSurfacesTest,
       SetCategoryPermissionForPattern_LastVisitedTracked_FeatureOff) {
  feature_list_.Reset();
  feature_list_.InitAndDisableFeature(
      permissions::features::
          kSafetyHubUnusedPermissionRevocationForAllSurfaces);

  constexpr char kOrigin[] = "https://www.google.com";
  const std::string primary_pattern(kOrigin);
  const std::string secondary_pattern;
  const GURL primary_url(kOrigin);
  const GURL secondary_url;

  // Allow GEOLOCATION for a pattern from Site Settings UI.
  base::ListValue set_args;
  set_args.Append(primary_pattern);
  set_args.Append(secondary_pattern);
  set_args.Append(kGeolocation);
  set_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_ALLOW));
  set_args.Append(false);  // Incognito.
  handler()->HandleSetCategoryPermissionForPattern(set_args);

  // Verify that 'last_visited` is not recorded when the feature is off.
  content_settings::SettingInfo info;
  HostContentSettingsMap* map =
      HostContentSettingsMapFactory::GetForProfile(profile());
  map->GetWebsiteSetting(primary_url, secondary_url,
                         ContentSettingsType::GEOLOCATION, &info);
  EXPECT_EQ(base::Time(), info.metadata.last_visited());
}

using SiteSettingsHandlerNotificationRevocationHistogramTest =
    SiteSettingsHandlerTest;

TEST_F(SiteSettingsHandlerNotificationRevocationHistogramTest,
       HandleSetOriginPermissionsAllowToBlock) {
  base::HistogramTester histograms;
  const GURL primary_url("https://example.com");
  base::ListValue allow_args;
  allow_args.Append(primary_url.spec());
  allow_args.Append(std::move(kNotifications));
  allow_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_ALLOW));
  handler()->HandleSetOriginPermissions(allow_args);

  // Update NOTIFICATIONS to BLOCK.
  base::ListValue block_args;
  block_args.Append(primary_url.spec());
  block_args.Append(std::move(kNotifications));
  block_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_BLOCK));
  handler()->HandleSetOriginPermissions(block_args);

  // Verify histogram is logged as expected.
  histograms.ExpectUniqueSample("SafeBrowsing.NotificationRevocationSource",
                                safe_browsing::NotificationRevocationSource::
                                    kUserManuallyChangedSiteSetting,
                                1);
}

TEST_F(SiteSettingsHandlerNotificationRevocationHistogramTest,
       HandleSetOriginPermissionsAllowToAsk) {
  base::HistogramTester histograms;
  const GURL primary_url("https://example.com");
  base::ListValue allow_args;
  allow_args.Append(primary_url.spec());
  allow_args.Append(std::move(kNotifications));
  allow_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_ALLOW));
  handler()->HandleSetOriginPermissions(allow_args);

  // Update NOTIFICATIONS to ASK.
  base::ListValue block_args;
  block_args.Append(primary_url.spec());
  block_args.Append(std::move(kNotifications));
  block_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_ASK));
  handler()->HandleSetOriginPermissions(block_args);

  // Verify histogram is logged as expected.
  histograms.ExpectUniqueSample("SafeBrowsing.NotificationRevocationSource",
                                safe_browsing::NotificationRevocationSource::
                                    kUserManuallyChangedSiteSetting,
                                1);
}

TEST_F(SiteSettingsHandlerNotificationRevocationHistogramTest,
       HandleSetOriginPermissionsAllowToDefault) {
  base::HistogramTester histograms;
  const GURL primary_url("https://example.com");
  base::ListValue allow_args;
  allow_args.Append(primary_url.spec());
  allow_args.Append(std::move(kNotifications));
  allow_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_ALLOW));
  handler()->HandleSetOriginPermissions(allow_args);

  // Update NOTIFICATIONS to DEFAULT.
  base::ListValue block_args;
  block_args.Append(primary_url.spec());
  block_args.Append(std::move(kNotifications));
  block_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_DEFAULT));
  handler()->HandleSetOriginPermissions(block_args);

  // Verify histogram is logged as expected.
  histograms.ExpectUniqueSample("SafeBrowsing.NotificationRevocationSource",
                                safe_browsing::NotificationRevocationSource::
                                    kUserManuallyChangedSiteSetting,
                                1);
}

TEST_F(SiteSettingsHandlerNotificationRevocationHistogramTest,
       HandleResetCategoryPermissionForPattern) {
  base::HistogramTester histograms;

  constexpr char kOrigin[] = "https://www.test.com:443";
  base::ListValue set_args;
  set_args.Append(kOrigin);        // Primary pattern.
  set_args.Append(std::string());  // Secondary pattern.
  set_args.Append(kNotifications);
  set_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_ALLOW));
  set_args.Append(false);  // Incognito.
  handler()->HandleSetCategoryPermissionForPattern(set_args);

  base::ListValue reset_args;
  reset_args.Append(kOrigin);        // Primary pattern.
  reset_args.Append(std::string());  // Secondary pattern.
  reset_args.Append(kNotifications);
  reset_args.Append(false);  // Incognito
  handler()->HandleResetCategoryPermissionForPattern(reset_args);

  // Verify histogram is logged as expected.
  histograms.ExpectUniqueSample("SafeBrowsing.NotificationRevocationSource",
                                safe_browsing::NotificationRevocationSource::
                                    kUserManuallyChangedSiteSetting,
                                1);
}

TEST_F(SiteSettingsHandlerNotificationRevocationHistogramTest,
       HandleSetCategoryPermissionForPatternAllowToAsk) {
  base::HistogramTester histograms;

  constexpr char kOrigin[] = "https://www.test.com:443";
  base::ListValue set_args;
  set_args.Append(kOrigin);        // Primary pattern.
  set_args.Append(std::string());  // Secondary pattern.
  set_args.Append(kNotifications);
  set_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_ALLOW));
  set_args.Append(false);  // Incognito.
  handler()->HandleSetCategoryPermissionForPattern(set_args);

  base::ListValue reset_args;
  reset_args.Append(kOrigin);        // Primary pattern.
  reset_args.Append(std::string());  // Secondary pattern.
  reset_args.Append(kNotifications);
  reset_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_ASK));
  reset_args.Append(false);  // Incognito.
  handler()->HandleSetCategoryPermissionForPattern(reset_args);

  // Verify histogram is logged as expected.
  histograms.ExpectUniqueSample("SafeBrowsing.NotificationRevocationSource",
                                safe_browsing::NotificationRevocationSource::
                                    kUserManuallyChangedSiteSetting,
                                1);
}

TEST_F(SiteSettingsHandlerNotificationRevocationHistogramTest,
       HandleSetCategoryPermissionForPatternAllowToBlock) {
  base::HistogramTester histograms;

  constexpr char kOrigin[] = "https://www.test.com:443";
  base::ListValue set_args;
  set_args.Append(kOrigin);        // Primary pattern.
  set_args.Append(std::string());  // Secondary pattern.
  set_args.Append(kNotifications);
  set_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_ALLOW));
  set_args.Append(false);  // Incognito.
  handler()->HandleSetCategoryPermissionForPattern(set_args);

  base::ListValue reset_args;
  reset_args.Append(kOrigin);        // Primary pattern.
  reset_args.Append(std::string());  // Secondary pattern.
  reset_args.Append(kNotifications);
  reset_args.Append(
      content_settings::ContentSettingToString(CONTENT_SETTING_BLOCK));
  reset_args.Append(false);  // Incognito.
  handler()->HandleSetCategoryPermissionForPattern(reset_args);

  // Verify histogram is logged as expected.
  histograms.ExpectUniqueSample("SafeBrowsing.NotificationRevocationSource",
                                safe_browsing::NotificationRevocationSource::
                                    kUserManuallyChangedSiteSetting,
                                1);
}

}  // namespace settings
