// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <memory>
#include <string>
#include <variant>
#include <vector>

#include "base/feature_list.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "base/run_loop.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/scoped_logging_settings.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "chrome/browser/password_manager/factories/profile_password_store_factory.h"
#include "chrome/browser/signin/identity_test_environment_profile_adaptor.h"
#include "chrome/browser/ssl/cert_verifier_browser_test.h"
#include "chrome/browser/sync/sync_service_factory.h"
#include "chrome/browser/ui/autofill/autofill_popup_controller_impl.h"
#include "chrome/browser/ui/autofill/autofill_popup_controller_impl_test_api.h"
#include "chrome/browser/ui/autofill/autofill_suggestion_controller.h"
#include "chrome/browser/ui/autofill/chrome_autofill_client.h"
#include "chrome/browser/webauthn/chrome_authenticator_request_delegate.h"
#include "chrome/browser/webauthn/gpm_enclave_controller.h"
#include "chrome/browser/webauthn/passkey_model_factory.h"
#if BUILDFLAG(ENABLE_DICE_SUPPORT)
#include "chrome/browser/signin/dice_tab_helper.h"
#include "components/signin/public/base/signin_metrics.h"
#endif
#include "chrome/browser/webauthn/test_util.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/autofill/core/browser/suggestions/suggestion_hiding_reason.h"
#include "components/autofill/core/browser/suggestions/suggestion_type.h"
#include "components/autofill/core/common/autofill_features.h"
#include "components/keyed_service/content/browser_context_dependency_manager.h"
#include "components/password_manager/core/browser/features/password_features.h"
#include "components/password_manager/core/browser/password_store/password_form_converters.h"
#include "components/password_manager/core/browser/password_store/password_store_interface.h"
#include "components/signin/public/base/signin_switches.h"
#include "components/strings/grit/components_strings.h"
#include "components/sync/base/features.h"
#include "components/sync/test/test_sync_service.h"
#include "components/sync_device_info/device_info.h"
#include "components/sync_device_info/fake_device_info_sync_service.h"
#include "components/sync_device_info/fake_device_info_tracker.h"
#include "components/trusted_vault/proto/vault.pb.h"
#include "components/trusted_vault/test/mock_trusted_vault_throttling_connection.h"
#include "components/trusted_vault/trusted_vault_connection.h"
#include "components/webauthn/core/browser/test_passkey_model.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/scoped_authenticator_environment_for_testing.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "crypto/scoped_fake_unexportable_key_provider.h"
#include "crypto/scoped_fake_user_verifying_key_provider.h"
#include "device/bluetooth/bluetooth_adapter_factory.h"
#include "device/bluetooth/test/mock_bluetooth_adapter.h"
#include "device/fido/cable/v2_handshake.h"
#include "device/fido/fido_request_handler_base.h"
#include "device/fido/public/features.h"
#include "device/fido/public/fido_transport_protocol.h"
#include "device/fido/virtual_ctap2_device.h"
#include "device/fido/virtual_fido_device_factory.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/l10n/l10n_util.h"
#include "url/gurl.h"

#if BUILDFLAG(IS_WIN)
#include "device/fido/win/fake_webauthn_api.h"
#include "device/fido/win/util.h"
#endif  // BUILDFLAG(IS_WIN)

#if BUILDFLAG(IS_MAC)
#include "device/fido/mac/util.h"
#endif  // BUILDFLAG(IS_MAC)

namespace {

static constexpr char kRpId[] = "example.com";
static constexpr uint8_t kCredentialID1[] = {1, 2,  3,  4,  5,  6,  7,  8,
                                             9, 10, 11, 12, 13, 14, 15, 16};
static constexpr uint8_t kCredentialID2[] = {2, 3, 4, 5};

static constexpr char kConditionalUIRequest[] = R"((() => {
window.requestAbortController = new AbortController();
navigator.credentials.get({
  signal: window.requestAbortController.signal,
  mediation: 'conditional',
  publicKey: {
    challenge: new Uint8Array([1,2,3,4]),
    timeout: 10000,
    allowCredentials: [],
  }}).then(c => window.domAutomationController.send('webauthn: OK'),
           e => window.domAutomationController.send('error ' + e));
})())";

static constexpr char kConditionalUIRequestFiltered[] = R"((() => {
  window.requestAbortController = new AbortController();
  let cred_id = new Uint8Array([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]);
  navigator.credentials.get({
    signal: window.requestAbortController.signal,
    mediation: 'conditional',
    publicKey: {
      challenge: cred_id,
      timeout: 10000,
      userVerification: 'discouraged',
      allowCredentials: [{type: 'public-key', id: cred_id}],
  }}).then(c => window.domAutomationController.send('webauthn: OK'),
           e => window.domAutomationController.send('error ' + e));
})())";

