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

#include "chrome/browser/ui/safety_hub/revoked_permissions_service.h"

#include <algorithm>

#include "base/json/values_util.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/simple_test_clock.h"
#include "base/time/default_clock.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
#include "chrome/browser/engagement/site_engagement_service_factory.h"
#include "chrome/browser/notifications/notification_display_service_tester.h"
#include "chrome/browser/notifications/platform_notification_service_factory.h"
#include "chrome/browser/notifications/platform_notification_service_impl.h"
#include "chrome/browser/permissions/notifications_engagement_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/safe_browsing/test_safe_browsing_service.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/safety_hub/disruptive_notification_permissions_manager.h"
#include "chrome/browser/ui/safety_hub/mock_safe_browsing_database_manager.h"
#include "chrome/browser/ui/safety_hub/revoked_permissions_service_factory.h"
#include "chrome/browser/ui/safety_hub/safety_hub_result.h"
#include "chrome/browser/ui/safety_hub/safety_hub_test_util.h"
#include "chrome/browser/ui/safety_hub/safety_hub_util.h"
#include "chrome/browser/ui/safety_hub/unused_site_permissions_manager.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/webui_url_constants.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/content_settings/core/browser/content_settings_registry.h"
#include "components/content_settings/core/browser/content_settings_uma_util.h"
#include "components/content_settings/core/browser/content_settings_utils.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_constraints.h"
#include "components/content_settings/core/common/features.h"
#include "components/permissions/constants.h"
#include "components/permissions/features.h"
#include "components/permissions/notifications_engagement_service.h"
#include "components/safe_browsing/core/common/features.h"
#include "components/ukm/test_ukm_recorder.h"
#include "content/public/test/browser_test.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/notifications/platform_notification_data.h"

namespace {

using testing::Eq;
using testing::Field;
using testing::Ge;
using testing::Key;
using testing::Not;
using testing::Optional;
using testing::Pointee;
using testing::UnorderedElementsAre;

const char histogram_name[] =
    "Settings.SafetyHub.UnusedSitePermissionsModule.AutoRevoked2";

}  // namespace

class RevokedPermissionsServiceBrowserTest : public InProcessBrowserTest {
 public:
  using DisruptiveNotificationRevocationEntry =
      DisruptiveNotificationPermissionsManager::RevocationEntry;
  using DisruptiveNotificationContentSettingHelper =
      DisruptiveNotificationPermissionsManager::ContentSettingHelper;
  using DisruptiveNotificationRevocationState =
      DisruptiveNotificationPermissionsManager::RevocationState;

  RevokedPermissionsServiceBrowserTest() {
    feature_list.InitWithFeatures(
        /*enabled_features=*/
        {content_settings::features::kSafetyCheckUnusedSitePermissions},
        /*disabled_features=*/{});
  }

  void SetUpOnMainThread() override {
    InProcessBrowserTest::SetUpOnMainThread();
    ASSERT_TRUE(embedded_test_server()->Start());
  }

  ContentSettingsForOneType GetRevokedUnusedPermissions(
      HostContentSettingsMap* hcsm) {
    return hcsm->GetSettingsForOneType(
        ContentSettingsType::REVOKED_UNUSED_SITE_PERMISSIONS);
  }

 private:
  base::test::ScopedFeatureList feature_list;
};

IN_PROC_BROWSER_TEST_F(RevokedPermissionsServiceBrowserTest,
                       TestNavigationUpdatesLastUsedDate) {
  auto* map =
      HostContentSettingsMapFactory::GetForProfile(browser()->GetProfile());
  auto* service =
      RevokedPermissionsServiceFactory::GetForProfile(browser()->GetProfile());
  GURL url = embedded_test_server()->GetURL("/title1.html");

  // Create content setting 20 days in the past.
  // TODO(crbug.com/40250875): Move code below to a helper method.
  base::Time now(base::Time::Now());
  base::Time past(now - base::Days(20));
  base::SimpleTestClock clock;
  clock.SetNow(past);
  map->SetClockForTesting(&clock);
  service->SetClockForTesting(&clock);
  content_settings::ContentSettingConstraints constraints;
  constraints.set_track_last_visit_for_autoexpiration(true);
  map->SetContentSettingDefaultScope(url, url, ContentSettingsType::GEOLOCATION,
                                     CONTENT_SETTING_ALLOW, constraints);
  clock.SetNow(now);
  safety_hub_test_util::UpdateRevokedPermissionsServiceAsync(service);
  ASSERT_EQ(service->GetTrackedUnusedPermissionsForTesting().size(), 1u);
  ASSERT_EQ(GetRevokedUnusedPermissions(map).size(), 0u);

  // Check that the timestamp is initially in the past.
  content_settings::SettingInfo info;
  map->GetWebsiteSetting(url, url, ContentSettingsType::GEOLOCATION, &info);
  ASSERT_FALSE(info.metadata.last_visited().is_null());
  EXPECT_GE(info.metadata.last_visited(),
            past - content_settings::GetCoarseVisitedTimePrecision());
  EXPECT_LE(info.metadata.last_visited(), past);

  // Navigate to |url|.
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));

  // Check that the timestamp is updated after a navigation.
  map->GetWebsiteSetting(url, url, ContentSettingsType::GEOLOCATION, &info);
  EXPECT_GE(info.metadata.last_visited(),
            now - content_settings::GetCoarseVisitedTimePrecision());
  EXPECT_LE(info.metadata.last_visited(), now);

  map->SetClockForTesting(base::DefaultClock::GetInstance());
}

