// Copyright 2017 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/safe_browsing/chrome_password_protection_service.h"

#include <algorithm>
#include <memory>
#include <string>

#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/metrics/user_metrics.h"
#include "base/metrics/user_metrics_action.h"
#include "base/observer_list.h"
#include "base/rand_util.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/types/pass_key.h"
#include "build/build_config.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
#include "chrome/browser/enterprise/connectors/reporting/reporting_event_router_factory.h"
#include "chrome/browser/history/history_service_factory.h"
#include "chrome/browser/password_manager/factories/account_password_store_factory.h"
#include "chrome/browser/password_manager/factories/password_reuse_manager_factory.h"
#include "chrome/browser/policy/chrome_browser_policy_connector.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/safe_browsing/advanced_protection_status_manager.h"
#include "chrome/browser/safe_browsing/advanced_protection_status_manager_factory.h"
#include "chrome/browser/safe_browsing/chrome_user_population_helper.h"
#include "chrome/browser/safe_browsing/safe_browsing_metrics_collector_factory.h"
#include "chrome/browser/safe_browsing/safe_browsing_navigation_observer_manager_factory.h"
#include "chrome/browser/safe_browsing/safe_browsing_service.h"
#include "chrome/browser/safe_browsing/verdict_cache_manager_factory.h"
#include "chrome/browser/signin/identity_manager_factory.h"
#include "chrome/browser/sync/sync_service_factory.h"
#include "chrome/browser/sync/user_event_service_factory.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/pref_names.h"
#include "chrome/common/url_constants.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "components/enterprise/connectors/core/reporting_event_router.h"
#include "components/google/core/common/google_util.h"
#include "components/omnibox/common/omnibox_features.h"
#include "components/os_crypt/async/browser/os_crypt_async.h"
#include "components/password_manager/core/browser/features/password_features.h"
#include "components/password_manager/core/browser/form_parsing/form_data_parser.h"
#include "components/password_manager/core/browser/hash_password_manager.h"
#include "components/password_manager/core/browser/insecure_credentials_helper.h"
#include "components/password_manager/core/browser/leak_detection_dialog_utils.h"
#include "components/password_manager/core/browser/password_sync_util.h"
#include "components/password_manager/core/browser/ui/password_check_referrer.h"
#include "components/prefs/pref_change_registrar.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "components/safe_browsing/content/browser/client_side_detection_host.h"
#include "components/safe_browsing/content/browser/content_unsafe_resource_util.h"
#include "components/safe_browsing/content/browser/password_protection/password_protection_commit_deferring_condition.h"
#include "components/safe_browsing/content/browser/password_protection/password_protection_request_content.h"
#include "components/safe_browsing/content/browser/safe_browsing_navigation_observer_manager.h"
#include "components/safe_browsing/content/browser/safe_browsing_tab_observer.h"
#include "components/safe_browsing/content/browser/triggers/trigger_throttler.h"
#include "components/safe_browsing/content/browser/ui_manager.h"
#include "components/safe_browsing/content/browser/web_contents_key.h"
#include "components/safe_browsing/content/browser/web_ui/safe_browsing_ui.h"
#include "components/safe_browsing/content/browser/web_ui/web_ui_content_info_singleton.h"
#include "components/safe_browsing/core/browser/db/database_manager.h"
#include "components/safe_browsing/core/browser/realtime/policy_engine.h"
#include "components/safe_browsing/core/browser/safe_browsing_metrics_collector.h"
#include "components/safe_browsing/core/browser/sync/safe_browsing_primary_account_token_fetcher.h"
#include "components/safe_browsing/core/browser/verdict_cache_manager.h"
#include "components/safe_browsing/core/common/features.h"
#include "components/safe_browsing/core/common/proto/csd.pb.h"
#include "components/safe_browsing/core/common/safe_browsing_prefs.h"
#include "components/safe_browsing/core/common/safebrowsing_constants.h"
#include "components/safe_browsing/core/common/utils.h"
#include "components/security_interstitials/core/unsafe_resource.h"
#include "components/security_interstitials/core/unsafe_resource_locator.h"
#include "components/signin/public/base/consent_level.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/identity_manager.h"
#include "components/site_engagement/content/site_engagement_service.h"
#include "components/strings/grit/components_strings.h"
#include "components/sync/protocol/user_event_specifics.pb.h"
#include "components/sync/service/sync_service.h"
#include "components/sync_user_events/user_event_service.h"
#include "components/unified_consent/pref_names.h"
#include "components/variations/service/variations_service.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/global_routing_id.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/browser/web_contents.h"
#include "google_apis/gaia/gaia_auth_util.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/geometry/size.h"
#include "url/gurl.h"
#include "url/url_util.h"

#if BUILDFLAG(FULL_SAFE_BROWSING)
#include "chrome/browser/extensions/api/safe_browsing_private/safe_browsing_private_event_router.h"
#include "chrome/browser/extensions/api/safe_browsing_private/safe_browsing_private_event_router_factory.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"  // nogncheck crbug.com/40147906
#include "chrome/browser/ui/browser_window/public/desktop_browser_window_capabilities.h"  // nogncheck crbug.com/40147906
#include "chrome/browser/ui/browser_window/public/global_browser_collection.h"
#include "chrome/browser/ui/chrome_pages.h"
#endif

#if BUILDFLAG(IS_ANDROID)
#include "chrome/browser/password_manager/android/password_checkup_launcher_helper_impl.h"
#include "chrome/browser/safe_browsing/android/password_reuse_controller_android.h"
#include "chrome/browser/safe_browsing/android/safe_browsing_referring_app_bridge_android.h"
#include "components/enterprise/connectors/core/features.h"
#include "components/password_manager/core/browser/password_check_referrer_android.h"
#include "ui/android/window_android.h"
#else
#include "chrome/browser/ui/browser_window/public/browser_window_interface_iterator.h"  // nogncheck crbug.com/40147906
#include "chrome/browser/ui/hats/trust_safety_sentiment_service.h"
#include "chrome/browser/ui/hats/trust_safety_sentiment_service_factory.h"
#endif

using base::RecordAction;
using base::UserMetricsAction;
using content::BrowserThread;
using enum safe_browsing::ExtendedReportingLevel;
using sync_pb::GaiaPasswordReuse;
using sync_pb::UserEventSpecifics;
using GaiaPasswordCaptured = UserEventSpecifics::GaiaPasswordCaptured;
using PasswordReuseDialogInteraction =
    GaiaPasswordReuse::PasswordReuseDialogInteraction;
using PasswordReuseLookup = GaiaPasswordReuse::PasswordReuseLookup;
using PasswordReuseEvent =
    safe_browsing::LoginReputationClientRequest::PasswordReuseEvent;
using SafeBrowsingStatus =
    GaiaPasswordReuse::PasswordReuseDetected::SafeBrowsingStatus;