// Autofill integration tests. This file contains end-to-end tests for
// integration between WebAuthn and Autofill. These tests are sensitive to focus
// changes, so they are interactive UI tests.

// Base class for autofill integration tests, contains the actual test code but
// no setup.
class WebAuthnAutofillIntegrationTest : public CertVerifierBrowserTest {
 public:
  class DelegateObserver
      : public ChromeAuthenticatorRequestDelegate::TestObserver {
   public:
    explicit DelegateObserver(WebAuthnAutofillIntegrationTest* test_instance)
        : test_instance_(test_instance) {
      run_loop_ = std::make_unique<base::RunLoop>();
    }
    virtual ~DelegateObserver() = default;

    void WaitForUI() {
      run_loop_->Run();
      run_loop_ = std::make_unique<base::RunLoop>();
    }

    // ChromeAuthenticatorRequestDelegate::TestObserver:
    void Created(ChromeAuthenticatorRequestDelegate* delegate) override {
      auto connection = std::make_unique<testing::NiceMock<
          trusted_vault::MockTrustedVaultThrottlingConnection>>();
      ON_CALL(*connection, DownloadAuthenticationFactorsRegistrationState(
                               testing::_, testing::_, testing::_))
          .WillByDefault(
              [](const CoreAccountInfo&,
                 base::OnceCallback<void(
                     trusted_vault::
                         DownloadAuthenticationFactorsRegistrationStateResult)>
                     callback,
                 base::RepeatingClosure _) mutable {
                trusted_vault::
                    DownloadAuthenticationFactorsRegistrationStateResult result;
                result.state = trusted_vault::
                    DownloadAuthenticationFactorsRegistrationStateResult::
                        State::kEmpty;
                std::move(callback).Run(std::move(result));
                return std::make_unique<
                    trusted_vault::TrustedVaultConnection::Request>();
              });

      GpmTrustedVaultConnectionProvider::SetOverrideForFrame(
          delegate->GetRenderFrameHost(), std::move(connection));
    }

    void UIShown(ChromeAuthenticatorRequestDelegate* delegate) override {
      run_loop_->QuitWhenIdle();
    }

   private:
    const raw_ptr<WebAuthnAutofillIntegrationTest> test_instance_;
    std::unique_ptr<base::RunLoop> run_loop_;
  };

  WebAuthnAutofillIntegrationTest() = default;

  WebAuthnAutofillIntegrationTest(const WebAuthnAutofillIntegrationTest&) =
      delete;
  WebAuthnAutofillIntegrationTest& operator=(
      const WebAuthnAutofillIntegrationTest&) = delete;

  void SetUp() override {
    https_server_.SetCertHostnames({kRpId});
    ASSERT_TRUE(https_server_.InitializeAndListen());

    create_services_subscription_ =
        BrowserContextDependencyManager::GetInstance()
            ->RegisterCreateServicesCallbackForTesting(base::BindRepeating(
                &WebAuthnAutofillIntegrationTest::RegisterTestServiceFactories,
                base::Unretained(this)));

    CertVerifierBrowserTest::SetUp();

    // Log call `FIDO_LOG` messages.
    scoped_vmodule_.InitWithSwitches("device_event_log_impl=2");
  }

