// Copyright 2020 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/signin/dice_web_signin_interceptor.h"

#include <optional>
#include <string>
#include <string_view>

#include "base/check.h"
#include "base/check_deref.h"
#include "base/functional/callback_helpers.h"
#include "base/hash/hash.h"
#include "base/i18n/case_conversion.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/field_trial_params.h"
#include "base/metrics/histogram_functions.h"
#include "base/not_fatal_until.h"
#include "base/notreached.h"
#include "base/strings/strcat.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/time.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/enterprise/browser_management/management_service_factory.h"
#include "chrome/browser/enterprise/signin/profile_management_disclaimer_service.h"
#include "chrome/browser/enterprise/signin/profile_management_disclaimer_service_factory.h"
#include "chrome/browser/enterprise/util/managed_browser_utils.h"
#include "chrome/browser/net/system_network_context_manager.h"
#include "chrome/browser/new_tab_page/chrome_colors/generated_colors_info.h"
#include "chrome/browser/password_manager/chrome_password_manager_client.h"
#include "chrome/browser/policy/chrome_browser_policy_connector.h"
#include "chrome/browser/policy/cloud/user_policy_signin_service_factory.h"
#include "chrome/browser/policy/profile_policy_connector.h"
#include "chrome/browser/profiles/profile_attributes_entry.h"
#include "chrome/browser/profiles/profile_attributes_storage.h"
#include "chrome/browser/profiles/profile_avatar_icon_util.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/profiles/profile_metrics.h"
#include "chrome/browser/profiles/profiles_state.h"
#include "chrome/browser/search_engine_choice/search_engine_choice_dialog_service.h"
#include "chrome/browser/search_engines/template_url_service_factory.h"
#include "chrome/browser/signin/account_preview_data_service_factory.h"
#include "chrome/browser/signin/chrome_signin_pref_names.h"
#include "chrome/browser/signin/dice_intercepted_session_startup_helper.h"
#include "chrome/browser/signin/dice_signed_in_profile_creator.h"
#include "chrome/browser/signin/dice_web_signin_interceptor_factory.h"
#include "chrome/browser/signin/identity_manager_factory.h"
#include "chrome/browser/signin/signin_hats_util.h"
#include "chrome/browser/signin/signin_util.h"
#include "chrome/browser/signin/web_signin_interceptor.h"
#include "chrome/browser/themes/theme_service.h"
#include "chrome/browser/themes/theme_service_factory.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/browser_window/public/profile_browser_collection.h"
#include "chrome/browser/ui/hats/survey_config.h"
#include "chrome/browser/ui/passwords/manage_passwords_ui_controller.h"
#include "chrome/browser/ui/profiles/profile_colors_util.h"
#include "chrome/browser/ui/signin/dice_web_signin_interceptor_delegate.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/browser/ui/webui/signin/signin_ui_error.h"
#include "chrome/common/channel_info.h"
#include "chrome/common/pref_names.h"
#include "chrome/common/themes/autogenerated_theme_util.h"
#include "components/metrics/profile_metrics_service.h"
#include "components/password_manager/core/browser/password_manager.h"
#include "components/password_manager/core/common/password_manager_ui.h"
#include "components/policy/core/browser/signin/profile_separation_policies.h"
#include "components/policy/core/browser/signin/user_cloud_signin_restriction_policy_fetcher.h"
#include "components/policy/core/common/policy_map.h"
#include "components/policy/core/common/policy_namespace.h"
#include "components/policy/core/common/policy_service.h"
#include "components/policy/core/common/policy_utils.h"
#include "components/policy/policy_constants.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "components/search_engines/search_engine_choice/search_engine_choice_utils.h"
#include "components/search_engines/template_url_data.h"
#include "components/search_engines/template_url_service.h"
#include "components/signin/public/base/consent_level.h"
#include "components/signin/public/base/signin_metrics.h"
#include "components/signin/public/base/signin_pref_names.h"
#include "components/signin/public/base/signin_prefs.h"
#include "components/signin/public/base/signin_switches.h"
#include "components/signin/public/identity_manager/account_capabilities.h"
#include "components/signin/public/identity_manager/account_info.h"
#include "components/signin/public/identity_manager/account_managed_status_finder.h"
#include "components/signin/public/identity_manager/accounts_mutator.h"
#include "components/signin/public/identity_manager/identity_manager.h"
#include "components/signin/public/identity_manager/identity_utils.h"
#include "components/signin/public/identity_manager/primary_account_mutator.h"
#include "components/signin/public/identity_manager/tribool.h"
#include "components/sync/base/features.h"
#include "google_apis/gaia/core_account_id.h"
#include "google_apis/gaia/gaia_auth_util.h"
#include "google_apis/gaia/gaia_id.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/mojom/themes.mojom.h"

namespace {

constexpr char kChromeSingInInterceptionSupervisionStateHistogramPrefix[] =
    "Signin.Intercept.Heuristic.SupervisionState";

constexpr size_t kMaxChromeSigninInterceptionDismissCount = 5;

// The user will only see the Chrome Signin bubble reprompt a maximum of 4 times
// (not including the initial time the bubble was declined).
static constexpr int kMaxChromeSigninBubbleRepromptCountAllowed = 4;
// The Chrome Signin bubble can be reprompted only if a minimum duration time
// has passed since the last bubble was shown (either initial bubble or a
// reprompt).
static constexpr base::TimeDelta kMinimumDurationForChromeSigninBubbleReprompt =
    base::Days(60);

// Helper function to return the primary account info. The returned info is
// empty if there is no primary account, and non-empty otherwise. Extended
// fields may be missing if they are not available.
AccountInfo GetPrimaryAccountInfo(signin::IdentityManager* manager) {
  CoreAccountInfo primary_core_account_info =
      manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin);
  if (primary_core_account_info.IsEmpty()) {
    return AccountInfo();
  }

  AccountInfo primary_account_info =
      manager->FindExtendedAccountInfo(primary_core_account_info);

  if (!primary_account_info.IsEmpty()) {
    return primary_account_info;
  }

  // Return an AccountInfo without extended fields, based on the core info.
  return AccountInfo::Builder(primary_core_account_info).Build();
}

// This function is based on the email rather than the GaiaID, because the Gaia
// ID is not always available at the start of the interception process.
bool IsFirstAccount(signin::IdentityManager* manager, std::string_view email) {
  std::vector<CoreAccountInfo> accounts_in_chrome =
      manager->GetAccountsWithRefreshTokens();
  // There is not guarantee that the added email/account have a refresh token
  // yet.
  // So we either check that no account exists, or that the only account that
  // has a refresh token is the account we are interested in, to make sure it is
  // the first account.
  return accounts_in_chrome.size() == 0 ||
         (accounts_in_chrome.size() == 1 &&
          gaia::AreEmailsSame(email, accounts_in_chrome[0].email));
}

// Returns the time elapsed since the last Chrome Signin Bubble decline. It is
// expected that a first decline was made and that the setting was not manually
// overridden by the user to do not sign in automatically. In case the
// conditions are not met to retrieve a proper delta time, a default value of 0
// is returned.
base::TimeDelta GetTimeSinceLastChromeSigninDecline(
    const SigninPrefs& signin_prefs,
    const GaiaId& gaia_id) {
  std::optional<base::Time> last_bubble_decline_time =
      signin_prefs.GetChromeSigninInterceptionLastBubbleDeclineTime(gaia_id);
  // If the value does not exist, this means that the user is either not in the
  // `ChromeSigninUserChoice::kDoNotSignin` or they explicitly changed the
  // setting from the settings page.
  if (!last_bubble_decline_time.has_value()) {
    return base::TimeDelta();
  }

  return base::Time::Now() - last_bubble_decline_time.value();
}

// When a user declines the bubble, potential reprompts are allowed in future
// based on the time that passes since the bubble decline and the last
// reprompts, with a fixed amount of allowed reprompts per account.
// Reprompts are not allowed if the user explicitly set the Chrome Signin
// setting to do not signin.
bool ShouldAllowChromeSigninBubbleReprompt(const SigninPrefs& signin_prefs,
                                           const GaiaId& gaia_id) {
  // Reprompts are only allowed if the user choice is to not sign in
  // automatically.
  if (signin_prefs.GetChromeSigninInterceptionUserChoice(gaia_id) !=
      ChromeSigninUserChoice::kDoNotSignin) {
    return false;
  }

  // Maximum reprompt count check.
  int reprompt_count = signin_prefs.GetChromeSigninBubbleRepromptCount(gaia_id);
  if (reprompt_count >= kMaxChromeSigninBubbleRepromptCountAllowed) {
    return false;
  }

  // If the user has set the setting manually, this value will be 0, making sure
  // it does not satisfy the minimum requirement for repromts.
  base::TimeDelta time_since_last_decline =
      GetTimeSinceLastChromeSigninDecline(signin_prefs, gaia_id);

  // Minimum duration since last chrome signin decline check.
  return time_since_last_decline >=
         kMinimumDurationForChromeSigninBubbleReprompt;
}

// Set showing the bubble as a reprompt if the user previously declined the
// bubble and did not override the choice in the settings page (having a
// declined choice time).
void MaybeUpdateRepromptInfoAfterDecline(SigninPrefs& signin_prefs,
                                         const GaiaId& gaia_id) {
  // Check if this is was a reprompt.
  if (!ShouldAllowChromeSigninBubbleReprompt(signin_prefs, gaia_id)) {
    return;
  }

  // Record this value before updating the new decline time.
  // There is no need to record values with less than the minimum duration, and
  // recording max values with up to 1 year (365 days). There is no need for
  // more granularity.
  base::UmaHistogramCustomCounts(
      "Signin.Intercept.ChromeSignin.NumberOfDaysSinceLastDecline",
      GetTimeSinceLastChromeSigninDecline(signin_prefs, gaia_id).InDays(),
      /*min=*/kMinimumDurationForChromeSigninBubbleReprompt.InDays(),
      /*exclusive_max=*/365 /*days*/,
      /*buckets=*/50);

  signin_prefs.SetChromeSigninInterceptionLastBubbleDeclineTime(
      gaia_id, base::Time::Now());
  int new_reprompt_count =
      signin_prefs.IncrementChromeSigninBubbleRepromptCount(gaia_id);
  base::UmaHistogramExactLinear("Signin.Intercept.ChromeSignin.RepromptCount",
                                new_reprompt_count,
                                kMaxChromeSigninBubbleRepromptCountAllowed + 1);
}

