// Copyright 2023 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/web_applications/web_app_pref_guardrails.h"

#include <memory>
#include <string_view>

#include "base/json/values_util.h"
#include "base/metrics/field_trial_params.h"
#include "base/rand_util.h"
#include "base/time/time.h"
#include "chrome/browser/web_applications/test/web_app_test.h"
#include "chrome/browser/web_applications/web_app_constants.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/pref_names.h"
#include "components/sync_preferences/testing_pref_service_syncable.h"
#include "components/webapps/browser/features.h"
#include "components/webapps/common/web_app_id.h"
#include "content/public/common/content_features.h"
#include "content/public/test/browser_task_environment.h"
#include "testing/gtest/include/gtest/gtest.h"

#if BUILDFLAG(IS_MAC)
#include "base/mac/mac_util.h"
#endif

namespace web_app {

namespace {
const webapps::AppId app_id = "app_id";
}

class WebAppGuardrailsTest : public testing::Test {
 public:
  WebAppGuardrailsTest() {
    WebAppPrefGuardrails::RegisterProfilePrefs(prefs_.registry());
  }

  sync_preferences::TestingPrefServiceSyncable* prefs() { return &prefs_; }

 protected:
  std::optional<int> GetIntWebAppPref(const webapps::AppId& app,
                                      std::string_view path) {
    return ::web_app::GetIntWebAppPref(prefs(), app, path);
  }

  std::optional<base::Time> GetTimeWebAppPref(const webapps::AppId& app,
                                              std::string_view path) {
    return ::web_app::GetTimeWebAppPref(prefs(), app, path);
  }

 private:
  content::BrowserTaskEnvironment task_environment_;
  sync_preferences::TestingPrefServiceSyncable prefs_;
};

class WebAppGuardrailsIphTest : public WebAppGuardrailsTest {
 public:
  WebAppGuardrailsIphTest()
      : guardrails_(WebAppPrefGuardrails::GetForDesktopInstallIph(prefs())) {}

 protected:
  WebAppPrefGuardrails& guardrails() { return guardrails_; }
  bool ShouldShowIph(const webapps::AppId& app) {
    return !guardrails().IsBlockedByGuardrails(app);
  }

  const webapps::AppId app_id_2 = "test_app_2";
  const base::Time time_before_app_mute =
      base::Time::Now() -
      base::Days(kIphGuardrails.app_specific_mute_after_ignore_days.value()) -
      base::Hours(1);
  const base::Time time_before_global_mute =
      base::Time::Now() -
      base::Days(kIphGuardrails.global_mute_after_ignore_days.value()) -
      base::Hours(1);