  void SetUpOnMainThread() override {
    CertVerifierBrowserTest::SetUpOnMainThread();

    https_server_.ServeFilesFromSourceDirectory(GetChromeTestDataDir());
    https_server_.StartAcceptingConnections();
    host_resolver()->AddRule("*", "127.0.0.1");

    // Allowlist all certs for the HTTPS server.
    auto cert = https_server_.GetCertificate();
    net::CertVerifyResult verify_result;
    verify_result.cert_status = 0;
    verify_result.verified_cert = cert;
    mock_cert_verifier()->AddResultForCert(cert.get(), verify_result, net::OK);

    // Save a credential to the password store. This will let us wait on the
    // popup to appear after aborting the request.
    password_manager::PasswordStoreInterface* password_store =
        ProfilePasswordStoreFactory::GetForProfile(
            browser()->GetProfile(), ServiceAccessType::EXPLICIT_ACCESS)
            .get();
    password_manager::PasswordForm signin_form;
    GURL url = https_server_.GetURL(kRpId, "/");
    signin_form.signon_realm = url.spec();
    signin_form.url = url;
    signin_form.action = url;
    signin_form.username_value = u"remilia";
    signin_form.password_value = u"shouldbeusingapasskeyinstead";
    base::RunLoop run_loop;
    password_store->AddLogin(password_manager::FromPasswordForm(signin_form),
                             run_loop.QuitClosure());

    // Mock bluetooth support to allow discovery of fake hybrid devices.
    mock_bluetooth_adapter_ =
        base::MakeRefCounted<testing::NiceMock<device::MockBluetoothAdapter>>();
    ON_CALL(*mock_bluetooth_adapter_, IsPresent)
        .WillByDefault(testing::Return(true));
    ON_CALL(*mock_bluetooth_adapter_, IsPowered)
        .WillByDefault(testing::Return(true));
    device::BluetoothAdapterFactory::SetAdapterForTesting(
        mock_bluetooth_adapter_);
    // Other parts of Chrome may keep a reference to the bluetooth adapter.
    // Since we do not verify any expectations, it is okay to leak this mock.
    testing::Mock::AllowLeak(mock_bluetooth_adapter_.get());

    identity_test_env_adaptor_ =
        std::make_unique<IdentityTestEnvironmentProfileAdaptor>(
            browser()->GetProfile());
    identity_test_env_adaptor_->identity_test_env()->SetPrimaryAccount(
        "test@gmail.com", signin::ConsentLevel::kSignin);

    delegate_observer_ = std::make_unique<DelegateObserver>(this);
    ChromeAuthenticatorRequestDelegate::SetGlobalObserverForTesting(
        delegate_observer_.get());

    fake_hw_provider_ =
        std::make_unique<crypto::ScopedFakeUnexportableKeyProvider>();
    fake_uv_provider_ =
        std::make_unique<crypto::ScopedFakeUserVerifyingKeyProvider>();

#if BUILDFLAG(IS_MAC)
    biometrics_override_.reset();
    biometrics_override_ =
        std::make_unique<device::fido::mac::ScopedBiometricsOverride>(true);
#elif BUILDFLAG(IS_WIN)
    biometrics_override_.reset();
    biometrics_override_ =
        std::make_unique<device::fido::win::ScopedBiometricsOverride>(true);
#endif

    ASSERT_TRUE(ui_test_utils::NavigateToURL(
        browser(),
        https_server_.GetURL(kRpId, "/webauthn_conditional_mediation.html")));
  }

  void RegisterTestServiceFactories(content::BrowserContext* context) {
    IdentityTestEnvironmentProfileAdaptor::
        SetIdentityTestEnvironmentFactoriesOnBrowserContext(context);
    PasskeyModelFactory::GetInstance()->SetTestingFactory(
        context,
        base::BindRepeating(
            [](content::BrowserContext*) -> std::unique_ptr<KeyedService> {
              return std::make_unique<webauthn::TestPasskeyModel>();
            }));
    // Disable the sync service by injecting a test fake. The sync service fails
    // to start when overriding the DeviceInfoSyncService with a test fake.
    SyncServiceFactory::GetInstance()->SetTestingFactory(
        context,
        base::BindRepeating(
            [](content::BrowserContext*) -> std::unique_ptr<KeyedService> {
              return std::make_unique<syncer::TestSyncService>();
            }));
  }

  base::WeakPtr<autofill::AutofillSuggestionController>
  GetSuggestionsControllerForWebContents(content::WebContents* web_contents) {
    return autofill::ChromeAutofillClient::FromWebContentsForTesting(
               web_contents)
        ->suggestion_controller_for_testing();
  }

  bool HasWebauthnSuggestionInPopup(content::WebContents* web_contents) {
    base::WeakPtr<autofill::AutofillSuggestionController>
        suggestion_controller =
            GetSuggestionsControllerForWebContents(web_contents);
    if (!suggestion_controller) {
      return false;  // No popup is open.
    }
    return std::ranges::count(suggestion_controller->GetSuggestions(),
                              autofill::SuggestionType::kWebauthnCredential,
                              &autofill::Suggestion::type) != 0;
  }

  // Interact with the username field until the popup shows up. This has the
  // effect of waiting for the browser to send the renderer the password
  // information, and waiting for the UI to render.
  void TapUsernameFieldUntilPopupWithWebauthnSuggestionAppears(
      content::WebContents* web_contents) {
    base::TimeTicks start_time = base::TimeTicks::Now();
    while (!HasWebauthnSuggestionInPopup(web_contents)) {
      if (base::TimeTicks::Now() - start_time > base::Seconds(2)) {
        ASSERT_TRUE(GetSuggestionsControllerForWebContents(web_contents))
            << "Timed out waiting for suggestion popup.";
        FAIL() << "Timed out waiting for WebAuthn suggestion in popup.";
      }
      content::SimulateMouseClickOrTapElementWithId(web_contents, "username");
    }
  }