// Returns whether we can offer sign-in for the given account. Returns false
// error if the account does not match the signin pattern policy.
bool IsUsernameAllowedForInterceptionByPattern(std::string_view email) {
  return !syncer::IsReplaceSyncPromosWithSignInPromosEnabled() ||
         signin::IsUsernameAllowedByPatternFromPrefs(
             g_browser_process->local_state(), email);
}

// Returns true if we have the minimum extended account information needed to
// make a best-effort intercept heuristic decision. If we fail to retrieve
// this information we will cancel the interception completely.
// Returns false otherwise.
bool IsRequiredExtendedAccountInfoAvailable(const AccountInfo& account_info) {
  return account_info.IsValid() &&
         account_info.CanApplyAccountLevelEnterprisePolicies() !=
             signin::Tribool::kUnknown;
}

// Returns true if enterprise separation is required.
// Returns false is enterprise separation is not required.
// Returns no value if info is required to determine if enterprise separation
// is required. If `profile_separation_policies` is `std::nullopt` then the
// user cloud profile separation policies have not yet been fetched.
// This function is based on the email rather than the GaiaID, because the Gaia
// ID is not always available at the start of the interception process.
std::optional<bool> EnterpriseSeparationMaybeRequired(
    Profile* profile,
    signin::IdentityManager* identity_manager,
    std::string_view email,
    bool is_new_account_interception,
    const std::optional<policy::ProfileSeparationPolicies>&
        intercepted_profile_separation_policies,
    bool expects_intercepted_profile_separation_policies_for_testing) {
  CoreAccountInfo primary_core_account_info =
      identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin);

  // Enforce separation for new accounts or re-auth of existing secondary
  // accounts.
  if ((is_new_account_interception ||
       !gaia::AreEmailsSame(primary_core_account_info.email, email)) &&
      !signin_util::IsAccountExemptedFromEnterpriseProfileSeparation(profile,
                                                                     email)) {
    return true;
  }

  // No enterprise separation required for consumer accounts.
  if (!signin::AccountManagedStatusFinder::MayBeEnterpriseUserBasedOnEmail(
          email)) {
    return false;
  }

  auto intercepted_account_info =
      identity_manager->FindExtendedAccountInfoByEmailAddress(email);
  // If the account info is not found, we need to wait for the info to be
  // available.
  if (!IsRequiredExtendedAccountInfoAvailable(intercepted_account_info)) {
    return std::nullopt;
  }
  // If the intercepted account is not managed, no interception required.
  if (!signin::TriboolToBoolOrDie(
          intercepted_account_info.CanApplyAccountLevelEnterprisePolicies())) {
    return false;
  }
  // If `profile` requires enterprise profile separation, return true.
  // Here we only check the legacy policy by passing an empty email since the
  // new ProfileSeparationSetting policy is checked early in the function.
  if (signin_util::IsProfileSeparationEnforcedByProfile(
          profile,
          /*intercepted_account_email=*/std::string())) {
    return true;
  }

  if (signin_util::IsProfileSeparationEnforcedByPolicies(
          intercepted_profile_separation_policies.value_or(
              policy::ProfileSeparationPolicies()))) {
    return true;
  }

  // If we still do not know if profile separation is required, the account
  // level policies for the intercepted account must be fetched if possible.
  // If `g_browser_process->system_network_context_manager()` is  equal to
  // nullptr, we are probably in tests and should not try to fetch any policies.
  // If `expects_intercepted_profile_separation_policies_for_testing`, even if
  // we are in tests, we have a value set locally that will not require us to
  // make a network call.
  // Fetching the value will not be possible isf we cannot make network calls
  // nor have a value set locally for testing.
  if (!intercepted_profile_separation_policies.has_value() &&
      (g_browser_process->system_network_context_manager() ||
       expects_intercepted_profile_separation_policies_for_testing)) {
    return std::nullopt;
  }

  return false;
}

bool IsPrimaryAccountInterception(const CoreAccountId& account_id,
                                  signin::IdentityManager* identity_manager) {
  return account_id ==
         identity_manager->GetPrimaryAccountId(signin::ConsentLevel::kSignin);
}

bool IsReauthPrimaryAccount(bool new_account_interception,
                            const CoreAccountId& account_id,
                            signin::IdentityManager* identity_manager) {
  return !new_account_interception &&
         IsPrimaryAccountInterception(account_id, identity_manager);
}

// Returns true if the current state is inconsistent, which happens by
// signing into the web with an account B while being in sign in pending state
// with account A. Returns false otherwise. Used for metrics.
bool IsInInconsistentStateWithPrimaryAccount(
    const CoreAccountId& account_id,
    signin::IdentityManager* identity_manager) {
  return signin_util::IsSigninPending(identity_manager) &&
         identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin)
                 .account_id != account_id;
}

SinginInterceptSupervisionState CapabilityToSupervisionState(
    const AccountCapabilities& capabilities) {
  const signin::Tribool is_supervised =
      capabilities.is_subject_to_parental_controls();
  switch (is_supervised) {
    case (signin::Tribool::kTrue):
      return SinginInterceptSupervisionState::kSupervisedUser;
    case (signin::Tribool::kFalse):
      return SinginInterceptSupervisionState::kRegularUser;
    case (signin::Tribool::kUnknown): {
      return SinginInterceptSupervisionState::kUnknownSupervision;
    }
  }
  NOTREACHED();
}

void MaybeRecordSupervisedUserStateMetrics(
    const AccountInfo& intercepted_account_info,
    WebSigninInterceptor::SigninInterceptionType interception_type) {
  if (interception_type !=
          WebSigninInterceptor::SigninInterceptionType::kChromeSignin &&
      interception_type !=
          WebSigninInterceptor::SigninInterceptionType::kMultiUser &&
      interception_type !=
          WebSigninInterceptor::SigninInterceptionType::kProfileSwitch) {
    return;
  }

  base::UmaHistogramEnumeration(
      base::StrCat({kChromeSingInInterceptionSupervisionStateHistogramPrefix,
                    DiceWebSigninInterceptorDelegate::GetHistogramSuffix(
                        interception_type)}),
      CapabilityToSupervisionState(
          intercepted_account_info.GetAccountCapabilities()));
}

}  // namespace

DiceWebSigninInterceptor::DiceWebSigninInterceptor(
    Profile* profile,
    std::unique_ptr<WebSigninInterceptor::Delegate> delegate,
    metrics::ProfileMetricsService* profile_metrics_service)
    : profile_(profile),
      identity_manager_(IdentityManagerFactory::GetForProfile(profile)),
      delegate_(std::move(delegate)),
      profile_metrics_service_(CHECK_DEREF(profile_metrics_service)),
      state_(std::make_unique<ResetableState>()) {
  DCHECK(profile_);
  DCHECK(identity_manager_);
  DCHECK(delegate_);
}

DiceWebSigninInterceptor::~DiceWebSigninInterceptor() = default;

DiceWebSigninInterceptor::ResetableState::ResetableState() = default;
DiceWebSigninInterceptor::ResetableState::~ResetableState() = default;

DiceWebSigninInterceptor::ProfilePresets::ProfilePresets(SkColor profile_color)
    : profile_color(profile_color) {}

DiceWebSigninInterceptor::ProfilePresets::ProfilePresets(ProfilePresets&&) =
    default;
DiceWebSigninInterceptor::ProfilePresets&
DiceWebSigninInterceptor::ProfilePresets::operator=(ProfilePresets&&) = default;

DiceWebSigninInterceptor::ProfilePresets::~ProfilePresets() = default;

// static
void DiceWebSigninInterceptor::RegisterProfilePrefs(
    user_prefs::PrefRegistrySyncable* registry) {
  registry->RegisterDictionaryPref(prefs::kProfileCreationInterceptionDeclined);
  registry->RegisterBooleanPref(prefs::kSigninInterceptionEnabled, true);
  registry->RegisterStringPref(prefs::kManagedAccountsSigninRestriction,
                               std::string());
  registry->RegisterStringPref(prefs::kSigninInterceptionIDPCookiesUrl,
                               std::string());
  registry->RegisterBooleanPref(
      prefs::kManagedAccountsSigninRestrictionScopeMachine, false);
  registry->RegisterIntegerPref(prefs::kProfileSeparationSettings, 0);
  registry->RegisterIntegerPref(prefs::kProfileSeparationDataMigrationSettings,
                                1);
  registry->RegisterListPref(prefs::kProfileSeparationDomainExceptionList);
  registry->RegisterStringPref(
      prefs::kUserCloudSigninPolicyResponseFromPolicyTestPage, std::string());
}