// Test that navigations work fine in incognito mode.
IN_PROC_BROWSER_TEST_F(RevokedPermissionsServiceBrowserTest,
                       TestIncognitoProfile) {
  GURL url = embedded_test_server()->GetURL("/title1.html");
  auto* otr_browser = OpenURLOffTheRecord(browser()->GetProfile(), url);
  ASSERT_FALSE(RevokedPermissionsServiceFactory::GetForProfile(
      otr_browser->GetProfile()));
}

// Test that revocation is happen correctly when auto-revoke is on.
IN_PROC_BROWSER_TEST_F(RevokedPermissionsServiceBrowserTest,
                       TestRevokeUnusedPermissions) {
  auto* map =
      HostContentSettingsMapFactory::GetForProfile(browser()->GetProfile());
  auto* service =
      RevokedPermissionsServiceFactory::GetForProfile(browser()->GetProfile());
  GURL url = embedded_test_server()->GetURL("/title1.html");

  // Create content setting 20 days in the past.
  base::Time now(base::Time::Now());
  base::Time past(now - base::Days(20));
  base::SimpleTestClock clock;
  clock.SetNow(past);
  map->SetClockForTesting(&clock);
  service->SetClockForTesting(&clock);
  content_settings::ContentSettingConstraints constraints;
  constraints.set_track_last_visit_for_autoexpiration(true);
  map->SetContentSettingDefaultScope(url, url, ContentSettingsType::GEOLOCATION,
                                     CONTENT_SETTING_ALLOW, constraints);
  clock.SetNow(now);

  // Check if the content setting is still ALLOW, before auto-revocation.
  safety_hub_test_util::UpdateRevokedPermissionsServiceAsync(service);
  ASSERT_EQ(service->GetTrackedUnusedPermissionsForTesting().size(), 1u);
  ASSERT_EQ(GetRevokedUnusedPermissions(map).size(), 0u);
  EXPECT_EQ(CONTENT_SETTING_ALLOW,
            map->GetContentSetting(url, url, ContentSettingsType::GEOLOCATION));

  // Travel through time for 40 days to make permissions be revoked.
  clock.Advance(base::Days(40));

  // Check if the content setting turn to ASK, when auto-revocation happens.
  safety_hub_test_util::UpdateRevokedPermissionsServiceAsync(service);
  ASSERT_EQ(service->GetTrackedUnusedPermissionsForTesting().size(), 0u);
  ASSERT_EQ(GetRevokedUnusedPermissions(map).size(), 1u);
  EXPECT_EQ(CONTENT_SETTING_ASK,
            map->GetContentSetting(url, url, ContentSettingsType::GEOLOCATION));
}

// Test that revocation happens correctly for all content setting types.
IN_PROC_BROWSER_TEST_F(RevokedPermissionsServiceBrowserTest,
                       RevokeAllContentSettingTypes) {
  auto* map =
      HostContentSettingsMapFactory::GetForProfile(browser()->GetProfile());
  auto* service =
      RevokedPermissionsServiceFactory::GetForProfile(browser()->GetProfile());

  base::Time time;
  ASSERT_TRUE(base::Time::FromString("2022-09-07 13:00", &time));
  base::SimpleTestClock clock;
  clock.SetNow(time);
  map->SetClockForTesting(&clock);
  service->SetClockForTesting(&clock);

  GURL url = embedded_test_server()->GetURL("/title1.html");
  base::HistogramTester histogram_tester;

  // TODO(b/338365161): Remove the skip list, once the bug is fixed. Currently,
  // when CAMERA_PAN_TILT_ZOOM is allowed, MEDIASTREAM_CAMERA is also allowed
  // under the hood without passing predefined constraints. Since the
  // auto-revokation relies on the constraint, the MEDIASTREAM_CAMERA ends up
  // not being revoked - although it is should be.
  const std::vector<ContentSettingsType> skip_list = {
      ContentSettingsType::CAMERA_PAN_TILT_ZOOM};

  // Allow all content settings in the content setting registry.
  std::vector<ContentSettingsType> allowed_permission_types;
  auto* content_settings_registry =
      content_settings::ContentSettingsRegistry::GetInstance();
  for (const content_settings::ContentSettingsInfo* info :
       *content_settings_registry) {
    ContentSettingsType type = info->website_settings_info()->type();

    // Skip if the setting's last visit can not be tracked.
    content_settings::ContentSettingConstraints constraint;
    if (!content_settings::CanTrackLastVisit(type)) {
      continue;
    }
    // Add last visited timestamp if the setting can be tracked.
    constraint.set_track_last_visit_for_autoexpiration(true);

    // Skip if the setting can not be set to ALLOW.
    if (!content_settings_registry->Get(type)->IsSettingValid(
            ContentSetting::CONTENT_SETTING_ALLOW)) {
      continue;
    }

    // Skip if the setting produces patterns that do not get revoked.
    content_settings::PatternPair patterns =
        map->GetPatternsForContentSettingsType(url, url, type);
    if (!patterns.first.MatchesSingleOrigin() ||
        patterns.second != ContentSettingsPattern::Wildcard()) {
      continue;
    }

    // Skip if the setting in the skip list.
    if (std::ranges::contains(skip_list, type)) {
      continue;
    }

    map->SetContentSettingDefaultScope(GURL(url), GURL(url), type,
                                       ContentSetting::CONTENT_SETTING_ALLOW,
                                       constraint);
    allowed_permission_types.push_back(type);
  }

  // Travel through time for 70 days to make permissions be revoked.
  clock.Advance(base::Days(70));
  safety_hub_test_util::UpdateRevokedPermissionsServiceAsync(service);

  // Assert there are revoked permission only for one origin.
  ContentSettingsForOneType revoked_permissions =
      GetRevokedUnusedPermissions(map);
  EXPECT_EQ(revoked_permissions.size(), 1u);

  // Get the revoked permission types for the origin.
  const base::DictValue& permission_types_by_values =
      revoked_permissions[0].setting_value.GetDict();
  const base::ListValue revoked_permission_types =
      permission_types_by_values.FindList(permissions::kRevokedKey)->Clone();

  // Assert all the allowed permissions are revoked.
  EXPECT_EQ(allowed_permission_types.size(), revoked_permission_types.size());

  for (int i = 0; i < (int)revoked_permission_types.size(); i++) {
    ContentSettingsType revoked_permission_type =
        UnusedSitePermissionsManager::ConvertKeyToContentSettingsType(
            revoked_permission_types[i].GetString());
    EXPECT_EQ(allowed_permission_types[i], revoked_permission_type);
  }

  // Assert all auto-revocations are recorded in UMA metrics.
  EXPECT_EQ(allowed_permission_types.size(),
            histogram_tester.GetAllSamples(histogram_name).size());
  for (const ContentSettingsType type : allowed_permission_types) {
    histogram_tester.ExpectBucketCount(
        histogram_name,
        content_settings_uma_util::ContentSettingTypeToHistogramValue(type), 1);
  }

  // Navigate to content settings page.
  ASSERT_TRUE(ui_test_utils::NavigateToURL(
      browser(), GURL(chrome::kChromeUIContentSettingsURL)));
}