namespace safe_browsing {

using ReusedPasswordAccountType =
    LoginReputationClientRequest::PasswordReuseEvent::ReusedPasswordAccountType;

namespace {

// The number of user gestures we trace back for login event attribution.
const int kPasswordEventAttributionUserGestureLimit = 2;

// Probability for sending password protection reports for domains on the
// allowlist for users opted into extended reporting, from non-incognito window.
const float kProbabilityForSendingReportsFromSafeURLs = 0.01;

// If user specifically mark a site as legitimate, we will keep this decision
// for 2 days.
const int kOverrideVerdictCacheDurationSec = 2 * 24 * 60 * 60;

// Frequency to log PasswordCapture event log. Random 24-28 days.
const int kPasswordCaptureEventLogFreqDaysMin = 24;
const int kPasswordCaptureEventLogFreqDaysExtra = 4;

int64_t GetMicrosecondsSinceWindowsEpoch(base::Time time) {
  return (time - base::Time()).InMicroseconds();
}

PasswordReuseLookup::ReputationVerdict GetVerdictToLogFromResponse(
    LoginReputationClientResponse::VerdictType response_verdict) {
  switch (response_verdict) {
    case LoginReputationClientResponse::SAFE:
      return PasswordReuseLookup::SAFE;
    case LoginReputationClientResponse::LOW_REPUTATION:
      return PasswordReuseLookup::LOW_REPUTATION;
    case LoginReputationClientResponse::PHISHING:
      return PasswordReuseLookup::PHISHING;
    case LoginReputationClientResponse::VERDICT_TYPE_UNSPECIFIED:
      return PasswordReuseLookup::VERDICT_UNSPECIFIED;
  }
  NOTREACHED() << "Unexpected response_verdict: " << response_verdict;
}

// Given a |web_contents|, returns the navigation id of its last committed
// navigation.
int64_t GetLastCommittedNavigationID(content::WebContents* web_contents) {
  if (!web_contents)
    return 0;
  content::NavigationEntry* navigation =
      web_contents->GetController().GetLastCommittedEntry();
  return navigation
             ? GetMicrosecondsSinceWindowsEpoch(navigation->GetTimestamp())
             : 0;
}

// Opens a |url| from |current_web_contents| with |referrer|. |in_new_tab|
// indicates if opening in a new foreground tab or in current tab.
void OpenUrl(content::WebContents* current_web_contents,
             const GURL& url,
             const content::Referrer& referrer,
             bool in_new_tab) {
  content::OpenURLParams params(url, referrer,
                                in_new_tab
                                    ? WindowOpenDisposition::NEW_FOREGROUND_TAB
                                    : WindowOpenDisposition::CURRENT_TAB,
                                ui::PAGE_TRANSITION_LINK,
                                /*is_renderer_initiated=*/false);
  current_web_contents->OpenURL(params, /*navigation_handle_callback=*/{});
}

int64_t GetNavigationIDFromPrefsByOrigin(PrefService* prefs,
                                         const Origin& origin) {
  const base::DictValue& unhandled_sync_password_reuses =
      prefs->GetDict(prefs::kSafeBrowsingUnhandledGaiaPasswordReuses);

  const base::Value* navigation_id_value =
      unhandled_sync_password_reuses.Find(origin.Serialize());

  int64_t navigation_id;
  return navigation_id_value &&
                 base::StringToInt64(navigation_id_value->GetString(),
                                     &navigation_id)
             ? navigation_id
             : 0;
}

// Return a new UserEventSpecifics w/o the navigation_id populated
std::unique_ptr<UserEventSpecifics> GetNewUserEventSpecifics() {
  auto specifics = std::make_unique<UserEventSpecifics>();
  specifics->set_event_time_usec(
      GetMicrosecondsSinceWindowsEpoch(base::Time::Now()));
  return specifics;
}

// Return a new UserEventSpecifics w/ the navigation_id populated
std::unique_ptr<UserEventSpecifics> GetUserEventSpecificsWithNavigationId(
    int64_t navigation_id) {
  if (navigation_id <= 0)
    return nullptr;

  auto specifics = GetNewUserEventSpecifics();
  specifics->set_navigation_id(navigation_id);
  return specifics;
}

// Return a new UserEventSpecifics populated from the web_contents
std::unique_ptr<UserEventSpecifics> GetUserEventSpecifics(
    content::WebContents* web_contents) {
  return GetUserEventSpecificsWithNavigationId(
      GetLastCommittedNavigationID(web_contents));
}

#if BUILDFLAG(IS_ANDROID)
struct CredentialFoundInStore {
  bool is_account_store;
  bool is_profile_store;
};

// Check whether the compromised credential is saved in the account or
// profile store.
CredentialFoundInStore CheckCredentialsStore(
    const std::vector<password_manager::MatchingReusedCredential>&
        matching_reused_credentials) {
  bool is_account_credential = false;
  bool is_profile_credential = false;

  for (const password_manager::MatchingReusedCredential& credential :
       matching_reused_credentials) {
    // After the store split, the same credential could be stored in both
    // account and profile store, so both checks are necessary.
    if ((credential.in_store &
         password_manager::PasswordForm::Store::kAccountStore) ==
        password_manager::PasswordForm::Store::kAccountStore) {
      is_account_credential = true;
    }
    if ((credential.in_store &
         password_manager::PasswordForm::Store::kProfileStore) ==
        password_manager::PasswordForm::Store::kProfileStore) {
      is_profile_credential = true;
    }
  }

  return CredentialFoundInStore(is_account_credential, is_profile_credential);
}
#endif

}  // namespace

ChromePasswordProtectionService::ChromePasswordProtectionService(
    SafeBrowsingService* sb_service,
    Profile* profile)
    : PasswordProtectionService(
          sb_service->database_manager(),
          sb_service->GetURLLoaderFactory(profile),
          HistoryServiceFactory::GetForProfile(
              profile,
              ServiceAccessType::EXPLICIT_ACCESS),
          profile->GetPrefs(),
          std::make_unique<SafeBrowsingPrimaryAccountTokenFetcher>(
              IdentityManagerFactory::GetForProfile(profile)),
          profile->IsOffTheRecord(),
          IdentityManagerFactory::GetForProfile(profile),
          /*try_token_fetch=*/true,
          SafeBrowsingMetricsCollectorFactory::GetForProfile(profile)),
      sb_service_(sb_service),
      ui_manager_(sb_service->ui_manager()),
      profile_(profile),
      pref_change_registrar_(new PrefChangeRegistrar),
      cache_manager_(VerdictCacheManagerFactory::GetForProfile(profile)) {
  pref_change_registrar_->Init(profile_->GetPrefs());

  pref_change_registrar_->Add(
      prefs::kPasswordProtectionWarningTrigger,
      base::BindRepeating(
          &ChromePasswordProtectionService::OnWarningTriggerChanged,
          base::Unretained(this)));
  pref_change_registrar_->Add(
      prefs::kPasswordProtectionLoginURLs,
      base::BindRepeating(
          &ChromePasswordProtectionService::OnEnterprisePasswordUrlChanged,
          base::Unretained(this)));
  pref_change_registrar_->Add(
      prefs::kPasswordProtectionChangePasswordURL,
      base::BindRepeating(
          &ChromePasswordProtectionService::OnEnterprisePasswordUrlChanged,
          base::Unretained(this)));

  add_phished_credentials_ =
      base::BindRepeating(&password_manager::AddPhishedCredentials);
  remove_phished_credentials_ =
      base::BindRepeating(&password_manager::RemovePhishedCredentials);

#if BUILDFLAG(IS_ANDROID)
  checkup_launcher_ = std::make_unique<PasswordCheckupLauncherHelperImpl>();
#endif
  // TODO(nparker) Move the rest of the above code into Init()
  // without crashing unittests.
  Init();
}

void ChromePasswordProtectionService::Init() {
  password_manager::PasswordReuseManager* reuse_manager =
      PasswordReuseManagerFactory::GetForProfile(profile_);
  // Reuse manager can be null in tests.
  if (reuse_manager) {
    // Subscribe to gaia hash password changes change notifications.
    scoped_observation_.Observe(reuse_manager);
    if (auto* hash_password_manager = reuse_manager->GetHashPasswordManager()) {
      HashPasswordManagerAvailable(hash_password_manager);
    }
  }

  if (sync_password_hash_provider_for_testing_) {
    SetSyncPasswordHash(sync_password_hash_provider_for_testing_.Run());
  }
}

void ChromePasswordProtectionService::SetSyncPasswordHash(
    const std::string& sync_password_hash) {
// The following code is disabled on Android. RefreshTokenIsAvailable cannot be
// used in unit tests, because it needs to interact with system accounts.
// Considering avoid running it during unit tests. See: crbug.com/40101266.
#if !BUILDFLAG(IS_ANDROID)
  // This code is shared by the normal ctor and testing ctor.
  sync_password_hash_ = sync_password_hash;
  if (!sync_password_hash_.empty()) {
    // Set a timer for when next to log the PasswordCapture event. The timer
    // value is stored in a pref to carry across restarts.
    base::TimeDelta delay =
        GetDelayFromPref(profile_->GetPrefs(),
                         prefs::kSafeBrowsingNextPasswordCaptureEventLogTime);

    // Bound it between 1 min and 28 days. Handles clock-resets.  We wait
    // 1 min to not slowdown browser-startup, and to improve the
    // probability that the sync system is initialized.
    base::TimeDelta min_delay = base::Minutes(1);
    base::TimeDelta max_delay =
        base::Days(kPasswordCaptureEventLogFreqDaysMin +
                   kPasswordCaptureEventLogFreqDaysExtra);
    delay = std::clamp(delay, min_delay, max_delay);
    SetLogPasswordCaptureTimer(delay);
  }
#endif
}

void ChromePasswordProtectionService::Shutdown() {
  if (pref_change_registrar_)
    pref_change_registrar_->RemoveAll();
  scoped_observation_.Reset();
}

void ChromePasswordProtectionService::RequestFinished(
    PasswordProtectionRequest* request,
    RequestOutcome outcome,
    std::unique_ptr<LoginReputationClientResponse> response) {
  if (response) {
    RecordSiteEngagementScore(request->main_frame_url(),
                              request->trigger_type(),
                              response->verdict_type());
  }
  PasswordProtectionService::RequestFinished(request, outcome,
                                             std::move(response));
}

void ChromePasswordProtectionService::HashPasswordManagerAvailable(
    password_manager::HashPasswordManager* hash_password_manager) {
  SetSyncPasswordHash(GetSyncPasswordHashFromPrefs(hash_password_manager));
}

void ChromePasswordProtectionService::HashPasswordStateMaybeChanged(
    const std::string& username,
    password_manager::HashPasswordManager* hash_password_manager) {
  CheckGaiaPasswordChangeForAllSignedInUsers(username, hash_password_manager);
}

ChromePasswordProtectionService::~ChromePasswordProtectionService() = default;

// static
ChromePasswordProtectionService*
ChromePasswordProtectionService::GetPasswordProtectionService(
    Profile* profile) {
  if (g_browser_process && g_browser_process->safe_browsing_service()) {
    return static_cast<safe_browsing::ChromePasswordProtectionService*>(
        g_browser_process->safe_browsing_service()
            ->GetPasswordProtectionService(profile));
  }
  return nullptr;
}

// static
bool ChromePasswordProtectionService::ShouldShowPasswordReusePageInfoBubble(
    content::WebContents* web_contents,
    PasswordType password_type) {
  Profile* profile =
      Profile::FromBrowserContext(web_contents->GetBrowserContext());
  ChromePasswordProtectionService* service =
      ChromePasswordProtectionService::GetPasswordProtectionService(profile);

  // |service| could be null if safe browsing service is disabled.
  if (!service)
    return false;

  if (password_type == PasswordType::ENTERPRISE_PASSWORD)
    return service->HasUnhandledEnterprisePasswordReuse(web_contents);

  DCHECK(password_type == PasswordType::PRIMARY_ACCOUNT_PASSWORD ||
         password_type == PasswordType::SAVED_PASSWORD ||
         password_type == PasswordType::OTHER_GAIA_PASSWORD);
  // Otherwise, checks if there's any unhandled sync password reuses matches
  // this origin.
  const auto& unhandled_sync_password_reuses = profile->GetPrefs()->GetDict(
      prefs::kSafeBrowsingUnhandledGaiaPasswordReuses);
  return unhandled_sync_password_reuses.Find(web_contents->GetPrimaryMainFrame()
                                                 ->GetLastCommittedOrigin()
                                                 .Serialize());
}

safe_browsing::LoginReputationClientRequest::UrlDisplayExperiment
ChromePasswordProtectionService::GetUrlDisplayExperiment() const {
  safe_browsing::LoginReputationClientRequest::UrlDisplayExperiment experiment;
  // Delayed warnings parameters:
  experiment.set_delayed_warnings_enabled(
      base::FeatureList::IsEnabled(safe_browsing::kDelayedWarnings));
  experiment.set_delayed_warnings_mouse_clicks_enabled(
      safe_browsing::kDelayedWarningsEnableMouseClicks.Get());
  return experiment;
}

void ChromePasswordProtectionService::ShowModalWarning(
    PasswordProtectionRequest* request,
    LoginReputationClientResponse::VerdictType verdict_type,
    const std::string& verdict_token,
    ReusedPasswordAccountType password_type) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DCHECK(password_type.account_type() == ReusedPasswordAccountType::GMAIL ||
         password_type.account_type() == ReusedPasswordAccountType::GSUITE ||
         password_type.account_type() ==
             ReusedPasswordAccountType::NON_GAIA_ENTERPRISE ||
         (password_type.account_type() ==
          ReusedPasswordAccountType::SAVED_PASSWORD));
  PasswordProtectionRequestContent* request_content =
      static_cast<PasswordProtectionRequestContent*>(request);
  content::WebContents* raw_web_contents = request_content->web_contents();
  if (!raw_web_contents) {
    return;
  }
  base::WeakPtr<content::WebContents> web_contents =
      raw_web_contents->GetWeakPtr();
  RequestOutcome outcome = request->request_outcome();
  // Don't show warning again if there is already a modal warning showing.
  if (IsModalWarningShowingInWebContents(web_contents.get())) {
    return;
  }

  // Exit fullscreen if this |web_contents| is showing in fullscreen mode.
  if (web_contents->IsFullscreen())
    web_contents->ExitFullscreen(true);
  // On MacOS, displaying a modal triggers a nested run loop where WebContents
  // could have been destroyed. So, if WebContents was destroyed, return early.
  if (!web_contents) {
    return;
  }

#if BUILDFLAG(IS_ANDROID)
  (new PasswordReuseControllerAndroid(
       web_contents.get(), this, profile_->GetPrefs(), password_type,
       base::BindOnce(&ChromePasswordProtectionService::OnUserAction,
                      weak_ptr_factory_.GetWeakPtr(), web_contents,
                      password_type, outcome, verdict_type, verdict_token,
                      WarningUIType::MODAL_DIALOG)))
      ->ShowDialog();
#else   // !BUILDFLAG(IS_ANDROID)
  ShowPasswordReuseModalWarningDialog(
      web_contents.get(), this, password_type,
      base::BindOnce(&ChromePasswordProtectionService::OnUserAction,
                     weak_ptr_factory_.GetWeakPtr(), web_contents,
                     password_type, outcome, verdict_type, verdict_token,
                     WarningUIType::MODAL_DIALOG));
#endif  // BUILDFLAG(IS_ANDROID)

  // If web_contents was destroyed during the nested run loop (e.g. on Mac),
  // we must not proceed.
  if (!web_contents) {
    return;
  }

  LogWarningAction(WarningUIType::MODAL_DIALOG, WarningAction::SHOWN,
                   password_type);
  switch (password_type.account_type()) {
    case ReusedPasswordAccountType::SAVED_PASSWORD:
      OnModalWarningShownForSavedPassword(web_contents.get(), password_type,
                                          verdict_token);
      break;
    case ReusedPasswordAccountType::GMAIL:
    case ReusedPasswordAccountType::GSUITE:
      OnModalWarningShownForGaiaPassword(web_contents.get(), password_type,
                                         verdict_token);
      break;
    case ReusedPasswordAccountType::NON_GAIA_ENTERPRISE:
      OnModalWarningShownForEnterprisePassword(web_contents.get(),
                                               password_type, verdict_token);
      break;
    default:
      return;
  }
}