std::optional<SigninInterceptionHeuristicOutcome>
DiceWebSigninInterceptor::GetHeuristicOutcome(
    bool is_new_account,
    bool is_sync_signin,
    std::string_view email,
    const GaiaId& gaia_id,
    const ProfileAttributesEntry** entry,
    signin::Tribool primary_is_connected) const {
  bool signin_interception_enabled =
      profile_->GetPrefs()->GetBoolean(prefs::kSigninInterceptionEnabled);

  if (is_sync_signin) {
    // Do not intercept signins from the Sync startup flow.
    // Note: |is_sync_signin| is an approximation, and in rare cases it may be
    // true when in fact the signin was not a sync signin. In this case the
    // interception is missed.
    return SigninInterceptionHeuristicOutcome::kAbortSyncSignin;
  }

  // When Gaia indicates that the account is already connected to the primary
  // account, suppress interception immediately so added accounts settle
  // cleanly into the active profile without user prompting.
  if (primary_is_connected == signin::Tribool::kTrue) {
    return SigninInterceptionHeuristicOutcome::kAbortAccountConnected;
  }

  auto enforce_enterprise_separation = EnterpriseSeparationMaybeRequired(
      profile_, identity_manager_, email, is_new_account,
      /*intercepted_profile_separation_policies=*/std::nullopt,
      /*expects_intercepted_profile_separation_policies_for_testing=*/
      intercepted_account_profile_separation_policies_response_for_testing_
          .has_value());

  // If we do not have all the information to enforce or not enterprise profile
  // separation, return `std::nullopt` so that we can try and get more info on
  // the intercepted account.
  if (!enforce_enterprise_separation.has_value()) {
    return std::nullopt;
  }

  const ProfileAttributesEntry* switch_to_entry = ShouldShowProfileSwitchBubble(
      gaia_id /*maybe empty*/, email,
      &g_browser_process->profile_manager()->GetProfileAttributesStorage());
  if (switch_to_entry && entry) {
    *entry = switch_to_entry;
  }

  if (enforce_enterprise_separation.value()) {
    return switch_to_entry
               ? SigninInterceptionHeuristicOutcome::
                     kInterceptEnterpriseForcedProfileSwitch
               : SigninInterceptionHeuristicOutcome::kInterceptEnterpriseForced;
  }

  CHECK(!enforce_enterprise_separation.value());

  // If interception is disabled and there are no enforce enterprise
  // profile separation policies abort.
  if (!signin_interception_enabled) {
    return SigninInterceptionHeuristicOutcome::kAbortInterceptionDisabled;
  }

  // Don't show profile switch for reauth.
  if (switch_to_entry && is_new_account) {
    return SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch;
  }

  // The `gaia_id` must have a value to properly read the prefs related to the
  // account and the Chrome Signin Bubble.
  if (!gaia_id.empty()) {
    // Chrome sign in bubble is shown if chrome isn't signed in and the account
    // is eligible.
    if (ShouldShowChromeSigninBubble(gaia_id, email)) {
      return SigninInterceptionHeuristicOutcome::kInterceptChromeSignin;
    }
  }

  // Do not intercept reauth.
  // Reauth is subject to chrome sign in bubble which is checked earlier in this
  // function.
  if (!is_new_account) {
    return SigninInterceptionHeuristicOutcome::kAbortAccountNotNew;
  }

  // From this point the remaining possible interceptions involve creating a new
  // profile.
  if (!profiles::IsProfileCreationAllowed()) {
    return SigninInterceptionHeuristicOutcome::kAbortProfileCreationDisallowed;
  }

  if (IsFirstAccount(identity_manager_, email)) {
    // Enterprise and multi-user bubbles are only shown if there are multiple
    // accounts. The intercepted account may not be added to chrome yet.
    return SigninInterceptionHeuristicOutcome::kAbortSingleAccount;
  }

  if (!identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSignin)) {
    // This is not the first account in the identity manager but there is no
    // primary account, all the accounts are in the UNO web-only state, so do
    // not intercept.
    return SigninInterceptionHeuristicOutcome::
        kAbortNotFirstAccountButNoPrimaryAccount;
  }

  if (HasUserDeclinedProfileCreation(email)) {
    return SigninInterceptionHeuristicOutcome::
        kAbortUserDeclinedProfileForAccount;
  }

  // When Gaia explicitly states that the account is not connected (kFalse),
  // immediately enforce multi-user interception to prevent unconnected accounts
  // from mixing in the same profile, even if given names match later.
  if (primary_is_connected == signin::Tribool::kFalse) {
    return SigninInterceptionHeuristicOutcome::kInterceptMultiUser;
  }

  return std::nullopt;
}

void DiceWebSigninInterceptor::MaybeInterceptWebSignin(
    content::WebContents* web_contents,
    CoreAccountId account_id,
    signin_metrics::AccessPoint access_point,
    bool is_new_account,
    bool is_sync_signin,
    signin::Tribool primary_is_connected) {
  // If the user is in sign in pending state and signs in with a different
  // account, it means that they enter an inconsistent state. Record this event
  // so that we can check afterwards if this state is resolved by accepting the
  // ProfileSwitchBubble or MultiUserBubble.
  if (IsInInconsistentStateWithPrimaryAccount(account_id, identity_manager_)) {
    profile_metrics_service_->UmaHistogramBoolean(
        "Signin.SigninPending.InconsistentStateInvoked", true);
  }

  if (state_->is_interception_in_progress_) {
    // Multiple concurrent interceptions are not supported.
    RecordSigninInterceptionHeuristicOutcome(
        SigninInterceptionHeuristicOutcome::kAbortInterceptInProgress);
    return;
  }
  state_->access_point_ = access_point;

  if (!web_contents) {
    // The tab has been closed (typically during the token exchange, which may
    // take some time).
    RecordSigninInterceptionHeuristicOutcome(
        SigninInterceptionHeuristicOutcome::kAbortTabClosed);
    Reset();
    return;
  }

  if (!delegate_->IsSigninInterceptionSupported(*web_contents)) {
    RecordSigninInterceptionHeuristicOutcome(
        SigninInterceptionHeuristicOutcome::kAbortNoSupportedBrowser);
    Reset();
    return;
  }

  // Do not show the interception UI if a password update is required: both
  // bubbles cannot be shown at the same time and the password update is more
  // important.
  ChromePasswordManagerClient* password_manager_client =
      ChromePasswordManagerClient::FromWebContents(web_contents);
  if (password_manager_client && password_manager_client->GetPasswordManager()
                                     ->IsFormManagerPendingPasswordUpdate()) {
    RecordSigninInterceptionHeuristicOutcome(
        SigninInterceptionHeuristicOutcome::kAbortPasswordUpdatePending);
    Reset();
    return;
  }

  ManagePasswordsUIController* password_controller =
      ManagePasswordsUIController::FromWebContents(web_contents);
  if (password_controller &&
      password_controller->GetState() ==
          password_manager::ui::State::PENDING_PASSWORD_UPDATE_STATE) {
    RecordSigninInterceptionHeuristicOutcome(
        SigninInterceptionHeuristicOutcome::kAbortPasswordUpdate);
    Reset();
    return;
  }

  AccountInfo account_info =
      identity_manager_->FindExtendedAccountInfoByAccountId(account_id);
  DCHECK(!account_info.IsEmpty()) << "Intercepting unknown account.";
  const ProfileAttributesEntry* entry = nullptr;
  std::optional<SigninInterceptionHeuristicOutcome> heuristic_outcome =
      GetHeuristicOutcome(is_new_account, is_sync_signin,
                          account_info.GetEmail(), account_info.GetGaiaId(),
                          &entry, primary_is_connected);
  state_->account_id_ = account_id;
  state_->is_interception_in_progress_ = true;
  state_->new_account_interception_ = is_new_account;
  state_->web_contents_ = web_contents->GetWeakPtr();
  state_->primary_is_connected_ = primary_is_connected;

  if (heuristic_outcome &&
      !SigninInterceptionHeuristicOutcomeIsSuccess(*heuristic_outcome)) {
    RecordSigninInterceptionHeuristicOutcome(*heuristic_outcome);
    Reset();
    return;
  }

  // Start a timeout for the async operations we're kicking off.
  state_->interception_info_available_timeout_.Reset(
      base::BindOnce(&DiceWebSigninInterceptor::OnInterceptionInfoFetchTimeout,
                     base::Unretained(this)));
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
      FROM_HERE, state_->interception_info_available_timeout_.callback(),
      base::Seconds(5));

  // Process the interception (maybe kicking off async fetches).
  ProcessInterceptionOrWait(account_info, /*timed_out=*/false);
}

void DiceWebSigninInterceptor::CreateBrowserAfterSigninInterception(
    CoreAccountId account_id,
    content::WebContents* intercepted_contents,
    std::unique_ptr<ScopedWebSigninInterceptionBubbleHandle> bubble_handle,
    bool is_new_profile,
    WebSigninInterceptor::SigninInterceptionType interception_type) {
  DCHECK(!state_->session_startup_helper_);
  DCHECK(bubble_handle);
  state_->interception_bubble_handle_ = std::move(bubble_handle);
  state_->account_id_ = account_id;
  state_->interception_type_ = interception_type;
  state_->session_startup_helper_ =
      std::make_unique<DiceInterceptedSessionStartupHelper>(
          profile_, is_new_profile, account_id, intercepted_contents);
  state_->session_startup_helper_->Startup(
      base::BindOnce(&DiceWebSigninInterceptor::OnNewBrowserCreated,
                     base::Unretained(this), is_new_profile));
}

void DiceWebSigninInterceptor::Shutdown() {
  if (state_->is_interception_in_progress_ &&
      !state_->was_interception_ui_displayed_) {
    RecordSigninInterceptionHeuristicOutcome(
        SigninInterceptionHeuristicOutcome::kAbortShutdown);
  }
  Reset();
}