  void RunSelectAccountTest(const char* request) {
    // Make sure input events cannot close the autofill popup.
    content::WebContents* web_contents =
        browser()->tab_strip_model()->GetActiveWebContents();
    autofill::ChromeAutofillClient* autofill_client =
        autofill::ChromeAutofillClient::FromWebContentsForTesting(web_contents);
    autofill_client->SetKeepPopupOpenForTesting(true);

    // Execute the Conditional UI request.
    content::DOMMessageQueue message_queue(web_contents);
    content::ExecuteScriptAsync(web_contents, request);

    delegate_observer_->WaitForUI();

    TapUsernameFieldUntilPopupWithWebauthnSuggestionAppears(web_contents);
    base::WeakPtr<autofill::AutofillSuggestionController>
        suggestion_controller =
            autofill_client->suggestion_controller_for_testing();
    const std::vector<autofill::Suggestion>& suggestions =
        suggestion_controller->GetSuggestions();

    // Find the webauthn credential on the suggestions list.
    auto it = std::ranges::find(suggestions,
                                autofill::SuggestionType::kWebauthnCredential,
                                &autofill::Suggestion::type);
    ASSERT_EQ(std::ranges::count(suggestions,
                                 autofill::SuggestionType::kWebauthnCredential,
                                 &autofill::Suggestion::type),
              1u);
    ASSERT_NE(it, suggestions.end()) << "WebAuthn entry not found";
    EXPECT_EQ(it->main_text.value, u"flandre");
    EXPECT_EQ(it->labels.at(0).at(0).value, GetDeviceString());
    EXPECT_EQ(it->icon, autofill::Suggestion::Icon::kGlobe);

    // Click the credential.
    test_api(static_cast<autofill::AutofillPopupControllerImpl&>(
                 *suggestion_controller))
        .DisableThreshold(true);
    suggestion_controller->AcceptSuggestion(
        it - suggestions.begin(),
        autofill::AutofillMetrics::SuggestionAcceptedMethod::kMouse);
    std::string result;
    ASSERT_TRUE(message_queue.WaitForMessage(&result));
    EXPECT_EQ(result, "\"webauthn: OK\"");
  }

  void RunAbortTest() {
    // Make sure input events cannot close the autofill popup.
    content::WebContents* web_contents =
        browser()->tab_strip_model()->GetActiveWebContents();
    autofill::ChromeAutofillClient* autofill_client =
        autofill::ChromeAutofillClient::FromWebContentsForTesting(web_contents);
    autofill_client->SetKeepPopupOpenForTesting(true);

    // Execute the Conditional UI request.
    content::DOMMessageQueue message_queue(web_contents);
    content::ExecuteScriptAsync(web_contents, kConditionalUIRequest);

    delegate_observer_->WaitForUI();

    TapUsernameFieldUntilPopupWithWebauthnSuggestionAppears(web_contents);
    base::WeakPtr<autofill::AutofillSuggestionController>
        suggestion_controller =
            autofill_client->suggestion_controller_for_testing();

    // Find the webauthn credential on the suggestions list.
    const std::vector<autofill::Suggestion>& suggestions =
        suggestion_controller->GetSuggestions();
    auto it = std::ranges::find(suggestions,
                                autofill::SuggestionType::kWebauthnCredential,
                                &autofill::Suggestion::type);
    ASSERT_NE(it, suggestions.end()) << "WebAuthn entry not found";
    EXPECT_EQ(it->main_text.value, u"flandre");
    EXPECT_EQ(it->labels.at(0).at(0).value, GetDeviceString());
    EXPECT_EQ(it->icon, autofill::Suggestion::Icon::kGlobe);

    // Abort the request.
    content::ExecuteScriptAsync(web_contents,
                                "window.requestAbortController.abort()");
    std::string result;
    ASSERT_TRUE(message_queue.WaitForMessage(&result));
    EXPECT_EQ(result, "\"error AbortError: signal is aborted without reason\"");

    // The popup may have gone away while waiting. If not, make sure it's gone.
    if (suggestion_controller) {
      suggestion_controller->Hide(
          autofill::SuggestionHidingReason::kUserAborted);
    }

    // Interact with the username field. Since there is still a saved password,
    // the popup should eventually show up.
    while (!suggestion_controller) {
      content::SimulateMouseClickOrTapElementWithId(web_contents, "username");
      suggestion_controller =
          autofill_client->suggestion_controller_for_testing();
    }
    for (const auto& suggestion : suggestion_controller->GetSuggestions()) {
      EXPECT_NE(suggestion.type, autofill::SuggestionType::kWebauthnCredential);
      EXPECT_NE(suggestion.type,
                autofill::SuggestionType::kWebauthnSignInWithAnotherDevice);
    }
  }