class AbusiveNotificationPermissionsRevocationBrowserTest
    : public RevokedPermissionsServiceBrowserTest {
 public:
  AbusiveNotificationPermissionsRevocationBrowserTest() {
    safe_browsing_factory_ =
        std::make_unique<safe_browsing::TestSafeBrowsingServiceFactory>();
    feature_list_.InitWithFeatures(
        /*enabled_features=*/
        {content_settings::features::kSafetyCheckUnusedSitePermissions},
        /*disabled_features=*/{});
  }

  void CreatedBrowserMainParts(
      content::BrowserMainParts* browser_main_parts) override {
    fake_database_manager_ =
        base::MakeRefCounted<MockSafeBrowsingDatabaseManager>();
    safe_browsing_factory_->SetTestDatabaseManager(
        fake_database_manager_.get());
    safe_browsing::SafeBrowsingServiceInterface::RegisterFactory(
        safe_browsing_factory_.get());
  }

  void AddDangerousUrl(const GURL& url) {
    mock_database_manager()->SetThreatTypeForUrl(
        url, safe_browsing::SBThreatType::SB_THREAT_TYPE_URL_PHISHING);
  }

  MockSafeBrowsingDatabaseManager* mock_database_manager() {
    return fake_database_manager_.get();
  }

 private:
  scoped_refptr<MockSafeBrowsingDatabaseManager> fake_database_manager_;
  std::unique_ptr<safe_browsing::TestSafeBrowsingServiceFactory>
      safe_browsing_factory_;
  base::test::ScopedFeatureList feature_list_;
};

// TODO(crbug.com/400648091): Re-enable the test when it's fixed.
// Test that revocation is happen correctly when auto-revoke is on.
IN_PROC_BROWSER_TEST_F(AbusiveNotificationPermissionsRevocationBrowserTest,
                       DISABLED_TestRevokeAbusiveNotificationPermissions) {
  auto* map =
      HostContentSettingsMapFactory::GetForProfile(browser()->GetProfile());
  auto* service =
      RevokedPermissionsServiceFactory::GetForProfile(browser()->GetProfile());
  const GURL url("https://example1.com");
  AddDangerousUrl(url);
  base::HistogramTester histogram_tester;

  // Create granted abusive notification permission.
  map->SetContentSettingDefaultScope(
      url, url, ContentSettingsType::NOTIFICATIONS, CONTENT_SETTING_ALLOW);
  ASSERT_EQ(
      safety_hub_util::GetRevokedAbusiveNotificationPermissions(map).size(),
      0u);

  // Check if the content setting turn to ASK, when auto-revocation happens.
  safety_hub_test_util::UpdateRevokedPermissionsServiceAsync(service);
  ASSERT_EQ(
      safety_hub_util::GetRevokedAbusiveNotificationPermissions(map).size(),
      1u);
  EXPECT_EQ(
      CONTENT_SETTING_ASK,
      map->GetContentSetting(url, url, ContentSettingsType::NOTIFICATIONS));

  // Assert notification auto-revocation is recorded in UMA metrics.
  EXPECT_EQ(1u, histogram_tester.GetAllSamples(histogram_name).size());
  histogram_tester.ExpectBucketCount(
      histogram_name,
      content_settings_uma_util::ContentSettingTypeToHistogramValue(
          ContentSettingsType::NOTIFICATIONS),
      1);
}