void ChromePasswordProtectionService::OnModalWarningShownForSavedPassword(
    content::WebContents* web_contents,
    ReusedPasswordAccountType password_type,
    const std::string& verdict_token) {
  UpdateSecurityState(SBThreatType::SB_THREAT_TYPE_SAVED_PASSWORD_REUSE,
                      password_type, web_contents);
  // Starts preparing post-warning report.
  MaybeStartThreatDetailsCollection(web_contents, verdict_token, password_type);
}

void ChromePasswordProtectionService::OnModalWarningShownForGaiaPassword(
    content::WebContents* web_contents,
    ReusedPasswordAccountType password_type,
    const std::string& verdict_token) {
  if (!IsIncognito()) {
    ScopedDictPrefUpdate update(
        profile_->GetPrefs(), prefs::kSafeBrowsingUnhandledGaiaPasswordReuses);
    // Since base::Value doesn't support int64_t type, we convert the navigation
    // ID to string format and store it in the preference dictionary.
    update->Set(
        web_contents->GetPrimaryMainFrame()
            ->GetLastCommittedOrigin()
            .Serialize(),
        base::NumberToString(GetLastCommittedNavigationID(web_contents)));
  }
  SBThreatType threat_type;
  if (password_type.is_account_syncing()) {
    threat_type = SBThreatType::SB_THREAT_TYPE_SIGNED_IN_SYNC_PASSWORD_REUSE;
  } else {
    threat_type =
        SBThreatType::SB_THREAT_TYPE_SIGNED_IN_NON_SYNC_PASSWORD_REUSE;
  }
  UpdateSecurityState(threat_type, password_type, web_contents);

  // Starts preparing post-warning report.
  MaybeStartThreatDetailsCollection(web_contents, verdict_token, password_type);
}

void ChromePasswordProtectionService::OnModalWarningShownForEnterprisePassword(
    content::WebContents* web_contents,
    ReusedPasswordAccountType password_type,
    const std::string& verdict_token) {
  web_contents_with_unhandled_enterprise_reuses_.insert(web_contents);
  UpdateSecurityState(SBThreatType::SB_THREAT_TYPE_ENTERPRISE_PASSWORD_REUSE,
                      password_type, web_contents);
  // Starts preparing post-warning report.
  MaybeStartThreatDetailsCollection(web_contents, verdict_token, password_type);
}

void ChromePasswordProtectionService::ShowInterstitial(
    content::WebContents* web_contents,
    ReusedPasswordAccountType password_type) {
  DCHECK(password_type.account_type() ==
             ReusedPasswordAccountType::NON_GAIA_ENTERPRISE ||
         password_type.account_type() == ReusedPasswordAccountType::GSUITE);
  // Exit fullscreen if this |web_contents| is showing in fullscreen mode.
  if (web_contents->IsFullscreen())
    web_contents->ExitFullscreen(/*will_cause_resize=*/true);

  content::OpenURLParams params(
      GURL(chrome::kChromeUIResetPasswordURL), content::Referrer(),
      WindowOpenDisposition::NEW_FOREGROUND_TAB, ui::PAGE_TRANSITION_LINK,
      /*is_renderer_initiated=*/false);
  std::string post_data =
      base::NumberToString(static_cast<std::underlying_type_t<PasswordType>>(
          ConvertReusedPasswordAccountTypeToPasswordType(password_type)));

  params.post_data = network::ResourceRequestBody::CreateFromCopyOfBytes(
      base::as_byte_span(post_data));
  web_contents->OpenURL(params, /*navigation_handle_callback=*/{});

  LogWarningAction(WarningUIType::INTERSTITIAL, WarningAction::SHOWN,
                   password_type);
}

void ChromePasswordProtectionService::MaybeTriggerClientSideDetectionScan(
    content::WebContents* web_contents) {
  SafeBrowsingTabObserver* tab_observer =
      SafeBrowsingTabObserver::FromWebContents(web_contents);
  if (tab_observer && tab_observer->client_side_detection_host()) {
    tab_observer->client_side_detection_host()->OnUnfamiliarLoginPageDetected();
  }
}

void ChromePasswordProtectionService::OnUserAction(
    base::WeakPtr<content::WebContents> web_contents,
    ReusedPasswordAccountType password_type,
    RequestOutcome outcome,
    LoginReputationClientResponse::VerdictType verdict_type,
    const std::string& verdict_token,
    WarningUIType ui_type,
    WarningAction action) {
  if (!web_contents || web_contents->IsBeingDestroyed()) {
    return;
  }
  // Only log modal warning dialog action for all password types except for
  // signed-in non-syncing type for now. We log for signed-in non-syncing type
  // only when we are about to send the event to SecurityEventRecorder because
  // we don't want to count non-unconsented primary accounts.
  bool is_signed_in_non_syncing =
      !password_type.is_account_syncing() &&
      (password_type.account_type() == ReusedPasswordAccountType::GMAIL ||
       password_type.account_type() == ReusedPasswordAccountType::GSUITE);
  if (!is_signed_in_non_syncing) {
    LogWarningAction(ui_type, action, password_type);
  }

  switch (ui_type) {
    case WarningUIType::PAGE_INFO:
      HandleUserActionOnPageInfo(web_contents.get(), password_type, action);
      break;
    case WarningUIType::MODAL_DIALOG:
      HandleUserActionOnModalWarning(web_contents.get(), password_type, outcome,
                                     verdict_type, verdict_token, action);
      break;
    case WarningUIType::INTERSTITIAL:
      DCHECK_EQ(WarningAction::CHANGE_PASSWORD, action);
      HandleResetPasswordOnInterstitial(web_contents.get(), action);
      break;
    default:
      NOTREACHED();
  }

#if !BUILDFLAG(IS_ANDROID)
  if (safe_browsing::IsSafeBrowsingSurveysEnabled(*profile_->GetPrefs())) {
    TrustSafetySentimentService* trust_safety_sentiment_service =
        TrustSafetySentimentServiceFactory::GetForProfile(profile_);
    if (trust_safety_sentiment_service) {
      // Use trigger that delays survey when user changes password so we don't
      // interrupt their password change.
      if (action == WarningAction::CHANGE_PASSWORD) {
        trust_safety_sentiment_service->ProtectResetOrCheckPasswordClicked(
            ui_type);
      } else if (action == WarningAction::IGNORE_WARNING ||
                 action == WarningAction::CLOSE ||
                 action == WarningAction::MARK_AS_LEGITIMATE) {
        trust_safety_sentiment_service->PhishedPasswordUpdateNotClicked(ui_type,
                                                                        action);
      }
    }
  }
#endif
}

void ChromePasswordProtectionService::AddObserver(Observer* observer) {
  observer_list_.AddObserver(observer);
}

void ChromePasswordProtectionService::RemoveObserver(Observer* observer) {
  observer_list_.RemoveObserver(observer);
}

void ChromePasswordProtectionService::MaybeStartThreatDetailsCollection(
    content::WebContents* web_contents,
    const std::string& token,
    ReusedPasswordAccountType password_type) {
  // Can be null in tests.
  if (!GetTriggerManager()) {
    return;
  }

  auto* primary_main_frame = web_contents->GetPrimaryMainFrame();
  const content::GlobalRenderFrameHostId primary_main_frame_id =
      primary_main_frame->GetGlobalId();
  security_interstitials::UnsafeResource resource;
  if (password_type.account_type() ==
      ReusedPasswordAccountType::NON_GAIA_ENTERPRISE) {
    resource.threat_type =
        SBThreatType::SB_THREAT_TYPE_ENTERPRISE_PASSWORD_REUSE;
  } else if (password_type.account_type() ==
             ReusedPasswordAccountType::SAVED_PASSWORD) {
    resource.threat_type = SBThreatType::SB_THREAT_TYPE_SAVED_PASSWORD_REUSE;
  } else if (password_type.is_account_syncing()) {
    resource.threat_type =
        SBThreatType::SB_THREAT_TYPE_SIGNED_IN_SYNC_PASSWORD_REUSE;
  } else {
    resource.threat_type =
        SBThreatType::SB_THREAT_TYPE_SIGNED_IN_NON_SYNC_PASSWORD_REUSE;
  }
  resource.url = web_contents->GetLastCommittedURL();
  resource.rfh_locator =
      security_interstitials::UnsafeResourceLocator::CreateForRenderFrameToken(
          primary_main_frame_id.child_id.value(),
          primary_main_frame->GetFrameToken().value());
  resource.token = token;
  scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory =
      profile_->GetDefaultStoragePartition()
          ->GetURLLoaderFactoryForBrowserProcess();
  // Ignores the return of |StartCollectingThreatDetails()| here and
  // let TriggerManager decide whether it should start data
  // collection.
  GetTriggerManager()->StartCollectingThreatDetails(
      safe_browsing::TriggerType::GAIA_PASSWORD_REUSE, web_contents, resource,
      url_loader_factory, /*history_service=*/nullptr,
      SafeBrowsingNavigationObserverManagerFactory::GetForBrowserContext(
          profile_),
      TriggerManager::GetDataCollectionPermissions(*profile_->GetPrefs(),
                                                   web_contents));
}

void ChromePasswordProtectionService::MaybeFinishCollectingThreatDetails(
    content::WebContents* web_contents,
    bool did_proceed) {
  // Can be null in tests.
  if (!GetTriggerManager()) {
    return;
  }

  // Get the num_visits for the URL from the history service. If the value is
  // not able to be retrieved, set it to 0.
  history::HistoryService* history_service =
      HistoryServiceFactory::GetForProfile(profile_,
                                           ServiceAccessType::EXPLICIT_ACCESS);
  if (history_service) {
    history_service->GetVisibleVisitCountToHost(
        web_contents->GetLastCommittedURL(),
        base::BindOnce(
            &ChromePasswordProtectionService::OnGetVisibleVisitCountToHost,
            weak_ptr_factory_.GetWeakPtr(),
            safe_browsing::TriggerType::GAIA_PASSWORD_REUSE,
            GetWebContentsKey(web_contents), base::Milliseconds(0), did_proceed,
            TriggerManager::GetDataCollectionPermissions(*profile_->GetPrefs(),
                                                         web_contents)),
        &task_tracker_);
  } else {
    // Since we don't keep track the threat details in progress, it is safe to
    // ignore the result of |FinishCollectingThreatDetails()|. TriggerManager
    // will take care of whether report should be sent.
    GetTriggerManager()->FinishCollectingThreatDetails(
        safe_browsing::TriggerType::GAIA_PASSWORD_REUSE,
        GetWebContentsKey(web_contents), base::Milliseconds(0), did_proceed,
        /*num_visits=*/0,
        TriggerManager::GetDataCollectionPermissions(*profile_->GetPrefs(),
                                                     web_contents));
  }
}

void ChromePasswordProtectionService::MaybeLogPasswordReuseDetectedEvent(
    content::WebContents* web_contents) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  if (IsIncognito() &&
      !WebUIContentInfoSingleton::GetInstance()->HasListener()) {
    return;
  }

  syncer::UserEventService* user_event_service =
      browser_sync::UserEventServiceFactory::GetForProfile(profile_);
  if (!user_event_service)
    return;

  std::unique_ptr<UserEventSpecifics> specifics =
      GetUserEventSpecifics(web_contents);
  if (!specifics)
    return;

  auto* const status = specifics->mutable_gaia_password_reuse_event()
                           ->mutable_reuse_detected()
                           ->mutable_status();
  status->set_enabled(IsSafeBrowsingEnabled());

  ExtendedReportingLevel erl = GetExtendedReportingLevel(*GetPrefs());
  switch (erl) {
    case SBER_LEVEL_OFF:
      status->set_safe_browsing_reporting_population(SafeBrowsingStatus::NONE);
      break;
    case SBER_LEVEL_LEGACY:
      status->set_safe_browsing_reporting_population(
          SafeBrowsingStatus::EXTENDED_REPORTING);
      break;
    case SBER_LEVEL_SCOUT:
      status->set_safe_browsing_reporting_population(SafeBrowsingStatus::SCOUT);
      break;
    case SBER_LEVEL_ENHANCED_PROTECTION:
      status->set_safe_browsing_reporting_population(
          SafeBrowsingStatus::ENHANCED_PROTECTION);
      break;
  }

  WebUIContentInfoSingleton::GetInstance()->AddToPGEvents(*specifics);
  user_event_service->RecordUserEvent(std::move(specifics));
}