  virtual std::u16string GetDeviceString() = 0;

  std::unique_ptr<IdentityTestEnvironmentProfileAdaptor>
      identity_test_env_adaptor_;
  scoped_refptr<device::MockBluetoothAdapter> mock_bluetooth_adapter_ = nullptr;
  base::CallbackListSubscription create_services_subscription_;
  net::EmbeddedTestServer https_server_{net::EmbeddedTestServer::TYPE_HTTPS};
  device::FidoRequestHandlerBase::ScopedAlwaysAllowBLECalls always_allow_ble_;
  std::unique_ptr<DelegateObserver> delegate_observer_;
  base::test::ScopedFeatureList scoped_feature_list_;
  logging::ScopedVmoduleSwitches scoped_vmodule_;
  std::unique_ptr<crypto::ScopedFakeUnexportableKeyProvider> fake_hw_provider_;
  std::unique_ptr<crypto::ScopedFakeUserVerifyingKeyProvider> fake_uv_provider_;

#if BUILDFLAG(IS_WIN)
  std::unique_ptr<device::fido::win::ScopedBiometricsOverride>
      biometrics_override_;
#elif BUILDFLAG(IS_MAC)
  std::unique_ptr<device::fido::mac::ScopedBiometricsOverride>
      biometrics_override_;
#endif
};

// Autofill integration test using the devtools virtual environment.
class WebAuthnDevtoolsAutofillIntegrationTest
    : public WebAuthnAutofillIntegrationTest {
 public:
  void SetUpOnMainThread() override {
    WebAuthnAutofillIntegrationTest::SetUpOnMainThread();

    // Set up a fake virtual device.
    auto virtual_device_factory =
        std::make_unique<device::test::VirtualFidoDeviceFactory>();
    virtual_device_factory->SetTransport(
        device::FidoTransportProtocol::kInternal);
    virtual_device_factory_ = virtual_device_factory.get();
    virtual_device_factory->mutable_state()->InjectResidentKey(
        kCredentialID1, kRpId, std::vector<uint8_t>{5, 6, 7, 8}, "flandre",
        "Flandre Scarlet");
    virtual_device_factory->mutable_state()->fingerprints_enrolled = true;
    device::VirtualCtap2Device::Config config;
    config.resident_key_support = true;
    config.internal_uv_support = true;
    virtual_device_factory->SetCtap2Config(std::move(config));
    scoped_auth_env_ =
        std::make_unique<content::ScopedAuthenticatorEnvironmentForTesting>(
            std::move(virtual_device_factory));
  }

  void PostRunTestOnMainThread() override {
    // To avoid dangling raw_ptr's, these objects need to be destroyed before
    // the test class.
    virtual_device_factory_ = nullptr;
    scoped_auth_env_.reset();
    WebAuthnAutofillIntegrationTest::PostRunTestOnMainThread();
  }

  std::u16string GetDeviceString() override {
    return l10n_util::GetStringUTF16(IDS_PASSWORD_MANAGER_USE_GENERIC_DEVICE);
  }

 protected:
  std::unique_ptr<content::ScopedAuthenticatorEnvironmentForTesting>
      scoped_auth_env_;
  raw_ptr<device::test::VirtualFidoDeviceFactory> virtual_device_factory_;
};

IN_PROC_BROWSER_TEST_F(WebAuthnDevtoolsAutofillIntegrationTest, SelectAccount) {
  RunSelectAccountTest(kConditionalUIRequest);
}

IN_PROC_BROWSER_TEST_F(WebAuthnDevtoolsAutofillIntegrationTest, Abort) {
  RunAbortTest();
}

IN_PROC_BROWSER_TEST_F(WebAuthnDevtoolsAutofillIntegrationTest,
                       SelectAccountWithAllowCredentials) {
  RunSelectAccountTest(kConditionalUIRequestFiltered);
}