// TODO(crbug.com/400645286): Re-enable the test when it's fixed.
// Test that revocation is happen correctly when auto-revoke is on for a site
// that is unused then abusive.
IN_PROC_BROWSER_TEST_F(AbusiveNotificationPermissionsRevocationBrowserTest,
                       DISABLED_TestSiteWithFirstUnusedThenAbusivePermissions) {
  auto* map =
      HostContentSettingsMapFactory::GetForProfile(browser()->GetProfile());
  auto* service =
      RevokedPermissionsServiceFactory::GetForProfile(browser()->GetProfile());
  // Test cases where there is a single URL which is both abusive and unused,
  // then when there are separate abusive and unused URLs.
  for (const auto& [abusive_url, unused_url] :
       std::vector<std::pair<GURL, GURL>>{
           {GURL("https://example1.com"), GURL("https://example1.com")},
           {GURL("https://example2.com"), GURL("https://example3.com")}}) {
    AddDangerousUrl(abusive_url);

    // Create content setting 20 days in the past.
    base::Time now(base::Time::Now());
    base::Time past(now - base::Days(20));
    base::SimpleTestClock clock;
    clock.SetNow(past);
    map->SetClockForTesting(&clock);
    service->SetClockForTesting(&clock);
    content_settings::ContentSettingConstraints constraints;
    constraints.set_track_last_visit_for_autoexpiration(true);
    map->SetContentSettingDefaultScope(unused_url, unused_url,
                                       ContentSettingsType::GEOLOCATION,
                                       CONTENT_SETTING_ALLOW, constraints);
    clock.SetNow(now);

    // Check if the content setting is still ALLOW, before auto-revocation.
    safety_hub_test_util::UpdateRevokedPermissionsServiceAsync(service);
    ASSERT_EQ(GetRevokedUnusedPermissions(map).size(), 0u);
    EXPECT_EQ(CONTENT_SETTING_ALLOW,
              map->GetContentSetting(unused_url, unused_url,
                                     ContentSettingsType::GEOLOCATION));

    // Travel through time for 40 days to make permissions be revoked.
    clock.Advance(base::Days(40));

    // Check if the content setting turn to ASK, when auto-revocation happens.
    safety_hub_test_util::UpdateRevokedPermissionsServiceAsync(service);
    ASSERT_EQ(GetRevokedUnusedPermissions(map).size(), 1u);
    EXPECT_EQ(CONTENT_SETTING_ASK,
              map->GetContentSetting(unused_url, unused_url,
                                     ContentSettingsType::GEOLOCATION));

    // Travel through time for 20 more days, then revoke abusive notifications
    // immediately.
    clock.Advance(base::Days(20));
    map->SetContentSettingDefaultScope(abusive_url, abusive_url,
                                       ContentSettingsType::NOTIFICATIONS,
                                       CONTENT_SETTING_ALLOW);
    safety_hub_test_util::UpdateRevokedPermissionsServiceAsync(service);
    ASSERT_GT(
        safety_hub_util::GetRevokedAbusiveNotificationPermissions(map).size(),
        0u);
    EXPECT_TRUE(safety_hub_test_util::IsUrlInSettingsList(
        safety_hub_util::GetRevokedAbusiveNotificationPermissions(map),
        abusive_url));
    EXPECT_EQ(CONTENT_SETTING_ASK,
              map->GetContentSetting(unused_url, unused_url,
                                     ContentSettingsType::GEOLOCATION));
    EXPECT_EQ(CONTENT_SETTING_ASK,
              map->GetContentSetting(abusive_url, abusive_url,
                                     ContentSettingsType::NOTIFICATIONS));
  }
}

// TODO(crbug.com/400648762): Re-enable the test when it's fixed.
// Test that revocation is happen correctly when auto-revoke is on for a site
// that is abusive then unused.
IN_PROC_BROWSER_TEST_F(AbusiveNotificationPermissionsRevocationBrowserTest,
                       DISABLED_TestSiteWithFirstAbusiveThenUnusedPermissions) {
  auto* map =
      HostContentSettingsMapFactory::GetForProfile(browser()->GetProfile());
  auto* service =
      RevokedPermissionsServiceFactory::GetForProfile(browser()->GetProfile());

  // Test cases where there is a single URL which is both abusive and unused,
  // then when there are separate abusive and unused URLs.
  for (const auto& [abusive_url, unused_url] :
       std::vector<std::pair<GURL, GURL>>{
           {GURL("https://example1.com"), GURL("https://example1.com")},
           {GURL("https://example2.com"), GURL("https://example3.com")}}) {
    AddDangerousUrl(abusive_url);

    // Create content setting 20 days in the past.
    base::Time now(base::Time::Now());
    base::Time past(now - base::Days(20));
    base::SimpleTestClock clock;
    clock.SetNow(past);
    map->SetClockForTesting(&clock);
    service->SetClockForTesting(&clock);
    content_settings::ContentSettingConstraints constraints;
    constraints.set_track_last_visit_for_autoexpiration(true);
    map->SetContentSettingDefaultScope(unused_url, unused_url,
                                       ContentSettingsType::GEOLOCATION,
                                       CONTENT_SETTING_ALLOW, constraints);
    clock.SetNow(now);

    // Check if the content setting is still ALLOW, before auto-revocation.
    safety_hub_test_util::UpdateRevokedPermissionsServiceAsync(service);
    ASSERT_LT(GetRevokedUnusedPermissions(map).size(), 2u);
    EXPECT_FALSE(safety_hub_test_util::IsUrlInSettingsList(
        GetRevokedUnusedPermissions(map), unused_url));
    EXPECT_EQ(CONTENT_SETTING_ALLOW,
              map->GetContentSetting(unused_url, unused_url,
                                     ContentSettingsType::GEOLOCATION));

    // Travel through time for 20 more days, then revoke abusive notifications
    // immediately. Note the unused permission is not yet revoked.
    clock.Advance(base::Days(20));
    map->SetContentSettingDefaultScope(abusive_url, abusive_url,
                                       ContentSettingsType::NOTIFICATIONS,
                                       CONTENT_SETTING_ALLOW);
    safety_hub_test_util::UpdateRevokedPermissionsServiceAsync(service);
    ASSERT_LT(GetRevokedUnusedPermissions(map).size(), 2u);
    EXPECT_FALSE(safety_hub_test_util::IsUrlInSettingsList(
        GetRevokedUnusedPermissions(map), unused_url));
    ASSERT_GT(
        safety_hub_util::GetRevokedAbusiveNotificationPermissions(map).size(),
        0u);
    EXPECT_TRUE(safety_hub_test_util::IsUrlInSettingsList(
        safety_hub_util::GetRevokedAbusiveNotificationPermissions(map),
        abusive_url));
    EXPECT_EQ(CONTENT_SETTING_ALLOW,
              map->GetContentSetting(unused_url, unused_url,
                                     ContentSettingsType::GEOLOCATION));
    EXPECT_EQ(CONTENT_SETTING_ASK,
              map->GetContentSetting(abusive_url, abusive_url,
                                     ContentSettingsType::NOTIFICATIONS));

    // Travel through time for 20 days to make unused permissions revoked.
    // Abusive permission is still revoked.
    clock.Advance(base::Days(20));
    safety_hub_test_util::UpdateRevokedPermissionsServiceAsync(service);
    ASSERT_GT(GetRevokedUnusedPermissions(map).size(), 0u);
    EXPECT_TRUE(safety_hub_test_util::IsUrlInSettingsList(
        GetRevokedUnusedPermissions(map), unused_url));
    ASSERT_GT(
        safety_hub_util::GetRevokedAbusiveNotificationPermissions(map).size(),
        0u);
    EXPECT_TRUE(safety_hub_test_util::IsUrlInSettingsList(
        safety_hub_util::GetRevokedAbusiveNotificationPermissions(map),
        abusive_url));
    EXPECT_EQ(CONTENT_SETTING_ASK,
              map->GetContentSetting(unused_url, unused_url,
                                     ContentSettingsType::GEOLOCATION));
    EXPECT_EQ(CONTENT_SETTING_ASK,
              map->GetContentSetting(abusive_url, abusive_url,
                                     ContentSettingsType::NOTIFICATIONS));
  }
}