void DiceWebSigninInterceptor::OnDiceSigninSessionComplete(
    const CoreAccountId& initiator_account_id,
    std::vector<CoreAccountId> secondary_accounts) {
  if (!state_->is_interception_in_progress_ ||
      state_->account_id_ != initiator_account_id) {
    return;
  }

  state_->dice_signin_session_complete_ = true;
  state_->secondary_accounts_ = std::move(secondary_accounts);

  if (state_->waiting_for_dice_signin_session_completion_) {
    state_->waiting_for_dice_signin_session_completion_ = false;
    CHECK(state_->deferred_action_callback_);
    std::move(state_->deferred_action_callback_).Run();
  }
}

void DiceWebSigninInterceptor::Reset() {
  state_ = std::make_unique<ResetableState>();
  account_info_update_observation_.Reset();
  weak_factory_.InvalidateWeakPtrs();
}

const ProfileAttributesEntry*
DiceWebSigninInterceptor::ShouldShowProfileSwitchBubble(
    const GaiaId& intercepted_gaia_id,
    std::string_view intercepted_email_fallback,
    ProfileAttributesStorage* profile_attribute_storage) const {
  // TODO(crbug.com/438165525): Consider reusing parts of `CanOfferSignin()`.
  // Check if there is already an existing profile with this account.
  base::FilePath profile_path = profile_->GetPath();
  std::vector<ProfileAttributesEntry*> attributes =
      profile_attribute_storage->GetAllProfilesAttributes();
  auto it =
      std::find_if(attributes.begin(), attributes.end(),
                   [&profile_path, &intercepted_email_fallback,
                    &intercepted_gaia_id](const ProfileAttributesEntry* entry) {
                     if (entry->GetPath() == profile_path) {
                       // Skip the current profile.
                       return false;
                     }
                     // Compare GaiaIds, but fallback to email if the GaiaId is
                     // missing (which can happen at the heuristic step).
                     return intercepted_gaia_id.empty()
                                ? gaia::AreEmailsSame(
                                      intercepted_email_fallback,
                                      base::UTF16ToUTF8(entry->GetUserName()))
                                : intercepted_gaia_id == entry->GetGAIAId();
                   });
  return it == attributes.end() ? nullptr : *it;
}

bool DiceWebSigninInterceptor::ShouldEnforceEnterpriseProfileSeparation(
    const AccountInfo& intercepted_account_info) const {
  DCHECK(IsRequiredExtendedAccountInfoAvailable(intercepted_account_info));
  CoreAccountInfo primary_account =
      identity_manager_->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin);
  bool intercepted_account_managed = signin::TriboolToBoolOrDie(
      intercepted_account_info.CanApplyAccountLevelEnterprisePolicies());
  // In case of re-auth of a managed primary account, do not show the enterprise
  // separation dialog if the user already consented to enterprise management.
  if (intercepted_account_managed &&
      IsReauthPrimaryAccount(state_->new_account_interception_,
                             intercepted_account_info.GetAccountId(),
                             identity_manager_) &&
      (identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSync) ||
       enterprise_util::UserAcceptedAccountManagement(profile_))) {
    return false;
  }

  if (!signin_util::IsAccountExemptedFromEnterpriseProfileSeparation(
          profile_, intercepted_account_info.GetEmail())) {
    return true;
  }

  if (!signin_util::IsProfileSeparationEnforcedByProfile(
          profile_, intercepted_account_info.GetEmail()) &&
      !signin_util::IsProfileSeparationEnforcedByPolicies(
          state_->intercepted_account_profile_separation_policies_.value_or(
              policy::ProfileSeparationPolicies()))) {
    return false;
  }

  return intercepted_account_managed;
}

bool DiceWebSigninInterceptor::ShouldShowEnterpriseDialog(
    const AccountInfo& intercepted_account_info) const {
  DCHECK(IsRequiredExtendedAccountInfoAvailable(intercepted_account_info));

  if (intercepted_account_info.CanApplyAccountLevelEnterprisePolicies() !=
      signin::Tribool::kTrue) {
    return false;
  }

  // If the user has declined profile creation twice, stop asking them.
  // When in `ChromeSigninUserChoice::kAlwaysAsk` setting mode, the decline
  // is not remembered, so the user can still see the dialog.
  if (SigninPrefs(*profile_->GetPrefs())
              .GetChromeSigninInterceptionUserChoice(
                  intercepted_account_info.GetGaiaId()) !=
          ChromeSigninUserChoice::kAlwaysAsk &&
      HasUserDeclinedProfileCreation(intercepted_account_info.GetEmail())) {
    return false;
  }

  // Enterprise dialog not shown if profile separation is enforced (another
  // dialog will be shown) or disabled.
  if (state_->intercepted_account_profile_separation_policies_
          .value_or(policy::ProfileSeparationPolicies())
          .profile_separation_settings()
          .value_or(policy::ProfileSeparationSettings::SUGGESTED) !=
      policy::ProfileSeparationSettings::SUGGESTED) {
    return false;
  }

  // Primary account re-auth should not see any dialogs.
  if (IsPrimaryAccountInterception(intercepted_account_info.GetAccountId(),
                                   identity_manager_)) {
    return false;
  }

  // If there is no primary account, propose to the user to sign into Chrome.
  // Here account who do not have management enabled might see this, but it is
  // fine, because they were not even signed in the browser.
  return !identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSignin);
}

bool DiceWebSigninInterceptor::ShouldShowEnterpriseBubble(
    const AccountInfo& intercepted_account_info) const {
  DCHECK(IsRequiredExtendedAccountInfoAvailable(intercepted_account_info));

  if (!IsUsernameAllowedForInterceptionByPattern(
          intercepted_account_info.GetEmail())) {
    return false;
  }

  // Check if the intercepted account or the primary account is managed.
  AccountInfo primary_acccount = GetPrimaryAccountInfo(identity_manager_);
  if (primary_acccount.IsEmpty() ||
      IsPrimaryAccountInterception(intercepted_account_info.GetAccountId(),
                                   identity_manager_)) {
    return false;
  }

  return signin::TriboolToBoolOrDie(
             intercepted_account_info
                 .CanApplyAccountLevelEnterprisePolicies()) ||
         primary_acccount.CanApplyAccountLevelEnterprisePolicies() ==
             signin::Tribool::kTrue;
}

bool DiceWebSigninInterceptor::ShouldShowMultiUserBubble(
    const AccountInfo& intercepted_account_info) const {
  DCHECK(IsRequiredExtendedAccountInfoAvailable(intercepted_account_info));

  if (!IsUsernameAllowedForInterceptionByPattern(
          std::string(intercepted_account_info.GetEmail()))) {
    return false;
  }

  if (identity_manager_->GetAccountsWithRefreshTokens().size() <= 1u ||
      !identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSignin)) {
    return false;
  }

  if (state_->primary_is_connected_ == signin::Tribool::kFalse) {
    // When Gaia explicitly states the account is not connected (kFalse),
    // we must intercept to prevent unconnected accounts from mixing in the same
    // profile, even if the given names match.
    return true;
  }

  // Check if the account has the same name as another account in the profile.
  for (const auto& account_info :
       identity_manager_->GetExtendedAccountInfoForAccountsWithRefreshToken()) {
    if (account_info.GetAccountId() ==
        intercepted_account_info.GetAccountId()) {
      continue;
    }
    // Case-insensitve comparison supporting non-ASCII characters.
    if (base::i18n::FoldCase(
            base::UTF8ToUTF16(account_info.GetGivenName().value_or(""))) ==
        base::i18n::FoldCase(base::UTF8ToUTF16(
            intercepted_account_info.GetGivenName().value_or("")))) {
      return false;
    }
  }
  return true;
}

// Returns true if sign in happens through the web and Chrome isn't already
// signed in. Also checks if the account with `gaia_id` is eligible.
bool DiceWebSigninInterceptor::ShouldShowChromeSigninBubble(
    const GaiaId& gaia_id,
    std::string_view email) const {
  // If the access point is not set, we cannot accurately know if we have to
  // show the bubble or not, so we will not show it.
  if (!state_->access_point_.has_value()) {
    return false;
  }

  // Only show the Chrome Signin Bubble when the signin event occurred through
  // a regular web signin in (not triggered through a chrome feature).
  if (state_->access_point_ != signin_metrics::AccessPoint::kWebSignin) {
    return false;
  }

  // Check if an account is already signed in to Chrome.
  if (identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSignin)) {
    return false;
  }

  // Check for the Chrome Signin setting value and possible reprompts.
  SigninPrefs signin_prefs(*profile_->GetPrefs());
  ChromeSigninUserChoice user_choice =
      signin_prefs.GetChromeSigninInterceptionUserChoice(gaia_id);
  switch (user_choice) {
    case ChromeSigninUserChoice::kNoChoice:
    case ChromeSigninUserChoice::kAlwaysAsk:
      break;
    case ChromeSigninUserChoice::kSignin:
      // This should not happen in a regular case, but rather an edge case; if
      // the user changed their preference while the interception is in
      // progress. Might also happen during tests that do not test the full
      // flow; mainly the early flow that automatically signs in and do not
      // get to this point.
      return false;
    case ChromeSigninUserChoice::kDoNotSignin:
      if (!ShouldAllowChromeSigninBubbleReprompt(signin_prefs, gaia_id)) {
        return false;
      }
      break;
  }

  return IsUsernameAllowedForInterceptionByPattern(email);
}