 private:
  WebAppPrefGuardrails guardrails_;
};

TEST_F(WebAppGuardrailsIphTest, Ignore) {
  guardrails().RecordIgnore(app_id, base::Time::Now());
  EXPECT_EQ(GetIntWebAppPref(app_id, kIphPrefNames.not_accepted_count_name)
                .value_or(0),
            1);
  auto last_ignore_time =
      GetTimeWebAppPref(app_id, kIphPrefNames.last_ignore_time_name);
  {
    const auto& dict = prefs()->GetDict(prefs::kWebAppsAppAgnosticIphState);
    EXPECT_EQ(dict.FindInt(kIphPrefNames.not_accepted_count_name).value_or(0),
              1);
    EXPECT_EQ(base::ValueToTime(dict.Find(kIphPrefNames.last_ignore_time_name)),
              last_ignore_time.value());
  }
}

TEST_F(WebAppGuardrailsIphTest, IgnoreRecordUpdated) {
  guardrails().RecordIgnore(app_id, base::Time::Now());
  auto last_ignore_time =
      GetTimeWebAppPref(app_id, kIphPrefNames.last_ignore_time_name);
  EXPECT_TRUE(last_ignore_time.has_value());

  guardrails().RecordIgnore(app_id, base::Time::Now());
  EXPECT_EQ(GetIntWebAppPref(app_id, kIphPrefNames.not_accepted_count_name)
                .value_or(0),
            2);
  EXPECT_NE(
      GetTimeWebAppPref(app_id, kIphPrefNames.last_ignore_time_name).value(),
      last_ignore_time.value());
  {
    const auto& dict = prefs()->GetDict(prefs::kWebAppsAppAgnosticIphState);
    EXPECT_EQ(dict.FindInt(kIphPrefNames.not_accepted_count_name).value_or(0),
              2);
    EXPECT_NE(base::ValueToTime(dict.Find(kIphPrefNames.last_ignore_time_name)),
              last_ignore_time.value());
  }
}

TEST_F(WebAppGuardrailsIphTest, InstallResetCounters) {
  guardrails().RecordIgnore(app_id, base::Time::Now());
  EXPECT_EQ(GetIntWebAppPref(app_id, kIphPrefNames.not_accepted_count_name)
                .value_or(0),
            1);
  {
    const auto& dict = prefs()->GetDict(prefs::kWebAppsAppAgnosticIphState);
    EXPECT_EQ(dict.FindInt(kIphPrefNames.not_accepted_count_name).value_or(0),
              1);
  }
  guardrails().RecordAccept(app_id);
  EXPECT_EQ(GetIntWebAppPref(app_id, kIphPrefNames.not_accepted_count_name)
                .value_or(0),
            0);
  {
    const auto& dict = prefs()->GetDict(prefs::kWebAppsAppAgnosticIphState);
    EXPECT_EQ(dict.FindInt(kIphPrefNames.not_accepted_count_name).value_or(0),
              0);
  }
}

TEST_F(WebAppGuardrailsIphTest, AppIgnoredRecently) {
  EXPECT_TRUE(ShouldShowIph(app_id));
  guardrails().RecordIgnore(app_id, base::Time::Now());
  EXPECT_FALSE(ShouldShowIph(app_id));
}

TEST_F(WebAppGuardrailsIphTest, GlobalIgnoredRecently) {
  EXPECT_TRUE(ShouldShowIph(app_id));
  guardrails().RecordIgnore(app_id_2, base::Time::Now());
  EXPECT_FALSE(ShouldShowIph(app_id));
}

TEST_F(WebAppGuardrailsIphTest, GlobalIgnoredPassedMuteTime) {
  guardrails().RecordIgnore(app_id_2, time_before_global_mute);
  EXPECT_TRUE(ShouldShowIph(app_id));
}

TEST_F(WebAppGuardrailsIphTest, TestIphAppIgnoredPassedMuteTime) {
  guardrails().RecordIgnore(app_id, time_before_app_mute);
  EXPECT_TRUE(ShouldShowIph(app_id));
}

TEST_F(WebAppGuardrailsIphTest, TestIphConsecutiveAppIgnore) {
  guardrails().RecordIgnore(app_id, time_before_app_mute);
  guardrails().RecordIgnore(app_id, base::Time::Now());
  guardrails().RecordIgnore(app_id, base::Time::Now());
  guardrails().RecordIgnore(app_id, base::Time::Now());
  EXPECT_FALSE(ShouldShowIph(app_id));
}

TEST_F(WebAppGuardrailsIphTest, TestGlobalConsecutiveAppIgnore) {
  guardrails().RecordIgnore(app_id_2, base::Time::Now());
  guardrails().RecordIgnore(app_id, base::Time::Now());
  guardrails().RecordIgnore(app_id_2, base::Time::Now());
  guardrails().RecordIgnore(app_id, base::Time::Now());
  guardrails().RecordIgnore(app_id_2, base::Time::Now());
  EXPECT_FALSE(ShouldShowIph(app_id));
}

class WebAppGuardrailsMLTest : public WebAppGuardrailsTest {
 public:
  WebAppGuardrailsMLTest()
      : guardrails_(WebAppPrefGuardrails::GetForMlInstallPrompt(prefs())) {}

 protected:
  WebAppPrefGuardrails& guardrails() { return guardrails_; }
  bool IsMLBlockedByGuardrails(const webapps::AppId& app) {
    return guardrails().IsBlockedByGuardrails(app);
  }