void ChromePasswordProtectionService::MaybeLogPasswordReuseDialogInteraction(
    int64_t navigation_id,
    PasswordReuseDialogInteraction::InteractionResult interaction_result) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  if (IsIncognito() &&
      !WebUIContentInfoSingleton::GetInstance()->HasListener()) {
    return;
  }

  syncer::UserEventService* user_event_service =
      browser_sync::UserEventServiceFactory::GetForProfile(profile_);
  if (!user_event_service)
    return;

  std::unique_ptr<UserEventSpecifics> specifics =
      GetUserEventSpecificsWithNavigationId(navigation_id);
  if (!specifics)
    return;

  PasswordReuseDialogInteraction* const dialog_interaction =
      specifics->mutable_gaia_password_reuse_event()
          ->mutable_dialog_interaction();
  dialog_interaction->set_interaction_result(interaction_result);

  WebUIContentInfoSingleton::GetInstance()->AddToPGEvents(*specifics);
  user_event_service->RecordUserEvent(std::move(specifics));
}

void ChromePasswordProtectionService::MaybeLogPasswordReuseLookupResult(
    content::WebContents* web_contents,
    PasswordReuseLookup::LookupResult result) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  if (IsIncognito() &&
      !WebUIContentInfoSingleton::GetInstance()->HasListener()) {
    return;
  }

  syncer::UserEventService* user_event_service =
      browser_sync::UserEventServiceFactory::GetForProfile(profile_);
  if (!user_event_service)
    return;

  std::unique_ptr<UserEventSpecifics> specifics =
      GetUserEventSpecifics(web_contents);
  if (!specifics)
    return;

  auto* const reuse_lookup =
      specifics->mutable_gaia_password_reuse_event()->mutable_reuse_lookup();
  reuse_lookup->set_lookup_result(result);
  WebUIContentInfoSingleton::GetInstance()->AddToPGEvents(*specifics);
  user_event_service->RecordUserEvent(std::move(specifics));
}

void ChromePasswordProtectionService::
    MaybeLogPasswordReuseLookupResultWithVerdict(
        content::WebContents* web_contents,
        PasswordType password_type,
        PasswordReuseLookup::LookupResult result,
        PasswordReuseLookup::ReputationVerdict verdict,
        const std::string& verdict_token) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  if (IsIncognito() &&
      !WebUIContentInfoSingleton::GetInstance()->HasListener()) {
    return;
  }

  PasswordReuseLookup reuse_lookup;
  reuse_lookup.set_lookup_result(result);
  reuse_lookup.set_verdict(verdict);
  reuse_lookup.set_verdict_token(verdict_token);

  // If password_type == OTHER_GAIA_PASSWORD, the account is not syncing.
  // Therefore, we have to use the security event recorder to log events to mark
  // the account at risk.
  if (password_type == PasswordType::OTHER_GAIA_PASSWORD) {
    sync_pb::GaiaPasswordReuse gaia_password_reuse_event;
    *gaia_password_reuse_event.mutable_reuse_lookup() = reuse_lookup;

    auto* identity_manager = IdentityManagerFactory::GetForProfileIfExists(
        profile_->GetOriginalProfile());
    if (identity_manager) {
      CoreAccountInfo unconsented_primary_account_info =
          identity_manager->GetPrimaryAccountInfo(
              signin::ConsentLevel::kSignin);
      // SecurityEventRecorder only supports unconsented primary accounts.
      if (gaia::AreEmailsSame(unconsented_primary_account_info.email,
                              username_for_last_shown_warning())) {
        // We currently only send a security event recorder ONLY when a
        // signed-in non-syncing user clicks on "Protect Account" button.
        LogWarningAction(WarningUIType::MODAL_DIALOG,
                         WarningAction::CHANGE_PASSWORD,
                         GetPasswordProtectionReusedPasswordAccountType(
                             password_type, username_for_last_shown_warning()));
        WebUIContentInfoSingleton::GetInstance()->AddToSecurityEvents(
            gaia_password_reuse_event);
        SecurityEventRecorderFactory::GetForProfile(profile_)
            ->RecordGaiaPasswordReuse(gaia_password_reuse_event);
      }
    }
  } else {
    syncer::UserEventService* user_event_service =
        browser_sync::UserEventServiceFactory::GetForProfile(profile_);
    if (!user_event_service)
      return;

    std::unique_ptr<UserEventSpecifics> specifics =
        GetUserEventSpecifics(web_contents);
    if (!specifics)
      return;

    *(specifics->mutable_gaia_password_reuse_event())->mutable_reuse_lookup() =
        reuse_lookup;
    WebUIContentInfoSingleton::GetInstance()->AddToPGEvents(*specifics);
    user_event_service->RecordUserEvent(std::move(specifics));
  }
}

void ChromePasswordProtectionService::MaybeLogPasswordReuseLookupEvent(
    content::WebContents* web_contents,
    RequestOutcome outcome,
    PasswordType password_type,
    const LoginReputationClientResponse* response) {
  switch (outcome) {
    case RequestOutcome::MATCHED_ALLOWLIST:
      MaybeLogPasswordReuseLookupResult(web_contents,
                                        PasswordReuseLookup::ALLOWLIST_HIT);
      break;
    case RequestOutcome::RESPONSE_ALREADY_CACHED:
      MaybeLogPasswordReuseLookupResultWithVerdict(
          web_contents, password_type, PasswordReuseLookup::CACHE_HIT,
          GetVerdictToLogFromResponse(response->verdict_type()),
          response->verdict_token());
      break;
    case RequestOutcome::SUCCEEDED:
      MaybeLogPasswordReuseLookupResultWithVerdict(
          web_contents, password_type, PasswordReuseLookup::REQUEST_SUCCESS,
          GetVerdictToLogFromResponse(response->verdict_type()),
          response->verdict_token());
      break;
    case RequestOutcome::URL_NOT_VALID_FOR_REPUTATION_COMPUTING:
      MaybeLogPasswordReuseLookupResult(web_contents,
                                        PasswordReuseLookup::URL_UNSUPPORTED);
      break;
    case RequestOutcome::MATCHED_ENTERPRISE_ALLOWLIST:
    case RequestOutcome::MATCHED_ENTERPRISE_LOGIN_URL:
    case RequestOutcome::MATCHED_ENTERPRISE_CHANGE_PASSWORD_URL:
      MaybeLogPasswordReuseLookupResult(
          web_contents, PasswordReuseLookup::ENTERPRISE_ALLOWLIST_HIT);
      break;
    case RequestOutcome::PASSWORD_ALERT_MODE:
    case RequestOutcome::TURNED_OFF_BY_ADMIN:
      MaybeLogPasswordReuseLookupResult(
          web_contents, PasswordReuseLookup::TURNED_OFF_BY_POLICY);
      break;
    case RequestOutcome::CANCELED:
    case RequestOutcome::TIMEDOUT:
    case RequestOutcome::DISABLED_DUE_TO_INCOGNITO:
    case RequestOutcome::REQUEST_MALFORMED:
    case RequestOutcome::FETCH_FAILED:
    case RequestOutcome::RESPONSE_MALFORMED:
    case RequestOutcome::SERVICE_DESTROYED:
    case RequestOutcome::DISABLED_DUE_TO_FEATURE_DISABLED:
    case RequestOutcome::DISABLED_DUE_TO_USER_POPULATION:
    case RequestOutcome::SAFE_BROWSING_DISABLED:
    case RequestOutcome::USER_NOT_SIGNED_IN:
    case RequestOutcome::EXCLUDED_COUNTRY:
      MaybeLogPasswordReuseLookupResult(web_contents,
                                        PasswordReuseLookup::REQUEST_FAILURE);
      break;
    case RequestOutcome::UNKNOWN:
    case RequestOutcome::DEPRECATED_NO_EXTENDED_REPORTING:
      NOTREACHED() << __FUNCTION__
                   << ": outcome: " << static_cast<int>(outcome);
  }
}

void ChromePasswordProtectionService::
    CheckGaiaPasswordChangeForAllSignedInUsers(
        const std::string& username,
        password_manager::HashPasswordManager* hash_password_manager) {
  // If the sync password has changed, report the change.
  std::string new_sync_password_hash =
      GetSyncPasswordHashFromPrefs(hash_password_manager);
  if (sync_password_hash_ != new_sync_password_hash) {
    sync_password_hash_ = new_sync_password_hash;
    OnGaiaPasswordChanged(username, /*is_other_gaia_password=*/false);
    return;
  }

  // For non sync password changes, we have to loop through all the password
  // hashes and find the hash associated with the username.
  auto* old_prefs = hash_password_manager->set_prefs(profile_->GetPrefs());
  for (const auto& hash_data :
       hash_password_manager->RetrieveAllPasswordHashes()) {
    if (password_manager::AreUsernamesSame(
            hash_data.username, /*is_username1_gaia_account=*/true, username,
            /*is_username2_gaia_account=*/true)) {
      OnGaiaPasswordChanged(username, /*is_other_gaia_password=*/true);
      break;
    }
  }
  hash_password_manager->set_prefs(old_prefs);
}

void ChromePasswordProtectionService::OnGaiaPasswordChanged(
    const std::string& username,
    bool is_other_gaia_password) {
  profile_->GetPrefs()->SetDict(prefs::kSafeBrowsingUnhandledGaiaPasswordReuses,
                                base::DictValue());
  if (!is_other_gaia_password)
    MaybeLogPasswordCapture(/*did_log_in=*/true);
  for (auto& observer : observer_list_)
    observer.OnGaiaPasswordChanged();

  // Only report if the current password changed is the primary account and it's
  // not a consumer account or if the current password changed is a content area
  // account and it's not a consumer account.
  if (!IsAccountConsumer(username)) {
    ReportPasswordChanged();
  }
}

GURL ChromePasswordProtectionService::GetEnterpriseChangePasswordURL() const {
  // If change password URL is specified in preferences, returns the
  // corresponding pref value.
  GURL enterprise_change_password_url =
      GetPasswordProtectionChangePasswordURLPref(*profile_->GetPrefs());
  if (!enterprise_change_password_url.is_empty())
    return enterprise_change_password_url;

  return GetDefaultChangePasswordURL();
}

GURL ChromePasswordProtectionService::GetDefaultChangePasswordURL() const {
  // Computes the default GAIA change password URL.
  const AccountInfo account_info = GetAccountInfo();
  std::string account_email = account_info.email;
  // This page will prompt for re-auth and then will prompt for a new password.
  std::string account_url =
      "https://myaccount.google.com/signinoptions/"
      "password?utm_source=Google&utm_campaign=PhishGuard";
  GURL change_password_url = GURL(base::StrCat(
      {"https://accounts.google.com/AccountChooser?Email=",
       url::UriComponentEncoder(account_email).view(),
       "&continue=", url::UriComponentEncoder(account_url).view()}));
  return google_util::AppendGoogleLocaleParam(
      change_password_url, g_browser_process->GetApplicationLocale());
}