class DisruptiveNotificationPermissionsRevocationShadowRunBrowserTest
    : public RevokedPermissionsServiceBrowserTest {
 public:
  DisruptiveNotificationPermissionsRevocationShadowRunBrowserTest() {
    feature_list_.InitAndEnableFeatureWithParameters(
        features::kSafetyHubDisruptiveNotificationRevocation,
        {
            {features::kSafetyHubDisruptiveNotificationRevocationShadowRun.name,
             "true"},
        });
  }

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

IN_PROC_BROWSER_TEST_F(
    DisruptiveNotificationPermissionsRevocationShadowRunBrowserTest,
    TestProposeRevokeDisruptiveNotificationPermissions) {
  auto* hcsm =
      HostContentSettingsMapFactory::GetForProfile(browser()->GetProfile());
  auto* service =
      RevokedPermissionsServiceFactory::GetForProfile(browser()->GetProfile());
  GURL url = embedded_test_server()->GetURL("/title1.html");

  // Set up a disruptive notification permission.
  hcsm->SetContentSettingDefaultScope(
      url, GURL(), ContentSettingsType::NOTIFICATIONS, CONTENT_SETTING_ALLOW);
  auto* notifications_engagement_service =
      NotificationsEngagementServiceFactory::GetForProfile(
          browser()->GetProfile());
  notifications_engagement_service->RecordNotificationDisplayed(url, 50);

  safety_hub_test_util::UpdateRevokedPermissionsServiceAsync(service);

  // The url was stored in the disruptive notification content setting.
  EXPECT_THAT(
      DisruptiveNotificationContentSettingHelper(*hcsm).GetRevocationEntry(url),
      Not(Eq(std::nullopt)));

  safety_hub_test_util::UpdateRevokedPermissionsServiceAsync(service);
  std::optional<DisruptiveNotificationRevocationEntry> revocation_entry =
      DisruptiveNotificationContentSettingHelper(*hcsm).GetRevocationEntry(url);
  EXPECT_THAT(
      revocation_entry,
      Optional(Field(&DisruptiveNotificationRevocationEntry::revocation_state,
                     DisruptiveNotificationRevocationState::kProposed)));
  ASSERT_EQ(GetRevokedUnusedPermissions(hcsm).size(), 0u);
  std::optional<std::unique_ptr<SafetyHubResult>> opt_result =
      service->GetCachedResult();
  ASSERT_TRUE(opt_result.has_value());
  auto* result =
      static_cast<RevokedPermissionsResult*>(opt_result.value().get());
  EXPECT_EQ(result->GetRevokedPermissions().size(), 0u);
  EXPECT_EQ(
      CONTENT_SETTING_ALLOW,
      hcsm->GetContentSetting(url, url, ContentSettingsType::NOTIFICATIONS));
}

class DisruptiveNotificationPermissionsRevocationBrowserTest
    : public RevokedPermissionsServiceBrowserTest {
 public:
  DisruptiveNotificationPermissionsRevocationBrowserTest() {
#if BUILDFLAG(IS_ANDROID)
    feature_list_.InitAndEnableFeatureWithParameters(
        features::kSafetyHubDisruptiveNotificationRevocation,
        {{features::kSafetyHubDisruptiveNotificationRevocationShadowRun.name,
          "false"},
         {features::
              kSafetyHubDisruptiveNotificationRevocationWaitingForMetricsDays
                  .name,
          "7"}});
#else
    feature_list_.InitWithFeaturesAndParameters(
        /*enabled_features=*/
        {{features::kSafetyHubDisruptiveNotificationRevocation,
          {{features::kSafetyHubDisruptiveNotificationRevocationShadowRun.name,
            "false"},
           {features::
                kSafetyHubDisruptiveNotificationRevocationWaitingForMetricsDays
                    .name,
            "7"}}}},
        /*disabled_features=*/{
            safe_browsing::kShowWarningsForSuspiciousNotifications});
#endif
  }

  void SetUpOnMainThread() override {
    RevokedPermissionsServiceBrowserTest::SetUpOnMainThread();
    recorder_ = std::make_unique<ukm::TestAutoSetUkmRecorder>();
  }

  site_engagement::SiteEngagementService* site_engagement_service() {
    return site_engagement::SiteEngagementServiceFactory::GetForProfile(
        browser()->GetProfile());
  }

  std::unique_ptr<ukm::TestAutoSetUkmRecorder> recorder_;

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

// Test that revocation is happening correctly when auto-revoke is on.
IN_PROC_BROWSER_TEST_F(DisruptiveNotificationPermissionsRevocationBrowserTest,
                       TestRevokeDisruptiveNotificationPermissions) {
  auto* hcsm =
      HostContentSettingsMapFactory::GetForProfile(browser()->GetProfile());
  auto* service =
      RevokedPermissionsServiceFactory::GetForProfile(browser()->GetProfile());
  GURL url = embedded_test_server()->GetURL("/title1.html");

  base::SimpleTestClock clock;
  hcsm->SetClockForTesting(&clock);
  service->SetClockForTesting(&clock);
  clock.SetNow(base::Time::Now());

  // Force for the initial safety check to be complete before setting up a
  // disruptive notification. Otherwise, sometimes the check is finished before
  // and sometimes after the setup which causes flakes.
  safety_hub_test_util::UpdateRevokedPermissionsServiceAsync(service);

  // Set up a disruptive notification permission.
  hcsm->SetContentSettingDefaultScope(
      url, GURL(), ContentSettingsType::NOTIFICATIONS, CONTENT_SETTING_ALLOW);
  auto* notifications_engagement_service =
      NotificationsEngagementServiceFactory::GetForProfile(
          browser()->GetProfile());
  notifications_engagement_service->RecordNotificationDisplayed(url, 50);

  safety_hub_test_util::UpdateRevokedPermissionsServiceAsync(service);

  // The url was stored in the disruptive notification content setting.
  std::optional<DisruptiveNotificationRevocationEntry> revocation_entry =
      DisruptiveNotificationContentSettingHelper(*hcsm).GetRevocationEntry(url);
  EXPECT_THAT(
      revocation_entry,
      Optional(Field(&DisruptiveNotificationRevocationEntry::revocation_state,
                     DisruptiveNotificationRevocationState::kProposed)));

  // Wait for the disruptive metrics cooldown to expire.
  clock.Advance(
      features::kSafetyHubDisruptiveNotificationRevocationWaitingTimeAsProposed
          .Get() +
      base::Days(1));

  safety_hub_test_util::UpdateRevokedPermissionsServiceAsync(service);
  revocation_entry =
      DisruptiveNotificationContentSettingHelper(*hcsm).GetRevocationEntry(url);
  EXPECT_THAT(
      revocation_entry,
      Optional(Field(&DisruptiveNotificationRevocationEntry::revocation_state,
                     DisruptiveNotificationRevocationState::kRevoked)));

  std::optional<std::unique_ptr<SafetyHubResult>> opt_result =
      service->GetCachedResult();
  ASSERT_TRUE(opt_result.has_value());
  auto* result =
      static_cast<RevokedPermissionsResult*>(opt_result.value().get());
  EXPECT_EQ(result->GetRevokedPermissions().size(), 1u);
  EXPECT_EQ(
      CONTENT_SETTING_ASK,
      hcsm->GetContentSetting(url, url, ContentSettingsType::NOTIFICATIONS));
}

IN_PROC_BROWSER_TEST_F(
    DisruptiveNotificationPermissionsRevocationBrowserTest,
    TestRevokeFirstUnusedThenDisruptiveNotificationPermissions) {
  auto* hcsm =
      HostContentSettingsMapFactory::GetForProfile(browser()->GetProfile());
  auto* service =
      RevokedPermissionsServiceFactory::GetForProfile(browser()->GetProfile());
  GURL url = embedded_test_server()->GetURL("/title1.html");

  hcsm->SetContentSettingDefaultScope(
      url, GURL(), ContentSettingsType::NOTIFICATIONS, CONTENT_SETTING_ALLOW);

  // Create content setting 20 days in the past.
  base::Time now(base::Time::Now());
  base::Time past(now - base::Days(20));
  base::SimpleTestClock clock;
  clock.SetNow(past);
  hcsm->SetClockForTesting(&clock);
  service->SetClockForTesting(&clock);
  content_settings::ContentSettingConstraints constraints;
  constraints.set_track_last_visit_for_autoexpiration(true);
  hcsm->SetContentSettingDefaultScope(url, url,
                                      ContentSettingsType::GEOLOCATION,
                                      CONTENT_SETTING_ALLOW, constraints);
  clock.SetNow(now);

  safety_hub_test_util::UpdateRevokedPermissionsServiceAsync(service);

  // Travel through time for 40 days.
  clock.Advance(base::Days(40));

  // Set up a disruptive notification permission.
  auto* notifications_engagement_service =
      NotificationsEngagementServiceFactory::GetForProfile(
          browser()->GetProfile());
  notifications_engagement_service->RecordNotificationDisplayed(url, 50);

  // Check if the content setting turn to ASK, when auto-revocation happens.
  safety_hub_test_util::UpdateRevokedPermissionsServiceAsync(service);
  ASSERT_EQ(GetRevokedUnusedPermissions(hcsm).size(), 1u);
  EXPECT_EQ(
      CONTENT_SETTING_ASK,
      hcsm->GetContentSetting(url, url, ContentSettingsType::GEOLOCATION));

  // The url was stored in the disruptive notification content setting as
  // proposed revocation.
  std::optional<DisruptiveNotificationRevocationEntry> revocation_entry =
      DisruptiveNotificationContentSettingHelper(*hcsm).GetRevocationEntry(url);
  EXPECT_THAT(
      revocation_entry,
      Optional(Field(&DisruptiveNotificationRevocationEntry::revocation_state,
                     DisruptiveNotificationRevocationState::kProposed)));

  // Wait for the disruptive metrics cooldown to expire.
  clock.Advance(
      features::kSafetyHubDisruptiveNotificationRevocationWaitingTimeAsProposed
          .Get() +
      base::Days(1));

  safety_hub_test_util::UpdateRevokedPermissionsServiceAsync(service);
  // Both disruptive notifications and unused permissions were revoked for the
  // URL.
  std::optional<std::unique_ptr<SafetyHubResult>> opt_result =
      service->GetCachedResult();
  ASSERT_TRUE(opt_result.has_value());
  auto* result =
      static_cast<RevokedPermissionsResult*>(opt_result.value().get());
  EXPECT_EQ(result->GetRevokedPermissions().size(), 1u);

  revocation_entry =
      DisruptiveNotificationContentSettingHelper(*hcsm).GetRevocationEntry(url);
  EXPECT_THAT(
      revocation_entry,
      Optional(Field(&DisruptiveNotificationRevocationEntry::revocation_state,
                     DisruptiveNotificationRevocationState::kRevoked)));

  EXPECT_EQ(result->GetRevokedPermissions().size(), 1u);
  EXPECT_EQ(result->GetRevokedPermissions().front().permissions.size(), 2u);
  EXPECT_THAT(result->GetRevokedPermissions().front().permissions,
              UnorderedElementsAre(Key(ContentSettingsType::GEOLOCATION),
                                   Key(ContentSettingsType::NOTIFICATIONS)));
  for (auto&& [type, value] :
       result->GetRevokedPermissions().front().permissions) {
    if (type == ContentSettingsType::GEOLOCATION) {
      EXPECT_EQ(value, base::Value(CONTENT_SETTING_ALLOW));
    } else if (type == ContentSettingsType::NOTIFICATIONS) {
      EXPECT_EQ(value, base::Value());
    }
  }

  EXPECT_EQ(
      CONTENT_SETTING_ASK,
      hcsm->GetContentSetting(url, url, ContentSettingsType::NOTIFICATIONS));
  EXPECT_EQ(
      CONTENT_SETTING_ASK,
      hcsm->GetContentSetting(url, url, ContentSettingsType::GEOLOCATION));
}

IN_PROC_BROWSER_TEST_F(DisruptiveNotificationPermissionsRevocationBrowserTest,
                       TestProposedFalsePositiveOnPageVisit) {
  auto* hcsm =
      HostContentSettingsMapFactory::GetForProfile(browser()->GetProfile());
  GURL url = embedded_test_server()->GetURL("/title1.html");

  // Set up a proposed revoked notification.
  DisruptiveNotificationRevocationEntry revoked_entry(
      /*revocation_state=*/DisruptiveNotificationRevocationState::kProposed,
      /*site_engagement=*/0.0,
      /*daily_notification_count=*/5,
      /*timestamp=*/base::Time::Now() - base::Days(3));
  DisruptiveNotificationContentSettingHelper(*hcsm).PersistRevocationEntry(
      url, revoked_entry);

  // Visit the site.
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));

  auto interaction_entries = recorder_->GetEntriesByName(
      "SafetyHub.DisruptiveNotificationRevocations.FalsePositiveInteraction");
  ASSERT_EQ(1u, interaction_entries.size());
  auto* interaction_entry = interaction_entries[0].get();
  recorder_->ExpectEntryMetric(interaction_entry, "DaysSinceRevocation", 3);
  recorder_->ExpectEntryMetric(
      interaction_entry, "Reason",
      static_cast<int>(DisruptiveNotificationPermissionsManager::
                           FalsePositiveReason::kPageVisit));
  // Site engagement hasn't been updated yet.
  recorder_->ExpectEntryMetric(interaction_entry, "NewSiteEngagement", 0.0);
  recorder_->ExpectEntryMetric(interaction_entry, "OldSiteEngagement", 0.0);
  recorder_->ExpectEntryMetric(interaction_entry, "DailyAverageVolume", 5);
}