void DiceWebSigninInterceptor::ShowSigninInterceptionBubble(
    const WebSigninInterceptor::Delegate::BubbleParameters& bubble_parameters,
    base::OnceCallback<void(SigninInterceptionResult)> callback) {
  state_->was_interception_ui_displayed_ = true;
  state_->interception_type_ = bubble_parameters.interception_type;
  switch (bubble_parameters.interception_type) {
    case WebSigninInterceptor::SigninInterceptionType::kEnterprise:
    case WebSigninInterceptor::SigninInterceptionType::kEnterpriseForced:
    case WebSigninInterceptor::SigninInterceptionType::
        kEnterpriseAcceptManagement:
    case WebSigninInterceptor::SigninInterceptionType::kEnterpriseOIDC:
      signin_metrics::LogSignInOffered(
          signin_metrics::AccessPoint::kEnterpriseDialogAfterSigninInterception,
          signin_metrics::PromoAction::PROMO_ACTION_NO_SIGNIN_PROMO);
      break;
    case WebSigninInterceptor::SigninInterceptionType::kProfileSwitch:
    case WebSigninInterceptor::SigninInterceptionType::kMultiUser:
    case WebSigninInterceptor::SigninInterceptionType::kProfileSwitchForced:
    case WebSigninInterceptor::SigninInterceptionType::kChromeSignin:
      break;
  }
  state_->interception_bubble_handle_ = delegate_->ShowSigninInterceptionBubble(
      state_->web_contents_.get(), bubble_parameters, std::move(callback));
}

void DiceWebSigninInterceptor::EnsureObservingExtendedAccountInfo() {
  // Start an observation if one isn't already in progress.
  if (!account_info_update_observation_.IsObserving()) {
    account_info_update_observation_.Observe(identity_manager_.get());
  }
}

void DiceWebSigninInterceptor::ProcessInterceptionOrWait(
    const AccountInfo& info,
    bool timed_out) {
  DCHECK_EQ(info.GetAccountId(), state_->account_id_);

  if (!IsRequiredExtendedAccountInfoAvailable(info)) {
    // We can't process the interception with the information currently
    // available.
    //
    // If this is a timeout we abort the interception, otherwise we wait for
    // the remaining information to be fetched asynchronously.
    if (timed_out) {
      RecordSigninInterceptionHeuristicOutcome(
          SigninInterceptionHeuristicOutcome::kAbortAccountInfoTimeout);
      Reset();
      return;
    }
    EnsureObservingExtendedAccountInfo();
    return;
  }

  if (timed_out) {
    // We've timed out waiting for some optional information - process the
    // interception with what we have.
    OnInterceptionReadyToBeProcessed(info);
    return;
  }

  bool have_all_extended_account_info =
      IsFullExtendedAccountInfoAvailable(info);
  bool have_all_enterprise_info =
      EnterpriseSeparationMaybeRequired(
          profile_, identity_manager_, info.GetEmail(),
          state_->new_account_interception_,
          state_->intercepted_account_profile_separation_policies_,
          /*expects_intercepted_profile_separation_policies_for_testing=*/
          intercepted_account_profile_separation_policies_response_for_testing_
              .has_value())
          .has_value();

  if (!have_all_extended_account_info) {
    // We're need more extended account info - ensure we're waiting on that.
    EnsureObservingExtendedAccountInfo();
  } else {
    account_info_update_observation_.Reset();
  }

  if (!have_all_enterprise_info) {
    // Fetch the ManagedAccountsSigninRestriction policy value for the
    // intercepted account with a timeout.
    EnsureAccountLevelSigninRestrictionFetchInProgress(
        info, base::BindOnce(
                  &DiceWebSigninInterceptor::
                      OnAccountLevelManagedAccountsSigninRestrictionReceived,
                  base::Unretained(this), info));
  }

  if (have_all_extended_account_info && have_all_enterprise_info) {
    // We have all the information we need - process the interception.
    state_->interception_info_available_timeout_.Cancel();
    OnInterceptionReadyToBeProcessed(info);
    return;
  }
}

void DiceWebSigninInterceptor::OnInterceptionReadyToBeProcessed(
    const AccountInfo& info) {
  DCHECK_EQ(info.GetAccountId(), state_->account_id_);
  DCHECK(IsRequiredExtendedAccountInfoAvailable(info));

  std::optional<WebSigninInterceptor::SigninInterceptionType> interception_type;

  ProfileAttributesEntry* entry =
      g_browser_process->profile_manager()
          ->GetProfileAttributesStorage()
          .GetProfileAttributesWithPath(profile_->GetPath());
  SkColor profile_color = GenerateNewProfileColor(entry).color;

  const ProfileAttributesEntry* switch_to_entry = ShouldShowProfileSwitchBubble(
      info.GetGaiaId(), info.GetEmail(),
      &g_browser_process->profile_manager()->GetProfileAttributesStorage());

  bool force_profile_separation =
      ShouldEnforceEnterpriseProfileSeparation(info);
  bool reauth = !state_->new_account_interception_;
  bool show_link_data_option = false;

  // If the profile_management_disclaimer_service` is handling an account and
  // cannot be cancelled, we need to reset the interception, otherwise prevent
  // it from being shown until the reset.
  auto* profile_management_disclaimer_service =
      ProfileManagementDisclaimerServiceFactory::GetForProfile(profile_);
  if (!profile_management_disclaimer_service->StopCurrentProcessIfPossible()) {
    RecordSigninInterceptionHeuristicOutcome(
        SigninInterceptionHeuristicOutcome::kAbortDisclaimerServiceInProgress);
    Reset();
    return;
  }
  state_->disable_management_disclaimer_until_reset_ =
      profile_management_disclaimer_service
          ->DisableManagementDisclaimerUntilReset();

  if (force_profile_separation) {
    bool email_allowed =
        IsUsernameAllowedForInterceptionByPattern(info.GetEmail());
    if (!email_allowed) {
      // Profile separation is strictly enforced, so it's not possible to leave
      // the account signed in on the web only. It's also not possible to sign
      // the account into Chrome. The only option left is removing the account
      // and showing an error message.
      RecordSigninInterceptionHeuristicOutcome(
          SigninInterceptionHeuristicOutcome::kAbortAccountInfoNotCompatible);
      delegate_->ShowSigninError(
          state_->web_contents_.get(),
          SigninUIError::UsernameNotAllowedByPatternFromPrefs(info.GetEmail()));
      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE,
          base::BindOnce(
              [](base::WeakPtr<signin::IdentityManager> identity_manager,
                 const CoreAccountId& account_id) {
                if (!identity_manager) {
                  return;
                }
                identity_manager->GetAccountsMutator()->RemoveAccount(
                    account_id,
                    signin_metrics::SourceForRefreshTokenOperation::
                        kEnterprisePolicy_AccountNotAllowedInContentArea);
              },
              identity_manager_->GetWeakPtr(), info.GetAccountId()));
      Reset();
      return;
    }

    if (switch_to_entry) {
      interception_type =
          WebSigninInterceptor::SigninInterceptionType::kProfileSwitchForced;
      RecordSigninInterceptionHeuristicOutcome(
          SigninInterceptionHeuristicOutcome::
              kInterceptEnterpriseForcedProfileSwitch);
    } else {
      interception_type =
          WebSigninInterceptor::SigninInterceptionType::kEnterpriseForced;
      auto primary_account_id =
          identity_manager_->GetPrimaryAccountId(signin::ConsentLevel::kSignin);
      show_link_data_option =
          (primary_account_id.empty() ||
           primary_account_id == info.GetAccountId()) &&
          signin_util::
              ProfileSeparationAllowsKeepingUnmanagedBrowsingDataInManagedProfile(
                  profile_,
                  state_->intercepted_account_profile_separation_policies_
                      .value_or(policy::ProfileSeparationPolicies()));
      RecordSigninInterceptionHeuristicOutcome(
          SigninInterceptionHeuristicOutcome::kInterceptEnterpriseForced);
    }
  } else if (!switch_to_entry && ShouldShowEnterpriseDialog(info)) {
    interception_type = WebSigninInterceptor::SigninInterceptionType::
        kEnterpriseAcceptManagement;
    show_link_data_option = true;
    RecordSigninInterceptionHeuristicOutcome(
        SigninInterceptionHeuristicOutcome::kInterceptEnterprise);
  } else if (!profile_->GetPrefs()->GetBoolean(
                 prefs::kSigninInterceptionEnabled)) {
    RecordSigninInterceptionHeuristicOutcome(
        SigninInterceptionHeuristicOutcome::kAbortInterceptionDisabled);
    Reset();
    return;
  } else if (!reauth && switch_to_entry) {
    // Propose account switching if we skipped in GetHeuristicOutcome because we
    // returned a nullptr to get more information about forced enterprise
    // profile separation.
    interception_type =
        WebSigninInterceptor::SigninInterceptionType::kProfileSwitch;
    RecordSigninInterceptionHeuristicOutcome(
        SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch);
  } else if (ShouldShowChromeSigninBubble(info.GetGaiaId(), info.GetEmail())) {
    interception_type =
        WebSigninInterceptor::SigninInterceptionType::kChromeSignin;
    RecordSigninInterceptionHeuristicOutcome(
        SigninInterceptionHeuristicOutcome::kInterceptChromeSignin);
  } else if (reauth) {
    RecordSigninInterceptionHeuristicOutcome(
        SigninInterceptionHeuristicOutcome::kAbortAccountNotNew);
    Reset();
    return;
  } else if (HasUserDeclinedProfileCreation(info.GetEmail())) {
    RecordSigninInterceptionHeuristicOutcome(
        SigninInterceptionHeuristicOutcome::
            kAbortUserDeclinedProfileForAccount);
    Reset();
    return;
  } else if (ShouldShowEnterpriseBubble(info)) {
    interception_type =
        WebSigninInterceptor::SigninInterceptionType::kEnterprise;
    RecordSigninInterceptionHeuristicOutcome(
        SigninInterceptionHeuristicOutcome::kInterceptEnterprise);
  } else if (ShouldShowMultiUserBubble(info)) {
    interception_type =
        WebSigninInterceptor::SigninInterceptionType::kMultiUser;
    RecordSigninInterceptionHeuristicOutcome(
        SigninInterceptionHeuristicOutcome::kInterceptMultiUser);
  }

  if (!interception_type) {
    // Signin should not be intercepted.
    RecordSigninInterceptionHeuristicOutcome(
        SigninInterceptionHeuristicOutcome::kAbortAccountInfoNotCompatible);
    Reset();
    return;
  }

  bool show_managed_disclaimer =
      *interception_type !=
          WebSigninInterceptor::SigninInterceptionType::kProfileSwitch &&
      (info.CanApplyAccountLevelEnterprisePolicies() ==
           signin::Tribool::kTrue ||
       policy::ManagementServiceFactory::GetForPlatform()->IsManaged());

  MaybeRecordSupervisedUserStateMetrics(info, interception_type.value());

  SkColor highlight_color;
  if (switch_to_entry) {
    highlight_color =
        switch_to_entry->GetProfileThemeColors().profile_color_seed;
  } else {
    highlight_color = GetAutogeneratedThemeColors(profile_color).frame_color;
  }

  WebSigninInterceptor::Delegate::BubbleParameters bubble_parameters(
      *interception_type, info, GetPrimaryAccountInfo(identity_manager_),
      highlight_color, show_link_data_option, show_managed_disclaimer);

  base::OnceCallback<void(SigninInterceptionResult)> callback;
  switch (*interception_type) {
    case WebSigninInterceptor::SigninInterceptionType::kProfileSwitch:
    case WebSigninInterceptor::SigninInterceptionType::kProfileSwitchForced:
      callback =
          base::BindOnce(&DiceWebSigninInterceptor::OnProfileSwitchChoice,
                         base::Unretained(this), switch_to_entry->GetPath());
      break;
    case WebSigninInterceptor::SigninInterceptionType::kEnterpriseForced:
    case WebSigninInterceptor::SigninInterceptionType::
        kEnterpriseAcceptManagement:
      callback = base::BindOnce(
          &DiceWebSigninInterceptor::OnEnterpriseProfileCreationResult,
          base::Unretained(this), info, profile_color);
      break;
    case WebSigninInterceptor::SigninInterceptionType::kEnterprise:
    case WebSigninInterceptor::SigninInterceptionType::kMultiUser:
      callback =
          base::BindOnce(&DiceWebSigninInterceptor::OnProfileCreationChoice,
                         base::Unretained(this), info, profile_color);
      break;
    case WebSigninInterceptor::SigninInterceptionType::kChromeSignin:
      callback = base::BindOnce(&DiceWebSigninInterceptor::OnChromeSigninChoice,
                                base::Unretained(this), info);
      break;
    case WebSigninInterceptor::SigninInterceptionType::kEnterpriseOIDC:
      NOTREACHED() << "This interception type should not happen in DICE";
  }

  // Treat intercepts that require account preview fetching.
  if (!state_->account_preview_fetch_started_) {
    switch (*interception_type) {
      case WebSigninInterceptor::SigninInterceptionType::kMultiUser:
      case WebSigninInterceptor::SigninInterceptionType::kChromeSignin: {
        if (!base::FeatureList::IsEnabled(
                switches::kEnableAccountPreviewPreferredAccount)) {
          break;
        }

        // For supervised accounts, there is a dedicated subtitle, so there is
        // no need to fetch and wait for the account preview data.
        if (info.GetAccountCapabilities().is_subject_to_parental_controls() ==
            signin::Tribool::kTrue) {
          break;
        }

        state_->account_preview_fetch_started_ = true;
        signin::AccountPreviewDataService* preview_service =
            AccountPreviewDataServiceFactory::GetForProfile(profile_);
        if (!preview_service) {
          break;
        }
        state_->interception_bubble_callback_ = std::move(callback);
        state_->account_preview_timeout_timer_.Start(
            FROM_HERE,
            switches::
                kAccountPreviewPreferredAccountSingleAccountPromoFetchTimeout
                    .Get(),
            base::BindOnce(
                &DiceWebSigninInterceptor::OnAccountPreviewPreferenceReceived,
                weak_factory_.GetWeakPtr(), bubble_parameters,
                /*is_timeout=*/true, /*preference=*/std::nullopt));
        preview_service->GetPreviewPreferenceForAccount(
            info.GetGaiaId(),
            base::BindOnce(
                &DiceWebSigninInterceptor::OnAccountPreviewPreferenceReceived,
                weak_factory_.GetWeakPtr(), bubble_parameters,
                /*is_timeout=*/false));
        return;
      }
      case WebSigninInterceptor::SigninInterceptionType::kProfileSwitch:
      case WebSigninInterceptor::SigninInterceptionType::kProfileSwitchForced:
      case WebSigninInterceptor::SigninInterceptionType::kEnterpriseForced:
      case WebSigninInterceptor::SigninInterceptionType::
          kEnterpriseAcceptManagement:
      case WebSigninInterceptor::SigninInterceptionType::kEnterprise:
        break;
      case WebSigninInterceptor::SigninInterceptionType::kEnterpriseOIDC:
        NOTREACHED() << "This interception type should not happen in DICE";
    }
  }

  ShowSigninInterceptionBubble(bubble_parameters, std::move(callback));
}

