// 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.

#import "components/webauthn/ios/passkey_tab_helper.h"

#import "base/base64.h"
#import "base/rand_util.h"
#import "base/strings/string_number_conversions.h"
#import "base/strings/utf_string_conversions.h"
#import "base/test/metrics/histogram_tester.h"
#import "base/test/run_until.h"
#import "components/autofill/ios/browser/autofill_java_script_feature.h"
#import "components/autofill/ios/browser/autofill_util.h"
#import "components/autofill/ios/form_util/child_frame_registrar.h"
#import "components/password_manager/core/browser/mock_password_manager.h"
#import "components/password_manager/core/browser/password_store/password_form_converters.h"
#import "components/password_manager/core/browser/password_store/test_password_store.h"
#import "components/password_manager/ios/ios_password_manager_driver_factory.h"
#import "components/password_manager/ios/shared_password_controller.h"
#import "components/webauthn/core/browser/passkey_change_quota_tracker.h"
#import "components/webauthn/core/browser/passkey_model.h"
#import "components/webauthn/core/browser/test_passkey_model.h"
#import "components/webauthn/ios/fake_ios_passkey_client.h"
#import "components/webauthn/ios/ios_webauthn_credentials_delegate.h"
#import "components/webauthn/ios/ios_webauthn_credentials_delegate_factory.h"
#import "components/webauthn/ios/passkey_java_script_feature.h"
#import "components/webauthn/ios/passkey_test_util.h"
#import "ios/web/public/test/fakes/fake_browser_state.h"
#import "ios/web/public/test/fakes/fake_web_client.h"
#import "ios/web/public/test/fakes/fake_web_frame.h"
#import "ios/web/public/test/fakes/fake_web_frames_manager.h"
#import "ios/web/public/test/fakes/fake_web_state.h"
#import "ios/web/public/test/js_test_util.h"
#import "ios/web/public/test/scoped_testing_web_client.h"
#import "ios/web/public/test/web_task_environment.h"
#import "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
#import "services/network/test/test_url_loader_factory.h"
#import "testing/platform_test.h"
#import "third_party/ocmock/OCMock/OCMock.h"

namespace webauthn {

using PasskeyTabHelper::WebAuthenticationIOSContentAreaEvent::kCreateRequested;
using PasskeyTabHelper::WebAuthenticationIOSContentAreaEvent::
    kCreateResolvedGpm;
using PasskeyTabHelper::WebAuthenticationIOSContentAreaEvent::
    kCreateResolvedNonGpm;
using PasskeyTabHelper::WebAuthenticationIOSContentAreaEvent::kGetRequested;
using PasskeyTabHelper::WebAuthenticationIOSContentAreaEvent::kGetResolvedGpm;
using PasskeyTabHelper::WebAuthenticationIOSContentAreaEvent::
    kGetResolvedNonGpm;
using PasskeyTabHelper::WebAuthenticationIOSContentAreaEvent::
    kIncognitoInterstitialShown;

namespace {

constexpr char kCredentialId[] = "credential_id";
constexpr char kCredentialId2[] = "credential_id_2";
constexpr char kWellKnownURL[] = "https://example.com/.well-known/webauthn";
constexpr char kOriginURL[] = "https://example.com";
constexpr char kInsecureOriginURL[] = "http://example.com";
constexpr char kRelatedOriginURL[] = "https://example.ca";
constexpr char16_t kDeferToRendererJsCall[] = u"deferToRenderer";

constexpr char kWebAuthenticationIOSContentAreaEventHistogram[] =
    "WebAuthentication.IOS.ContentAreaEvent";

constexpr char kMainRemoteFrameId[] = "1effd8f52a067c8d3a01762d3c41dfda";

AssertionRequestParams BuildTestAssertionRequestParams(
    const std::vector<device::PublicKeyCredentialDescriptor>& allow_credentials,
    device::UserVerificationRequirement user_verification =
        device::UserVerificationRequirement::kPreferred,
    std::string request_id = kFakeRequestId,
    std::string frame_id = web::kMainFakeFrameId,
    std::optional<std::string> remote_frame_token = std::nullopt) {
  std::optional<autofill::RemoteFrameToken> remote_token;
  if (remote_frame_token && !remote_frame_token->empty()) {
    std::optional<base::UnguessableToken> deserialized =
        autofill::DeserializeJavaScriptFrameId(*remote_frame_token);
    if (deserialized) {
      remote_token = autofill::RemoteFrameToken(*deserialized);
    }
  }

  IOSPasskeyClient::RequestInfo request_info(frame_id, request_id,
                                             remote_token);
  device::PublicKeyCredentialRpEntity rp_entity(kRpId);
  std::vector<uint8_t> challenge;
  PasskeyRequestParams::RequestType request_type =
      PasskeyRequestParams::RequestType::kModal;
  PasskeyExtensionData extension_data;
  PasskeyRequestParams request_params(
      std::move(request_info), std::move(rp_entity), std::move(challenge),
      user_verification, request_type, std::move(extension_data));
  return AssertionRequestParams(std::move(request_params), allow_credentials);
}
}  // namespace

class PasskeyTabHelperTest : public PlatformTest {
 public:
  PasskeyTabHelperTest()
      : scoped_web_client_(std::make_unique<web::FakeWebClient>()) {
    static_cast<web::FakeWebClient*>(scoped_web_client_.Get())
        ->SetJavaScriptFeatures(
            {PasskeyJavaScriptFeature::GetInstance(),
             autofill::AutofillJavaScriptFeature::GetInstance()});
    fake_web_state_.SetBrowserState(&fake_browser_state_);
    fake_browser_state_.SetSharedURLLoaderFactory(
        base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
            &test_url_loader_factory_));
    web::test::OverrideJavaScriptFeatures(
        &fake_browser_state_,
        {PasskeyJavaScriptFeature::GetInstance(),
         autofill::AutofillJavaScriptFeature::GetInstance()});

    test_password_store_ =
        base::MakeRefCounted<password_manager::TestPasswordStore>();
    test_password_store_->Init();
    auto client = std::make_unique<FakeIOSPasskeyClient>();
    client_ = client.get();
    PasskeyTabHelper::CreateForWebState(&fake_web_state_, passkey_model_.get(),
                                        test_password_store_,
                                        std::move(client));
    PasskeyChangeQuotaTracker::GetInstance()->ResetForTesting();
  }

  ~PasskeyTabHelperTest() override {
    if (test_password_store_) {
      test_password_store_->ShutdownOnUIThread();
    }
    client_ = nullptr;
  }

 protected:
  void SetUpChildFrameRegistrarAndRegisterFrame(
      const std::string& local_frame_id,
      const std::string& remote_frame_id) {
    autofill::ChildFrameRegistrar::GetOrCreateForWebState(&fake_web_state_);
    RegisterFrame(local_frame_id, remote_frame_id);
  }

  void RegisterFrame(const std::string& local_frame_id,
                     const std::string& remote_frame_id) {
    autofill::LocalFrameToken local_token(
        *autofill::DeserializeJavaScriptFrameId(local_frame_id));
    autofill::RemoteFrameToken remote_token(
        *autofill::DeserializeJavaScriptFrameId(remote_frame_id));
    autofill::ChildFrameRegistrar::FromWebState(&fake_web_state_)
        ->RegisterMapping(remote_token, local_token);
  }

  PasskeyTabHelper* passkey_tab_helper() {
    return PasskeyTabHelper::FromWebState(&fake_web_state_);
  }

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

  void MaybeShowInterstitialAndRegister(RegistrationRequestParams params) {
    passkey_tab_helper()->MaybeShowInterstitialAndRegister(std::move(params));
  }

  bool HasExcludedPasskey(const RegistrationRequestParams& params) {
    return passkey_tab_helper()->HasExcludedPasskey(params);
  }

  bool CanPerformAutomaticPasskeyUpgrade(
      const RegistrationRequestParams& params,
      const std::vector<password_manager::PasswordForm>& logins) {
    return passkey_tab_helper()->CanPerformAutomaticPasskeyUpgrade(
        params, password_manager::FromPasswordForms(logins));
  }

  // Returns the list of passkeys filtered by the allowed credentials list.
  std::vector<sync_pb::WebauthnCredentialSpecifics> GetFilteredPasskeys(
      const AssertionRequestParams& params) {
    return passkey_tab_helper()->GetFilteredPasskeys(params);
  }