void ChromePasswordProtectionService::HandleUserActionOnModalWarning(
    content::WebContents* web_contents,
    ReusedPasswordAccountType password_type,
    RequestOutcome outcome,
    LoginReputationClientResponse::VerdictType verdict_type,
    const std::string& verdict_token,
    WarningAction action) {
  const Origin origin =
      web_contents->GetPrimaryMainFrame()->GetLastCommittedOrigin();
  int64_t navigation_id =
      GetNavigationIDFromPrefsByOrigin(profile_->GetPrefs(), origin);

  if (action == WarningAction::IGNORE_WARNING) {
    AddModelWarningBypasstoPref();
  }

  if (action == WarningAction::CHANGE_PASSWORD) {
    RecordAction(UserMetricsAction(
        "PasswordProtection.ModalWarning.ChangePasswordButtonClicked"));
    LogDialogMetricsOnChangePassword(web_contents, password_type, navigation_id,
                                     outcome, verdict_type, verdict_token);
    OpenPasswordCheck(web_contents, password_type);
  } else if (action == WarningAction::IGNORE_WARNING &&
             password_type.is_account_syncing()) {
    RecordAction(UserMetricsAction(
        "PasswordProtection.ModalWarning.IgnoreButtonClicked"));
    // No need to change state.
    MaybeLogPasswordReuseDialogInteraction(
        navigation_id, PasswordReuseDialogInteraction::WARNING_ACTION_IGNORED);
  } else if (action == WarningAction::CLOSE &&
             password_type.is_account_syncing()) {
    RecordAction(
        UserMetricsAction("PasswordProtection.ModalWarning.CloseWarning"));
    // No need to change state.
    MaybeLogPasswordReuseDialogInteraction(
        navigation_id, PasswordReuseDialogInteraction::WARNING_UI_IGNORED);
  }
  RemoveWarningRequestsByWebContents(web_contents);
  MaybeFinishCollectingThreatDetails(
      web_contents,
      /*did_proceed=*/action == WarningAction::CHANGE_PASSWORD);
}

void ChromePasswordProtectionService::LogDialogMetricsOnChangePassword(
    content::WebContents* web_contents,
    ReusedPasswordAccountType password_type,
    int64_t navigation_id,
    RequestOutcome outcome,
    LoginReputationClientResponse::VerdictType verdict_type,
    const std::string& verdict_token) {
  if (password_type.is_account_syncing() ||
      password_type.account_type() ==
          ReusedPasswordAccountType::SAVED_PASSWORD) {
    MaybeLogPasswordReuseDialogInteraction(
        navigation_id, PasswordReuseDialogInteraction::WARNING_ACTION_TAKEN);
  } else {
    // |outcome| is only recorded as succeeded or response_already_cached.
    MaybeLogPasswordReuseLookupResultWithVerdict(
        web_contents, PasswordType::OTHER_GAIA_PASSWORD,
        outcome == RequestOutcome::SUCCEEDED
            ? PasswordReuseLookup::REQUEST_SUCCESS
            : PasswordReuseLookup::CACHE_HIT,
        GetVerdictToLogFromResponse(verdict_type), verdict_token);
  }
}

void ChromePasswordProtectionService::RecordSiteEngagementScore(
    const GURL& url,
    LoginReputationClientRequest::TriggerType trigger_type,
    LoginReputationClientResponse::VerdictType verdict_type) {
  if (IsIncognito()) {
    return;
  }
  site_engagement::SiteEngagementService* engagement_service =
      site_engagement::SiteEngagementService::Get(profile_);
  if (!engagement_service) {
    return;
  }
  double score = engagement_service->GetScore(url);

  std::string trigger_string;
  switch (trigger_type) {
    case LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE:
      trigger_string = "UnfamiliarLoginPage";
      break;
    case LoginReputationClientRequest::PASSWORD_REUSE_EVENT:
      trigger_string = "PasswordReuseEvent";
      break;
    case LoginReputationClientRequest::ONE_TIME_PASSWORD_FIELD_DETECTED:
      trigger_string = "OtpFieldDetected";
      break;
    case LoginReputationClientRequest::TRIGGER_TYPE_UNSPECIFIED:
      trigger_string = "Unspecified";
      break;
    default:
      NOTREACHED();
  }

  std::string verdict_string;
  switch (verdict_type) {
    case LoginReputationClientResponse::SAFE:
      verdict_string = "Safe";
      break;
    case LoginReputationClientResponse::LOW_REPUTATION:
      verdict_string = "LowReputation";
      break;
    case LoginReputationClientResponse::PHISHING:
      verdict_string = "Phishing";
      break;
    case LoginReputationClientResponse::VERDICT_TYPE_UNSPECIFIED:
      verdict_string = "Unspecified";
      break;
    default:
      NOTREACHED();
  }
  base::UmaHistogramExactLinear(
      base::StrCat({"PasswordProtection.Verdict.", trigger_string,
                    ".SiteEngagementScore.", verdict_string}),
      static_cast<int>(score), 101);
}

void ChromePasswordProtectionService::AddModelWarningBypasstoPref() {
  auto* metrics_collector =
      SafeBrowsingMetricsCollectorFactory::GetForProfile(profile_);
  if (metrics_collector) {
    metrics_collector->AddSafeBrowsingEventToPref(
        SafeBrowsingMetricsCollector::EventType::PASSWORD_REUSE_MODAL_BYPASS);
  }
}

void ChromePasswordProtectionService::OpenPasswordCheck(
    content::WebContents* web_contents,
    ReusedPasswordAccountType password_type) {
  if (password_type.account_type() ==
      ReusedPasswordAccountType::NON_GAIA_ENTERPRISE) {
    // Directly open enterprise change password page for enterprise password
    // reuses.
    RecordAction(UserMetricsAction(
        "PasswordProtection.NonGaiaEnterprise.ChangePasswordButtonClicked"));
    OpenUrl(web_contents, GetEnterpriseChangePasswordURL(), content::Referrer(),
            /*in_new_tab=*/true);
    web_contents_with_unhandled_enterprise_reuses_.erase(web_contents);
  } else if (password_type.account_type() !=
             ReusedPasswordAccountType::SAVED_PASSWORD) {
    // Opens accounts.google.com in a new tab.
    if (password_type.account_type() == ReusedPasswordAccountType::GMAIL) {
      RecordAction(UserMetricsAction(
          "PasswordProtection.Gmail.ChangePasswordButtonClicked"));
    } else {
      RecordAction(UserMetricsAction(
          "PasswordProtection.GSuite.ChangePasswordButtonClicked"));
    }
    OpenUrl(web_contents, GetDefaultChangePasswordURL(), content::Referrer(),
            /*in_new_tab=*/true);
  } else {
    RecordAction(UserMetricsAction(
        "PasswordProtection.SavedPassword.ChangePasswordButtonClicked"));

#if BUILDFLAG(FULL_SAFE_BROWSING)
    // Opens chrome://settings/passwords/check in a new tab.
    chrome::ShowPasswordCheck(
        GlobalBrowserCollection::GetInstance()->FindBrowserWithTab(
            web_contents));
    password_manager::LogPasswordCheckReferrer(
        password_manager::PasswordCheckReferrer::kPhishGuardDialog);
#endif

#if BUILDFLAG(IS_ANDROID)
    JNIEnv* env = base::android::AttachCurrentThread();
    const syncer::SyncService* sync_service =
        SyncServiceFactory::GetForProfile(profile_);
    bool is_syncing_passwords =
        password_manager::sync_util::HasChosenToSyncPasswords(sync_service);
    std::string account =
        is_syncing_passwords ? sync_service->GetAccountInfo().email : "";

    CredentialFoundInStore credentials_store =
        CheckCredentialsStore(saved_passwords_matching_reused_credentials());

    if (credentials_store.is_account_store &&
        credentials_store.is_profile_store) {
      // If the compromised credential is saved in both stores, Safety Hub in
      // Chrome will open so the user can review the compromised credentials in
      // both stores.

      // TODO(crbug.com/397184847): While the local passwords module is not in
      // the most recent version of Safety Check (also known as Safety Hub),
      // show the old UI.
      if (base::FeatureList::IsEnabled(
              features::kSafetyHubLocalPasswordsModule)) {
        checkup_launcher_->LaunchSafetyHub(
            env, web_contents->GetTopLevelNativeWindow());
      } else {
        checkup_launcher_->LaunchSafetyCheck(
            env, web_contents->GetTopLevelNativeWindow());
      }
    } else {
      // In case the compromised credential is only saved in one of the stores,
      // checkup for that store will open.

      bool should_show_checkup_for_local = true;

      if (credentials_store.is_account_store) {
        should_show_checkup_for_local = false;
      }
      checkup_launcher_->LaunchCheckupOnDevice(
          env, profile_, web_contents->GetTopLevelNativeWindow(),
          password_manager::PasswordCheckReferrerAndroid::kPhishedWarningDialog,
          should_show_checkup_for_local ? "" : account);
    }
#endif
  }
}

void ChromePasswordProtectionService::HandleUserActionOnPageInfo(
    content::WebContents* web_contents,
    ReusedPasswordAccountType password_type,
    WarningAction action) {
  GURL url = web_contents->GetLastCommittedURL();
  const Origin origin = Origin::Create(url);

  if (action == WarningAction::CHANGE_PASSWORD) {
    RecordAction(UserMetricsAction(
        "PasswordProtection.PageInfo.ChangePasswordButtonClicked"));
    OpenPasswordCheck(web_contents, password_type);
    return;
  }

  if (action == WarningAction::MARK_AS_LEGITIMATE) {
    RecordAction(
        UserMetricsAction("PasswordProtection.PageInfo.MarkSiteAsLegitimate"));
    // TODO(vakh): There's no good enum to report this dialog interaction.
    // This needs to be investigated.
    UpdateSecurityState(SBThreatType::SB_THREAT_TYPE_SAFE, password_type,
                        web_contents);
    if (password_type.account_type() ==
        ReusedPasswordAccountType::NON_GAIA_ENTERPRISE) {
      web_contents_with_unhandled_enterprise_reuses_.erase(web_contents);
    } else {
      ScopedDictPrefUpdate update(
          profile_->GetPrefs(),
          prefs::kSafeBrowsingUnhandledGaiaPasswordReuses);
      update->Remove(origin.Serialize());
    }

    // If the site is marked as legitimate and the phished password is
    // a saved password, remove the matching saved password credentials
    // from the compromised credentials table as the user has considered
    // the site safe.
    if (password_type.account_type() ==
        ReusedPasswordAccountType::SAVED_PASSWORD) {
      RemovePhishedSavedPasswordCredential(
          saved_passwords_matching_reused_credentials());
    }
    for (auto& observer : observer_list_)
      observer.OnMarkingSiteAsLegitimate(url);
    return;
  }

  NOTREACHED();
}

void ChromePasswordProtectionService::HandleResetPasswordOnInterstitial(
    content::WebContents* web_contents,
    WarningAction action) {
  RecordAction(
      UserMetricsAction("PasswordProtection.Interstitial.ResetPassword"));
  // Opens enterprise change password page in current tab for user to change
  // password.
  OpenUrl(web_contents, GetEnterpriseChangePasswordURL(),
          content::Referrer(web_contents->GetLastCommittedURL(),
                            network::mojom::ReferrerPolicy::kDefault),
          /*in_new_tab=*/false);
}