void DiceWebSigninInterceptor::OnAccountPreviewPreferenceReceived(
    WebSigninInterceptor::Delegate::BubbleParameters bubble_parameters,
    bool is_timeout,
    std::optional<signin::AccountPreviewDataService::AccountPreviewPreference>
        preference) {
  if (state_->was_interception_ui_displayed_ ||
      !state_->is_interception_in_progress_ ||
      !state_->interception_bubble_callback_) {
    return;
  }

  base::TimeDelta elapsed_time;
  if (is_timeout) {
    elapsed_time = state_->account_preview_timeout_timer_.GetCurrentDelay();
  } else {
    CHECK(state_->account_preview_timeout_timer_.IsRunning());
    elapsed_time = base::TimeTicks::Now() -
                   (state_->account_preview_timeout_timer_.desired_run_time() -
                    state_->account_preview_timeout_timer_.GetCurrentDelay());
    state_->account_preview_timeout_timer_.Stop();
  }

  base::UmaHistogramBoolean("Signin.Intercept.AccountPreview.TimedOut",
                            is_timeout);
  base::UmaHistogramTimes("Signin.Intercept.AccountPreview.ResponseTime",
                          elapsed_time);

  bubble_parameters.account_preview_preference = std::move(preference);
  ShowSigninInterceptionBubble(
      bubble_parameters, std::move(state_->interception_bubble_callback_));
}

void DiceWebSigninInterceptor::OnExtendedAccountInfoUpdated(
    const AccountInfo& info) {
  if (info.GetAccountId() != state_->account_id_) {
    return;
  }
  ProcessInterceptionOrWait(info, false);
}

void DiceWebSigninInterceptor::OnExtendedAccountInfoRemoved(
    const AccountInfo& info) {
  if (info.GetAccountId() != state_->account_id_) {
    return;
  }
  RecordSigninInterceptionHeuristicOutcome(
      SigninInterceptionHeuristicOutcome::kAbortSignedOut);
  Reset();
}

void DiceWebSigninInterceptor::OnInterceptionInfoFetchTimeout() {
  state_->account_level_signin_restriction_policy_fetcher_.reset();
  account_info_update_observation_.Reset();
  if (!state_->intercepted_account_profile_separation_policies_.has_value()) {
    state_->intercepted_account_profile_separation_policies_ =
        policy::ProfileSeparationPolicies();
  }

  AccountInfo account_info =
      identity_manager_->FindExtendedAccountInfoByAccountId(
          state_->account_id_);
  ProcessInterceptionOrWait(account_info, /*timed_out=*/true);
}

void DiceWebSigninInterceptor::OnProfileCreationChoice(
    const AccountInfo& account_info,
    SkColor profile_color,
    SigninInterceptionResult create) {
  if (create != SigninInterceptionResult::kAccepted) {
    if (create == SigninInterceptionResult::kDeclined) {
      IncrementEmailToCountDictionaryPref(
          prefs::kProfileCreationInterceptionDeclined, account_info.GetEmail());
    }
    Reset();
    return;
  }

  // If the IdentityManager is still being observerd, this could cause a
  // potential UAF. `DiceSignedInProfileCreator` ends up moving the account from
  // this profile to another one, which will cause notifications coming from
  // the IdentityManager. If this class still listens to the notifications, it
  // may destroy `state_->dice_signed_in_profile_creator_` in
  // `DiceWebSigninInterceptor::OnExtendedAccountInfoRemoved()`, which will
  // cause the account_id (originating from `DiceSignedInProfileCreator`) being
  // passed around to be in turn destroyed and cause a UAF issue.
  // TODO(crbug.com/435076172): The issue is potentially already fixed, change
  // this to a regular `CHECK()` if no crashes are reported.
  CHECK(!account_info_update_observation_.IsObserving(),
        base::NotFatalUntil::M144);

  DCHECK(state_->interception_bubble_handle_);
  // base::Unretained(this) is safe here because the callback is owned by
  // `state_` (which is owned by `this`), guaranteeing it will not outlive
  // this interceptor instance.
  auto proceed_with_profile_creation =
      base::BindOnce(&DiceWebSigninInterceptor::ProceedWithProfileCreation,
                     base::Unretained(this), account_info, profile_color);

  if (state_->dice_signin_session_complete_) {
    std::move(proceed_with_profile_creation).Run();
  } else {
    state_->waiting_for_dice_signin_session_completion_ = true;
    state_->deferred_action_callback_ =
        std::move(proceed_with_profile_creation);
  }
}