  // Sets up a web frame manager with a web frame.
  void SetUpWebFramesManagerAndWebFrame(const GURL& origin) {
    web::ContentWorld passkey_world =
        PasskeyJavaScriptFeature::GetInstance()->GetSupportedContentWorld();
    auto frames_manager = std::make_unique<web::FakeWebFramesManager>();
    auto frame = web::FakeWebFrame::CreateMainWebFrame(origin);
    frame->set_browser_state(&fake_browser_state_);
    frames_manager->AddWebFrame(std::move(frame));
    fake_web_state_.SetWebFramesManager(passkey_world,
                                        std::move(frames_manager));

    // Also set up a manager for AutofillJavaScriptFeature if it uses a
    // different world.
    web::ContentWorld autofill_world =
        autofill::AutofillJavaScriptFeature::GetInstance()
            ->GetSupportedContentWorld();
    if (passkey_world != autofill_world) {
      auto autofill_frames_manager =
          std::make_unique<web::FakeWebFramesManager>();
      auto autofill_frame = web::FakeWebFrame::CreateMainWebFrame(origin);
      autofill_frame->set_browser_state(&fake_browser_state_);
      autofill_frames_manager->AddWebFrame(std::move(autofill_frame));
      fake_web_state_.SetWebFramesManager(autofill_world,
                                          std::move(autofill_frames_manager));
    }
  }

  // Sets up a web frame manager without any web frames, and returns the
  // manager.
  web::FakeWebFramesManager* SetUpWebFramesManagerWithoutFrame() {
    web::ContentWorld passkey_world =
        PasskeyJavaScriptFeature::GetInstance()->GetSupportedContentWorld();
    auto frames_manager = std::make_unique<web::FakeWebFramesManager>();
    web::FakeWebFramesManager* frames_manager_ptr = frames_manager.get();
    fake_web_state_.SetWebFramesManager(passkey_world,
                                        std::move(frames_manager));

    web::ContentWorld autofill_world =
        autofill::AutofillJavaScriptFeature::GetInstance()
            ->GetSupportedContentWorld();
    if (passkey_world != autofill_world) {
      fake_web_state_.SetWebFramesManager(
          autofill_world, std::make_unique<web::FakeWebFramesManager>());
    }

    // Manually register the tab helper as observer to the frames manager.
    frames_manager_ptr->AddObserver(passkey_tab_helper());
    return frames_manager_ptr;
  }

  // Sets up the IOSPasswordManagerDriver needed to retrieve the
  // WebAuthnCredentialsDelegate.
  void SetUpIOSPasswordManagerDriver() {
    IOSPasswordManagerDriverFactory::CreateForWebState(
        &fake_web_state_, OCMStrictClassMock([SharedPasswordController class]),
        &password_manager_);
  }

  void SetUpRelatedOrigin() {
    SetUpIOSPasswordManagerDriver();
    SetUpWebFramesManagerAndWebFrame(GURL(kRelatedOriginURL));
  }

  // Sets up mock response for .well-known/webauthn with provided parameters.
  void SetUpMockWellKnownResponse(const std::string& body,
                                  const std::string& content_type,
                                  const std::string& mime_type,
                                  net::Error net_error) {
    network::mojom::URLResponseHeadPtr head =
        network::mojom::URLResponseHead::New();
    head->headers = base::MakeRefCounted<net::HttpResponseHeaders>("");
    head->headers->SetHeader("Content-Type", content_type);
    head->mime_type = mime_type;
    test_url_loader_factory_.AddResponse(
        GURL(kWellKnownURL), std::move(head), body,
        network::URLLoaderCompletionStatus(net_error));
  }

  // Returns a random request ID of the same charset as the fake request ID.
  std::string GetUniqueRequestId() {
    return base::HexEncodeLower(base::RandBytesAsVector(16));
  }

  // Verifies that ShouldPerformUserVerification returns the expected results
  // with and without biometric authentication enabled.
  void VerifyShouldPerformUserVerification(const std::string& request_id,
                                           bool expected_with_biometrics,
                                           bool expected_without_biometrics) {
    SCOPED_TRACE(testing::Message() << "ID: " << request_id);
    client_->SetBiometricsEnabled(true);
    EXPECT_EQ(passkey_tab_helper()->ShouldPerformUserVerification(request_id),
              std::optional<bool>(expected_with_biometrics));
    client_->SetBiometricsEnabled(false);
    EXPECT_EQ(passkey_tab_helper()->ShouldPerformUserVerification(request_id),
              std::optional<bool>(expected_without_biometrics));
  }

  web::WebTaskEnvironment task_environment_;
  web::ScopedTestingWebClient scoped_web_client_;
  base::HistogramTester histogram_tester_;
  std::unique_ptr<PasskeyModel> passkey_model_ =
      std::make_unique<TestPasskeyModel>();
  web::FakeBrowserState fake_browser_state_;
  network::TestURLLoaderFactory test_url_loader_factory_;
  raw_ptr<FakeIOSPasskeyClient> client_ = nullptr;
  password_manager::MockPasswordManager password_manager_;
  scoped_refptr<password_manager::TestPasswordStore> test_password_store_;
  web::FakeWebState fake_web_state_;
};

TEST_F(PasskeyTabHelperTest, LogsEventFromGetRequested) {
  passkey_tab_helper()->LogEvent(kGetRequested);

  constexpr int kGetRequestedBucket = 0;
  histogram_tester_.ExpectUniqueSample(
      kWebAuthenticationIOSContentAreaEventHistogram, kGetRequestedBucket,
      /*count=*/1);
}

TEST_F(PasskeyTabHelperTest, LogsEventFromCreateRequested) {
  passkey_tab_helper()->LogEvent(kCreateRequested);

  constexpr int kCreateRequestedBucket = 1;
  histogram_tester_.ExpectUniqueSample(
      kWebAuthenticationIOSContentAreaEventHistogram, kCreateRequestedBucket,
      /*count=*/1);
}

TEST_F(PasskeyTabHelperTest, LogsGetResolvedEventGpmPasskey) {
  passkey_tab_helper()->LogEvent(kGetResolvedGpm);

  constexpr int kGetResolvedGpmBucket = 2;
  histogram_tester_.ExpectUniqueSample(
      kWebAuthenticationIOSContentAreaEventHistogram, kGetResolvedGpmBucket,
      /*count=*/1);
}

TEST_F(PasskeyTabHelperTest, LogsGetResolvedEventNonGpmPasskey) {
  passkey_tab_helper()->LogEvent(kGetResolvedNonGpm);

  constexpr int kGetResolvedNonGpmBucket = 3;
  histogram_tester_.ExpectUniqueSample(
      kWebAuthenticationIOSContentAreaEventHistogram, kGetResolvedNonGpmBucket,
      /*count=*/1);
}

TEST_F(PasskeyTabHelperTest, LogsEventFromCreateResolvedGpm) {
  passkey_tab_helper()->LogEvent(kCreateResolvedGpm);

  constexpr int kCreateRequestedBucket = 4;
  histogram_tester_.ExpectUniqueSample(
      kWebAuthenticationIOSContentAreaEventHistogram, kCreateRequestedBucket,
      /*count=*/1);
}

TEST_F(PasskeyTabHelperTest, LogsEventFromCreateResolvedNonGpm) {
  passkey_tab_helper()->LogEvent(kCreateResolvedNonGpm);

  constexpr int kCreateRequestedBucket = 5;
  histogram_tester_.ExpectUniqueSample(
      kWebAuthenticationIOSContentAreaEventHistogram, kCreateRequestedBucket,
      /*count=*/1);
}

TEST_F(PasskeyTabHelperTest, HasExcludedPasskey) {
  // Empty exclude credentials list, expecting no match.
  std::vector<device::PublicKeyCredentialDescriptor> exclude_credentials;
  ASSERT_FALSE(
      HasExcludedPasskey(BuildRegistrationRequestParams(exclude_credentials)));

  // Add passkey with kCredentialId.
  sync_pb::WebauthnCredentialSpecifics passkey = GetTestPasskey(kCredentialId);
  passkey_model_->AddNewPasskeyForTesting(std::move(passkey));

  // Add kCredentialId2 to exclude credentials list, expecting no match.
  exclude_credentials.push_back(
      {device::CredentialType::kPublicKey, AsByteVector(kCredentialId2)});
  ASSERT_FALSE(
      HasExcludedPasskey(BuildRegistrationRequestParams(exclude_credentials)));

  // Add kCredentialId to exclude credentials list, expecting a match.
  exclude_credentials.push_back(
      {device::CredentialType::kPublicKey, AsByteVector(kCredentialId)});
  ASSERT_TRUE(
      HasExcludedPasskey(BuildRegistrationRequestParams(exclude_credentials)));
}

