// Copyright 2025 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/webauthn/chrome_web_authentication_delegate.h"

#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>

#include "base/memory/scoped_refptr.h"
#include "base/rand_util.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_command_line.h"
#include "base/test/scoped_feature_list.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/webauthn/chrome_authenticator_request_delegate.h"
#include "chrome/browser/webauthn/passkey_model_factory.h"
#include "chrome/browser/webauthn/webauthn_pref_names.h"
#include "chrome/browser/webauthn/webauthn_switches.h"
#include "chrome/common/pref_names.h"
#include "chrome/test/base/chrome_render_view_host_test_harness.h"
#include "components/prefs/pref_service.h"
#include "components/sync/protocol/webauthn_credential_specifics.pb.h"
#include "components/webauthn/core/browser/passkey_change_quota_tracker.h"
#include "components/webauthn/core/browser/passkey_model.h"
#include "components/webauthn/core/browser/signal_api_utils.h"
#include "components/webauthn/core/browser/test_passkey_model.h"
#include "content/public/browser/authenticator_request_client_delegate.h"
#include "content/public/browser/browser_context.h"
#include "content/public/test/web_contents_tester.h"
#include "device/fido/fido_request_handler_base.h"
#include "device/fido/public/features.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_util.h"
#include "extensions/common/extension_builder.h"
#include "extensions/common/permissions/permissions_data.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
#include "url/origin.h"

#if BUILDFLAG(IS_MAC)
#include "chrome/test/base/testing_profile.h"
#include "device/fido/mac/authenticator_config.h"
#endif  // BUILDFLAG(IS_MAC)