IN_PROC_BROWSER_TEST_F(WebAuthnDevtoolsAutofillIntegrationTest,
                       SelectAccountWithAllowCredentialsFiltered) {
  virtual_device_factory_->mutable_state()->InjectResidentKey(
      kCredentialID2, kRpId, std::vector<uint8_t>{6, 7, 8, 9}, "sakuya",
      "Sakuya Izayoi");
  RunSelectAccountTest(kConditionalUIRequestFiltered);
}

#if BUILDFLAG(IS_WIN)
// Autofill integration test using the Windows fake API.
class WebAuthnWindowsAutofillIntegrationTest
    : public WebAuthnAutofillIntegrationTest {
 public:
  void SetUpOnMainThread() override {
    WebAuthnAutofillIntegrationTest::SetUpOnMainThread();

    // Set up the fake Windows platform authenticator.
    fake_webauthn_api_ = std::make_unique<device::FakeWinWebAuthnApi>();
    fake_webauthn_api_->set_version(WEBAUTHN_API_VERSION_4);
    fake_webauthn_api_->set_is_uvpaa(true);
    fake_webauthn_api_->set_supports_silent_discovery(true);
    device::PublicKeyCredentialUserEntity user({1, 2, 3, 4}, "flandre",
                                               "Flandre Scarlet");
    device::PublicKeyCredentialRpEntity rp(kRpId);
    fake_webauthn_api_->InjectDiscoverableCredential(
        kCredentialID1, std::move(rp), std::move(user),
        /*provider_name=*/std::nullopt);

    win_webauthn_api_override_ =
        std::make_unique<device::WinWebAuthnApi::ScopedOverride>(
            fake_webauthn_api_.get());
  }

  void PostRunTestOnMainThread() override {
    // To avoid dangling raw_ptr's, these objects need to be destroyed before
    // the test class.
    win_webauthn_api_override_.reset();
    WebAuthnAutofillIntegrationTest::PostRunTestOnMainThread();
  }

  std::u16string GetDeviceString() override {
    return l10n_util::GetStringUTF16(
        IDS_PASSWORD_MANAGER_PASSKEY_FROM_WINDOWS_HELLO);
  }

 protected:
  std::unique_ptr<device::FakeWinWebAuthnApi> fake_webauthn_api_;
  std::unique_ptr<device::WinWebAuthnApi::ScopedOverride>
      win_webauthn_api_override_;
};

IN_PROC_BROWSER_TEST_F(WebAuthnWindowsAutofillIntegrationTest, SelectAccount) {
  RunSelectAccountTest(kConditionalUIRequest);
}

IN_PROC_BROWSER_TEST_F(WebAuthnWindowsAutofillIntegrationTest,
                       SelectAccountWithAllowCredentials) {
  RunSelectAccountTest(kConditionalUIRequestFiltered);
}

IN_PROC_BROWSER_TEST_F(WebAuthnWindowsAutofillIntegrationTest,
                       SelectAccountWithAllowCredentialsFiltered) {
  device::PublicKeyCredentialUserEntity user({6, 7, 8, 9}, "sakuya",
                                             "Sakuya Izayoi");
  device::PublicKeyCredentialRpEntity rp(kRpId);
  fake_webauthn_api_->InjectDiscoverableCredential(
      kCredentialID2, std::move(rp), std::move(user),
      /*provider_name=*/std::nullopt);
  RunSelectAccountTest(kConditionalUIRequestFiltered);
}

IN_PROC_BROWSER_TEST_F(WebAuthnWindowsAutofillIntegrationTest, Abort) {
  RunAbortTest();
}
#endif  // BUILDFLAG(IS_WIN)