TEST_F(PasskeyTabHelperTest, FilterPasskeys) {
  // Add passkey with kCredentialId.
  sync_pb::WebauthnCredentialSpecifics passkey = GetTestPasskey(kCredentialId);
  passkey_model_->AddNewPasskeyForTesting(std::move(passkey));

  // Add passkey with kCredentialId2.
  sync_pb::WebauthnCredentialSpecifics passkey2 =
      GetTestPasskey(kCredentialId2);
  passkey_model_->AddNewPasskeyForTesting(std::move(passkey2));

  // Make sure 2 distinct passkeys were added.
  std::vector<sync_pb::WebauthnCredentialSpecifics> passkeys =
      passkey_model_->GetPasskeys(kRpId,
                                  PasskeyModel::ShadowedCredentials::kExclude);
  EXPECT_EQ(passkeys.size(), 2u);

  // Empty allow credentials list, expect no filtering.
  std::vector<device::PublicKeyCredentialDescriptor> allow_credentials;
  std::vector<sync_pb::WebauthnCredentialSpecifics> filtered_passkeys =
      GetFilteredPasskeys(BuildAssertionRequestParams(allow_credentials));
  EXPECT_EQ(filtered_passkeys.size(), 2u);

  // Add kCredentialId2 to allow credentials list, expect 1 filtered passkey.
  allow_credentials.push_back(
      {device::CredentialType::kPublicKey, AsByteVector(kCredentialId2)});
  filtered_passkeys =
      GetFilteredPasskeys(BuildAssertionRequestParams(allow_credentials));
  EXPECT_EQ(filtered_passkeys.size(), 1u);

  // Add kCredentialId to allow credentials list, expect 1 filtered passkey.
  allow_credentials.clear();
  allow_credentials.push_back(
      {device::CredentialType::kPublicKey, AsByteVector(kCredentialId)});
  filtered_passkeys =
      GetFilteredPasskeys(BuildAssertionRequestParams(allow_credentials));
  EXPECT_EQ(filtered_passkeys.size(), 1u);

  // Have both credentials in the allow credentials list, expect no filtering.
  allow_credentials.push_back(
      {device::CredentialType::kPublicKey, AsByteVector(kCredentialId2)});
  filtered_passkeys =
      GetFilteredPasskeys(BuildAssertionRequestParams(allow_credentials));
  EXPECT_EQ(filtered_passkeys.size(), 2u);
}

// Tests that the IOSWebAuthnCredentialsDelegate receives the available passkeys
// when a passkey assertion request is processed.
TEST_F(PasskeyTabHelperTest, SendPasskeysToWebAuthnCredentialsDelegate) {
  SetUpWebFramesManagerAndWebFrame(GURL(kOriginURL));
  SetUpIOSPasswordManagerDriver();
  SetUpChildFrameRegistrarAndRegisterFrame(web::kMainFakeFrameId,
                                           kMainRemoteFrameId);

  // Add passkey with `kCredentialId`.
  sync_pb::WebauthnCredentialSpecifics passkey = GetTestPasskey(kCredentialId);
  passkey_model_->AddNewPasskeyForTesting(std::move(passkey));

  IOSWebAuthnCredentialsDelegate* delegate =
      IOSWebAuthnCredentialsDelegateFactory::GetFactory(&fake_web_state_)
          ->GetDelegateForFrameId(web::kMainFakeFrameId);

  // Verify that the delegate has no passkeys initially.
  auto passkeys_before = delegate->GetPasskeys();
  ASSERT_FALSE(passkeys_before.has_value());
  EXPECT_EQ(passkeys_before.error(),
            password_manager::WebAuthnCredentialsDelegate::
                PasskeysUnavailableReason::kNotReceived);

  AssertionRequestParams params = BuildTestAssertionRequestParams(
      /*allow_credentials=*/{}, device::UserVerificationRequirement::kPreferred,
      kFakeRequestId, web::kMainFakeFrameId, kMainRemoteFrameId);
  passkey_tab_helper()->HandleGetRequestedEvent(std::move(params));

  // Verify that the delegate has received the passkey.
  auto passkeys_after = delegate->GetPasskeys();
  ASSERT_TRUE(passkeys_after.has_value());
  EXPECT_EQ(passkeys_after.value()->size(), 1u);
  EXPECT_EQ(passkeys_after.value()->at(0).credential_id(),
            AsByteVector(kCredentialId));
}

// Tests that marking a passkey as user verified is correctly reflected when the
// passkey is selected via the delegate.
TEST_F(PasskeyTabHelperTest, MarkPasskeyAsUserVerified) {
  SetUpWebFramesManagerAndWebFrame(GURL(kOriginURL));
  SetUpIOSPasswordManagerDriver();
  SetUpChildFrameRegistrarAndRegisterFrame(web::kMainFakeFrameId,
                                           kMainRemoteFrameId);

  // Add passkey with `kCredentialId`.
  sync_pb::WebauthnCredentialSpecifics passkey = GetTestPasskey(kCredentialId);
  passkey_model_->AddNewPasskeyForTesting(std::move(passkey));

  IOSWebAuthnCredentialsDelegate* delegate =
      IOSWebAuthnCredentialsDelegateFactory::GetFactory(&fake_web_state_)
          ->GetDelegateForFrameId(web::kMainFakeFrameId);

  AssertionRequestParams params = BuildTestAssertionRequestParams(
      /*allow_credentials=*/{}, device::UserVerificationRequirement::kPreferred,
      kFakeRequestId, web::kMainFakeFrameId, kMainRemoteFrameId);
  passkey_tab_helper()->HandleGetRequestedEvent(std::move(params));

  // Verify that the delegate has received the passkey.
  auto passkeys = delegate->GetPasskeys();
  ASSERT_TRUE(passkeys.has_value());
  ASSERT_EQ(passkeys.value()->size(), 1u);

  // Base64 encode the credential ID to use as backend_id.
  std::string backend_id = base::Base64Encode(kCredentialId);

  // Mark the passkey as user verified.
  delegate->MarkPasskeyAsUserVerified(backend_id);

  // Select the passkey.
  base::RunLoop run_loop;
  delegate->SelectPasskey(backend_id, run_loop.QuitClosure());
  run_loop.Run();

  // The last verification status should now be kCompleted.
  EXPECT_EQ(client_->last_user_verification_status(),
            PasskeyUserVerificationStatus::kCompleted);
}

// Tests that example.ca can access passkeys using relying party id
// example.com when remote validation passes.
TEST_F(PasskeyTabHelperTest, RequestPasskeyFromRelatedOriginSuccess) {
  SetUpRelatedOrigin();
  SetUpChildFrameRegistrarAndRegisterFrame(web::kMainFakeFrameId,
                                           kMainRemoteFrameId);

  SetUpMockWellKnownResponse(R"({ "origins": ["https://example.ca"] })",
                             "application/json", "application/json", net::OK);

  passkey_tab_helper()->HandleGetRequestedEvent(BuildTestAssertionRequestParams(
      {}, device::UserVerificationRequirement::kPreferred, kFakeRequestId,
      web::kMainFakeFrameId, kMainRemoteFrameId));

  EXPECT_TRUE(base::test::RunUntil(
      [&]() { return client_->DidShowSuggestionBottomSheet(); }));
}

// Tests that example.ca can not access passkeys using relying party id
// example.com when remote validation fails.
TEST_F(PasskeyTabHelperTest, RequestPasskeyFromRelatedOriginFailure) {
  SetUpRelatedOrigin();
  SetUpChildFrameRegistrarAndRegisterFrame(web::kMainFakeFrameId,
                                           kMainRemoteFrameId);

  SetUpMockWellKnownResponse(R"({ "origins": ["https://example.uk"] })",
                             "application/json", "application/json", net::OK);

  passkey_tab_helper()->HandleGetRequestedEvent(BuildTestAssertionRequestParams(
      {}, device::UserVerificationRequirement::kPreferred, kFakeRequestId,
      web::kMainFakeFrameId, kMainRemoteFrameId));

  EXPECT_TRUE(base::test::RunUntil([&]() {
    return !passkey_tab_helper()->HasPendingValidationForTesting();
  }));

  EXPECT_FALSE(client_->DidShowSuggestionBottomSheet());
}