namespace {

static constexpr char kCredentialId1[] = "credential_id_1";
static constexpr char kCredentialId2[] = "credential_id_2";
static constexpr char kUserId[] = "hmiku-userid";
static constexpr char kUserName1[] = "hmiku";
static constexpr char kUserDisplayName1[] = "Hatsune Miku";
static constexpr char kUserName2[] = "reimu";
static constexpr char kUserDisplayName2[] = "Reimu Hakurei";
static constexpr char kRpId[] = "example.com";

using ShadowedCredentials = ::webauthn::PasskeyModel::ShadowedCredentials;
using TransportAvailabilityInfo =
    device::FidoRequestHandlerBase::TransportAvailabilityInfo;

std::vector<uint8_t> ToByteVector(std::string_view string) {
  return std::vector<uint8_t>(string.begin(), string.end());
}

class Observer : public testing::NiceMock<
                     ChromeAuthenticatorRequestDelegate::TestObserver> {
 public:
  MOCK_METHOD(void,
              Created,
              (ChromeAuthenticatorRequestDelegate * delegate),
              (override));
  MOCK_METHOD(void,
              OnTransportAvailabilityEnumerated,
              (ChromeAuthenticatorRequestDelegate * delegate,
               TransportAvailabilityInfo* tai),
              (override));
  MOCK_METHOD(void,
              UIShown,
              (ChromeAuthenticatorRequestDelegate * delegate),
              (override));
};

class ChromeWebAuthenticationDelegateTest
    : public ChromeRenderViewHostTestHarness {
 public:
  void SetUp() override {
    ChromeRenderViewHostTestHarness::SetUp();
    PasskeyModelFactory::GetInstance()->SetTestingFactoryAndUse(
        profile(),
        base::BindRepeating(
            [](content::BrowserContext*) -> std::unique_ptr<KeyedService> {
              return std::make_unique<webauthn::TestPasskeyModel>();
            }));
    ChromeAuthenticatorRequestDelegate::SetGlobalObserverForTesting(&observer_);
  }

  void TearDown() override {
    webauthn::PasskeyChangeQuotaTracker::GetInstance()->ResetForTesting();
    ChromeAuthenticatorRequestDelegate::SetGlobalObserverForTesting(nullptr);
    ChromeRenderViewHostTestHarness::TearDown();
  }

 protected:
  Observer observer_;
};

TEST_F(ChromeWebAuthenticationDelegateTest, IndividualAttestation) {
  static const struct TestCase {
    std::string name;
    std::string origin;
    std::string rp_id;
    std::string enterprise_attestation_switch_value;
    std::vector<std::string> permit_attestation_policy_values;
    bool expected;
  } kTestCases[] = {
      {"Basic", "https://login.example.com", "example.com", "", {}, false},
      {"Policy permits RP ID",
       "https://login.example.com",
       "example.com",
       "",
       {"example.com", "other.com"},
       true},
      {"Policy doesn't permit RP ID",
       "https://login.example.com",
       "example.com",
       "",
       {"other.com", "login.example.com", "https://example.com",
        "http://example.com", "https://login.example.com", "com", "*"},
       false},
      {"Policy doesn't care about the origin",
       "https://login.example.com",
       "example.com",
       "",
       {"https://login.example.com", "https://example.com"},
       false},
      {"Switch permits origin",
       "https://login.example.com",
       "example.com",
       "https://login.example.com,https://other.com,xyz:/invalidorigin",
       {},
       true},
      {"Switch doesn't permit origin",
       "https://login.example.com",
       "example.com",
       "example.com,login.example.com,http://login.example.com,https://"
       "example.com,https://a.login.example.com,https://*.example.com",
       {},
       false},
  };
  for (const auto& test : kTestCases) {
    base::test::ScopedCommandLine scoped_command_line;
    scoped_command_line.GetProcessCommandLine()->AppendSwitchASCII(
        webauthn::switches::kPermitEnterpriseAttestationOriginList,
        test.enterprise_attestation_switch_value);
    PrefService* prefs =
        Profile::FromBrowserContext(GetBrowserContext())->GetPrefs();
    if (!test.permit_attestation_policy_values.empty()) {
      base::ListValue policy_values;
      for (const std::string& v : test.permit_attestation_policy_values) {
        policy_values.Append(v);
      }
      prefs->SetList(prefs::kSecurityKeyPermitAttestation,
                     std::move(policy_values));
    } else {
      prefs->ClearPref(prefs::kSecurityKeyPermitAttestation);
    }
    ChromeWebAuthenticationDelegate delegate;
    EXPECT_EQ(delegate.ShouldPermitIndividualAttestation(
                  GetBrowserContext(), url::Origin::Create(GURL(test.origin)),
                  test.rp_id),
              test.expected)
        << test.name;
  }
}

constexpr char kExtensionId[] = "extension-id";
constexpr char kExtensionOrigin[] = "chrome-extension://extension-id";

typedef struct {
  const char* pattern;
  const char* rp_id;
} PatternRpIdPair;

constexpr PatternRpIdPair kValidRelyingPartyTestCases[] = {
    // Extensions are always allowed to claim their own origins.
    {"", kExtensionId},

    {"<all_urls>", "google.com"},
    {"https://*/*", "google.com"},
    {"https://google.com/", "google.com"},
    {"https://*.google.com/", "google.com"},

    // Localhost is special cased.
    {"http://localhost/", "localhost"},

    // Sanity check empty domain parts.
    // URLPattern trims all trailing dots from hosts for matching (both from the
    // pattern and from the evaluated host). Thus, patterns or hosts with any
    // number of trailing dots are canonicalized to the version without trailing
    // dots and match.
    {"https://google.com./", "google.com"},
    {"https://google.com./", "google.com."},
    {"https://google.com/", "google.com."},
    {"https://google.com/", "google.com.."},
    {"https://google.com../", "google.com"},
};

constexpr PatternRpIdPair kInvalidRelyingPartyTestCases[] = {
    // Extensions are not allowed to claim RP IDs belonging to other extensions.
    {"chrome-extension://some-other-extension/",
     "chrome-extension://some-other-extension/"},
    {"chrome-extension://some-other-extension/", "some-other-extension"},

    // Extensions are not allowed to claim RP IDs matching eTLDs, even if they
    // have host permissions over their origins.
    {"<all_urls>", "com"},
    {"https://*/*", "com"},
    {"https://com/", "com"},

    // Extensions that have access to non default ports are not allowed to claim
    // RP IDs matching their origin.
    {"https://google.com:1337/", "google.com"},

    // Unlike regular WebAuthn processing rules, extensions are not allowed to
    // claim higher level domains if they have permissions over a subdomain.
    {"https://*.subdomain.google.com/", "google.com"},

    // Single component domains are considered eTLDs, even if not on the PSL.
    {"https://myawesomedomain/", "myawesomedomain"},

    // The rules below are a sanity check to verify that the implementation
    // matches webauthn rules and are copied from
    // content/browser/webauth/authenticator_impl_unittest.cc.
    {"https://google.com/", "com"},
    {"http://google.com/", "google.com"},
    {"http://myawesomedomain/", "myawesomedomain"},
    {"https://google.com/", "foo.bar.google.com"},
    {"http://myawesomedomain/", "randomdomain"},
    {"https://myawesomedomain/", "randomdomain"},
    {"https://notgoogle.com/", "google.com)"},
    {"https://not-google.com/", "google.com)"},
    {"https://evil.appspot.com/", "appspot.com"},
    {"https://evil.co.uk/", "co.uk"},
    {"https://google.com/", ".google.com"},
    {"https://.com/", "com."},
    {"https://.co.uk/", "co.uk."},
    {"https://1.2.3/", "1.2.3"},
    {"https://1.2.3/", "2.3"},
    {"https://127.0.0.1/", "127.0.0.1"},
    {"https://127.0.0.1/", "27.0.0.1"},
    {"https://127.0.0.1/", ".0.0.1"},
    {"https://127.0.0.1/", "0.0.1"},
    {"https://[::127.0.0.1]/", "127.0.0.1"},
    {"https://[::127.0.0.1]/", "[127.0.0.1]"},
    {"https://[::1]/", "1"},
    {"https://[::1]/", "1]"},
    {"https://[::1]/", "::1"},
    {"https://[::1]/", "[::1]"},
    {"https://[1::1]/", "::1"},
    {"https://[1::1]/", "::1]"},
    {"https://[1::1]/", "[::1]"},
    {"http://google.com:443/", "google.com"},
    {"data:google.com/", "google.com"},
    {"data:text/html,google.com/", "google.com"},
    {"ws://google.com/", "google.com"},
    {"ftp://google.com/", "google.com"},
    {"file://google.com/", "google.com"},
    {"wss://google.com/", "google.com"},
    {"data:,/", ""},
    {"https://google.com/", ""},
    {"ws://google.com/", ""},
    {"wss://google.com/", ""},
    {"ftp://google.com/", ""},
    {"file://google.com/", ""},
    {"https://login.awesomecompany/", "awesomecompany"},
};

// Tests that an extension origin can claim relying party IDs it has permissions
// for.
TEST_F(ChromeWebAuthenticationDelegateTest,
       OverrideValidateDomainAndRelyingPartyIDTest_ExtensionValidCases) {
  for (const auto& test : kValidRelyingPartyTestCases) {
    scoped_refptr<const extensions::Extension> extension =
        extensions::ExtensionBuilder("Extension name")
            .SetID(kExtensionId)
            .AddHostPermission(test.pattern)
            .Build();
    extensions::ExtensionRegistry::Get(browser_context())
        ->AddEnabled(extension);

    ChromeWebAuthenticationDelegate delegate;
    SCOPED_TRACE(::testing::Message() << "rp_id=" << test.rp_id);
    SCOPED_TRACE(::testing::Message() << "pattern=" << test.pattern);
    EXPECT_TRUE(delegate.OverrideCallerOriginAndRelyingPartyIdValidation(
        GetBrowserContext(), url::Origin::Create(GURL(kExtensionOrigin)),
        test.rp_id));
  }
}

// Tests that an extension origin cannot claim relying party IDs it does not
// have permissions for.
TEST_F(ChromeWebAuthenticationDelegateTest,
       OverrideValidateDomainAndRelyingPartyIDTest_ExtensionInvalidCases) {
  for (const auto& test : kInvalidRelyingPartyTestCases) {
    scoped_refptr<const extensions::Extension> extension =
        extensions::ExtensionBuilder("Extension name")
            .SetID(kExtensionId)
            .AddHostPermission(test.pattern)
            .Build();
    extensions::ExtensionRegistry::Get(browser_context())
        ->AddEnabled(extension);

    ChromeWebAuthenticationDelegate delegate;
    SCOPED_TRACE(::testing::Message() << "rp_id=" << test.rp_id);
    SCOPED_TRACE(::testing::Message() << "pattern=" << test.pattern);
    EXPECT_FALSE(delegate.OverrideCallerOriginAndRelyingPartyIdValidation(
        GetBrowserContext(), url::Origin::Create(GURL(kExtensionOrigin)),
        test.rp_id));
  }
}

// Tests that OverrideCallerOriginAndRelyingPartyIdValidation returns false for
// chrome-extension origins that don't match an active extension.
TEST_F(ChromeWebAuthenticationDelegateTest,
       OverrideValidateDomainAndRelyingPartyIDTest_ExtensionNotFound) {
  ChromeWebAuthenticationDelegate delegate;
  EXPECT_FALSE(delegate.OverrideCallerOriginAndRelyingPartyIdValidation(
      GetBrowserContext(), url::Origin::Create(GURL(kExtensionOrigin)),
      kExtensionId));
}

// Tests that OverrideCallerOriginAndRelyingPartyIdValidation returns false for
// chrome-extension origins that are restricted by policy.
// Regression test for https://crbug.com/494341321.
TEST_F(ChromeWebAuthenticationDelegateTest,
       OverrideValidateDomainAndRelyingPartyIDTest_PolicyRestriction) {
  // Block extensions from google.com through policy.
  int context_id = extensions::util::GetBrowserContextId(browser_context());
  URLPattern default_policy_blocked_pattern =
      URLPattern(URLPattern::SCHEME_ALL, "*://google.com/*");
  extensions::URLPatternSet default_allowed_hosts;
  extensions::URLPatternSet default_blocked_hosts;
  default_blocked_hosts.AddPattern(default_policy_blocked_pattern);
  extensions::PermissionsData::SetDefaultPolicyHostRestrictions(
      context_id, default_blocked_hosts, default_allowed_hosts);

  // Create an extension that has permissions over every site.
  ChromeWebAuthenticationDelegate delegate;
  scoped_refptr<const extensions::Extension> extension =
      extensions::ExtensionBuilder("Extension name")
          .SetID(kExtensionId)
          .AddHostPermission("<all_urls>")
          .Build();
  // Extensions need a context ID set to respect policy.
  extension->permissions_data()->SetContextId(context_id);
  extensions::ExtensionRegistry::Get(browser_context())->AddEnabled(extension);

  // Verify the extension is not allowed to claim google.com.
  EXPECT_FALSE(delegate.OverrideCallerOriginAndRelyingPartyIdValidation(
      GetBrowserContext(), url::Origin::Create(GURL(kExtensionOrigin)),
      "google.com"));
}

// Tests that OverrideCallerOriginAndRelyingPartyIdValidation returns false for
// web origins.
TEST_F(ChromeWebAuthenticationDelegateTest,
       OverrideValidateDomainAndRelyingPartyIDTest_WebOrigin) {
  ChromeWebAuthenticationDelegate delegate;
  EXPECT_FALSE(delegate.OverrideCallerOriginAndRelyingPartyIdValidation(
      GetBrowserContext(), url::Origin::Create(GURL("https://google.com")),
      kExtensionId));
}

TEST_F(ChromeWebAuthenticationDelegateTest, MaybeGetRelyingPartyIdOverride) {
  ChromeWebAuthenticationDelegate delegate;
  static const struct {
    std::string rp_id;
    std::string origin;
    std::optional<std::string> expected;
  } kTests[] = {
      {"example.com", "https://example.com", std::nullopt},
      {"foo.com", "https://example.com", std::nullopt},
      {"example.com", kExtensionOrigin, std::nullopt},
      {kExtensionId, kExtensionOrigin, kExtensionOrigin},
  };
  for (const auto& test : kTests) {
    EXPECT_EQ(delegate.MaybeGetRelyingPartyIdOverride(
                  test.rp_id, url::Origin::Create(GURL(test.origin))),
              test.expected);
  }
}

TEST_F(ChromeWebAuthenticationDelegateTest, UpdatePasskey) {
  const auto test_origin = url::Origin::Create(GURL("https://example.com"));
  std::vector<uint8_t> user_id = ToByteVector(kUserId);
  ChromeWebAuthenticationDelegate delegate;
  webauthn::PasskeyModel* passkey_model =
      PasskeyModelFactory::GetForProfile(profile());
  ASSERT_TRUE(passkey_model);
  {
    sync_pb::WebauthnCredentialSpecifics passkey;
    passkey.set_credential_id(kCredentialId1);
    passkey.set_rp_id(kRpId);
    passkey.set_user_id(kUserId);
    passkey.set_user_name(kUserName1);
    passkey.set_user_display_name(kUserDisplayName1);
    passkey_model->AddNewPasskeyForTesting(std::move(passkey));
  }
  {
    // Setting the same username/display name should not result in an update.
    base::HistogramTester histogram_tester;
    delegate.UpdateUserPasskeys(web_contents(), test_origin, kRpId, user_id,
                                kUserName1, kUserDisplayName1);
    histogram_tester.ExpectUniqueSample(
        "WebAuthentication.SignalCurrentUserDetailsUpdatedGPMPasskey",
        webauthn::SignalCurrentUserDetailsResult::kPasskeyNotUpdated, 1);
  }
  {
    // Setting a different username/display name should result in an update.
    base::HistogramTester histogram_tester;
    delegate.UpdateUserPasskeys(web_contents(), test_origin, kRpId, user_id,
                                kUserName2, kUserDisplayName2);
    histogram_tester.ExpectUniqueSample(
        "WebAuthentication.SignalCurrentUserDetailsUpdatedGPMPasskey",
        webauthn::SignalCurrentUserDetailsResult::kPasskeyUpdated, 1);
    sync_pb::WebauthnCredentialSpecifics passkey = *passkey_model->GetPasskey(
        kRpId, kCredentialId1, ShadowedCredentials::kExclude);
    EXPECT_EQ(kUserName2, passkey.user_name());
    EXPECT_EQ(kUserDisplayName2, passkey.user_display_name());
  }
  {
    // Exceed the quota and try updating a passkey.
    for (int i = 0; i < webauthn::PasskeyChangeQuotaTracker::kMaxTokensPerRP;
         ++i) {
      delegate.UpdateUserPasskeys(web_contents(), test_origin, kRpId, user_id,
                                  base::RandBytesAsString(8),
                                  base::RandBytesAsString(8));
    }
    base::HistogramTester histogram_tester;
    delegate.UpdateUserPasskeys(web_contents(), test_origin, kRpId, user_id,
                                kUserName1, kUserDisplayName1);
    sync_pb::WebauthnCredentialSpecifics passkey = *passkey_model->GetPasskey(
        kRpId, kCredentialId1, ShadowedCredentials::kExclude);
    EXPECT_NE(kUserName1, passkey.user_name());
    EXPECT_NE(kUserDisplayName1, passkey.user_display_name());
    histogram_tester.ExpectUniqueSample(
        "WebAuthentication.SignalCurrentUserDetailsUpdatedGPMPasskey",
        webauthn::SignalCurrentUserDetailsResult::kQuotaExceeded, 1);
  }
}

class ChromeWebAuthenticationSignalApiHidePasskeysTest
    : public ChromeWebAuthenticationDelegateTest {
 public:
  void SetUp() override {
    ChromeWebAuthenticationDelegateTest::SetUp();
    passkey_model_ = PasskeyModelFactory::GetForProfile(profile());
    ASSERT_TRUE(passkey_model_);
    histogram_tester_ = std::make_unique<base::HistogramTester>();
  }

  void TearDown() override {
    passkey_model_ = nullptr;
    ChromeWebAuthenticationDelegateTest::TearDown();
  }

  void AddPasskey(const std::string& credential_id) {
    sync_pb::WebauthnCredentialSpecifics passkey;
    passkey.set_credential_id(credential_id);
    passkey.set_rp_id(kRpId);
    passkey.set_user_id(kUserId);
    passkey_model_->AddNewPasskeyForTesting(std::move(passkey));
  }

  void AddHiddenPasskey(const std::string& credential_id) {
    sync_pb::WebauthnCredentialSpecifics passkey;
    passkey.set_credential_id(credential_id);
    passkey.set_rp_id(kRpId);
    passkey.set_user_id(kUserId);
    passkey.set_hidden(true);
    passkey_model_->AddNewPasskeyForTesting(std::move(passkey));
  }

 protected:
  sync_pb::WebauthnCredentialSpecifics GetPasskey(const std::string& cred_id) {
    return *passkey_model_->GetPasskey(kRpId, cred_id,
                                       ShadowedCredentials::kExclude);
  }

  const url::Origin test_origin_ =
      url::Origin::Create(GURL("https://example.com"));
  ChromeWebAuthenticationDelegate delegate_;
  raw_ptr<webauthn::PasskeyModel> passkey_model_;
  std::unique_ptr<base::HistogramTester> histogram_tester_;
};

TEST_F(ChromeWebAuthenticationSignalApiHidePasskeysTest, Unrecognized_Found) {
  AddPasskey(kCredentialId1);
  ASSERT_FALSE(GetPasskey(kCredentialId1).hidden());
  delegate_.PasskeyUnrecognized(web_contents(), test_origin_,
                                ToByteVector(kCredentialId1), kRpId);
  EXPECT_TRUE(GetPasskey(kCredentialId1).hidden());

  histogram_tester_->ExpectUniqueSample(
      "WebAuthentication.SignalUnknownCredentialRemovedGPMPasskey",
      webauthn::SignalUnknownCredentialResult::kPasskeyHidden, 1);
}

TEST_F(ChromeWebAuthenticationSignalApiHidePasskeysTest,
       Unrecognized_AlreadyHidden) {
  AddPasskey(kCredentialId1);
  passkey_model_->HidePasskey(kCredentialId1,
                              /*hidden_time=*/base::Time::Now());
  delegate_.PasskeyUnrecognized(web_contents(), test_origin_,
                                ToByteVector(kCredentialId1), kRpId);
  EXPECT_TRUE(GetPasskey(kCredentialId1).hidden());

  histogram_tester_->ExpectUniqueSample(
      "WebAuthentication.SignalUnknownCredentialRemovedGPMPasskey",
      webauthn::SignalUnknownCredentialResult::kPasskeyAlreadyHidden, 1);

  // Check that the quota does not apply if no change happens.
  for (int i = 0; i < webauthn::PasskeyChangeQuotaTracker::kMaxTokensPerRP;
       ++i) {
    delegate_.PasskeyUnrecognized(web_contents(), test_origin_,
                                  ToByteVector(kCredentialId1), kRpId);
  }
  passkey_model_->UnhidePasskey(kCredentialId1);
  delegate_.PasskeyUnrecognized(web_contents(), test_origin_,
                                ToByteVector(kCredentialId1), kRpId);
  EXPECT_TRUE(GetPasskey(kCredentialId1).hidden());
  histogram_tester_->ExpectBucketCount(
      "WebAuthentication.SignalUnknownCredentialRemovedGPMPasskey",
      webauthn::SignalUnknownCredentialResult::kQuotaExceeded, 0);
}

TEST_F(ChromeWebAuthenticationSignalApiHidePasskeysTest,
       Unrecognized_NotFound) {
  delegate_.PasskeyUnrecognized(web_contents(), test_origin_,
                                ToByteVector(kCredentialId1), kRpId);
  histogram_tester_->ExpectUniqueSample(
      "WebAuthentication.SignalUnknownCredentialRemovedGPMPasskey",
      webauthn::SignalUnknownCredentialResult::kPasskeyNotFound, 1);
}

TEST_F(ChromeWebAuthenticationSignalApiHidePasskeysTest,
       Unrecognized_QuotaExceeded) {
  AddPasskey(kCredentialId1);
  for (int i = 0; i < webauthn::PasskeyChangeQuotaTracker::kMaxTokensPerRP;
       ++i) {
    delegate_.PasskeyUnrecognized(web_contents(), test_origin_,
                                  ToByteVector(kCredentialId1), kRpId);
    passkey_model_->UnhidePasskey(kCredentialId1);
  }
  base::HistogramTester histogram_tester;
  delegate_.PasskeyUnrecognized(web_contents(), test_origin_,
                                ToByteVector(kCredentialId1), kRpId);
  histogram_tester.ExpectUniqueSample(
      "WebAuthentication.SignalUnknownCredentialRemovedGPMPasskey",
      webauthn::SignalUnknownCredentialResult::kQuotaExceeded, 1);
}

TEST_F(ChromeWebAuthenticationSignalApiHidePasskeysTest,
       SignalAllAcceptedCredentials_Hide) {
  base::HistogramTester histogram_tester;
  AddPasskey(kCredentialId1);

  // Pass a list that does not contain the hidden passkey.
  std::vector<std::vector<uint8_t>> credentials = {
      ToByteVector(kCredentialId2)};
  delegate_.SignalAllAcceptedCredentials(web_contents(), test_origin_, kRpId,
                                         ToByteVector(kUserId), credentials);
  histogram_tester.ExpectUniqueSample(
      "WebAuthentication.SignalAllAcceptedCredentialsRemovedGPMPasskey",
      webauthn::SignalAllAcceptedCredentialsResult::kPasskeyHidden, 1);
  // The originally active passkey should be hidden.
  EXPECT_TRUE(GetPasskey(kCredentialId1).hidden());
}

TEST_F(ChromeWebAuthenticationSignalApiHidePasskeysTest,
       SignalAllAcceptedCredentials_Restore) {
  base::HistogramTester histogram_tester;
  AddHiddenPasskey(kCredentialId1);

  // Pass a list that contains the hidden passkey.
  std::vector<std::vector<uint8_t>> credentials = {
      ToByteVector(kCredentialId1)};
  delegate_.SignalAllAcceptedCredentials(web_contents(), test_origin_, kRpId,
                                         ToByteVector(kUserId), credentials);
  histogram_tester.ExpectUniqueSample(
      "WebAuthentication.SignalAllAcceptedCredentialsRemovedGPMPasskey",
      webauthn::SignalAllAcceptedCredentialsResult::kPasskeyRestored, 1);
  // The passkey should have been restored.
  EXPECT_FALSE(GetPasskey(kCredentialId1).hidden());
}

TEST_F(ChromeWebAuthenticationSignalApiHidePasskeysTest,
       SignalAllAcceptedCredentials_NoChanges) {
  base::HistogramTester histogram_tester;
  AddPasskey(kCredentialId1);

  // Pass a list that contains the active passkey.
  std::vector<std::vector<uint8_t>> credentials = {
      ToByteVector(kCredentialId1)};
  delegate_.SignalAllAcceptedCredentials(web_contents(), test_origin_, kRpId,
                                         ToByteVector(kUserId), credentials);
  histogram_tester.ExpectUniqueSample(
      "WebAuthentication.SignalAllAcceptedCredentialsRemovedGPMPasskey",
      webauthn::SignalAllAcceptedCredentialsResult::kNoPasskeyChanged, 1);
  // The passkey should still be visible.
  EXPECT_FALSE(GetPasskey(kCredentialId1).hidden());
}

TEST_F(ChromeWebAuthenticationSignalApiHidePasskeysTest,
       SignalAllAcceptedCredentials_NoPasskeysMatch_RpId) {
  base::HistogramTester histogram_tester;
  AddPasskey(kCredentialId1);

  // Pass a list that contains passkeys from a different relying party.
  std::vector<std::vector<uint8_t>> credentials = {
      ToByteVector(kCredentialId1)};
  delegate_.SignalAllAcceptedCredentials(web_contents(), test_origin_,
                                         "another.com", ToByteVector(kUserId),
                                         credentials);
  histogram_tester.ExpectUniqueSample(
      "WebAuthentication.SignalAllAcceptedCredentialsRemovedGPMPasskey",
      webauthn::SignalAllAcceptedCredentialsResult::kNoPasskeyChanged, 1);
}

TEST_F(ChromeWebAuthenticationSignalApiHidePasskeysTest,
       SignalAllAcceptedCredentials_NoPasskeysMatch_UserId) {
  base::HistogramTester histogram_tester;
  AddPasskey(kCredentialId1);

  // Pass a list that contains passkeys from a different user id.
  std::vector<std::vector<uint8_t>> credentials = {
      ToByteVector(kCredentialId1)};
  delegate_.SignalAllAcceptedCredentials(web_contents(), test_origin_, kRpId,
                                         ToByteVector("another-userid"),
                                         credentials);
  histogram_tester.ExpectUniqueSample(
      "WebAuthentication.SignalAllAcceptedCredentialsRemovedGPMPasskey",
      webauthn::SignalAllAcceptedCredentialsResult::kNoPasskeyChanged, 1);
}

TEST_F(ChromeWebAuthenticationSignalApiHidePasskeysTest,
       SignalAllAcceptedCredentials_QuotaExceeded) {
  AddPasskey(kCredentialId1);

  // Exceed the quota.
  for (int i = 0; i < webauthn::PasskeyChangeQuotaTracker::kMaxTokensPerRP;
       ++i) {
    std::vector<std::vector<uint8_t>> credentials = {
        ToByteVector(i % 2 == 0 ? kCredentialId2 : kCredentialId1)};
    delegate_.SignalAllAcceptedCredentials(web_contents(), test_origin_, kRpId,
                                           ToByteVector(kUserId), credentials);
  }

  // Attempt making another change that would hide the passkey.
  passkey_model_->UnhidePasskey(kCredentialId1);
  base::HistogramTester histogram_tester;
  std::vector<std::vector<uint8_t>> credentials = {
      ToByteVector(kCredentialId2)};
  delegate_.SignalAllAcceptedCredentials(web_contents(), test_origin_, kRpId,
                                         ToByteVector(kUserId), credentials);
  histogram_tester.ExpectUniqueSample(
      "WebAuthentication.SignalAllAcceptedCredentialsRemovedGPMPasskey",
      webauthn::SignalAllAcceptedCredentialsResult::kQuotaExceeded, 1);
  EXPECT_FALSE(GetPasskey(kCredentialId1).hidden());
}

}  // namespace