std::u16string ChromePasswordProtectionService::GetWarningDetailText(
    ReusedPasswordAccountType password_type) const {
  DCHECK(password_type.account_type() == ReusedPasswordAccountType::GSUITE ||
         password_type.account_type() == ReusedPasswordAccountType::GMAIL ||
         password_type.account_type() ==
             ReusedPasswordAccountType::NON_GAIA_ENTERPRISE ||
         (password_type.account_type() ==
          ReusedPasswordAccountType::SAVED_PASSWORD));
  if (password_type.account_type() ==
      ReusedPasswordAccountType::NON_GAIA_ENTERPRISE) {
    return l10n_util::GetStringUTF16(
        IDS_PAGE_INFO_CHANGE_PASSWORD_DETAILS_ENTERPRISE);
  }

  if (password_type.account_type() ==
      ReusedPasswordAccountType::SAVED_PASSWORD) {
    return l10n_util::GetStringUTF16(
        IDS_PAGE_INFO_CHANGE_PASSWORD_DETAILS_SAVED);
  }

  if (!password_type.is_account_syncing()) {
    return l10n_util::GetStringUTF16(
        IDS_PAGE_INFO_CHANGE_PASSWORD_DETAILS_SIGNED_IN_NON_SYNC);
  }
  if (password_type.account_type() != ReusedPasswordAccountType::GSUITE) {
    return l10n_util::GetStringUTF16(
        IDS_PAGE_INFO_CHANGE_PASSWORD_DETAILS_SYNC);
  }

  std::string org_name = GetOrganizationName(password_type);
  if (!org_name.empty()) {
    return l10n_util::GetStringFUTF16(
        IDS_PAGE_INFO_CHANGE_PASSWORD_DETAILS_ENTERPRISE_WITH_ORG_NAME,
        base::UTF8ToUTF16(org_name));
  }
  return l10n_util::GetStringUTF16(
      IDS_PAGE_INFO_CHANGE_PASSWORD_DETAILS_ENTERPRISE);
}

std::string ChromePasswordProtectionService::GetOrganizationName(
    ReusedPasswordAccountType password_type) const {
  if (base::FeatureList::IsEnabled(
          safe_browsing::kEnterprisePasswordReuseUiRefresh)) {
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
    return GetPrefs()->GetString(prefs::kEnterpriseCustomLabelForProfile);
#else
    return std::string();
#endif
  }
  if (password_type.account_type() != ReusedPasswordAccountType::GSUITE) {
    return std::string();
  }

  std::string email =
      password_type.is_account_syncing()
          ? GetAccountInfo().email
          : GetAccountInfoForUsername(username_for_last_shown_warning()).email;
  return email.empty() ? std::string() : gaia::ExtractDomainName(email);
}

void ChromePasswordProtectionService::MaybeReportPasswordReuseDetected(
    const GURL& main_frame_url,
    const std::string& username,
    PasswordType password_type,
    bool is_phishing_url,
    bool warning_shown,
    const ReferrerChain& referrer_chain) {
  auto reused_password_account_type =
      GetPasswordProtectionReusedPasswordAccountType(password_type, username);
  if (reused_password_account_type.account_type() ==
      ReusedPasswordAccountType::UNKNOWN) {
    return;
  }

  // When a PasswordFieldFocus event is sent, a PasswordProtectionRequest is
  // sent which means the password reuse type is unknown. We do not want to
  // report these events as PasswordReuse events. Also do not send reports for
  // Gmail accounts.
  bool can_log_password_reuse_event =
      (password_type == PasswordType::ENTERPRISE_PASSWORD ||
       reused_password_account_type.account_type() ==
           ReusedPasswordAccountType::GSUITE) &&
      (password_type != PasswordType::PASSWORD_TYPE_UNKNOWN);
  if (!IsIncognito() && can_log_password_reuse_event) {
    // User name should only be empty when MaybeStartPasswordFieldOnFocusRequest
    // is called.
    std::string username_or_email =
        username.empty() ? GetAccountInfo().email : username;

// Disabled on Android, because enterprise reporting extension is not supported.
#if !BUILDFLAG(IS_ANDROID)
    auto* safe_browsing_event_router =
        extensions::SafeBrowsingPrivateEventRouterFactory::GetForProfile(
            profile_);

    if (safe_browsing_event_router) {
      safe_browsing_event_router->OnPolicySpecifiedPasswordReuseDetected(
          main_frame_url, username_or_email, is_phishing_url, warning_shown);

      base::UmaHistogramBoolean(
          "PasswordProtection.GmailReportSent",
          base::EndsWith(username_or_email, "@gmail.com"));
    }
#endif  // !BUILDFLAG(IS_ANDROID)

    auto* reporting_event_router = enterprise_connectors::
        ReportingEventRouterFactory::GetForBrowserContext(profile_);
    if (reporting_event_router) {
      reporting_event_router->OnPasswordReuse(main_frame_url, username_or_email,
                                              is_phishing_url, warning_shown,
                                              referrer_chain);
    }
  }
}

void ChromePasswordProtectionService::ReportPasswordChanged() {
  if (IsIncognito()) {
    return;
  }

// Disabled on Android, because enterprise reporting extension is not supported.
#if !BUILDFLAG(IS_ANDROID)
  auto* safe_browsing_event_router =
      extensions::SafeBrowsingPrivateEventRouterFactory::GetForProfile(
          profile_);
  if (safe_browsing_event_router) {
    safe_browsing_event_router->OnPolicySpecifiedPasswordChanged(
        GetAccountInfo().email);
  }
#endif  // !BUILDFLAG(IS_ANDROID)

  auto* reporting_event_router =
      enterprise_connectors::ReportingEventRouterFactory::GetForBrowserContext(
          profile_);
  if (reporting_event_router) {
    reporting_event_router->OnPasswordChanged(GetAccountInfo().email);
  }
}

bool ChromePasswordProtectionService::HasUnhandledEnterprisePasswordReuse(
    content::WebContents* web_contents) const {
  return web_contents_with_unhandled_enterprise_reuses_.find(web_contents) !=
         web_contents_with_unhandled_enterprise_reuses_.end();
}

void ChromePasswordProtectionService::OnWarningTriggerChanged() {
  const base::Value& pref_value = pref_change_registrar_->prefs()->GetValue(
      prefs::kPasswordProtectionWarningTrigger);
  // If password protection is not turned off, do nothing.
  if (static_cast<PasswordProtectionTrigger>(pref_value.GetInt()) !=
      PASSWORD_PROTECTION_OFF) {
    return;
  }

  // Clears captured enterprise password hashes or GSuite sync password hashes.
  password_manager::PasswordReuseManager* reuse_manager =
      GetPasswordReuseManager();

  if (!reuse_manager) {
    return;
  }

  reuse_manager->ClearAllNonGmailPasswordHash();
  reuse_manager->ClearAllEnterprisePasswordHash();
}

void ChromePasswordProtectionService::OnEnterprisePasswordUrlChanged() {
  password_manager::PasswordReuseManager* reuse_manager =
      GetPasswordReuseManager();

  if (!reuse_manager) {
    return;
  }

  reuse_manager->ScheduleEnterprisePasswordURLUpdate();
}

bool ChromePasswordProtectionService::CanShowInterstitial(
    ReusedPasswordAccountType password_type,
    const GURL& main_frame_url) {
  bool is_supported_password_type =
      password_type.account_type() == ReusedPasswordAccountType::GSUITE ||
      password_type.account_type() ==
          ReusedPasswordAccountType::NON_GAIA_ENTERPRISE;
  return IsInPasswordAlertMode(password_type) && is_supported_password_type &&
         !IsURLAllowlistedForPasswordEntry(main_frame_url);
}

void ChromePasswordProtectionService::SetLogPasswordCaptureTimer(
    const base::TimeDelta& delay) {
  // This will replace any pending timer.
  log_password_capture_timer_.Start(
      FROM_HERE, delay,
      base::BindOnce(&ChromePasswordProtectionService::MaybeLogPasswordCapture,
                     base::Unretained(this), false));
}

void ChromePasswordProtectionService::MaybeLogPasswordCapture(bool did_log_in) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  // We skip this event and not set a timer if the profile is in incognito. When
  // the user logs in in the future, MaybeLogPasswordCapture() will be called
  // immediately then and will restart the timer.
  if (IsIncognito() || sync_password_hash_.empty())
    return;

  syncer::UserEventService* user_event_service =
      browser_sync::UserEventServiceFactory::GetForProfile(profile_);
  if (!user_event_service)
    return;

  std::unique_ptr<UserEventSpecifics> specifics = GetNewUserEventSpecifics();
  auto* const password_captured =
      specifics->mutable_gaia_password_captured_event();
  password_captured->set_event_trigger(
      did_log_in ? GaiaPasswordCaptured::USER_LOGGED_IN
                 : GaiaPasswordCaptured::EXPIRED_28D_TIMER);

  WebUIContentInfoSingleton::GetInstance()->AddToPGEvents(*specifics);
  user_event_service->RecordUserEvent(std::move(specifics));

  // Set a timer to log it again in 24-28 days. Spread it to avoid hammering the
  // backend with fixed cycle after this code lands in Stable.
  base::TimeDelta delay = base::Days(
      (kPasswordCaptureEventLogFreqDaysMin +
       base::RandIntInclusive(0, kPasswordCaptureEventLogFreqDaysExtra)));
  SetLogPasswordCaptureTimer(delay);

  // Write the deadline to a pref to carry over restarts.
  SetDelayInPref(profile_->GetPrefs(),
                 prefs::kSafeBrowsingNextPasswordCaptureEventLogTime, delay);
}

void ChromePasswordProtectionService::UpdateSecurityState(
    SBThreatType threat_type,
    ReusedPasswordAccountType password_type,
    content::WebContents* web_contents) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  const GURL url = web_contents->GetLastCommittedURL();
  if (!url.is_valid())
    return;

  const GURL url_with_empty_path = url.GetWithEmptyPath();
  if (threat_type == SBThreatType::SB_THREAT_TYPE_SAFE) {
    ui_manager_->RemoveAllowlistUrlSet(
        base::PassKey<ChromePasswordProtectionService>(), url_with_empty_path,
        /*navigation_id=*/std::nullopt, web_contents,
        /*from_pending_only=*/false);
    // Overrides cached verdicts.
    LoginReputationClientResponse verdict;
    GetCachedVerdict(url, LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
                     password_type, &verdict);
    verdict.set_verdict_type(LoginReputationClientResponse::SAFE);
    verdict.set_cache_duration_sec(kOverrideVerdictCacheDurationSec);
    CacheVerdict(url, LoginReputationClientRequest::PASSWORD_REUSE_EVENT,
                 password_type, verdict, base::Time::Now());
    return;
  }

  SBThreatType current_threat_type = SBThreatType::SB_THREAT_TYPE_UNUSED;
  // If user already click-through interstitial warning, or if there's already
  // a dangerous security state showing, we'll override it.
  if (ui_manager_->IsUrlAllowlistedOrPendingForWebContents(
          url_with_empty_path,
          web_contents->GetController().GetLastCommittedEntry(), web_contents,
          /*allowlist_only=*/false, &current_threat_type)) {
    DCHECK_NE(SBThreatType::SB_THREAT_TYPE_UNUSED, current_threat_type);
    if (current_threat_type == threat_type)
      return;
    // Resets previous threat type.
    ui_manager_->RemoveAllowlistUrlSet(
        base::PassKey<ChromePasswordProtectionService>(), url_with_empty_path,
        /*navigation_id=*/std::nullopt, web_contents,
        /*from_pending_only=*/false);
  }
  ui_manager_->AddToAllowlistUrlSet(
      url_with_empty_path, /*navigation_id=*/std::nullopt, web_contents,
      /*is_pending=*/true, threat_type);
}