// Tests that example.ca can register a passkey using relying party id
// example.com when remote validation passes.
TEST_F(PasskeyTabHelperTest, CreatePasskeyFromRelatedOriginSuccess) {
  SetUpRelatedOrigin();

  SetUpMockWellKnownResponse(R"({ "origins": ["https://example.ca"] })",
                             "application/json", "application/json", net::OK);

  passkey_tab_helper()->HandleCreateRequestedEvent(
      BuildRegistrationRequestParams({}));

  EXPECT_TRUE(base::test::RunUntil([&]() {
    return !passkey_tab_helper()->HasPendingValidationForTesting();
  }));

  EXPECT_TRUE(client_->DidShowCreationBottomSheet());
}

// Tests that example.ca can not register a passkey using relying party id
// example.com when remote validation fails.
TEST_F(PasskeyTabHelperTest, CreatePasskeyFromRelatedOriginFailure) {
  SetUpRelatedOrigin();

  SetUpMockWellKnownResponse(R"({ "origins": ["https://example.uk"] })",
                             "application/json", "application/json", net::OK);

  passkey_tab_helper()->HandleCreateRequestedEvent(
      BuildRegistrationRequestParams({}));

  EXPECT_TRUE(base::test::RunUntil([&]() {
    return !passkey_tab_helper()->HasPendingValidationForTesting();
  }));

  EXPECT_FALSE(client_->DidShowCreationBottomSheet());
}

// Tests that ShouldPerformUserVerification returns the correct value for
// assertion and registration requests.
TEST_F(PasskeyTabHelperTest, ShouldPerformUserVerification) {
  SetUpWebFramesManagerAndWebFrame(GURL(kOriginURL));
  SetUpIOSPasswordManagerDriver();
  SetUpChildFrameRegistrarAndRegisterFrame(web::kMainFakeFrameId,
                                           kMainRemoteFrameId);

  // Test with non-existent request ID.
  EXPECT_EQ(passkey_tab_helper()->ShouldPerformUserVerification("non-existent"),
            std::nullopt);

  // An array of user verification requirements, and their expected values.
  struct UserVerificationRequirementTest {
    device::UserVerificationRequirement requirement;
    bool expected_with_biometrics;
    bool expected_without_biometrics;
  };

  std::vector<UserVerificationRequirementTest> user_verification_requirements =
      {{device::UserVerificationRequirement::kPreferred,
        /*expected_with_biometrics=*/true,
        /*expected_without_biometrics=*/false},
       {device::UserVerificationRequirement::kRequired,
        /*expected_with_biometrics=*/true,
        /*expected_without_biometrics=*/true},
       {device::UserVerificationRequirement::kDiscouraged,
        /*expected_with_biometrics=*/false,
        /*expected_without_biometrics=*/false}};

  // Tests assertion requests.
  for (const auto& test : user_verification_requirements) {
    std::string request_id = GetUniqueRequestId();
    passkey_tab_helper()->HandleGetRequestedEvent(
        BuildTestAssertionRequestParams({}, test.requirement, request_id,
                                        web::kMainFakeFrameId,
                                        kMainRemoteFrameId));
    VerifyShouldPerformUserVerification(request_id,
                                        test.expected_with_biometrics,
                                        test.expected_without_biometrics);
  }

  // Tests registration requests.
  for (const auto& test : user_verification_requirements) {
    std::string request_id = GetUniqueRequestId();
    passkey_tab_helper()->HandleCreateRequestedEvent(
        BuildRegistrationRequestParams({}, test.requirement, request_id));
    VerifyShouldPerformUserVerification(request_id,
                                        test.expected_with_biometrics,
                                        test.expected_without_biometrics);
  }
}

TEST_F(PasskeyTabHelperTest, MaybeShowInterstitialAndRegisterAndContinue) {
  fake_browser_state_.SetOffTheRecord(true);
  SetUpWebFramesManagerAndWebFrame(GURL(kOriginURL));

  client_->SetInterstitialProceeds(true);
  MaybeShowInterstitialAndRegister(BuildRegistrationRequestParams({}));

  EXPECT_TRUE(client_->DidShowInterstitial());
  EXPECT_TRUE(client_->DidShowCreationBottomSheet());
  histogram_tester_.ExpectUniqueSample(
      kWebAuthenticationIOSContentAreaEventHistogram,
      static_cast<int>(kIncognitoInterstitialShown),
      /*count=*/1);
}

TEST_F(PasskeyTabHelperTest, MaybeShowInterstitialAndRegisterAndCancel) {
  fake_browser_state_.SetOffTheRecord(true);
  SetUpWebFramesManagerAndWebFrame(GURL(kOriginURL));

  client_->SetInterstitialProceeds(false);
  MaybeShowInterstitialAndRegister(BuildRegistrationRequestParams({}));

  EXPECT_TRUE(client_->DidShowInterstitial());
  EXPECT_FALSE(client_->DidShowCreationBottomSheet());
  histogram_tester_.ExpectUniqueSample(
      kWebAuthenticationIOSContentAreaEventHistogram,
      static_cast<int>(kIncognitoInterstitialShown),
      /*count=*/1);
}

TEST_F(PasskeyTabHelperTest, NoInterstitial) {
  fake_browser_state_.SetOffTheRecord(false);
  SetUpWebFramesManagerAndWebFrame(GURL(kOriginURL));

  MaybeShowInterstitialAndRegister(BuildRegistrationRequestParams({}));
  EXPECT_FALSE(client_->DidShowInterstitial());
  EXPECT_TRUE(client_->DidShowCreationBottomSheet());
}

TEST_F(PasskeyTabHelperTest, HandleRegistrationDefersWhenGpmDisabled) {
  SetUpWebFramesManagerAndWebFrame(GURL(kOriginURL));
  SetUpIOSPasswordManagerDriver();

  client_->SetGpmPasskeySavingEnabled(false);

  passkey_tab_helper()->HandleCreateRequestedEvent(
      BuildRegistrationRequestParams({}));

  EXPECT_FALSE(client_->DidShowCreationBottomSheet());
  EXPECT_FALSE(client_->DidFetchKeys());
}

// Tests that automatic passkey upgrade is allowed for a valid, recent login.
TEST_F(PasskeyTabHelperTest, AutomaticPasskeyUpgradeSuccess) {
  password_manager::PasswordForm form;
  form.username_value = u"";
  form.url = GURL(kOriginURL);
  form.date_last_used = base::Time::Now();

  std::vector<password_manager::PasswordForm> results;
  results.push_back(form);

  RegistrationRequestParams params = BuildRegistrationRequestParams({});

  EXPECT_TRUE(CanPerformAutomaticPasskeyUpgrade(params, results));
}

// Tests that automatic passkey upgrade is denied if the login is too old.
TEST_F(PasskeyTabHelperTest, AutomaticPasskeyUpgradeThresholdEnforcement) {
  password_manager::PasswordForm form;
  form.username_value = u"";
  form.url = GURL(kOriginURL);
  form.date_last_used = base::Time::Now() - base::Minutes(6);

  std::vector<password_manager::PasswordForm> results;
  results.push_back(form);

  RegistrationRequestParams params = BuildRegistrationRequestParams({});

  EXPECT_FALSE(CanPerformAutomaticPasskeyUpgrade(params, results));
}

// Tests that automatic passkey upgrade is denied if no logins are found.
TEST_F(PasskeyTabHelperTest, AutomaticPasskeyUpgradeRemovalHandling) {
  password_manager::PasswordForm form;
  form.username_value = u"";
  form.url = GURL(kOriginURL);
  form.date_last_used = base::Time::Now();

  std::vector<password_manager::PasswordForm> results;
  results.push_back(form);

  RegistrationRequestParams params = BuildRegistrationRequestParams({});

  EXPECT_TRUE(CanPerformAutomaticPasskeyUpgrade(params, results));

  results.clear();
  EXPECT_FALSE(CanPerformAutomaticPasskeyUpgrade(params, results));
}

// Tests that automatic passkey upgrade allows matching subdomains via eTLD+1.
TEST_F(PasskeyTabHelperTest, AutomaticPasskeyUpgradeRpIdNormalization) {
  password_manager::PasswordForm form;
  form.username_value = u"";
  form.url = GURL("https://sub.example.com");
  form.date_last_used = base::Time::Now();

  std::vector<password_manager::PasswordForm> results;
  results.push_back(form);

  RegistrationRequestParams params = BuildRegistrationRequestParams({});

  EXPECT_TRUE(CanPerformAutomaticPasskeyUpgrade(params, results));
}