#if BUILDFLAG(ENABLE_DICE_SUPPORT)
class WebAuthnMagiChromeQrAutofillIntegrationTest
    : public WebAuthnAutofillIntegrationTest {
 public:
  WebAuthnMagiChromeQrAutofillIntegrationTest() {
    scoped_feature_list_.InitAndEnableFeatureWithParameters(
        switches::kMagiChromePasskeySignIn, {{"flow_type", "autofill"}});
  }

  void SetUpOnMainThread() override {
    WebAuthnAutofillIntegrationTest::SetUpOnMainThread();

    // Set up a fake virtual device supporting hybrid.
    auto virtual_device_factory =
        std::make_unique<device::test::VirtualFidoDeviceFactory>();
    virtual_device_factory->SetTransport(
        device::FidoTransportProtocol::kHybrid);
    virtual_device_factory_ = virtual_device_factory.get();
    virtual_device_factory->mutable_state()->InjectResidentKey(
        kCredentialID1, kRpId, std::vector<uint8_t>{5, 6, 7, 8}, "flandre",
        "Flandre Scarlet");
    virtual_device_factory->mutable_state()->fingerprints_enrolled = true;
    virtual_device_factory->mutable_state()->simulate_press_callback =
        base::BindLambdaForTesting(
            [](device::VirtualFidoDevice* device) { return false; });
    device::VirtualCtap2Device::Config config;
    config.resident_key_support = true;
    config.internal_uv_support = true;
    virtual_device_factory->SetCtap2Config(std::move(config));
    scoped_auth_env_ =
        std::make_unique<content::ScopedAuthenticatorEnvironmentForTesting>(
            std::move(virtual_device_factory));
  }

  void InitializeChromeSigninFlow() {
    content::WebContents* web_contents =
        browser()->tab_strip_model()->GetActiveWebContents();
    DiceTabHelper::CreateForWebContents(web_contents);
    DiceTabHelper::FromWebContents(web_contents)
        ->InitializeSigninFlow(
            https_server_.GetURL(kRpId, "/webauthn_conditional_mediation.html"),
            signin_metrics::AccessPoint::kSettings,
            signin_metrics::Reason::kSigninPrimaryAccount,
            signin_metrics::PromoAction::PROMO_ACTION_NO_SIGNIN_PROMO, GURL(),
            /*record_signin_started_metrics=*/false, base::DoNothing(),
            base::DoNothing(), base::DoNothing(), base::DoNothing());
  }

  void PostRunTestOnMainThread() override {
    virtual_device_factory_ = nullptr;
    scoped_auth_env_.reset();
    WebAuthnAutofillIntegrationTest::PostRunTestOnMainThread();
  }

  std::u16string GetDeviceString() override {
    return l10n_util::GetStringUTF16(IDS_PASSWORD_MANAGER_USE_GENERIC_DEVICE);
  }

  bool HasWebauthnQrCodeSuggestionInPopup(content::WebContents* web_contents) {
    base::WeakPtr<autofill::AutofillSuggestionController>
        suggestion_controller =
            GetSuggestionsControllerForWebContents(web_contents);
    if (!suggestion_controller) {
      return false;  // No popup is open.
    }
    return std::ranges::any_of(
        suggestion_controller->GetSuggestions(), [](const auto& suggestion) {
          return suggestion.type ==
                 autofill::SuggestionType::kWebauthnPasskeyQrCode;
        });
  }

  [[nodiscard]] testing::AssertionResult
  TapUsernameFieldUntilPopupWithWebauthnQrCodeSuggestionAppears(
      content::WebContents* web_contents) {
    base::TimeTicks start_time = base::TimeTicks::Now();
    while (!HasWebauthnQrCodeSuggestionInPopup(web_contents)) {
      if (base::TimeTicks::Now() - start_time > base::Seconds(5)) {
        return testing::AssertionFailure()
               << "Timed out waiting for WebAuthn QR Code suggestion in popup.";
      }
      content::SimulateMouseClickOrTapElementWithId(web_contents, "username");
    }
    return testing::AssertionSuccess();
  }

  bool HasWebauthnSignInWithAnotherDeviceInPopup(
      content::WebContents* web_contents) {
    base::WeakPtr<autofill::AutofillSuggestionController>
        suggestion_controller =
            GetSuggestionsControllerForWebContents(web_contents);
    if (!suggestion_controller) {
      return false;
    }
    return std::ranges::any_of(
        suggestion_controller->GetSuggestions(), [](const auto& suggestion) {
          return suggestion.type ==
                 autofill::SuggestionType::kWebauthnSignInWithAnotherDevice;
        });
  }

  [[nodiscard]] testing::AssertionResult
  TapUsernameFieldUntilPopupWithSignInWithAnotherDeviceAppears(
      content::WebContents* web_contents) {
    base::TimeTicks start_time = base::TimeTicks::Now();
    while (!HasWebauthnSignInWithAnotherDeviceInPopup(web_contents)) {
      if (base::TimeTicks::Now() - start_time > base::Seconds(5)) {
        return testing::AssertionFailure()
               << "Timed out waiting for WebAuthn Sign In With Another Device "
                  "suggestion in popup.";
      }
      content::SimulateMouseClickOrTapElementWithId(web_contents, "username");
    }
    return testing::AssertionSuccess();
  }

 private:
  std::unique_ptr<content::ScopedAuthenticatorEnvironmentForTesting>
      scoped_auth_env_;
  raw_ptr<device::test::VirtualFidoDeviceFactory> virtual_device_factory_;
};