void ChromePasswordProtectionService::FillReferrerChain(
    const GURL& event_url,
    SessionID event_tab_id,
    LoginReputationClientRequest::Frame* frame) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  SafeBrowsingNavigationObserverManager* navigation_observer_manager =
      SafeBrowsingNavigationObserverManagerFactory::GetForBrowserContext(
          profile_);
  SafeBrowsingNavigationObserverManager::AttributionResult result =
      navigation_observer_manager->IdentifyReferrerChainByEventURL(
          event_url, event_tab_id, content::GlobalRenderFrameHostId(),
          kPasswordEventAttributionUserGestureLimit,
          frame->mutable_referrer_chain());
  size_t referrer_chain_length = frame->referrer_chain().size();
  UMA_HISTOGRAM_COUNTS_100(
      "SafeBrowsing.ReferrerURLChainSize.PasswordEventAttribution",
      referrer_chain_length);
  UMA_HISTOGRAM_ENUMERATION(
      "SafeBrowsing.ReferrerAttributionResult.PasswordEventAttribution", result,
      SafeBrowsingNavigationObserverManager::ATTRIBUTION_FAILURE_TYPE_MAX);

  // Determines how many recent navigation events to append to referrer chain.
  size_t recent_navigations_to_collect =
      profile_ ? SafeBrowsingNavigationObserverManager::
                     CountOfRecentNavigationsToAppend(
                         profile_, profile_->GetPrefs(), result)
               : 0u;
  navigation_observer_manager->AppendRecentNavigations(
      recent_navigations_to_collect, frame->mutable_referrer_chain());
}

std::string ChromePasswordProtectionService::GetSyncPasswordHashFromPrefs(
    password_manager::HashPasswordManager* hash_password_manager) const {
  if (!sync_password_hash_provider_for_testing_.is_null())
    return sync_password_hash_provider_for_testing_.Run();

  auto* old_prefs = hash_password_manager->set_prefs(profile_->GetPrefs());
  std::optional<password_manager::PasswordHashData> sync_hash_data =
      hash_password_manager->RetrievePasswordHash(GetAccountInfo().email,
                                                  /*is_gaia_password=*/true);
  std::string result = sync_hash_data
                           ? base::NumberToString(sync_hash_data->hash)
                           : std::string();
  hash_password_manager->set_prefs(old_prefs);
  return result;
}

PrefService* ChromePasswordProtectionService::GetPrefs() const {
  return profile_->GetPrefs();
}

TriggerManager* ChromePasswordProtectionService::GetTriggerManager() {
  return sb_service_ ? sb_service_->trigger_manager() : nullptr;
}

bool ChromePasswordProtectionService::IsSafeBrowsingEnabled() {
  return ::safe_browsing::IsSafeBrowsingEnabled(*GetPrefs());
}

bool ChromePasswordProtectionService::IsExtendedReporting() {
  return IsExtendedReportingEnabled(*GetPrefs());
}

bool ChromePasswordProtectionService::IsIncognito() {
  return profile_->IsOffTheRecord();
}

bool ChromePasswordProtectionService::IsInPasswordAlertMode(
    ReusedPasswordAccountType password_type) {
  return GetPasswordProtectionWarningTriggerPref(password_type) ==
         PASSWORD_REUSE;
}

bool ChromePasswordProtectionService::IsPingingEnabled(
    LoginReputationClientRequest::TriggerType trigger_type,
    ReusedPasswordAccountType password_type) {
  if (!IsSafeBrowsingEnabled()) {
    return false;
  }
  bool extended_reporting_enabled = IsExtendedReporting();
  if (trigger_type == LoginReputationClientRequest::PASSWORD_REUSE_EVENT ||
      trigger_type ==
          LoginReputationClientRequest::ONE_TIME_PASSWORD_FIELD_DETECTED) {
    // Don't send a ping if the password protection setting is off
    if (GetPasswordProtectionWarningTriggerPref(password_type) ==
        PASSWORD_PROTECTION_OFF) {
      return false;
    }
    // Don't send a ping if in password alert mode.
    if (IsInPasswordAlertMode(password_type)) {
      return false;
    }
    // If the account type is UNKNOWN (i.e. AccountInfo fields could not be
    // retrieved from server) and it's not an OTP ping, a phishy verdict will
    // not be acted on. Therefore any ping sent would be a pure telemetry ping.
    // Such pings should be gated by SBER.
    if (password_type.account_type() == ReusedPasswordAccountType::UNKNOWN &&
        trigger_type !=
            LoginReputationClientRequest::ONE_TIME_PASSWORD_FIELD_DETECTED) {
      return extended_reporting_enabled;
    }

// Only saved password reuse, GAIA password reuse, and OTP field detection can
// result in enforcement for Android users. Therefore, other types of password
// reuse events should be gated by Safe Browsing extended reporting because
// phishy verdicts won't be enforced making the pings telemetry-only.
#if BUILDFLAG(IS_ANDROID)
    if (password_type.account_type() ==
            ReusedPasswordAccountType::SAVED_PASSWORD ||
        password_type.account_type() == ReusedPasswordAccountType::GMAIL ||
        trigger_type ==
            LoginReputationClientRequest::ONE_TIME_PASSWORD_FIELD_DETECTED) {
      return true;
    }

    return extended_reporting_enabled;
#else
    return true;
#endif
  }
  // Since it's possible that on-focus pings could trigger for many visited
  // pages, don't send the ping when a SBER user is in Incognito to reduce data
  // sent to Google.
  return !IsIncognito() && extended_reporting_enabled;
}

RequestOutcome ChromePasswordProtectionService::GetPingNotSentReason(
    LoginReputationClientRequest::TriggerType trigger_type,
    const GURL& url,
    ReusedPasswordAccountType password_type) {
  DCHECK(!CanSendPing(trigger_type, url, password_type));
  if (IsInExcludedCountry()) {
    return RequestOutcome::EXCLUDED_COUNTRY;
  }
  if (!IsSafeBrowsingEnabled()) {
    return RequestOutcome::SAFE_BROWSING_DISABLED;
  }
  if (IsIncognito()) {
    return RequestOutcome::DISABLED_DUE_TO_INCOGNITO;
  }
  if (trigger_type == LoginReputationClientRequest::PASSWORD_REUSE_EVENT &&
      GetPasswordProtectionWarningTriggerPref(password_type) ==
          PASSWORD_PROTECTION_OFF) {
    return RequestOutcome::TURNED_OFF_BY_ADMIN;
  }
  PrefService* prefs = profile_->GetPrefs();
  if (IsURLAllowlistedByPolicy(url, *prefs)) {
    return RequestOutcome::MATCHED_ENTERPRISE_ALLOWLIST;
  }
  if (MatchesPasswordProtectionChangePasswordURL(url, *prefs)) {
    return RequestOutcome::MATCHED_ENTERPRISE_CHANGE_PASSWORD_URL;
  }
  if (MatchesPasswordProtectionLoginURL(url, *prefs)) {
    return RequestOutcome::MATCHED_ENTERPRISE_LOGIN_URL;
  }
  if (IsInPasswordAlertMode(password_type)) {
    return RequestOutcome::PASSWORD_ALERT_MODE;
  }
  if (url != GURL("about:blank") && !CanGetReputationOfURL(url)) {
    return RequestOutcome::URL_NOT_VALID_FOR_REPUTATION_COMPUTING;
  }
  return RequestOutcome::DISABLED_DUE_TO_USER_POPULATION;
}

void ChromePasswordProtectionService::FillUserPopulation(
    const GURL& main_frame_url,
    LoginReputationClientRequest* request_proto) {
  *request_proto->mutable_population() = GetUserPopulationForProfile(profile_);

  ChromeUserPopulation::PageLoadToken token =
      cache_manager_->GetPageLoadToken(main_frame_url);
  if (RealTimePolicyEngine::CanPerformFullURLLookup(
          profile_->GetPrefs(), profile_->IsOffTheRecord(),
          g_browser_process->variations_service())) {
    base::UmaHistogramBoolean(
        "SafeBrowsing.PageLoadToken.PasswordProtectionHasToken",
        token.has_token_value());
  }
  // It's possible that the token is not found because real time URL check is
  // not performed for this navigation. Create a new page load token in this
  // case.
  if (!token.has_token_value()) {
    token = cache_manager_->CreatePageLoadToken(main_frame_url);
  }
  request_proto->mutable_population()->mutable_page_load_tokens()->Add()->Swap(
      &token);
}

bool ChromePasswordProtectionService::IsPrimaryAccountSyncingHistory() const {
  syncer::SyncService* sync = SyncServiceFactory::GetForProfile(profile_);
  return sync &&
         sync->GetActiveDataTypes().Has(syncer::HISTORY_DELETE_DIRECTIVES) &&
         !sync->IsLocalSyncEnabled();
}

bool ChromePasswordProtectionService::IsPrimaryAccountSignedIn() const {
  return !GetAccountInfo().account_id.empty() &&
         GetAccountInfo().GetHostedDomain().has_value();
}

bool ChromePasswordProtectionService::IsAccountConsumer(
    const std::string& username) const {
  // Check that |username| is likely an email address because if |username| has
  // no email domain MayBeEnterpriseUserBasedOnEmail will assume it is a
  // consumer account.
  AccountInfo account_info = GetAccountInfoForUsername(username);
  std::optional<std::string_view> hosted_domain =
      account_info.GetHostedDomain();
  return (username.find("@") != std::string::npos &&
          !signin::AccountManagedStatusFinder::MayBeEnterpriseUserBasedOnEmail(
              username)) ||
         (hosted_domain.has_value() && hosted_domain->empty());
}

AccountInfo ChromePasswordProtectionService::GetAccountInfoForUsername(
    const std::string& username) const {
  auto* identity_manager = IdentityManagerFactory::GetForProfileIfExists(
      profile_->GetOriginalProfile());

  if (!identity_manager)
    return AccountInfo();

  std::vector<CoreAccountInfo> signed_in_accounts =
      identity_manager->GetAccountsWithRefreshTokens();
  auto account_iterator =
      std::ranges::find_if(signed_in_accounts, [username](const auto& account) {
        return password_manager::AreUsernamesSame(
            account.email,
            /*is_username1_gaia_account=*/true, username,
            /*is_username2_gaia_account=*/true);
      });
  if (account_iterator == signed_in_accounts.end())
    return AccountInfo();

  return identity_manager->FindExtendedAccountInfo(*account_iterator);
}

bool ChromePasswordProtectionService::IsInExcludedCountry() {
  variations::VariationsService* variations_service =
      g_browser_process->variations_service();
  if (!variations_service)
    return false;
  return std::ranges::contains(GetExcludedCountries(),
                               variations_service->GetLatestCountry());
}

void ChromePasswordProtectionService::
    RemoveUnhandledSyncPasswordReuseOnURLsDeleted(
        bool all_history,
        const history::URLRows& deleted_rows) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  ScopedDictPrefUpdate unhandled_sync_password_reuses(
      profile_->GetPrefs(), prefs::kSafeBrowsingUnhandledGaiaPasswordReuses);
  if (all_history) {
    unhandled_sync_password_reuses->clear();
    return;
  }

  for (const history::URLRow& row : deleted_rows) {
    if (!row.url().SchemeIsHTTPOrHTTPS())
      continue;
    unhandled_sync_password_reuses->Remove(
        Origin::Create(row.url()).Serialize());
  }
}