SigninInterceptionResult
DiceWebSigninInterceptor::ProcessChromeSigninUserChoice(
    SigninInterceptionResult result,
    const GaiaId& gaia_id) {
  SigninPrefs signin_prefs(*profile_->GetPrefs());
  // When in `ChromeSigninUserChoice::kAlwaysAsk` setting mode, the bubble
  // result should not be remembered or affect the setting mode.
  if (signin_prefs.GetChromeSigninInterceptionUserChoice(gaia_id) ==
      ChromeSigninUserChoice::kAlwaysAsk) {
    return result;
  }

  SigninInterceptionResult processed_result = result;
  // Treat dismiss case: might turn into a decline if max dismiss count is
  // reached.
  if (processed_result == SigninInterceptionResult::kDismissed) {
    size_t dismiss_count =
        signin_prefs.IncrementChromeSigninInterceptionDismissCount(gaia_id);
    if (dismiss_count >= kMaxChromeSigninInterceptionDismissCount) {
      // Proceed with the result treated as declined since we reached the max
      // dismissal count, or the user is in the always ask mode.
      processed_result = SigninInterceptionResult::kDeclined;
    } else {
      // Max dismiss count not reached yet, proceed with a simple dismiss.
      return result;
    }
  }

  if (processed_result == SigninInterceptionResult::kAccepted) {
    // Save user choice as always sign in.
    signin_prefs.SetChromeSigninInterceptionUserChoice(
        gaia_id, ChromeSigninUserChoice::kSignin);
  }

  if (processed_result == SigninInterceptionResult::kDeclined) {
    // If the user had no explicit interaction with the bubble or the Chrome
    // Signin setting previously, then set it's first decline time.
    if (signin_prefs.GetChromeSigninInterceptionUserChoice(gaia_id) ==
        ChromeSigninUserChoice::kNoChoice) {
      signin_prefs.SetChromeSigninInterceptionLastBubbleDeclineTime(
          gaia_id, base::Time::Now());
    }

    MaybeUpdateRepromptInfoAfterDecline(signin_prefs, gaia_id);

    // Save user choice as do not sign in automatically.
    signin_prefs.SetChromeSigninInterceptionUserChoice(
        gaia_id, ChromeSigninUserChoice::kDoNotSignin);
  }

  return processed_result;
}

void DiceWebSigninInterceptor::OnChromeSigninChoice(
    const AccountInfo& account_info,
    SigninInterceptionResult result) {
  SigninInterceptionResult processed_result =
      ProcessChromeSigninUserChoice(result, account_info.GetGaiaId());

  switch (processed_result) {
    case SigninInterceptionResult::kIgnored:
      // Can happen if the browser is closed while the bubble is still opened.
    case SigninInterceptionResult::kNotDisplayed:
      // Can happen if the web contents is destroyed between the time the bubble
      // was requested to be displayed and actually being displayed.
    case SigninInterceptionResult::kDismissed:
      // Happens if the user closed the bubble without explicitly accepting or
      // declining.
      break;
    case SigninInterceptionResult::kDeclined:
      RecordChromeSigninNumberOfDismissesForAccount(account_info.GetGaiaId(),
                                                    processed_result);
      signin::LaunchHatsSurveyForProfile(
          kHatsSurveyTriggerIdentityDiceWebSigninDeclined, profile_);
      break;
    case SigninInterceptionResult::kAcceptedWithExistingProfile:
      NOTREACHED()
          << "Those results are not expected within the Chrome Signin Bubble.";
    case SigninInterceptionResult::kAccepted:
      RecordChromeSigninNumberOfDismissesForAccount(account_info.GetGaiaId(),
                                                    processed_result);

      auto access_point =
          signin_metrics::AccessPoint::kChromeSigninInterceptBubble;
      signin_metrics::LogSignInStarted(access_point,
                                       profile_metrics_service_.get());
      identity_manager_->GetPrimaryAccountMutator()->SetPrimaryAccount(
          account_info.GetAccountId(), signin::ConsentLevel::kSignin,
          access_point);
      signin::LaunchHatsSurveyForProfile(
          kHatsSurveyTriggerIdentityDiceWebSigninAccepted, profile_);
  }

  // In all cases we want to close the bubble after the choice is taken.
  Reset();
}

void DiceWebSigninInterceptor::OnProfileSwitchChoice(
    const base::FilePath& profile_path,
    SigninInterceptionResult switch_profile) {
  if (switch_profile != SigninInterceptionResult::kAccepted) {
    Reset();
    return;
  }

  // If the IdentityManager is still being observerd, this could cause a
  // potential UAF. `DiceSignedInProfileCreator` ends up moving the account from
  // this profile to another one, which will cause notifications coming from
  // the IdentityManager. If this class still listens to the notifications, it
  // may destroy `state_->dice_signed_in_profile_creator_` in
  // `DiceWebSigninInterceptor::OnExtendedAccountInfoRemoved()`, which will
  // cause the account_id (originating from `DiceSignedInProfileCreator`) being
  // passed around to be in turn destroyed and cause a UAF issue.
  // TODO(crbug.com/435076172): The issue is potentially already fixed, change
  // this to a regular `CHECK()` if no crashes are reported.
  CHECK(!account_info_update_observation_.IsObserving(),
        base::NotFatalUntil::M144);

  DCHECK(state_->interception_bubble_handle_);
  DCHECK(!state_->dice_signed_in_profile_creator_);

  // base::Unretained(this) is safe here because the callback is owned by
  // `state_` (which is owned by `this`), guaranteeing it will not outlive
  // this interceptor instance.
  auto proceed_with_profile_switch =
      base::BindOnce(&DiceWebSigninInterceptor::ProceedWithProfileSwitch,
                     base::Unretained(this), profile_path);

  if (state_->dice_signin_session_complete_) {
    std::move(proceed_with_profile_switch).Run();
  } else {
    state_->waiting_for_dice_signin_session_completion_ = true;
    state_->deferred_action_callback_ = std::move(proceed_with_profile_switch);
  }
}

void DiceWebSigninInterceptor::OnNewSignedInProfileCreated(
    std::optional<ProfilePresets> profile_presets,
    Profile* new_profile) {
  DCHECK(state_->dice_signed_in_profile_creator_);
  state_->dice_signed_in_profile_creator_.reset();

  if (!new_profile) {
    Reset();
    return;
  }

  // The profile presets are defined only when the profile has just been created
  // (with interception type kMultiUser or kEnterprise). If the profile is not
  // new (kProfileSwitch) or if it is a guest profile, then the presets are not
  // updated.
  bool is_new_profile = profile_presets.has_value();
  if (is_new_profile) {
    ProfileMetrics::LogProfileAddNewUser(
        ProfileMetrics::ADD_NEW_USER_SIGNIN_INTERCEPTION);
    if (!new_profile->IsGuestSession()) {
      // Apply the new color to the profile.
      ThemeServiceFactory::GetForProfile(new_profile)
          ->SetUserColorAndBrowserColorVariant(
              profile_presets->profile_color,
              ui::mojom::BrowserColorVariant::kTonalSpot);

      // The new profile inherits the default search provider and the search
      // engine choice timestamp from the previous profile.
      SearchEngineChoiceDialogService::UpdateProfileFromChoiceData(
          *new_profile, profile_presets->search_engine_choice_data);
    }

    // Set the ChromeSignin setting to always signin following accepting the
    // signin intercept and being signed in.

    CoreAccountInfo account_info =
        IdentityManagerFactory::GetForProfile(new_profile)
            ->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin);
    if (!account_info.IsEmpty()) {
      SigninPrefs(*new_profile->GetPrefs())
          .SetChromeSigninInterceptionUserChoice(
              account_info.gaia, ChromeSigninUserChoice::kSignin);
    }
  }

  enterprise_util::SetUserAcceptedAccountManagement(
      new_profile, state_->intercepted_account_management_accepted_);

  // Work is done in this profile, the flow continues in the
  // DiceWebSigninInterceptor that is attached to the new profile.
  // We pass relevant parameters from this instance to the new one.
  DiceWebSigninInterceptorFactory::GetForProfile(new_profile)
      ->CreateBrowserAfterSigninInterception(
          state_->account_id_, state_->web_contents_.get(),
          std::move(state_->interception_bubble_handle_), is_new_profile,
          *state_->interception_type_);
  Reset();
}

void DiceWebSigninInterceptor::ProceedWithProfileCreation(
    const AccountInfo& account_info,
    SkColor profile_color) {
  std::u16string profile_name =
      profiles::GetDefaultNameForNewSignedInProfile(account_info);
  ProfilePresets profile_presets(profile_color);
  profile_presets.search_engine_choice_data =
      SearchEngineChoiceDialogService::GetChoiceDataFromProfile(*profile_);

  DCHECK(!state_->dice_signed_in_profile_creator_);
  // Unretained is fine because the profile creator is owned by this.
  state_->dice_signed_in_profile_creator_ =
      std::make_unique<DiceSignedInProfileCreator>(
          profile_, state_->account_id_, state_->secondary_accounts_,
          profile_name, profiles::GetPlaceholderAvatarIndex(),
          base::BindOnce(
              &DiceWebSigninInterceptor::OnNewSignedInProfileCreated,
              base::Unretained(this),
              std::optional<ProfilePresets>(std::move(profile_presets))));
}

void DiceWebSigninInterceptor::ProceedWithProfileSwitch(
    const base::FilePath& profile_path) {
  DCHECK(!state_->dice_signed_in_profile_creator_);
  // Unretained is fine because the profile creator is owned by this.
  state_->dice_signed_in_profile_creator_ =
      std::make_unique<DiceSignedInProfileCreator>(
          profile_, state_->account_id_, state_->secondary_accounts_,
          profile_path,
          base::BindOnce(&DiceWebSigninInterceptor::OnNewSignedInProfileCreated,
                         base::Unretained(this), std::nullopt));
}

std::vector<CoreAccountId>
DiceWebSigninInterceptor::dice_signed_in_profile_creator_accounts_for_testing()
    const {
  if (!state_->dice_signed_in_profile_creator_) {
    return {};
  }
  return state_->dice_signed_in_profile_creator_->account_ids_for_testing();
}