IN_PROC_BROWSER_TEST_F(WebAuthnMagiChromeQrAutofillIntegrationTest,
                       ShowQrCodeSuggestion) {
  InitializeChromeSigninFlow();

  // Make sure input events cannot close the autofill popup.
  content::WebContents* web_contents =
      browser()->tab_strip_model()->GetActiveWebContents();
  autofill::ChromeAutofillClient* autofill_client =
      autofill::ChromeAutofillClient::FromWebContentsForTesting(web_contents);
  autofill_client->SetKeepPopupOpenForTesting(true);

  // Execute the Conditional UI request.
  content::DOMMessageQueue message_queue(web_contents);
  content::ExecuteScriptAsync(web_contents, kConditionalUIRequest);

  delegate_observer_->WaitForUI();

  ASSERT_TRUE(TapUsernameFieldUntilPopupWithWebauthnQrCodeSuggestionAppears(
      web_contents));
  base::WeakPtr<autofill::AutofillSuggestionController> suggestion_controller =
      autofill_client->suggestion_controller_for_testing();
  const std::vector<autofill::Suggestion>& suggestions =
      suggestion_controller->GetSuggestions();

  // Find the webauthn QR code suggestion on the suggestions list.
  auto it = std::ranges::find(suggestions,
                              autofill::SuggestionType::kWebauthnPasskeyQrCode,
                              &autofill::Suggestion::type);
  ASSERT_NE(it, suggestions.end()) << "WebAuthn QR Code suggestion not found";

  // Both inline QR code and "Sign in with another device..." should be present.
  auto another_device_it = std::ranges::find(
      suggestions, autofill::SuggestionType::kWebauthnSignInWithAnotherDevice,
      &autofill::Suggestion::type);
  EXPECT_NE(another_device_it, suggestions.end())
      << "'Sign in with another device...' suggestion should still be present";

  // Main text of QR suggestion should be correct:
  EXPECT_EQ(
      it->main_text.value,
      l10n_util::GetStringUTF16(IDS_PASSWORD_MANAGER_PASSKEY_QR_CODE_TITLE));

  // Suggestion payload should not be empty (it contains the CaBLE string)
  const autofill::Suggestion::Guid* qr_guid_payload =
      std::get_if<autofill::Suggestion::Guid>(&it->payload);
  ASSERT_TRUE(qr_guid_payload) << "QR Suggestion payload must be a Guid";
  EXPECT_FALSE(qr_guid_payload->value().empty())
      << "QR string payload is empty";
  EXPECT_THAT(qr_guid_payload->value(), testing::StartsWith("FIDO:/"));
}

IN_PROC_BROWSER_TEST_F(WebAuthnMagiChromeQrAutofillIntegrationTest,
                       NoQrCodeSuggestionOnRegularPage) {
  // Do NOT call InitializeChromeSigninFlow() to simulate a regular page.

  // Make sure input events cannot close the autofill popup.
  content::WebContents* web_contents =
      browser()->tab_strip_model()->GetActiveWebContents();
  autofill::ChromeAutofillClient* autofill_client =
      autofill::ChromeAutofillClient::FromWebContentsForTesting(web_contents);
  autofill_client->SetKeepPopupOpenForTesting(true);

  // Execute the Conditional UI request.
  content::ExecuteScriptAsync(web_contents, kConditionalUIRequest);

  delegate_observer_->WaitForUI();

  ASSERT_TRUE(TapUsernameFieldUntilPopupWithSignInWithAnotherDeviceAppears(web_contents));
  base::WeakPtr<autofill::AutofillSuggestionController> suggestion_controller =
      autofill_client->suggestion_controller_for_testing();
  const std::vector<autofill::Suggestion>& suggestions =
      suggestion_controller->GetSuggestions();

  // Find the standard hybrid suggestion on the suggestions list.
  auto another_device_it = std::ranges::find(
      suggestions, autofill::SuggestionType::kWebauthnSignInWithAnotherDevice,
      &autofill::Suggestion::type);
  ASSERT_NE(another_device_it, suggestions.end())
      << "Standard hybrid suggestion not found";

  // The inlined QR suggestion should NOT be present.
  auto qr_it = std::ranges::find(
      suggestions, autofill::SuggestionType::kWebauthnPasskeyQrCode,
      &autofill::Suggestion::type);
  EXPECT_EQ(qr_it, suggestions.end())
      << "Passkey QR Code suggestion should not be present on a regular page";
}
#endif  // BUILDFLAG(ENABLE_DICE_SUPPORT)
}  // namespace