bool ChromePasswordProtectionService::UserClickedThroughSBInterstitial(
    PasswordProtectionRequest* request) {
  PasswordProtectionRequestContent* request_content =
      static_cast<PasswordProtectionRequestContent*>(request);
  content::WebContents* web_contents = request_content->web_contents();
  SBThreatType current_threat_type;
  if (!ui_manager_->IsUrlAllowlistedOrPendingForWebContents(
          web_contents->GetLastCommittedURL().GetWithEmptyPath(),
          web_contents->GetController().GetLastCommittedEntry(), web_contents,
          /*allowlist_only=*/true, &current_threat_type)) {
    return false;
  }
  return current_threat_type == SBThreatType::SB_THREAT_TYPE_URL_PHISHING ||
         current_threat_type == SBThreatType::SB_THREAT_TYPE_URL_MALWARE ||
         current_threat_type == SBThreatType::SB_THREAT_TYPE_URL_UNWANTED ||
         current_threat_type ==
             SBThreatType::SB_THREAT_TYPE_URL_CLIENT_SIDE_PHISHING;
}

AccountInfo ChromePasswordProtectionService::GetAccountInfo() const {
  auto* identity_manager = IdentityManagerFactory::GetForProfileIfExists(
      profile_->GetOriginalProfile());
  if (!identity_manager)
    return AccountInfo();

  return identity_manager->FindExtendedAccountInfo(
      identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin));
}

ChromePasswordProtectionService::ChromePasswordProtectionService(
    Profile* profile,
    scoped_refptr<SafeBrowsingUIManager> ui_manager,
    StringProvider sync_password_hash_provider,
    VerdictCacheManager* cache_manager,
    ChangePhishedCredentialsCallback add_phished_credentials,
    ChangePhishedCredentialsCallback remove_phished_credentials)
    : PasswordProtectionService(
          nullptr,
          nullptr,
          nullptr,
          nullptr,
          nullptr,
          false,
          nullptr,
          /*try_token_fetch=*/false,
          SafeBrowsingMetricsCollectorFactory::GetForProfile(profile)),
      sb_service_(nullptr),
      ui_manager_(ui_manager),
      profile_(profile),
      cache_manager_(cache_manager),
      add_phished_credentials_(std::move(add_phished_credentials)),
      remove_phished_credentials_(std::move(remove_phished_credentials)),
      sync_password_hash_provider_for_testing_(sync_password_hash_provider) {
  Init();
}

#if BUILDFLAG(IS_ANDROID)
ChromePasswordProtectionService::ChromePasswordProtectionService(
    Profile* profile,
    scoped_refptr<SafeBrowsingUIManager> ui_manager,
    StringProvider sync_password_hash_provider,
    VerdictCacheManager* cache_manager,
    ChangePhishedCredentialsCallback add_phished_credentials,
    ChangePhishedCredentialsCallback remove_phished_credentials,
    std::unique_ptr<PasswordCheckupLauncherHelper> checkup_launcher)
    : PasswordProtectionService(
          nullptr,
          nullptr,
          nullptr,
          nullptr,
          nullptr,
          false,
          nullptr,
          /*try_token_fetch=*/false,
          SafeBrowsingMetricsCollectorFactory::GetForProfile(profile)),
      sb_service_(nullptr),
      ui_manager_(ui_manager),
      profile_(profile),
      cache_manager_(cache_manager),
      add_phished_credentials_(std::move(add_phished_credentials)),
      remove_phished_credentials_(std::move(remove_phished_credentials)),
      sync_password_hash_provider_for_testing_(sync_password_hash_provider),
      checkup_launcher_(std::move(checkup_launcher)) {
  Init();
}
#endif

std::unique_ptr<PasswordProtectionCommitDeferringCondition>
MaybeCreateCommitDeferringCondition(
    content::NavigationHandle& navigation_handle) {
  Profile* profile = Profile::FromBrowserContext(
      navigation_handle.GetWebContents()->GetBrowserContext());
  ChromePasswordProtectionService* service =
      ChromePasswordProtectionService::GetPasswordProtectionService(profile);
  // |service| can be null in tests.
  return service
             ? service->MaybeCreateCommitDeferringCondition(navigation_handle)
             : nullptr;
}

PasswordProtectionTrigger
ChromePasswordProtectionService::GetPasswordProtectionWarningTriggerPref(
    ReusedPasswordAccountType password_type) const {
  bool is_policy_managed = profile_->GetPrefs()->HasPrefPath(
      prefs::kPasswordProtectionWarningTrigger);
  PasswordProtectionTrigger trigger_level =
      static_cast<PasswordProtectionTrigger>(profile_->GetPrefs()->GetInteger(
          prefs::kPasswordProtectionWarningTrigger));
  if (is_policy_managed && trigger_level == PASSWORD_PROTECTION_OFF) {
    return PASSWORD_PROTECTION_OFF;
  }
  if (password_type.account_type() == ReusedPasswordAccountType::GMAIL) {
    return PHISHING_REUSE;
  }
  return is_policy_managed ? trigger_level : PHISHING_REUSE;
}

bool ChromePasswordProtectionService::IsURLAllowlistedForPasswordEntry(
    const GURL& url) const {
  if (!profile_)
    return false;

  PrefService* prefs = profile_->GetPrefs();
  bool is_url_allowlisted_by_policy = IsURLAllowlistedByPolicy(url, *prefs);
  bool matches_change_password_url =
      MatchesPasswordProtectionChangePasswordURL(url, *prefs);
  bool matches_login_url = MatchesPasswordProtectionLoginURL(url, *prefs);

  CRSBLOG << __func__ << " URL that is being checked if allowlisted: " << url
          << " matches URL allowlist? " << is_url_allowlisted_by_policy
          << " matches password protection change password URL? "
          << matches_change_password_url
          << " matches password protection login URL? " << matches_login_url;
  return is_url_allowlisted_by_policy || matches_change_password_url ||
         matches_login_url;
}

void ChromePasswordProtectionService::PersistPhishedSavedPasswordCredential(
    const std::vector<password_manager::MatchingReusedCredential>&
        matching_reused_credentials) {
  if (!profile_)
    return;

  for (const auto& credential : matching_reused_credentials) {
    password_manager::PasswordStoreInterface* password_store =
        GetStoreForReusedCredential(credential);
    // Password store can be null in tests.
    if (!password_store) {
      continue;
    }
    add_phished_credentials_.Run(password_store, credential);
  }
}

void ChromePasswordProtectionService::RemovePhishedSavedPasswordCredential(
    const std::vector<password_manager::MatchingReusedCredential>&
        matching_reused_credentials) {
  if (!profile_)
    return;

  for (const auto& credential : matching_reused_credentials) {
    password_manager::PasswordStoreInterface* password_store =
        GetStoreForReusedCredential(credential);
    // Password store can be null in tests.
    if (!password_store) {
      continue;
    }
    remove_phished_credentials_.Run(password_store, credential);
  }
}

#if BUILDFLAG(IS_ANDROID)
ReferringAppInfo ChromePasswordProtectionService::GetReferringAppInfo(
    content::WebContents* web_contents) {
  // Do not get WebAPK info for PhishGuard. We don't consume referring WebAPK
  // data for password reuse events.
  internal::ReferringAppInfo info_struct = safe_browsing::GetReferringAppInfo(
      web_contents, /*get_webapk_info=*/false);
  ReferringAppInfo info_proto;
  info_proto.set_referring_app_source(info_struct.referring_app_source);
  info_proto.set_referring_app_name(info_struct.referring_app_name);
  return info_proto;
}
#endif

password_manager::PasswordReuseManager*
ChromePasswordProtectionService::GetPasswordReuseManager() const {
  return PasswordReuseManagerFactory::GetForProfile(profile_);
}

password_manager::PasswordStoreInterface*
ChromePasswordProtectionService::GetProfilePasswordStore() const {
  // Always use EXPLICIT_ACCESS as the password manager checks IsIncognito
  // itself when it shouldn't access the PasswordStoreInterface.
  return ProfilePasswordStoreFactory::GetForProfile(
             profile_, ServiceAccessType::EXPLICIT_ACCESS)
      .get();
}

password_manager::PasswordStoreInterface*
ChromePasswordProtectionService::GetAccountPasswordStore() const {
  // Always use EXPLICIT_ACCESS as the password manager checks IsIncognito
  // itself when it shouldn't access the PasswordStoreInterface.
  return AccountPasswordStoreFactory::GetForProfile(
             profile_, ServiceAccessType::EXPLICIT_ACCESS)
      .get();
}

void ChromePasswordProtectionService::SanitizeReferrerChain(
    ReferrerChain* referrer_chain) {
  SafeBrowsingNavigationObserverManager::SanitizeReferrerChain(referrer_chain);
}

bool ChromePasswordProtectionService::CanSendSamplePing() {
  // Send a sample ping only 1% of the time.
  return IsExtendedReporting() && !IsIncognito() &&
         (bypass_probability_for_tests_ ||
          base::RandDouble() <= kProbabilityForSendingReportsFromSafeURLs);
}

// Stores |verdict| in |settings| based on its |trigger_type|, |url|,
// reused |password_type|, |verdict| and |receive_time|.
void ChromePasswordProtectionService::CacheVerdict(
    const GURL& url,
    LoginReputationClientRequest::TriggerType trigger_type,
    ReusedPasswordAccountType password_type,
    const LoginReputationClientResponse& verdict,
    const base::Time& receive_time) {
  if (!CanGetReputationOfURL(url) || IsIncognito())
    return;
  cache_manager_->CachePhishGuardVerdict(trigger_type, password_type, verdict,
                                         receive_time);
}

// Looks up |settings| to find the cached verdict response. If verdict is not
// available or is expired, return VERDICT_TYPE_UNSPECIFIED. Can be called on
// any thread.
LoginReputationClientResponse::VerdictType
ChromePasswordProtectionService::GetCachedVerdict(
    const GURL& url,
    LoginReputationClientRequest::TriggerType trigger_type,
    ReusedPasswordAccountType password_type,
    LoginReputationClientResponse* out_response) {
  if (!url.is_valid() || !CanGetReputationOfURL(url))
    return LoginReputationClientResponse::VERDICT_TYPE_UNSPECIFIED;
  return cache_manager_->GetCachedPhishGuardVerdict(
      url, trigger_type, password_type, out_response);
}

int ChromePasswordProtectionService::GetStoredVerdictCount(
    LoginReputationClientRequest::TriggerType trigger_type) {
  return cache_manager_->GetStoredPhishGuardVerdictCount(trigger_type);
}

#if BUILDFLAG(FULL_SAFE_BROWSING)
gfx::Size ChromePasswordProtectionService::GetCurrentContentAreaSize() const {
  return GetLastActiveBrowserWindowInterfaceWithAnyProfile()
      ->capabilities()
      ->GetContentsSize();
}
#endif  // FULL_SAFE_BROWSING

password_manager::PasswordStoreInterface*
ChromePasswordProtectionService::GetStoreForReusedCredential(
    const password_manager::MatchingReusedCredential& reused_credential) {
  if (!profile_)
    return nullptr;
  return reused_credential.in_store ==
                 password_manager::PasswordForm::Store::kAccountStore
             ? GetAccountPasswordStore()
             : GetProfilePasswordStore();
}

void ChromePasswordProtectionService::OnGetVisibleVisitCountToHost(
    const TriggerType trigger_type,
    WebContentsKey web_contents_key,
    const base::TimeDelta& delay,
    bool did_proceed,
    const TriggerManager::DataCollectionPermissions&
        data_collection_permissions,
    history::VisibleVisitCountToHostResult result) {
  int visit_count = result.success && result.count ? result.count : 0;
  GetTriggerManager()->FinishCollectingThreatDetails(
      trigger_type, web_contents_key, delay, did_proceed, visit_count,
      data_collection_permissions);
}

}  // namespace safe_browsing