void DiceWebSigninInterceptor::OnEnterpriseProfileCreationResult(
    const AccountInfo& account_info,
    SkColor profile_color,
    SigninInterceptionResult create) {
  signin_util::RecordEnterpriseProfileCreationUserChoice(
      /*enforced_by_policy=*/!signin_util::IsProfileSeparationEnforcedByProfile(
          profile_, account_info.GetEmail()) &&
          !signin_util::IsProfileSeparationEnforcedByPolicies(
              state_->intercepted_account_profile_separation_policies_.value_or(
                  policy::ProfileSeparationPolicies())),
      /*created=*/create == SigninInterceptionResult::kAccepted);

  // Make sure existing account is a non-signed in profile.
  if (create == SigninInterceptionResult::kAccepted) {
    state_->intercepted_account_management_accepted_ = true;
    // In case of a reauth if there was no consent for management, do not create
    // a new profile.
    if (IsReauthPrimaryAccount(state_->new_account_interception_,
                               account_info.GetAccountId(),
                               identity_manager_)) {
      enterprise_util::SetUserAcceptedAccountManagement(
          profile_, state_->intercepted_account_management_accepted_);
      Reset();
    } else {
      signin_metrics::LogSignInStarted(
          signin_metrics::AccessPoint::kEnterpriseDialogAfterSigninInterception,
          profile_metrics_service_.get());
      OnProfileCreationChoice(account_info, profile_color,
                              SigninInterceptionResult::kAccepted);
    }
  } else if (create == SigninInterceptionResult::kAcceptedWithExistingProfile) {
    state_->intercepted_account_management_accepted_ = true;
    if (GetPrimaryAccountInfo(identity_manager_).IsEmpty()) {
      signin_metrics::LogSignInStarted(
          signin_metrics::AccessPoint::kEnterpriseDialogAfterSigninInterception,
          profile_metrics_service_.get());
      identity_manager_->GetPrimaryAccountMutator()->SetPrimaryAccount(
          account_info.GetAccountId(), signin::ConsentLevel::kSignin,
          signin_metrics::AccessPoint::
              kEnterpriseDialogAfterSigninInterception);
    } else {
      DCHECK_EQ(GetPrimaryAccountInfo(identity_manager_).GetAccountId(),
                account_info.GetAccountId());
    }

    enterprise_util::SetUserAcceptedAccountManagement(
        profile_, state_->intercepted_account_management_accepted_);
    Reset();
  } else {
    DCHECK_EQ(SigninInterceptionResult::kDeclined, create)
        << "The user can only accept or decline";
    if (account_info == identity_manager_->GetPrimaryAccountInfo(
                            signin::ConsentLevel::kSignin)) {
      auto* primary_account_mutator =
          IdentityManagerFactory::GetForProfile(profile_)
              ->GetPrimaryAccountMutator();
      primary_account_mutator->ClearPrimaryAccount(
          signin_metrics::ProfileSignout::kAbortSignin);
    }
    if (state_->interception_type_ ==
        WebSigninInterceptor::SigninInterceptionType::kEnterpriseForced) {
      auto* accounts_mutator = identity_manager_->GetAccountsMutator();
      accounts_mutator->RemoveAccount(
          account_info.GetAccountId(),
          signin_metrics::SourceForRefreshTokenOperation::
              kEnterpriseForcedProfileCreation_UserDecline);
    }
    OnProfileCreationChoice(account_info, profile_color,
                            SigninInterceptionResult::kDeclined);
  }
}

void DiceWebSigninInterceptor::OnNewBrowserCreated(bool is_new_profile) {
  DCHECK(state_->interception_bubble_handle_);
  state_->interception_bubble_handle_.reset();  // Close the bubble now.
  state_->session_startup_helper_.reset();

  if (!is_new_profile || profile_->IsGuestSession()) {
    return;
  }

  BrowserWindowInterface* browser =
      ProfileBrowserCollection::GetForProfile(profile_)->GetLastActiveBrowser();
  DCHECK(browser);
  delegate_->ShowFirstRunExperienceInNewProfile(browser, state_->account_id_,
                                                *state_->interception_type_);
}

// static
std::string DiceWebSigninInterceptor::GetPersistentEmailHash(
    std::string_view email) {
  int hash = base::PersistentHash(
                 gaia::CanonicalizeEmail(gaia::SanitizeEmail(email))) &
             0xFF;
  return base::StringPrintf("email_%i", hash);
}

size_t DiceWebSigninInterceptor::IncrementEmailToCountDictionaryPref(
    const char* pref_name,
    std::string_view email) {
  // TODO(b/314079566): Consider merging the different similar pref counts into
  // a single pref where the email hash maps to multiple values.
  ScopedDictPrefUpdate update(profile_->GetPrefs(), pref_name);
  std::string key = GetPersistentEmailHash(email);
  std::optional<int> count = update->FindInt(key);

  int value = count.value_or(0) + 1;
  update->Set(key, value);

  CHECK_GE(value, 0);
  return value;
}

void DiceWebSigninInterceptor::RecordChromeSigninNumberOfDismissesForAccount(
    const GaiaId& gaia_id,
    SigninInterceptionResult result) {
  CHECK(result == SigninInterceptionResult::kAccepted ||
        result == SigninInterceptionResult::kDeclined)
      << "Recording results only for accepting/declining the bubble. "
         "Result: "
      << static_cast<int>(result);

  std::string_view action_string =
      result == SigninInterceptionResult::kAccepted ? "Accept" : "Decline";
  base::UmaHistogramCounts100(
      base::StrCat(
          {"Signin.Intercept.ChromeSignin.DismissesBefore", action_string}),
      SigninPrefs(*profile_->GetPrefs())
          .GetChromeSigninInterceptionDismissCount(gaia_id));
}

bool DiceWebSigninInterceptor::HasUserDeclinedProfileCreation(
    std::string_view email) const {
  const base::DictValue& pref_data = profile_->GetPrefs()->GetDict(
      prefs::kProfileCreationInterceptionDeclined);
  std::optional<int> declined_count =
      pref_data.FindInt(GetPersistentEmailHash(email));
  // Check if the user declined 2 times.
  constexpr int kMaxProfileCreationDeclinedCount = 2;
  return declined_count &&
         declined_count.value() >= kMaxProfileCreationDeclinedCount;
}

void DiceWebSigninInterceptor::
    EnsureAccountLevelSigninRestrictionFetchInProgress(
        const AccountInfo& account_info,
        base::OnceCallback<void(policy::ProfileSeparationPolicies)> callback) {
  if (state_->account_level_signin_restriction_policy_fetcher_ != nullptr) {
    // A fetch is already in progress, don't start a new one.
    DCHECK_EQ(account_info.GetAccountId(), state_->account_id_);
    return;
  }

  if (intercepted_account_profile_separation_policies_response_for_testing_
          .has_value()) {
    policy::ProfileSeparationPolicies profile_separation_policies =
        std::exchange(
            intercepted_account_profile_separation_policies_response_for_testing_,
            std::nullopt)
            .value();
    std::move(callback).Run(std::move(profile_separation_policies));
    return;
  }

  DCHECK(!state_->interception_info_available_timeout_.IsCancelled());

  state_->account_level_signin_restriction_policy_fetcher_ =
      std::make_unique<policy::UserCloudSigninRestrictionPolicyFetcher>(
          g_browser_process->browser_policy_connector(),
          g_browser_process->system_network_context_manager()
              ->GetSharedURLLoaderFactory());
  state_->account_level_signin_restriction_policy_fetcher_
      ->GetManagedAccountsSigninRestriction(
          identity_manager_, account_info.GetAccountId(), std::move(callback),
          policy::utils::IsPolicyTestingEnabled(profile_->GetPrefs(),
                                                chrome::GetChannel())
              ? profile_->GetPrefs()
                    ->GetDefaultPrefValue(
                        prefs::kUserCloudSigninPolicyResponseFromPolicyTestPage)
                    ->GetString()
              : std::string());
}

void DiceWebSigninInterceptor::
    OnAccountLevelManagedAccountsSigninRestrictionReceived(
        const AccountInfo& account_info,
        policy::ProfileSeparationPolicies profile_separation_policies) {
  state_->intercepted_account_profile_separation_policies_ =
      profile_separation_policies;
  ProcessInterceptionOrWait(account_info, /*timed_out=*/false);
}

void DiceWebSigninInterceptor::RecordSigninInterceptionHeuristicOutcome(
    SigninInterceptionHeuristicOutcome outcome) const {
  // Record the outcome.
  base::UmaHistogramEnumeration("Signin.Intercept.HeuristicOutcome", outcome);
}

bool DiceWebSigninInterceptor::IsFullExtendedAccountInfoAvailable(
    const AccountInfo& account_info) const {
  if (!IsRequiredExtendedAccountInfoAvailable(account_info)) {
    return false;
  }
  return account_info.GetAccountCapabilities()
             .is_subject_to_parental_controls() != signin::Tribool::kUnknown;
}

bool DiceWebSigninInterceptor::managed_profile_creation_required_by_policy()
    const {
  return state_->interception_type_ ==
         WebSigninInterceptor::SigninInterceptionType::kEnterpriseForced;
}

AccountInfo DiceWebSigninInterceptor::intercepted_account_info() const {
  return identity_manager_->FindExtendedAccountInfoByAccountId(
      state_->account_id_);
}

// static
base::TimeDelta
DiceWebSigninInterceptor::GetTimeSinceLastChromeSigninDeclineForTesting(
    const SigninPrefs& signin_prefs,
    const GaiaId& gaia_id) {
  return GetTimeSinceLastChromeSigninDecline(signin_prefs, gaia_id);
}