// Tests that a conditional create request does NOT show the incognito
// interstitial when automatic passkey upgrade is denied.
TEST_F(PasskeyTabHelperTest, ConditionalCreateOffTheRecordUpgradeDeny) {
  fake_browser_state_.SetOffTheRecord(true);
  SetUpWebFramesManagerAndWebFrame(GURL(kOriginURL));
  SetUpIOSPasswordManagerDriver();

  IOSPasskeyClient::RequestInfo request_info(web::kMainFakeFrameId,
                                             kFakeRequestId);
  device::PublicKeyCredentialRpEntity rp_entity(kRpId);
  std::vector<uint8_t> challenge;
  PasskeyRequestParams::RequestType request_type =
      PasskeyRequestParams::RequestType::kConditionalCreate;
  PasskeyExtensionData extension_data;
  PasskeyRequestParams request_params(
      std::move(request_info), std::move(rp_entity), std::move(challenge),
      device::UserVerificationRequirement::kPreferred, request_type,
      std::move(extension_data));
  device::PublicKeyCredentialUserEntity user_entity;
  RegistrationRequestParams params(std::move(request_params),
                                   std::move(user_entity),
                                   /*exclude_credentials=*/{});

  passkey_tab_helper()->HandleCreateRequestedEvent(std::move(params));

  EXPECT_FALSE(client_->DidShowInterstitial());

  base::RunLoop().RunUntilIdle();

  EXPECT_FALSE(client_->DidShowInterstitial());
  EXPECT_FALSE(client_->DidFetchKeys());
}

// Tests that a conditional create request shows the incognito interstitial
// when automatic passkey upgrade is allowed, and creation proceeds if the user
// chooses to proceed.
TEST_F(PasskeyTabHelperTest, ConditionalCreateOffTheRecordUpgradeAllowProceed) {
  fake_browser_state_.SetOffTheRecord(true);
  SetUpWebFramesManagerAndWebFrame(GURL(kOriginURL));
  SetUpIOSPasswordManagerDriver();

  password_manager::PasswordForm form;
  form.username_value = u"";
  form.url = GURL(kOriginURL);
  form.date_last_used = base::Time::Now();
  test_password_store_->AddLogin(password_manager::FromPasswordForm(form));
  base::RunLoop().RunUntilIdle();

  IOSPasskeyClient::RequestInfo request_info(web::kMainFakeFrameId,
                                             kFakeRequestId);
  device::PublicKeyCredentialRpEntity rp_entity(kRpId);
  std::vector<uint8_t> challenge;
  PasskeyRequestParams::RequestType request_type =
      PasskeyRequestParams::RequestType::kConditionalCreate;
  PasskeyExtensionData extension_data;
  PasskeyRequestParams request_params(
      std::move(request_info), std::move(rp_entity), std::move(challenge),
      device::UserVerificationRequirement::kPreferred, request_type,
      std::move(extension_data));
  device::PublicKeyCredentialUserEntity user_entity;
  RegistrationRequestParams params(std::move(request_params),
                                   std::move(user_entity),
                                   /*exclude_credentials=*/{});

  client_->SetInterstitialProceeds(true);

  passkey_tab_helper()->HandleCreateRequestedEvent(std::move(params));

  EXPECT_FALSE(client_->DidShowInterstitial());

  base::RunLoop().RunUntilIdle();

  EXPECT_TRUE(client_->DidShowInterstitial());
  EXPECT_TRUE(client_->DidFetchKeys());
  EXPECT_FALSE(client_->DidOnPasskeyCreated());
}

// Tests that a conditional create request shows the incognito interstitial
// when automatic passkey upgrade is allowed, and creation is cancelled if the
// user cancels.
TEST_F(PasskeyTabHelperTest, ConditionalCreateOffTheRecordUpgradeAllowCancel) {
  fake_browser_state_.SetOffTheRecord(true);
  SetUpWebFramesManagerAndWebFrame(GURL(kOriginURL));
  SetUpIOSPasswordManagerDriver();

  password_manager::PasswordForm form;
  form.username_value = u"";
  form.url = GURL(kOriginURL);
  form.date_last_used = base::Time::Now();
  test_password_store_->AddLogin(password_manager::FromPasswordForm(form));
  base::RunLoop().RunUntilIdle();

  IOSPasskeyClient::RequestInfo request_info(web::kMainFakeFrameId,
                                             kFakeRequestId);
  device::PublicKeyCredentialRpEntity rp_entity(kRpId);
  std::vector<uint8_t> challenge;
  PasskeyRequestParams::RequestType request_type =
      PasskeyRequestParams::RequestType::kConditionalCreate;
  PasskeyExtensionData extension_data;
  PasskeyRequestParams request_params(
      std::move(request_info), std::move(rp_entity), std::move(challenge),
      device::UserVerificationRequirement::kPreferred, request_type,
      std::move(extension_data));
  device::PublicKeyCredentialUserEntity user_entity;
  RegistrationRequestParams params(std::move(request_params),
                                   std::move(user_entity),
                                   /*exclude_credentials=*/{});

  client_->SetInterstitialProceeds(false);

  passkey_tab_helper()->HandleCreateRequestedEvent(std::move(params));

  base::RunLoop().RunUntilIdle();

  EXPECT_TRUE(client_->DidShowInterstitial());
  EXPECT_FALSE(client_->DidFetchKeys());
}

// Tests that a passkey assertion request defers back to the renderer when
// OriginAllowedToMakeWebAuthnRequests check fails.
TEST_F(PasskeyTabHelperTest, HandleGetRequestedEventDefersOnInvalidOrigin) {
  SetUpWebFramesManagerAndWebFrame(GURL(kInsecureOriginURL));
  SetUpIOSPasswordManagerDriver();

  passkey_tab_helper()->HandleGetRequestedEvent(
      BuildAssertionRequestParams({}));

  web::FakeWebFramesManager* frames_manager =
      static_cast<web::FakeWebFramesManager*>(
          fake_web_state_.GetWebFramesManager(
              PasskeyJavaScriptFeature::GetInstance()
                  ->GetSupportedContentWorld()));
  web::FakeWebFrame* frame = static_cast<web::FakeWebFrame*>(
      frames_manager->GetFrameWithId(web::kMainFakeFrameId));

  EXPECT_NE(frame->GetLastJavaScriptCall().find(kDeferToRendererJsCall),
            std::u16string::npos);
}

// Tests that a passkey registration request defers back to the renderer when
// OriginAllowedToMakeWebAuthnRequests check fails.
TEST_F(PasskeyTabHelperTest, HandleCreateRequestedEventDefersOnInvalidOrigin) {
  SetUpWebFramesManagerAndWebFrame(GURL(kInsecureOriginURL));
  SetUpIOSPasswordManagerDriver();

  passkey_tab_helper()->HandleCreateRequestedEvent(
      BuildRegistrationRequestParams({}));

  web::FakeWebFramesManager* frames_manager =
      static_cast<web::FakeWebFramesManager*>(
          fake_web_state_.GetWebFramesManager(
              PasskeyJavaScriptFeature::GetInstance()
                  ->GetSupportedContentWorld()));
  web::FakeWebFrame* frame = static_cast<web::FakeWebFrame*>(
      frames_manager->GetFrameWithId(web::kMainFakeFrameId));

  EXPECT_NE(frame->GetLastJavaScriptCall().find(kDeferToRendererJsCall),
            std::u16string::npos);
}