 private:
  WebAppPrefGuardrails guardrails_;
};

TEST_F(WebAppGuardrailsMLTest, MLInstallIgnored) {
  EXPECT_FALSE(
      GetTimeWebAppPref(app_id, kMlPromoPrefNames.last_ignore_time_name)
          .has_value());
  EXPECT_FALSE(
      GetIntWebAppPref(app_id, kMlPromoPrefNames.not_accepted_count_name)
          .has_value());

  guardrails().RecordIgnore(app_id, base::Time::Now());
  EXPECT_EQ(GetIntWebAppPref(app_id, kMlPromoPrefNames.not_accepted_count_name)
                .value_or(0),
            1);
  auto last_ignore_time =
      GetTimeWebAppPref(app_id, kMlPromoPrefNames.last_ignore_time_name);
  EXPECT_TRUE(last_ignore_time.has_value());
  {
    const auto& dict = prefs()->GetDict(prefs::kWebAppsAppAgnosticMlState);
    EXPECT_EQ(
        dict.FindInt(kMlPromoPrefNames.not_accepted_count_name).value_or(0), 1);
    EXPECT_EQ(
        base::ValueToTime(dict.Find(kMlPromoPrefNames.last_ignore_time_name)),
        last_ignore_time.value());
  }
}

TEST_F(WebAppGuardrailsMLTest, MLInstallDismissed) {
  EXPECT_FALSE(
      GetTimeWebAppPref(app_id, kMlPromoPrefNames.last_dismiss_time_name)
          .has_value());
  EXPECT_FALSE(
      GetIntWebAppPref(app_id, kMlPromoPrefNames.not_accepted_count_name)
          .has_value());

  guardrails().RecordDismiss(app_id, base::Time::Now());
  EXPECT_EQ(GetIntWebAppPref(app_id, kMlPromoPrefNames.not_accepted_count_name)
                .value_or(0),
            1);
  auto last_dismissed_time =
      GetTimeWebAppPref(app_id, kMlPromoPrefNames.last_dismiss_time_name);
  EXPECT_TRUE(last_dismissed_time.has_value());
  {
    const auto& dict = prefs()->GetDict(prefs::kWebAppsAppAgnosticMlState);
    EXPECT_EQ(
        dict.FindInt(kMlPromoPrefNames.not_accepted_count_name).value_or(0), 1);
    EXPECT_EQ(
        base::ValueToTime(dict.Find(kMlPromoPrefNames.last_dismiss_time_name)),
        last_dismissed_time.value());
  }
}

TEST_F(WebAppGuardrailsMLTest, MLAcceptResetsCounters) {
  guardrails().RecordIgnore(app_id, base::Time::Now());
  EXPECT_EQ(GetIntWebAppPref(app_id, kMlPromoPrefNames.not_accepted_count_name)
                .value_or(0),
            1);
  {
    const auto& dict = prefs()->GetDict(prefs::kWebAppsAppAgnosticMlState);
    EXPECT_EQ(
        dict.FindInt(kMlPromoPrefNames.not_accepted_count_name).value_or(0), 1);
  }
  guardrails().RecordAccept(app_id);
  EXPECT_EQ(GetIntWebAppPref(app_id, kMlPromoPrefNames.not_accepted_count_name)
                .value_or(0),
            0);
  {
    const auto& dict = prefs()->GetDict(prefs::kWebAppsAppAgnosticMlState);
    EXPECT_EQ(
        dict.FindInt(kMlPromoPrefNames.not_accepted_count_name).value_or(0), 0);
  }
}

TEST_F(WebAppGuardrailsMLTest, MLGuardrailConsecutiveAppSpecificIgnores) {
  guardrails().RecordIgnore(app_id, base::Time::Now());
  EXPECT_EQ(GetIntWebAppPref(app_id, kMlPromoPrefNames.not_accepted_count_name)
                .value_or(0),
            1);
  guardrails().RecordIgnore(app_id, base::Time::Now());
  EXPECT_EQ(GetIntWebAppPref(app_id, kMlPromoPrefNames.not_accepted_count_name)
                .value_or(0),
            2);
  guardrails().RecordIgnore(app_id, base::Time::Now());
  EXPECT_EQ(GetIntWebAppPref(app_id, kMlPromoPrefNames.not_accepted_count_name)
                .value_or(0),
            3);
  EXPECT_TRUE(IsMLBlockedByGuardrails(app_id));
  {
    const auto& dict = prefs()->GetDict(prefs::kWebAppsAppAgnosticMlState);
    EXPECT_EQ(*dict.FindString(kMlPromoPrefNames.block_reason_name),
              "app_specific_not_accept_count_exceeded:app_id");
  }
}

TEST_F(WebAppGuardrailsMLTest, MLGuardrailAppSpecificIgnoreForDays) {
  base::Time now_time = base::Time::Now();
  guardrails().RecordIgnore(app_id, now_time);
  auto ignore_time =
      GetTimeWebAppPref(app_id, kMlPromoPrefNames.last_ignore_time_name);
  EXPECT_TRUE(ignore_time.has_value());
  EXPECT_EQ(now_time, ignore_time);

  base::Time forwarded_time = base::Time::Now() + base::Days(1);
  guardrails().RecordIgnore(app_id, forwarded_time);
  auto ignore_time_new =
      GetTimeWebAppPref(app_id, kMlPromoPrefNames.last_ignore_time_name);
  EXPECT_TRUE(ignore_time_new.has_value());
  EXPECT_EQ(forwarded_time, ignore_time_new);
  EXPECT_TRUE(IsMLBlockedByGuardrails(app_id));
  {
    const auto& dict = prefs()->GetDict(prefs::kWebAppsAppAgnosticMlState);
    EXPECT_EQ(*dict.FindString(kMlPromoPrefNames.block_reason_name),
              "app_specific_ignore_days_hit:app_id");
  }
}

TEST_F(WebAppGuardrailsMLTest, MLGuardrailAppSpecificDismissForDays) {
  base::Time now_time = base::Time::Now();
  guardrails().RecordDismiss(app_id, now_time);
  auto dismiss_time =
      GetTimeWebAppPref(app_id, kMlPromoPrefNames.last_dismiss_time_name);
  EXPECT_TRUE(dismiss_time.has_value());
  EXPECT_EQ(now_time, dismiss_time);

  // Dismissing the same app within 14 days should trigger the guardrail
  // response.
  int randDays = base::RandIntInclusive(1, 13);
  base::Time forwarded_time = base::Time::Now() + base::Days(randDays);
  guardrails().RecordDismiss(app_id, forwarded_time);
  auto dismiss_time_new =
      GetTimeWebAppPref(app_id, kMlPromoPrefNames.last_dismiss_time_name);
  EXPECT_TRUE(dismiss_time_new.has_value());
  EXPECT_EQ(forwarded_time, dismiss_time_new);
  EXPECT_TRUE(IsMLBlockedByGuardrails(app_id));
  {
    const auto& dict = prefs()->GetDict(prefs::kWebAppsAppAgnosticMlState);
    EXPECT_EQ(*dict.FindString(kMlPromoPrefNames.block_reason_name),
              "app_specific_dismiss_days_hit:app_id");
  }
}

TEST_F(WebAppGuardrailsMLTest, MLGuardrailConsecutiveAppAgnosticIgnores) {
  const webapps::AppId& app_id1 = "app1";
  const webapps::AppId& app_id2 = "app2";
  const webapps::AppId& app_id3 = "app3";
  const webapps::AppId& app_id4 = "app4";
  const webapps::AppId& app_id5 = "app5";
  guardrails().RecordIgnore(app_id1, base::Time::Now());
  {
    const auto& dict = prefs()->GetDict(prefs::kWebAppsAppAgnosticMlState);
    EXPECT_EQ(
        dict.FindInt(kMlPromoPrefNames.not_accepted_count_name).value_or(0), 1);
  }
  guardrails().RecordDismiss(app_id2, base::Time::Now());
  {
    const auto& dict = prefs()->GetDict(prefs::kWebAppsAppAgnosticMlState);
    EXPECT_EQ(
        dict.FindInt(kMlPromoPrefNames.not_accepted_count_name).value_or(0), 2);
  }
  guardrails().RecordDismiss(app_id3, base::Time::Now());
  {
    const auto& dict = prefs()->GetDict(prefs::kWebAppsAppAgnosticMlState);
    EXPECT_EQ(
        dict.FindInt(kMlPromoPrefNames.not_accepted_count_name).value_or(0), 3);
  }
  guardrails().RecordDismiss(app_id4, base::Time::Now());
  {
    const auto& dict = prefs()->GetDict(prefs::kWebAppsAppAgnosticMlState);
    EXPECT_EQ(
        dict.FindInt(kMlPromoPrefNames.not_accepted_count_name).value_or(0), 4);
  }
  guardrails().RecordIgnore(app_id5, base::Time::Now());
  {
    const auto& dict = prefs()->GetDict(prefs::kWebAppsAppAgnosticMlState);
    EXPECT_EQ(
        dict.FindInt(kMlPromoPrefNames.not_accepted_count_name).value_or(0), 5);
  }
  EXPECT_TRUE(IsMLBlockedByGuardrails(app_id));
  {
    const auto& dict = prefs()->GetDict(prefs::kWebAppsAppAgnosticMlState);
    EXPECT_EQ(*dict.FindString(kMlPromoPrefNames.block_reason_name),
              "global_not_accept_count_exceeded");
  }
}

TEST_F(WebAppGuardrailsMLTest, MLGuardrailConsecutiveAppAgnosticIgnoreDays) {
  const webapps::AppId& app_id1 = "app1";
  guardrails().RecordIgnore(app_id1, base::Time::Now());
  auto last_ignored_time =
      GetTimeWebAppPref(app_id1, kMlPromoPrefNames.last_ignore_time_name);
  EXPECT_TRUE(last_ignored_time.has_value());
  {
    const auto& dict = prefs()->GetDict(prefs::kWebAppsAppAgnosticMlState);
    EXPECT_EQ(
        base::ValueToTime(dict.Find(kMlPromoPrefNames.last_ignore_time_name)),
        last_ignored_time.value());
  }
  EXPECT_TRUE(IsMLBlockedByGuardrails(app_id));
  {
    const auto& dict = prefs()->GetDict(prefs::kWebAppsAppAgnosticMlState);
    EXPECT_EQ(*dict.FindString(kMlPromoPrefNames.block_reason_name),
              "global_ignore_days_hit");
  }
}

TEST_F(WebAppGuardrailsMLTest, MLGuardrailConsecutiveAppAgnosticDismissDays) {
  const webapps::AppId& app_id1 = "app1";

  // Dismissing any app within the last 7 days should trigger the app agnostic
  // dismiss guardrail response.
  int randDays = base::RandIntInclusive(0, 6);
  guardrails().RecordDismiss(app_id1, base::Time::Now() - base::Days(randDays));
  auto last_dismissed_time =
      GetTimeWebAppPref(app_id1, kMlPromoPrefNames.last_dismiss_time_name);
  EXPECT_TRUE(last_dismissed_time.has_value());
  {
    const auto& dict = prefs()->GetDict(prefs::kWebAppsAppAgnosticMlState);
    EXPECT_EQ(
        base::ValueToTime(dict.Find(kMlPromoPrefNames.last_dismiss_time_name)),
        last_dismissed_time.value());
  }
  EXPECT_TRUE(IsMLBlockedByGuardrails(app_id));
  {
    const auto& dict = prefs()->GetDict(prefs::kWebAppsAppAgnosticMlState);
    EXPECT_EQ(*dict.FindString(kMlPromoPrefNames.block_reason_name),
              "global_dismiss_days_hit");
  }
}

// TODO(b/308774918): Consider using ScopedTimeClockOverrides instead of
// moving time forward.
class WebAppPrefsMLGuardrailsMaxStorageTest : public WebAppTest {
 public:
  WebAppPrefsMLGuardrailsMaxStorageTest()
      : WebAppTest(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {
    WebAppPrefGuardrails::RegisterProfilePrefs(prefs_.registry());
    base::FieldTrialParams params;
    params["max_days_to_store_guardrails"] = "2";
    feature_list_.InitAndEnableFeatureWithParameters(
        webapps::features::kWebAppsEnableMLModelForPromotion,
        std::move(params));
  }

  void SetUp() override { WebAppTest::SetUp(); }

  bool IsMLPromoBlockedTimeSet() {
    const auto& dict = prefs()->GetDict(prefs::kWebAppsAppAgnosticMlState);
    return dict.contains(kMlPromoPrefNames.all_blocked_time_name);
  }

  std::optional<base::Time> GetMLPromoBlockedTime() {
    const auto& dict = prefs()->GetDict(prefs::kWebAppsAppAgnosticMlState);
    auto* value =
        dict.FindByDottedPath(kMlPromoPrefNames.all_blocked_time_name);
    EXPECT_NE(value, nullptr) << " kAllMLPromosBlockedTime not set.";
    return base::ValueToTime(value);
  }

  void FastForwardTimeForMaxDaysToStoreGuardrails() {
    task_environment()->FastForwardBy(base::Days(
        webapps::features::kMaxDaysForMLPromotionGuardrailStorage.Get()));
  }

  bool IsMLBlockedByGuardrails(const webapps::AppId& app) {
    return guardrails().IsBlockedByGuardrails(app);
  }

  // Mimic a user blocked by guardrails for continuous 5 dismissals or ignores.
  void ForceMLPromoAgnosticGuardrailsBlocked() {
    const webapps::AppId& app_id1 = "app1";
    const webapps::AppId& app_id2 = "app2";
    const webapps::AppId& app_id3 = "app3";
    const webapps::AppId& app_id4 = "app4";
    const webapps::AppId& app_id5 = "app5";
    guardrails().RecordIgnore(app_id1, base::Time::Now());
    task_environment()->FastForwardBy(base::Milliseconds(1));
    guardrails().RecordDismiss(app_id2, base::Time::Now());
    task_environment()->FastForwardBy(base::Milliseconds(1));
    guardrails().RecordIgnore(app_id3, base::Time::Now());
    task_environment()->FastForwardBy(base::Milliseconds(1));
    guardrails().RecordDismiss(app_id4, base::Time::Now());
    task_environment()->FastForwardBy(base::Milliseconds(1));
    guardrails().RecordIgnore(app_id5, base::Time::Now());
    task_environment()->FastForwardBy(base::Milliseconds(1));
    EXPECT_TRUE(IsMLBlockedByGuardrails("app_id"));
    task_environment()->FastForwardBy(base::Milliseconds(1));
  }

 protected:
  WebAppPrefGuardrails guardrails() {
    return WebAppPrefGuardrails::GetForMlInstallPrompt(prefs());
  }
  sync_preferences::TestingPrefServiceSyncable* prefs() { return &prefs_; }

 private:
  base::test::ScopedFeatureList feature_list_;
  sync_preferences::TestingPrefServiceSyncable prefs_;
};

TEST_F(WebAppPrefsMLGuardrailsMaxStorageTest,
       BasicBehaviorGuardrailBlockedAfter5NonAccepts) {
  EXPECT_FALSE(IsMLPromoBlockedTimeSet());

  ForceMLPromoAgnosticGuardrailsBlocked();
  EXPECT_TRUE(IsMLPromoBlockedTimeSet());
  EXPECT_TRUE(GetMLPromoBlockedTime().has_value());
}

TEST_F(WebAppPrefsMLGuardrailsMaxStorageTest,
       MLBlockedPrefClearedOnInstallAccept) {
  EXPECT_FALSE(IsMLPromoBlockedTimeSet());

  ForceMLPromoAgnosticGuardrailsBlocked();
  EXPECT_TRUE(IsMLPromoBlockedTimeSet());

  guardrails().RecordAccept("app");
  EXPECT_FALSE(IsMLPromoBlockedTimeSet());
}

TEST_F(WebAppPrefsMLGuardrailsMaxStorageTest,
       MoreThan5NonAcceptsDoesNotUpdateBlockTime) {
  ForceMLPromoAgnosticGuardrailsBlocked();

  // Triggering a non-acceptance of the dialog after already not accepting 5
  // times should not update the time the blocked pref was set.
  const base::Time time_ml_install_dismissed_again = base::Time::Now();
  guardrails().RecordDismiss("app", time_ml_install_dismissed_again);
  EXPECT_TRUE(IsMLPromoBlockedTimeSet());

  std::optional<base::Time> ml_promo_time_blocked_from_pref =
      GetMLPromoBlockedTime();
  EXPECT_TRUE(ml_promo_time_blocked_from_pref.has_value());
  EXPECT_NE(ml_promo_time_blocked_from_pref, time_ml_install_dismissed_again);
}

TEST_F(WebAppPrefsMLGuardrailsMaxStorageTest, ClearAndResetGuardrails) {
#if BUILDFLAG(IS_MAC)
  // TODO(crbug.com/434660312): Re-enable on macOS 26 once issues with
  // unexpected test timeout failures are resolved.
  if (base::mac::MacOSMajorVersion() == 26) {
    GTEST_SKIP() << "Disabled on macOS Tahoe.";
  }
#endif
  ForceMLPromoAgnosticGuardrailsBlocked();
  // This is important so that the global guardrail dismisses are not hit, and
  // tests can verify a clean guardrail reset, i.e. once reset, an app is
  // fully unblocked.
  task_environment()->FastForwardBy(
      base::Days(*kMlPromoGuardrails.global_mute_after_dismiss_days));
  EXPECT_TRUE(IsMLPromoBlockedTimeSet());

  FastForwardTimeForMaxDaysToStoreGuardrails();

  EXPECT_FALSE(IsMLBlockedByGuardrails("app"));
  EXPECT_FALSE(IsMLPromoBlockedTimeSet());

  const base::DictValue& dict =
      prefs()->GetDict(prefs::kWebAppsAppAgnosticMlState);
  std::optional<int> agnostic_not_installed_count =
      dict.FindInt(kMlPromoPrefNames.not_accepted_count_name);
  EXPECT_TRUE(agnostic_not_installed_count.has_value());
  EXPECT_EQ(agnostic_not_installed_count, 0);
}

#if !BUILDFLAG(IS_CHROMEOS)
class WebAppPrefsLinkCapturingIPHGuardrailsTest : public WebAppTest {
 public:
  WebAppPrefsLinkCapturingIPHGuardrailsTest()
      : WebAppTest(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {
    WebAppPrefGuardrails::RegisterProfilePrefs(prefs_.registry());
    base::FieldTrialParams params;
    params["link_capturing_guardrail_storage_duration"] = "2";
    feature_list_.InitAndEnableFeatureWithParameters(
        features::kPwaNavigationCapturing, std::move(params));
  }

  void SetUp() override { WebAppTest::SetUp(); }

  bool IsDesktopIphBlockedTimeSet() {
    const auto& dict =
        prefs()->GetDict(prefs::kWebAppsAppAgnosticIPHLinkCapturingState);
    return dict.contains(
        kIPHNavigationCapturingPrefNames.all_blocked_time_name);
  }

  std::optional<base::Time> GetIphBlockedTime() {
    const auto& dict =
        prefs()->GetDict(prefs::kWebAppsAppAgnosticIPHLinkCapturingState);
    auto* value = dict.FindByDottedPath(
        kIPHNavigationCapturingPrefNames.all_blocked_time_name);
    EXPECT_NE(value, nullptr) << " ";
    return base::ValueToTime(value);
  }

  void FastForwardTimeForMaxDaysToStoreGuardrails() {
    task_environment()->FastForwardBy(base::Days(
        features::kNavigationCapturingIPHGuardrailStorageDuration.Get()));
  }

  bool IsDesktopLinkCapturingIphBlocked(const webapps::AppId& app) {
    return guardrails().IsBlockedByGuardrails(app);
  }

  void ForceUserBlockedOnIphGuardrails() {
    const std::vector<webapps::AppId> apps{"app1", "app2", "app3",
                                           "app4", "app5", "app6"};
    for (const webapps::AppId& app : apps) {
      guardrails().RecordDismiss(app, base::Time::Now());
      task_environment()->FastForwardBy(base::Milliseconds(1));
    }
    EXPECT_TRUE(IsDesktopLinkCapturingIphBlocked("app_id"));
    task_environment()->FastForwardBy(base::Milliseconds(1));
  }

 protected:
  WebAppPrefGuardrails guardrails() {
    return WebAppPrefGuardrails::GetForNavigationCapturingIph(prefs());
  }
  sync_preferences::TestingPrefServiceSyncable* prefs() { return &prefs_; }

 private:
  base::test::ScopedFeatureList feature_list_;
  sync_preferences::TestingPrefServiceSyncable prefs_;
};

TEST_F(WebAppPrefsLinkCapturingIPHGuardrailsTest, Dismiss) {
  base::Time dismiss_time = base::Time::Now();
  guardrails().RecordDismiss(app_id, dismiss_time);
  {
    const auto& dict =
        prefs()->GetDict(prefs::kWebAppsAppAgnosticIPHLinkCapturingState);
    EXPECT_EQ(
        dict.FindInt(kIPHNavigationCapturingPrefNames.not_accepted_count_name)
            .value_or(0),
        1);
    EXPECT_EQ(base::ValueToTime(dict.Find(
                  kIPHNavigationCapturingPrefNames.last_dismiss_time_name)),
              dismiss_time);
  }
}

TEST_F(WebAppPrefsLinkCapturingIPHGuardrailsTest, Accept) {
  guardrails().RecordDismiss(app_id, base::Time::Now());
  {
    const auto& dict =
        prefs()->GetDict(prefs::kWebAppsAppAgnosticIPHLinkCapturingState);
    EXPECT_EQ(
        dict.FindInt(kIPHNavigationCapturingPrefNames.not_accepted_count_name)
            .value_or(0),
        1);
  }
  guardrails().RecordAccept(app_id);
  {
    const auto& dict =
        prefs()->GetDict(prefs::kWebAppsAppAgnosticIPHLinkCapturingState);
    EXPECT_EQ(
        dict.FindInt(kIPHNavigationCapturingPrefNames.not_accepted_count_name)
            .value_or(0),
        0);
  }
}

TEST_F(WebAppPrefsLinkCapturingIPHGuardrailsTest,
       GuardrailsBlockedAfter6Dismisses) {
  EXPECT_FALSE(IsDesktopIphBlockedTimeSet());

  ForceUserBlockedOnIphGuardrails();
  EXPECT_TRUE(IsDesktopIphBlockedTimeSet());
  EXPECT_TRUE(GetIphBlockedTime().has_value());
}

TEST_F(WebAppPrefsLinkCapturingIPHGuardrailsTest, ClearAndResetGuardrails) {
#if BUILDFLAG(IS_MAC)
  // TODO(crbug.com/434660312): Re-enable on macOS 26 once issues with
  // unexpected test timeout failures are resolved.
  if (base::mac::MacOSMajorVersion() == 26) {
    GTEST_SKIP() << "Disabled on macOS Tahoe.";
  }
#endif
  ForceUserBlockedOnIphGuardrails();
  EXPECT_TRUE(IsDesktopIphBlockedTimeSet());

  FastForwardTimeForMaxDaysToStoreGuardrails();

  EXPECT_FALSE(IsDesktopLinkCapturingIphBlocked("app"));
  EXPECT_FALSE(IsDesktopIphBlockedTimeSet());

  const base::DictValue& dict =
      prefs()->GetDict(prefs::kWebAppsAppAgnosticIPHLinkCapturingState);
  std::optional<int> agnostic_not_installed_count =
      dict.FindInt(kIPHNavigationCapturingPrefNames.not_accepted_count_name);
  EXPECT_TRUE(agnostic_not_installed_count.has_value());
  EXPECT_EQ(*agnostic_not_installed_count, 0);
}

#endif  // !BUILDFLAG(IS_CHROMEOS)

}  // namespace web_app