IN_PROC_BROWSER_TEST_F(DisruptiveNotificationPermissionsRevocationBrowserTest,
                       TestRevokedFalsePositiveOnPageVisit) {
  auto* hcsm =
      HostContentSettingsMapFactory::GetForProfile(browser()->GetProfile());
  GURL url = embedded_test_server()->GetURL("/title1.html");

  // Set up a revoked notification.
  DisruptiveNotificationRevocationEntry proposed_entry(
      /*revocation_state=*/DisruptiveNotificationRevocationState::kRevoked,
      /*site_engagement=*/0.0,
      /*daily_notification_count=*/5,
      /*timestamp=*/base::Time::Now() - base::Days(3));

  DisruptiveNotificationContentSettingHelper(*hcsm).PersistRevocationEntry(
      url, proposed_entry);

  site_engagement_service()->AddPointsForTesting(url, 4.0);
  // Visit the page.
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));

  auto interaction_entries = recorder_->GetEntriesByName(
      "SafetyHub.DisruptiveNotificationRevocations.FalsePositiveInteraction");
  ASSERT_EQ(1u, interaction_entries.size());
  auto* interaction_entry = interaction_entries[0].get();
  recorder_->ExpectEntryMetric(interaction_entry, "DaysSinceRevocation", 3);
  recorder_->ExpectEntryMetric(
      interaction_entry, "Reason",
      static_cast<int>(DisruptiveNotificationPermissionsManager::
                           FalsePositiveReason::kPageVisit));
  EXPECT_THAT(recorder_->GetEntryMetric(interaction_entry, "NewSiteEngagement"),
              Pointee(Ge(4.0)));
  recorder_->ExpectEntryMetric(interaction_entry, "OldSiteEngagement", 0.0);
  recorder_->ExpectEntryMetric(interaction_entry, "DailyAverageVolume", 5);

  auto revocation_entries = recorder_->GetEntriesByName(
      "SafetyHub.DisruptiveNotificationRevocations.FalsePositiveRevocation");
  ASSERT_EQ(1u, revocation_entries.size());
  auto* revocation_entry = revocation_entries[0].get();
  recorder_->ExpectEntryMetric(revocation_entry, "DaysSinceRevocation", 3);
  recorder_->ExpectEntryMetric(revocation_entry, "PageVisitCount", 1);
  recorder_->ExpectEntryMetric(revocation_entry, "NotificationClickCount", 0);
  EXPECT_THAT(recorder_->GetEntryMetric(revocation_entry, "NewSiteEngagement"),
              Pointee(Ge(4.0)));
  recorder_->ExpectEntryMetric(revocation_entry, "OldSiteEngagement", 0.0);
  recorder_->ExpectEntryMetric(revocation_entry, "DailyAverageVolume", 5);

  // Switch to a different site.
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(),
                                           GURL("https://www.another.site")));

  // Visit the page again, the revocation has already reported so it won't be
  // reported again.
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
  EXPECT_EQ(
      2u, recorder_
              ->GetEntriesByName("SafetyHub.DisruptiveNotificationRevocations."
                                 "FalsePositiveInteraction")
              .size());
  EXPECT_EQ(
      1u, recorder_
              ->GetEntriesByName("SafetyHub.DisruptiveNotificationRevocations."
                                 "FalsePositiveRevocation")
              .size());
}