// Tests that when a passkey creation request is initiated from a cross-origin
// iframe, the client data JSON is built with the correct top origin and
// is_cross_origin_iframe = true.
TEST_F(PasskeyTabHelperTest, StartPasskeyCreationFromCrossOriginIframe) {
  // Set up main frame and same-site cross-origin subframe.
  web::ContentWorld passkey_world =
      PasskeyJavaScriptFeature::GetInstance()->GetSupportedContentWorld();
  auto frames_manager = std::make_unique<web::FakeWebFramesManager>();

  auto main_frame =
      web::FakeWebFrame::Create(web::kMainFakeFrameId, /*is_main_frame=*/true,
                                GURL("https://example.com"));
  main_frame->set_browser_state(&fake_browser_state_);
  frames_manager->AddWebFrame(std::move(main_frame));

  auto sub_frame =
      web::FakeWebFrame::Create(web::kChildFakeFrameId, /*is_main_frame=*/false,
                                GURL("https://sub.example.com"));
  sub_frame->set_browser_state(&fake_browser_state_);
  web::FakeWebFrame* sub_frame_ptr = sub_frame.get();
  frames_manager->AddWebFrame(std::move(sub_frame));

  fake_web_state_.SetWebFramesManager(passkey_world, std::move(frames_manager));
  SetUpIOSPasswordManagerDriver();

  // Handle the creation request.
  RegistrationRequestParams params = BuildRegistrationRequestParams(
      /*exclude_credentials=*/{},
      device::UserVerificationRequirement::kPreferred, kFakeRequestId,
      web::kChildFakeFrameId);

  passkey_tab_helper()->HandleCreateRequestedEvent(std::move(params));
  EXPECT_TRUE(client_->DidShowCreationBottomSheet());

  // Trigger start of creation.
  passkey_tab_helper()->StartPasskeyCreation(kFakeRequestId,
                                             /*did_complete_uv=*/false);
  EXPECT_TRUE(client_->DidFetchKeys());
  EXPECT_TRUE(client_->DidOnPasskeyCreated());

  // Verify that ResolveAttestationRequest was called on the subframe with the
  // correct client data JSON.
  std::u16string last_call = sub_frame_ptr->GetLastJavaScriptCall();
  EXPECT_NE(last_call.find(u"resolveAttestationRequest"), std::u16string::npos);
  EXPECT_NE(last_call.find(u"\\\"crossOrigin\\\":true"), std::u16string::npos);
  EXPECT_NE(last_call.find(u"\\\"topOrigin\\\":\\\"https://example.com\\\""),
            std::u16string::npos);
}

// Tests that when a passkey creation request is initiated and the passkey model
// already contains a passkey in the exclude list, an InvalidStateError is
// returned to the website.
TEST_F(PasskeyTabHelperTest, StartPasskeyCreationWithExcludedCredential) {
  SetUpWebFramesManagerAndWebFrame(GURL(kOriginURL));
  SetUpIOSPasswordManagerDriver();

  // Add passkey with kCredentialId to the passkey model.
  sync_pb::WebauthnCredentialSpecifics passkey = GetTestPasskey(kCredentialId);
  passkey_model_->AddNewPasskeyForTesting(std::move(passkey));

  web::FakeWebFramesManager* frames_manager =
      static_cast<web::FakeWebFramesManager*>(
          fake_web_state_.GetWebFramesManager(
              PasskeyJavaScriptFeature::GetInstance()
                  ->GetSupportedContentWorld()));
  web::FakeWebFrame* frame = static_cast<web::FakeWebFrame*>(
      frames_manager->GetFrameWithId(web::kMainFakeFrameId));

  // Build registration params with kCredentialId in the exclude credentials
  // list.
  std::vector<device::PublicKeyCredentialDescriptor> exclude_credentials;
  exclude_credentials.push_back(
      {device::CredentialType::kPublicKey, AsByteVector(kCredentialId)});
  RegistrationRequestParams params = BuildRegistrationRequestParams(
      exclude_credentials, device::UserVerificationRequirement::kPreferred,
      kFakeRequestId, web::kMainFakeFrameId);

  // Handle creation request.
  passkey_tab_helper()->HandleCreateRequestedEvent(std::move(params));
  EXPECT_TRUE(client_->DidShowCreationBottomSheet());

  // Trigger start of creation.
  passkey_tab_helper()->StartPasskeyCreation(kFakeRequestId,
                                             /*did_complete_uv=*/false);

  // Verify that keys were NOT fetched.
  EXPECT_FALSE(client_->DidFetchKeys());
  EXPECT_FALSE(client_->DidOnPasskeyCreated());

  // Verify that rejectPasskeyRequest was called on the frame with the correct
  // error parameters.
  std::u16string last_call = frame->GetLastJavaScriptCall();
  EXPECT_NE(last_call.find(u"rejectPasskeyRequest"), std::u16string::npos);
  EXPECT_NE(last_call.find(
                base::UTF8ToUTF16(std::string_view(kInvalidStateErrorName))),
            std::u16string::npos);
  EXPECT_NE(last_call.find(base::UTF8ToUTF16(
                std::string_view(kCredentialExcludedErrorMessage))),
            std::u16string::npos);
}

// Tests that when the passkey model is not ready, assertion requests are
// queued and then executed when the model becomes ready.
TEST_F(PasskeyTabHelperTest, GetRequestedDeferredUntilModelReady) {
  SetUpWebFramesManagerAndWebFrame(GURL(kOriginURL));
  SetUpIOSPasswordManagerDriver();
  SetUpChildFrameRegistrarAndRegisterFrame(web::kMainFakeFrameId,
                                           kMainRemoteFrameId);

  // Make model not ready.
  static_cast<TestPasskeyModel*>(passkey_model_.get())->SetReady(false);

  // Send get request.
  AssertionRequestParams params = BuildTestAssertionRequestParams(
      /*allow_credentials=*/{}, device::UserVerificationRequirement::kPreferred,
      kFakeRequestId, web::kMainFakeFrameId, kMainRemoteFrameId);
  passkey_tab_helper()->HandleGetRequestedEvent(std::move(params));

  // Suggestion sheet should NOT be shown.
  EXPECT_FALSE(client_->DidShowSuggestionBottomSheet());

  // Make model ready.
  static_cast<TestPasskeyModel*>(passkey_model_.get())->SetReady(true);

  // Suggestion sheet should now be shown.
  EXPECT_TRUE(client_->DidShowSuggestionBottomSheet());
}

// Tests that when the passkey model is not ready, registration requests are
// queued and then executed when the model becomes ready.
TEST_F(PasskeyTabHelperTest, CreateRequestedDeferredUntilModelReady) {
  SetUpWebFramesManagerAndWebFrame(GURL(kOriginURL));
  SetUpIOSPasswordManagerDriver();

  // Make model not ready.
  static_cast<TestPasskeyModel*>(passkey_model_.get())->SetReady(false);

  // Send create request.
  RegistrationRequestParams params = BuildRegistrationRequestParams({});
  passkey_tab_helper()->HandleCreateRequestedEvent(std::move(params));

  // Creation bottom sheet should NOT be shown.
  EXPECT_FALSE(client_->DidShowCreationBottomSheet());

  // Make model ready.
  static_cast<TestPasskeyModel*>(passkey_model_.get())->SetReady(true);

  // Creation bottom sheet should now be shown.
  EXPECT_TRUE(client_->DidShowCreationBottomSheet());
}
// Tests that when neither the passkey model nor the web frame is ready when the
// request is made, and the passkey model becomes ready first, followed by the
// web frame, the request is executed successfully.
TEST_F(PasskeyTabHelperTest, SequentiallyAvailable_ModelThenFrame) {
  web::FakeWebFramesManager* frames_manager_ptr =
      SetUpWebFramesManagerWithoutFrame();

  SetUpIOSPasswordManagerDriver();
  SetUpChildFrameRegistrarAndRegisterFrame(web::kMainFakeFrameId,
                                           kMainRemoteFrameId);

  // Make passkey model not ready.
  static_cast<TestPasskeyModel*>(passkey_model_.get())->SetReady(false);

  // Send get request.
  AssertionRequestParams params = BuildTestAssertionRequestParams(
      /*allow_credentials=*/{}, device::UserVerificationRequirement::kPreferred,
      kFakeRequestId, web::kMainFakeFrameId, kMainRemoteFrameId);
  passkey_tab_helper()->HandleGetRequestedEvent(std::move(params));

  // Neither ready -> shouldn't show suggestion sheet.
  EXPECT_FALSE(client_->DidShowSuggestionBottomSheet());

  // Make the model ready first.
  static_cast<TestPasskeyModel*>(passkey_model_.get())->SetReady(true);

  // Model is ready but frame is still not ready -> shouldn't show suggestion
  // sheet.
  EXPECT_FALSE(client_->DidShowSuggestionBottomSheet());

  // Make the web frame available.
  auto frame = web::FakeWebFrame::CreateMainWebFrame(GURL(kOriginURL));
  frame->set_browser_state(&fake_browser_state_);
  frames_manager_ptr->AddWebFrame(std::move(frame));

  // Now both are ready -> suggestion sheet should be shown!
  EXPECT_TRUE(client_->DidShowSuggestionBottomSheet());
}

// Tests that when neither the passkey model nor the web frame is ready when the
// request is made, and the web frame becomes ready first, followed by the
// passkey model, the request is executed successfully.
TEST_F(PasskeyTabHelperTest, SequentiallyAvailable_FrameThenModel) {
  web::FakeWebFramesManager* frames_manager_ptr =
      SetUpWebFramesManagerWithoutFrame();

  SetUpIOSPasswordManagerDriver();
  SetUpChildFrameRegistrarAndRegisterFrame(web::kMainFakeFrameId,
                                           kMainRemoteFrameId);

  // Make passkey model not ready.
  static_cast<TestPasskeyModel*>(passkey_model_.get())->SetReady(false);

  // Send get request.
  AssertionRequestParams params = BuildTestAssertionRequestParams(
      /*allow_credentials=*/{}, device::UserVerificationRequirement::kPreferred,
      kFakeRequestId, web::kMainFakeFrameId, kMainRemoteFrameId);
  passkey_tab_helper()->HandleGetRequestedEvent(std::move(params));

  // Neither ready -> shouldn't show suggestion sheet.
  EXPECT_FALSE(client_->DidShowSuggestionBottomSheet());

  // Make the web frame available first.
  auto frame = web::FakeWebFrame::CreateMainWebFrame(GURL(kOriginURL));
  frame->set_browser_state(&fake_browser_state_);
  frames_manager_ptr->AddWebFrame(std::move(frame));

  // Web frame is ready but model is still not ready -> shouldn't show
  // suggestion sheet.
  EXPECT_FALSE(client_->DidShowSuggestionBottomSheet());

  // Make the passkey model ready.
  static_cast<TestPasskeyModel*>(passkey_model_.get())->SetReady(true);

  // Now both are ready -> suggestion sheet should be shown!
  EXPECT_TRUE(client_->DidShowSuggestionBottomSheet());
}

// Tests that a passkey assertion request defers back to the renderer and
// returns early gracefully when the remote frame ID is empty.
TEST_F(PasskeyTabHelperTest, HandleAssertionEmptyRemoteFrameIdGraceful) {
  SetUpWebFramesManagerAndWebFrame(GURL(kOriginURL));
  SetUpIOSPasswordManagerDriver();
  SetUpChildFrameRegistrarAndRegisterFrame(web::kMainFakeFrameId,
                                           kMainRemoteFrameId);

  // Send a get request with an empty remote frame ID.
  AssertionRequestParams params = BuildTestAssertionRequestParams(
      /*allow_credentials=*/{}, device::UserVerificationRequirement::kPreferred,
      kFakeRequestId, web::kMainFakeFrameId, /*remote_frame_id=*/"");

  passkey_tab_helper()->HandleGetRequestedEvent(std::move(params));

  // The assertion request should defer back to the renderer.
  web::FakeWebFramesManager* frames_manager =
      static_cast<web::FakeWebFramesManager*>(
          fake_web_state_.GetWebFramesManager(
              PasskeyJavaScriptFeature::GetInstance()
                  ->GetSupportedContentWorld()));
  web::FakeWebFrame* frame = static_cast<web::FakeWebFrame*>(
      frames_manager->GetFrameWithId(web::kMainFakeFrameId));

  EXPECT_NE(frame->GetLastJavaScriptCall().find(kDeferToRendererJsCall),
            std::u16string::npos);

  // Verify that the suggestion bottom sheet was NOT shown.
  EXPECT_FALSE(client_->DidShowSuggestionBottomSheet());
}

// Tests that a passkey assertion request defers back to the renderer and
// returns early gracefully when the remote frame ID is malformed.
TEST_F(PasskeyTabHelperTest, HandleAssertionMalformedRemoteFrameIdGraceful) {
  SetUpWebFramesManagerAndWebFrame(GURL(kOriginURL));
  SetUpIOSPasswordManagerDriver();
  SetUpChildFrameRegistrarAndRegisterFrame(web::kMainFakeFrameId,
                                           kMainRemoteFrameId);

  // Send a get request with a malformed remote frame ID.
  AssertionRequestParams params = BuildTestAssertionRequestParams(
      /*allow_credentials=*/{}, device::UserVerificationRequirement::kPreferred,
      kFakeRequestId, web::kMainFakeFrameId,
      /*remote_frame_id=*/"malformed_frame_id");

  passkey_tab_helper()->HandleGetRequestedEvent(std::move(params));

  // The assertion request should defer back to the renderer.
  web::FakeWebFramesManager* frames_manager =
      static_cast<web::FakeWebFramesManager*>(
          fake_web_state_.GetWebFramesManager(
              PasskeyJavaScriptFeature::GetInstance()
                  ->GetSupportedContentWorld()));
  web::FakeWebFrame* frame = static_cast<web::FakeWebFrame*>(
      frames_manager->GetFrameWithId(web::kMainFakeFrameId));

  EXPECT_NE(frame->GetLastJavaScriptCall().find(kDeferToRendererJsCall),
            std::u16string::npos);

  // Verify that the suggestion bottom sheet was NOT shown.
  EXPECT_FALSE(client_->DidShowSuggestionBottomSheet());
}

// Tests that HandleSignalUnknownCredential hides the passkey matching the
// provided credential ID from the model when the origin is valid.
TEST_F(PasskeyTabHelperTest, HandleSignalUnknownCredentialEventSuccess) {
  SetUpWebFramesManagerAndWebFrame(GURL(kOriginURL));

  sync_pb::WebauthnCredentialSpecifics passkey = GetTestPasskey(kCredentialId);
  static_cast<TestPasskeyModel*>(passkey_model_.get())
      ->AddNewPasskeyForTesting(passkey);

  SignalUnknownCredentialParams params{kRpId, AsByteVector(kCredentialId)};

  passkey_tab_helper()->HandleSignalUnknownCredentialEvent(
      url::Origin::Create(GURL(kOriginURL)), std::move(params));

  // The passkey should now be marked as hidden.
  EXPECT_TRUE(GetPasskey(kCredentialId).hidden());
}

// Tests that HandleSignalUnknownCredentialEvent drops requests with invalid
// origins.
TEST_F(PasskeyTabHelperTest, HandleSignalUnknownCredentialEventInvalidOrigin) {
  SetUpWebFramesManagerAndWebFrame(GURL(kOriginURL));

  sync_pb::WebauthnCredentialSpecifics passkey = GetTestPasskey(kCredentialId);
  static_cast<TestPasskeyModel*>(passkey_model_.get())
      ->AddNewPasskeyForTesting(passkey);

  SignalUnknownCredentialParams params{"otherdomain.com",
                                       AsByteVector(kCredentialId)};

  // The call should be ignored and passkey remain unhidden.
  passkey_tab_helper()->HandleSignalUnknownCredentialEvent(
      url::Origin::Create(GURL(kOriginURL)), std::move(params));
  EXPECT_FALSE(GetPasskey(kCredentialId).hidden());
}

// Tests that HandleSignalUnknownCredentialEvent ignores requests when quota is
// exceeded.
TEST_F(PasskeyTabHelperTest, HandleSignalUnknownCredentialEventQuotaExceeded) {
  SetUpWebFramesManagerAndWebFrame(GURL(kOriginURL));

  sync_pb::WebauthnCredentialSpecifics passkey = GetTestPasskey(kCredentialId);
  static_cast<TestPasskeyModel*>(passkey_model_.get())
      ->AddNewPasskeyForTesting(passkey);

  const url::Origin origin = url::Origin::Create(GURL(kOriginURL));

  for (int i = 0; i < PasskeyChangeQuotaTracker::kMaxTokensPerRP; ++i) {
    SignalUnknownCredentialParams params{kRpId, AsByteVector(kCredentialId)};
    passkey_tab_helper()->HandleSignalUnknownCredentialEvent(origin,
                                                             std::move(params));
    static_cast<TestPasskeyModel*>(passkey_model_.get())
        ->UnhidePasskey(kCredentialId);
  }

  // Quota is now exhausted. Subsequent calls should be ignored.
  SignalUnknownCredentialParams params{kRpId, AsByteVector(kCredentialId)};
  passkey_tab_helper()->HandleSignalUnknownCredentialEvent(origin,
                                                           std::move(params));
  EXPECT_FALSE(GetPasskey(kCredentialId).hidden());
}