// TODO(crbug.com/406472515): Add a test for non persistent notification click.
IN_PROC_BROWSER_TEST_F(DisruptiveNotificationPermissionsRevocationBrowserTest,
                       TestProposedFalsePositiveOnPersistentNotificationClick) {
  auto* hcsm =
      HostContentSettingsMapFactory::GetForProfile(browser()->GetProfile());
  auto display_service_tester =
      std::make_unique<NotificationDisplayServiceTester>(
          browser()->GetProfile());
  auto* notifications_service =
      PlatformNotificationServiceFactory::GetForProfile(
          browser()->GetProfile());

  GURL url = embedded_test_server()->GetURL("/title1.html");
  const char kNotificationId[] = "my-notification-id";

  // Set up a proposed revoked disruptive notification. The notification are not
  // yet revoked.
  hcsm->SetContentSettingDefaultScope(
      url, GURL(), ContentSettingsType::NOTIFICATIONS, CONTENT_SETTING_ALLOW);

  DisruptiveNotificationRevocationEntry proposed_entry(
      /*revocation_state=*/DisruptiveNotificationRevocationState::kProposed,
      /*site_engagement=*/0.0,
      /*daily_notification_count=*/5,
      /*timestamp=*/base::Time::Now() - base::Days(3));
  DisruptiveNotificationContentSettingHelper(*hcsm).PersistRevocationEntry(
      url, proposed_entry);

  // Show a notification.
  blink::PlatformNotificationData data;
  data.title = u"My notification's title";
  data.body = u"Hello, world!";
  notifications_service->DisplayPersistentNotification(
      kNotificationId, url, url, data, blink::NotificationResources());

  // Click on the notification.
  display_service_tester->SimulateClick(
      NotificationHandler::Type::WEB_PERSISTENT, kNotificationId,
      0 /*action_index*/, std::nullopt);

  auto interaction_entries = recorder_->GetEntriesByName(
      "SafetyHub.DisruptiveNotificationRevocations.FalsePositiveInteraction");
  ASSERT_EQ(1u, interaction_entries.size());
  auto* interaction_entry = interaction_entries[0].get();
  recorder_->ExpectEntryMetric(interaction_entry, "DaysSinceRevocation", 3);
  recorder_->ExpectEntryMetric(
      interaction_entry, "Reason",
      static_cast<int>(DisruptiveNotificationPermissionsManager::
                           FalsePositiveReason::kPersistentNotificationClick));
  recorder_->ExpectEntryMetric(interaction_entry, "NewSiteEngagement", 2.0);
  recorder_->ExpectEntryMetric(interaction_entry, "OldSiteEngagement", 0.0);
  recorder_->ExpectEntryMetric(interaction_entry, "DailyAverageVolume", 5);

  auto revocation_entries = recorder_->GetEntriesByName(
      "SafetyHub.DisruptiveNotificationRevocations.FalsePositiveRevocation");
  ASSERT_EQ(0u, revocation_entries.size());
}