// Tests that HandleSignalCurrentUserDetailsEvent updates matching passkey user
// details in the model when the origin is valid.
TEST_F(PasskeyTabHelperTest, HandleSignalCurrentUserDetailsEventSuccess) {
  SetUpWebFramesManagerAndWebFrame(GURL(kOriginURL));

  sync_pb::WebauthnCredentialSpecifics passkey = GetTestPasskey(kCredentialId);
  static_cast<TestPasskeyModel*>(passkey_model_.get())
      ->AddNewPasskeyForTesting(passkey);

  const std::vector<uint8_t> user_id(passkey.user_id().begin(),
                                     passkey.user_id().end());
  SignalCurrentUserDetailsParams params{kRpId, user_id, "newname@example.com",
                                        "New Display Name"};

  passkey_tab_helper()->HandleSignalCurrentUserDetailsEvent(
      url::Origin::Create(GURL(kOriginURL)), std::move(params));

  // The passkey user details should now be updated in the model.
  sync_pb::WebauthnCredentialSpecifics updated_passkey =
      GetPasskey(kCredentialId);
  EXPECT_EQ(updated_passkey.user_name(), "newname@example.com");
  EXPECT_EQ(updated_passkey.user_display_name(), "New Display Name");
}

// Tests that HandleSignalCurrentUserDetailsEvent drops requests with invalid
// origins.
TEST_F(PasskeyTabHelperTest, HandleSignalCurrentUserDetailsEventInvalidOrigin) {
  SetUpWebFramesManagerAndWebFrame(GURL(kOriginURL));

  sync_pb::WebauthnCredentialSpecifics passkey = GetTestPasskey(kCredentialId);
  static_cast<TestPasskeyModel*>(passkey_model_.get())
      ->AddNewPasskeyForTesting(passkey);

  const std::vector<uint8_t> user_id(passkey.user_id().begin(),
                                     passkey.user_id().end());
  // Mismatched relying party ID / origin.
  SignalCurrentUserDetailsParams params{
      "otherdomain.com", user_id, "newname@example.com", "New Display Name"};

  passkey_tab_helper()->HandleSignalCurrentUserDetailsEvent(
      url::Origin::Create(GURL(kOriginURL)), std::move(params));

  // The passkey user details should remain unchanged.
  sync_pb::WebauthnCredentialSpecifics unchanged_passkey =
      GetPasskey(kCredentialId);
  EXPECT_NE(unchanged_passkey.user_name(), "newname@example.com");
  EXPECT_NE(unchanged_passkey.user_display_name(), "New Display Name");
}

// Tests that HandleSignalCurrentUserDetailsEvent ignores requests when quota is
// exceeded.
TEST_F(PasskeyTabHelperTest, HandleSignalCurrentUserDetailsEventQuotaExceeded) {
  SetUpWebFramesManagerAndWebFrame(GURL(kOriginURL));

  sync_pb::WebauthnCredentialSpecifics passkey = GetTestPasskey(kCredentialId);
  static_cast<TestPasskeyModel*>(passkey_model_.get())
      ->AddNewPasskeyForTesting(passkey);

  const url::Origin origin = url::Origin::Create(GURL(kOriginURL));
  const std::vector<uint8_t> user_id(passkey.user_id().begin(),
                                     passkey.user_id().end());

  for (int i = 0; i < PasskeyChangeQuotaTracker::kMaxTokensPerRP; ++i) {
    SignalCurrentUserDetailsParams params{
        kRpId, user_id, base::NumberToString(i), base::NumberToString(i)};
    passkey_tab_helper()->HandleSignalCurrentUserDetailsEvent(
        origin, std::move(params));
  }

  // Quota is now exhausted. Subsequent call should be ignored and passkey
  // details remain unchanged from previous iteration.
  SignalCurrentUserDetailsParams params{kRpId, user_id, "exhausted@example.com",
                                        "Exhausted"};
  passkey_tab_helper()->HandleSignalCurrentUserDetailsEvent(origin,
                                                            std::move(params));

  sync_pb::WebauthnCredentialSpecifics final_passkey =
      GetPasskey(kCredentialId);
  EXPECT_NE(final_passkey.user_name(), "exhausted@example.com");
  EXPECT_NE(final_passkey.user_display_name(), "Exhausted");
}

// Tests that HandleSignalAllAcceptedCredentialsEvent hides a passkey missing
// from the accepted list.
TEST_F(PasskeyTabHelperTest, HandleSignalAllAcceptedCredentialsEventHide) {
  SetUpWebFramesManagerAndWebFrame(GURL(kOriginURL));

  sync_pb::WebauthnCredentialSpecifics passkey = GetTestPasskey(kCredentialId);
  static_cast<TestPasskeyModel*>(passkey_model_.get())
      ->AddNewPasskeyForTesting(passkey);

  const std::vector<uint8_t> user_id(passkey.user_id().begin(),
                                     passkey.user_id().end());
  // Empty list of accepted credentials -> passkey should be hidden.
  SignalAllAcceptedCredentialsParams params{kRpId, user_id, {}};

  passkey_tab_helper()->HandleSignalAllAcceptedCredentialsEvent(
      url::Origin::Create(GURL(kOriginURL)), std::move(params));

  EXPECT_TRUE(GetPasskey(kCredentialId).hidden());
}

// Tests that HandleSignalAllAcceptedCredentialsEvent restores/unhides a passkey
// present in the accepted list.
TEST_F(PasskeyTabHelperTest, HandleSignalAllAcceptedCredentialsEventRestore) {
  SetUpWebFramesManagerAndWebFrame(GURL(kOriginURL));

  sync_pb::WebauthnCredentialSpecifics passkey = GetTestPasskey(kCredentialId);
  static_cast<TestPasskeyModel*>(passkey_model_.get())
      ->AddNewPasskeyForTesting(passkey);
  static_cast<TestPasskeyModel*>(passkey_model_.get())
      ->HidePasskey(kCredentialId, base::Time::Now());

  ASSERT_TRUE(GetPasskey(kCredentialId).hidden());

  const std::vector<uint8_t> user_id(passkey.user_id().begin(),
                                     passkey.user_id().end());
  SignalAllAcceptedCredentialsParams params{
      kRpId, user_id, {AsByteVector(kCredentialId)}};

  passkey_tab_helper()->HandleSignalAllAcceptedCredentialsEvent(
      url::Origin::Create(GURL(kOriginURL)), std::move(params));

  EXPECT_FALSE(GetPasskey(kCredentialId).hidden());
}

// Tests that HandleSignalAllAcceptedCredentialsEvent drops requests with
// invalid origins.
TEST_F(PasskeyTabHelperTest,
       HandleSignalAllAcceptedCredentialsEventInvalidOrigin) {
  SetUpWebFramesManagerAndWebFrame(GURL(kOriginURL));

  sync_pb::WebauthnCredentialSpecifics passkey = GetTestPasskey(kCredentialId);
  static_cast<TestPasskeyModel*>(passkey_model_.get())
      ->AddNewPasskeyForTesting(passkey);

  const std::vector<uint8_t> user_id(passkey.user_id().begin(),
                                     passkey.user_id().end());
  // Mismatched origin.
  SignalAllAcceptedCredentialsParams params{"otherdomain.com", user_id, {}};

  passkey_tab_helper()->HandleSignalAllAcceptedCredentialsEvent(
      url::Origin::Create(GURL(kOriginURL)), std::move(params));

  EXPECT_FALSE(GetPasskey(kCredentialId).hidden());
}

// Tests that HandleSignalAllAcceptedCredentialsEvent ignores requests when
// quota is exceeded.
TEST_F(PasskeyTabHelperTest,
       HandleSignalAllAcceptedCredentialsEventQuotaExceeded) {
  SetUpWebFramesManagerAndWebFrame(GURL(kOriginURL));

  sync_pb::WebauthnCredentialSpecifics passkey = GetTestPasskey(kCredentialId);
  static_cast<TestPasskeyModel*>(passkey_model_.get())
      ->AddNewPasskeyForTesting(passkey);

  const url::Origin origin = url::Origin::Create(GURL(kOriginURL));
  const std::vector<uint8_t> user_id(passkey.user_id().begin(),
                                     passkey.user_id().end());

  for (int i = 0; i < PasskeyChangeQuotaTracker::kMaxTokensPerRP; ++i) {
    SignalAllAcceptedCredentialsParams params{kRpId, user_id, {}};
    passkey_tab_helper()->HandleSignalAllAcceptedCredentialsEvent(
        origin, std::move(params));
    static_cast<TestPasskeyModel*>(passkey_model_.get())
        ->UnhidePasskey(kCredentialId);
  }

  // Quota is now exhausted. Subsequent call should be ignored and passkey
  // remains unhidden.
  SignalAllAcceptedCredentialsParams params{kRpId, user_id, {}};
  passkey_tab_helper()->HandleSignalAllAcceptedCredentialsEvent(
      origin, std::move(params));

  EXPECT_FALSE(GetPasskey(kCredentialId).hidden());
}

}  // namespace webauthn
