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

#include "chrome/browser/webauthn/authenticator_request_dialog_model.h"

#include <algorithm>
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <tuple>
#include <utility>
#include <variant>
#include <vector>

#include "base/check.h"
#include "base/check_op.h"
#include "base/containers/flat_set.h"
#include "base/containers/to_vector.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "base/notreached.h"
#include "base/numerics/safe_conversions.h"
#include "base/scoped_observation.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "base/test/run_until.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "base/time/time.h"
#include "base/types/strong_alias.h"
#include "build/build_config.h"
#include "chrome/app/vector_icons/vector_icons.h"
#include "chrome/browser/password_manager/chrome_webauthn_credentials_delegate.h"
#include "chrome/browser/password_manager/chrome_webauthn_credentials_delegate_factory.h"
#include "chrome/browser/webauthn/authenticator_reference.h"
#include "chrome/browser/webauthn/authenticator_request_dialog_controller.h"
#include "chrome/browser/webauthn/authenticator_transport.h"
#include "chrome/browser/webauthn/webauthn_pref_names.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/test/base/chrome_render_view_host_test_harness.h"
#include "components/password_manager/core/browser/passkey_credential.h"
#include "components/password_manager/core/browser/password_form.h"
#include "components/prefs/pref_service.h"
#include "components/strings/grit/components_strings.h"
#include "components/vector_icons/vector_icons.h"
#include "components/webauthn/core/browser/gpm_user_verification_policy.h"
#include "components/webauthn/core/browser/passkey_model.h"
#include "content/public/browser/authenticator_request_client_delegate.h"
#include "device/fido/cable/pairing.h"
#include "device/fido/cable/v2_constants.h"
#include "device/fido/discoverable_credential_metadata.h"
#include "device/fido/fido_request_handler_base.h"
#include "device/fido/public/fido_constants.h"
#include "device/fido/public/fido_transport_protocol.h"
#include "device/fido/public/fido_types.h"
#include "device/fido/public/public_key_credential_user_entity.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/ui_base_features.h"
#include "url/gurl.h"

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

namespace {

using testing::ElementsAre;

using BleStatus = device::FidoRequestHandlerBase::BleStatus;
using RequestType = device::FidoRequestType;
using Step = AuthenticatorRequestDialogModel::Step;
using UIPresentation =
    content::AuthenticatorRequestClientDelegate::UIPresentation;

const base::flat_set<AuthenticatorTransport> kAllTransports = {
    AuthenticatorTransport::kUsbHumanInterfaceDevice,
    AuthenticatorTransport::kNearFieldCommunication,
    AuthenticatorTransport::kInternal,
    AuthenticatorTransport::kHybrid,
};

const base::flat_set<AuthenticatorTransport> kAllTransportsWithoutCable = {
    AuthenticatorTransport::kUsbHumanInterfaceDevice,
    AuthenticatorTransport::kNearFieldCommunication,
    AuthenticatorTransport::kInternal,
};

using TransportAvailabilityInfo =
    device::FidoRequestHandlerBase::TransportAvailabilityInfo;

class MockDialogModelObserver
    : public AuthenticatorRequestDialogModel::Observer {
 public:
  MockDialogModelObserver() = default;

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

  MOCK_METHOD0(OnStartOver, void());
  MOCK_METHOD1(OnModelDestroyed, void(AuthenticatorRequestDialogModel*));
  MOCK_METHOD0(OnStepTransition, void());
  MOCK_METHOD0(OnCancelRequest, void());
  MOCK_METHOD0(OnBluetoothPoweredStateChanged, void());
};

class BluetoothAdapterPowerOnCallbackReceiver {
 public:
  BluetoothAdapterPowerOnCallbackReceiver() = default;

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

  base::RepeatingClosure GetCallback() {
    return base::BindRepeating(
        &BluetoothAdapterPowerOnCallbackReceiver::CallbackTarget,
        base::Unretained(this));
  }

  bool was_called() const { return was_called_; }

 private:
  void CallbackTarget() {
    ASSERT_FALSE(was_called_);
    was_called_ = true;
  }

  bool was_called_ = false;
};

std::string_view RequestTypeToString(RequestType req_type) {
  switch (req_type) {
    case RequestType::kGetAssertion:
      return "GetAssertion";
    case RequestType::kMakeCredential:
      return "MakeCredential";
  }
}

enum class TransportAvailabilityParam {
  kMaybeHasPlatformCredential,
  kHasPlatformCredential,
  kOneRecognizedCred,
  kTwoRecognizedCreds,
  kOneTouchIDRecognizedCred,
  kEmptyAllowList,
  kOnlyInternal,
  kOnlyHybridOrInternal,
  kHasWinNativeAuthenticator,
  kWindowsHandlesHybrid,
  kRequireResidentKey,
  kIsConditionalUI,
  kAttachmentAny,
  kAttachmentCrossPlatform,
  kBleDisabled,
  kBleAccessDenied,
  kHasICloudKeychain,
  kHasICloudKeychainCreds,
  kCreateInICloudKeychain,
  kNoTouchId,
  kUVRequired,
  kUVPreferred,
  kHintSecurityKeys,
  kHintHybrid,
  kHintClientDevice,
  kEnclaveCred,
  kEnclaveNeedsSignIn,
};

std::string_view TransportAvailabilityParamToString(
    TransportAvailabilityParam param) {
  switch (param) {
    case TransportAvailabilityParam::kMaybeHasPlatformCredential:
      return "kMaybeHasPlatformCredential";
    case TransportAvailabilityParam::kHasPlatformCredential:
      return "kHasPlatformCredential";
    case TransportAvailabilityParam::kOneRecognizedCred:
      return "kOneRecognizedCred";
    case TransportAvailabilityParam::kTwoRecognizedCreds:
      return "kTwoRecognizedCreds";
    case TransportAvailabilityParam::kOneTouchIDRecognizedCred:
      return "kOneTouchIDRecognizedCred";
    case TransportAvailabilityParam::kEmptyAllowList:
      return "kEmptyAllowList";
    case TransportAvailabilityParam::kOnlyInternal:
      return "kOnlyInternal";
    case TransportAvailabilityParam::kOnlyHybridOrInternal:
      return "kOnlyHybridOrInternal";
    case TransportAvailabilityParam::kHasWinNativeAuthenticator:
      return "kHasWinNativeAuthenticator";
    case TransportAvailabilityParam::kWindowsHandlesHybrid:
      return "kWindowsHandlesHybrid";
    case TransportAvailabilityParam::kRequireResidentKey:
      return "kRequireResidentKey";
    case TransportAvailabilityParam::kIsConditionalUI:
      return "kIsConditionalUI";
    case TransportAvailabilityParam::kAttachmentAny:
      return "kAttachmentAny";
    case TransportAvailabilityParam::kAttachmentCrossPlatform:
      return "kAttachmentCrossPlatform";
    case TransportAvailabilityParam::kBleDisabled:
      return "kBleDisabled";
    case TransportAvailabilityParam::kBleAccessDenied:
      return "kBleAccessDenied";
    case TransportAvailabilityParam::kHasICloudKeychain:
      return "kHasICloudKeychain";
    case TransportAvailabilityParam::kHasICloudKeychainCreds:
      return "kHasICloudKeychainCreds";
    case TransportAvailabilityParam::kCreateInICloudKeychain:
      return "kCreateInICloudKeychain";
    case TransportAvailabilityParam::kNoTouchId:
      return "kNoTouchId";
    case TransportAvailabilityParam::kUVRequired:
      return "kUVRequired";
    case TransportAvailabilityParam::kUVPreferred:
      return "kUVPreferred";
    case TransportAvailabilityParam::kHintSecurityKeys:
      return "kHintSecurityKeys";
    case TransportAvailabilityParam::kHintHybrid:
      return "kHintHybrid";
    case TransportAvailabilityParam::kHintClientDevice:
      return "kHintClientDevice";
    case TransportAvailabilityParam::kEnclaveCred:
      return "kEnclaveCred";
    case TransportAvailabilityParam::kEnclaveNeedsSignIn:
      return "kEnclaveNeedsSignIn";
  }
}

template <typename T, std::string_view (*F)(T)>
std::string SetToString(base::flat_set<T> s) {
  return base::JoinString(base::ToVector(s, F), ", ");
}

const device::PublicKeyCredentialUserEntity kUser1({1, 2, 3, 4},
                                                   "A",
                                                   std::nullopt);
const device::PublicKeyCredentialUserEntity kUser2({5, 6, 7, 8},
                                                   "B",
                                                   std::nullopt);
const device::PublicKeyCredentialUserEntity kPhoneUser1({9, 0, 1, 2},
                                                        "C",
                                                        std::nullopt);
const device::PublicKeyCredentialUserEntity kPhoneUser2({3, 4, 5, 6},
                                                        "D",
                                                        std::nullopt);

const device::DiscoverableCredentialMetadata kCred1(
    device::AuthenticatorType::kOther,
    "rp.com",
    {0},
    kUser1,
    /*provider_name=*/std::nullopt);
const device::DiscoverableCredentialMetadata kCred1FromICloudKeychain(
    device::AuthenticatorType::kICloudKeychain,
    "rp.com",
    {4},
    kUser1,
    /*provider_name=*/std::nullopt);
const device::DiscoverableCredentialMetadata kCred1From3p(
    device::AuthenticatorType::kICloudKeychain,
    "rp.com",
    {4},
    kUser1,
    "Bitwarden");
const device::DiscoverableCredentialMetadata kCred1FromChromeOS(
    device::AuthenticatorType::kChromeOS,
    "rp.com",
    {4},
    kUser1,
    /*provider_name=*/std::nullopt);
const device::DiscoverableCredentialMetadata kCred2(
    device::AuthenticatorType::kOther,
    "rp.com",
    {1},
    kUser2,
    /*provider_name=*/std::nullopt);
const device::DiscoverableCredentialMetadata kWinCred1(
    device::AuthenticatorType::kWinNative,
    "rp.com",
    {0},
    kUser1,
    /*provider_name=*/std::nullopt);
const device::DiscoverableCredentialMetadata kWinCred2(
    device::AuthenticatorType::kWinNative,
    "rp.com",
    {1},
    kUser2,
    /*provider_name=*/std::nullopt);
const device::DiscoverableCredentialMetadata kTouchIDCred1(
    device::AuthenticatorType::kTouchID,
    "rp.com",
    {4},
    kUser1,
    /*provider_name=*/std::nullopt);
const device::DiscoverableCredentialMetadata kEnclaveCred1(
    device::AuthenticatorType::kEnclave,
    "rp.com",
    {1},
    kUser1,
    /*provider_name=*/std::nullopt);

AuthenticatorRequestDialogModel::Mechanism::CredentialInfo CredentialInfoFrom(
    const device::DiscoverableCredentialMetadata& metadata) {
  return AuthenticatorRequestDialogModel::Mechanism::CredentialInfo(
      metadata.source, metadata.user.id, metadata.last_used_time);
}

template <class Value, class ArgumentType = Value>
class RepeatingValueCallbackReceiver {
 public:
  base::RepeatingCallback<void(ArgumentType)> Callback() {
    return future_.template GetRepeatingCallback<ArgumentType>();
  }

  Value WaitForResult() {
    return future_.Take();
  }

 private:
  base::test::TestFuture<Value> future_{base::test::TestFutureMode::kQueue};
};

using RequestCallbackReceiver =
    RepeatingValueCallbackReceiver<std::string, const std::string&>;

void UpdateModelBeforeStartFlow(AuthenticatorRequestDialogModel* model,
                                TransportAvailabilityInfo tai,
                                bool is_off_the_record) {
  model->request_type = tai.request_type;
  model->resident_key_requirement = tai.resident_key_requirement;
  model->attestation_conveyance_preference =
      tai.attestation_conveyance_preference;
  model->ble_adapter_is_powered =
      tai.ble_status == device::FidoRequestHandlerBase::BleStatus::kOn;
  model->show_security_key_on_qr_sheet = tai.available_transports.contains(
      device::FidoTransportProtocol::kUsbHumanInterfaceDevice);
  model->is_off_the_record = is_off_the_record;
  model->platform_has_biometrics = tai.platform_has_biometrics;
}

}  // namespace

class AuthenticatorRequestDialogControllerTest
    : public ChromeRenderViewHostTestHarness {
 public:
  AuthenticatorRequestDialogControllerTest()
      : ChromeRenderViewHostTestHarness(
            base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}

  AuthenticatorRequestDialogControllerTest(
      const AuthenticatorRequestDialogControllerTest&) = delete;
  AuthenticatorRequestDialogControllerTest& operator=(
      const AuthenticatorRequestDialogControllerTest&) = delete;
};

constexpr bool kIsMac = BUILDFLAG(IS_MAC);

class FakeEnclaveController : public AuthenticatorRequestDialogModel::Observer {
 public:
  explicit FakeEnclaveController(AuthenticatorRequestDialogModel* model)
      : model_(model) {
    model_observer_.Observe(model_);
  }

  void OnGPMPasskeySelected(std::vector<uint8_t> credential_id) override {
    if (webauthn::GpmWillDoUserVerification(
            device::UserVerificationRequirement::kPreferred,
            *model_->platform_has_biometrics)) {
      if (kIsMac) {
        model_->SetStep(Step::kGPMTouchID);
      } else {
        model_->SetStep(Step::kGPMEnterPin);
      }
    } else {
      model_->SetStep(Step::kSelectAccount);
    }
  }

  void OnModelDestroyed(AuthenticatorRequestDialogModel*) override {
    model_observer_.Reset();
    model_ = nullptr;
  }

 private:
  raw_ptr<AuthenticatorRequestDialogModel> model_;
  base::ScopedObservation<AuthenticatorRequestDialogModel,
                          AuthenticatorRequestDialogModel::Observer>
      model_observer_{this};
};

TEST_F(AuthenticatorRequestDialogControllerTest, Mechanisms) {
  const auto mc = RequestType::kMakeCredential;
  const auto ga = RequestType::kGetAssertion;
  const auto usb = AuthenticatorTransport::kUsbHumanInterfaceDevice;
  const auto internal = AuthenticatorTransport::kInternal;
  const auto cable = AuthenticatorTransport::kHybrid;
  const auto cred1 = CredentialInfoFrom(kCred1);
  const auto cred2 = CredentialInfoFrom(kCred2);
  const auto ickc_cred1 = CredentialInfoFrom(kCred1FromICloudKeychain);
  const auto wincred1 = CredentialInfoFrom(kWinCred1);
  const auto wincred2 = CredentialInfoFrom(kWinCred2);
  [[maybe_unused]] const auto touchid_cred1 = CredentialInfoFrom(kTouchIDCred1);
  [[maybe_unused]] const auto enclave_cred1 = CredentialInfoFrom(kEnclaveCred1);
  const auto has_winapi =
      TransportAvailabilityParam::kHasWinNativeAuthenticator;
  [[maybe_unused]] const auto win_hybrid =
      TransportAvailabilityParam::kWindowsHandlesHybrid;
  const auto has_plat = TransportAvailabilityParam::kHasPlatformCredential;
  const auto maybe_plat =
      TransportAvailabilityParam::kMaybeHasPlatformCredential;
  const auto one_cred = TransportAvailabilityParam::kOneRecognizedCred;
  [[maybe_unused]] const auto one_touchid_cred =
      TransportAvailabilityParam::kOneTouchIDRecognizedCred;
  const auto two_cred = TransportAvailabilityParam::kTwoRecognizedCreds;
  const auto empty_al = TransportAvailabilityParam::kEmptyAllowList;
  [[maybe_unused]] const auto enclave_cred =
      TransportAvailabilityParam::kEnclaveCred;
  [[maybe_unused]] const auto only_internal =
      TransportAvailabilityParam::kOnlyInternal;
  const auto only_hybrid_or_internal =
      TransportAvailabilityParam::kOnlyHybridOrInternal;
  const auto rk = TransportAvailabilityParam::kRequireResidentKey;
  const auto c_ui = TransportAvailabilityParam::kIsConditionalUI;
  const auto att_any = TransportAvailabilityParam::kAttachmentAny;
  const auto att_xplat = TransportAvailabilityParam::kAttachmentCrossPlatform;
  const auto ble_off = TransportAvailabilityParam::kBleDisabled;
  const auto ble_denied = TransportAvailabilityParam::kBleAccessDenied;
  const auto hint_sk = TransportAvailabilityParam::kHintSecurityKeys;
  const auto hint_hybrid = TransportAvailabilityParam::kHintHybrid;
  const auto hint_plat = TransportAvailabilityParam::kHintClientDevice;
  [[maybe_unused]] const auto has_ickc =
      TransportAvailabilityParam::kHasICloudKeychain;
  [[maybe_unused]] const auto create_ickc =
      TransportAvailabilityParam::kCreateInICloudKeychain;
  [[maybe_unused]] const auto no_touchid =
      TransportAvailabilityParam::kNoTouchId;
  [[maybe_unused]] const auto ickc_creds =
      TransportAvailabilityParam::kHasICloudKeychainCreds;
  [[maybe_unused]] const auto uv_pref =
      TransportAvailabilityParam::kUVPreferred;
  [[maybe_unused]] const auto uv_req = TransportAvailabilityParam::kUVRequired;
  const auto enclave_needs_sign_in =
      TransportAvailabilityParam::kEnclaveNeedsSignIn;
  using c = AuthenticatorRequestDialogModel::Mechanism::Credential;
  using t = AuthenticatorRequestDialogModel::Mechanism::Transport;
  const auto winapi = AuthenticatorRequestDialogModel::Mechanism::WindowsAPI();
  const auto hybrid = AuthenticatorRequestDialogModel::Mechanism::Hybrid();
  const auto sign_in_again =
      AuthenticatorRequestDialogModel::Mechanism::SignInAgain();
  [[maybe_unused]] const auto ickc =
      AuthenticatorRequestDialogModel::Mechanism::ICloudKeychain();
  const auto usb_ui = Step::kUsbInsertAndActivate;
  const auto mss = Step::kMechanismSelection;
  const auto plat_ui = Step::kPlatformAuthenticator;
  [[maybe_unused]] const auto create_pk = Step::kChromeProfileCreatePasskey;
  const auto create_pk_or_mss =
#if BUILDFLAG(IS_MAC)
      Step::kChromeProfileCreatePasskey;
#else
      Step::kMechanismSelection;
#endif
  const auto create_pk_or_plat_ui =
#if BUILDFLAG(IS_MAC)
      Step::kChromeProfileCreatePasskey;
#else
      Step::kPlatformAuthenticator;
#endif
  const auto create_pk_or_qr =
#if BUILDFLAG(IS_MAC)
      Step::kChromeProfileCreatePasskey;
#else
      Step::kCableV2QRCode;
#endif
  [[maybe_unused]] const auto use_pk = Step::kPreSelectAccount;
  const auto qr = Step::kCableV2QRCode;
  const auto hero = Step::kSelectPriorityMechanism;
  [[maybe_unused]] const auto enclave_touchid = Step::kGPMTouchID;
  [[maybe_unused]] const auto enclave_pin = Step::kGPMEnterPin;

  struct Test {
    int line_num;
    RequestType request_type;
    base::flat_set<AuthenticatorTransport> transports;
    base::flat_set<TransportAvailabilityParam> params;
    std::vector<AuthenticatorRequestDialogModel::Mechanism::Type>
        expected_mechanisms;
    Step expected_first_step;
  };

#define L __LINE__
  // clang-format off
  Test kTests[]{
      // If there's only a single mechanism, it should activate.
      {L, mc, {usb}, {}, {t(usb)}, usb_ui},
      {L, ga, {usb}, {}, {t(usb)}, usb_ui},
      {L, ga, {usb, cable}, {}, {hybrid}, qr},
      {L, ga, {usb, cable}, {}, {hybrid}, qr},
      // If the platform authenticator has a credential it should activate.
      {L,
       ga,
       {},
       {has_plat, one_cred},
       {c(cred1)},
       plat_ui,
     },
      // If the platform authenticator has a credential it should activate.
      {L,
       ga,
       {usb, internal},
       {has_plat, one_cred},
       {c(cred1), t(usb)},
#if BUILDFLAG(IS_MAC)
       plat_ui
#else
       use_pk
#endif
      },
#if BUILDFLAG(IS_MAC)
       // Without Touch ID, the profile authenticator will show a confirmation
       // prompt.
      {L, ga, {usb, internal}, {has_plat, one_cred, no_touchid},
       {c(cred1), t(usb)}, use_pk},
      // When a single profile credential is available with uv!=required and no
      // Touch ID, the UI must show the confirmation because, otherwise,
      // there'll be no UI at all.
      {L, ga, {internal}, {has_plat, one_touchid_cred, no_touchid},
       {c(touchid_cred1)}, hero},
      // When TouchID is present, we can jump directly to the platform UI, which
      // will be a Touch ID prompt.
      {L, ga, {internal}, {has_plat, one_touchid_cred, uv_pref},
       {c(touchid_cred1)}, plat_ui},
      // Or if uv=required, plat_ui is also ok because it'll be a password
      // prompt.
      {L, ga, {internal}, {has_plat, one_touchid_cred, uv_req, no_touchid},
       {c(touchid_cred1)}, plat_ui},
      // The profile authenticator does UV even for uv=discouraged.
      {L, ga, {internal}, {has_plat, one_touchid_cred}, {c(touchid_cred1)},
       plat_ui},
#endif
      // Even with an empty allow list.
      {L,
       ga,
       {usb, internal},
       {has_plat, one_cred, empty_al},
       {c(cred1), t(usb)},
       hero},
      // Two credentials shows mechanism selection.
      {L,
       ga,
       {usb, internal},
       {has_plat, two_cred, empty_al},
       {c(cred1), c(cred2), t(usb)},
       mss},

      // MakeCredential with attachment=platform shows the 'Create a passkey'
      // step, but only on macOS. On other OSes, we defer to the platform.
      {L,
       mc,
       {internal},
       {},
       {t(internal)},
       create_pk_or_plat_ui,
      },
      // MakeCredential with attachment=undefined also shows the 'Create a
      // passkey' step on macOS. On other OSes, we show mechanism selection.
      {L,
       mc,
       {usb, internal},
       {},
       {t(internal), t(usb)},
       create_pk_or_mss,
      },

      // If the Windows API is available without caBLE, it should activate.
      {L, mc, {}, {has_winapi}, {winapi}, plat_ui},
      {L, ga, {}, {has_winapi}, {winapi}, plat_ui},
      // ...even if there are discovered Windows credentials.
      {L, ga, {}, {has_winapi, one_cred}, {c(wincred1), winapi}, plat_ui},

      // If this is a Conditional UI request, don't offer the platform
      // authenticator.
      {L, ga, {usb, internal}, {c_ui}, {t(usb)}, usb_ui},
      {L,
       ga,
       {usb, internal, cable},
       {c_ui},
       {hybrid},
       qr},

      // On Windows, mc with rk=required jumps to the platform UI when caBLE
      // isn't an option. The case where caBLE is possible is tested below.
      {L, mc, {}, {has_winapi, rk}, {winapi}, plat_ui},
      // For rk=discouraged, always jump to Windows UI.
      {L, mc, {cable}, {has_winapi}, {winapi, hybrid}, plat_ui},
      {L, mc, {}, {has_winapi}, {winapi}, plat_ui},

      // On Windows, ga with an empty allow list goes to the platform UI unless
      // caBLE is an option and resident-key is required, which is tested below.
      {L, ga, {}, {has_winapi, empty_al}, {winapi}, plat_ui},
      // With a non-empty allow list containing non phone credentials, always
      // jump to Windows UI.
      // TODO(NEWUI): we should maintain this behaviour on Windows.
      {L, ga, {cable}, {has_winapi}, {hybrid, winapi}, mss},
      {L, ga, {}, {has_winapi}, {winapi}, plat_ui},

       // With attachment=undefined, the UI should still default to a platform
       // authenticator.
       {L, mc, {usb, internal, cable}, {att_any}, {hybrid, t(internal)},
        create_pk_or_mss},
       {L, mc, {usb, internal}, {att_any, rk}, {t(internal), t(usb)},
        create_pk_or_mss},

      // QR code first: Make credential should jump to the QR code with
      // RK=true.
      {L,
       mc,
       {usb, internal, cable},
       {rk, att_xplat},
       {hybrid, t(internal)},
       qr},
      // Or if attachment=any
      {L,
       mc,
       {usb, internal, cable},
       {rk, att_any},
       {hybrid, t(internal)},
       create_pk_or_qr},
      // If RK=false, go to the default for the platform instead.
      {
          L,
          mc,
          {usb, internal, cable},
          {},
          {hybrid, t(internal)},
          create_pk_or_mss,
      },
      // Windows should also jump to the QR code first.
      {L, mc, {cable}, {att_xplat, rk, has_winapi}, {hybrid, winapi}, qr},
      // ... but not for attachment=undefined.
      {L, mc, {cable}, {rk, has_winapi}, {winapi, hybrid}, plat_ui},

      // QR code first: Get assertion should jump to the QR code with empty
      // allow-list.
      {L,
       ga,
       {usb, internal, cable},
       {empty_al},
       {hybrid},
       qr},
      // And if the allow list only contains phones.
      {L,
       ga,
       {internal, cable},
       {only_hybrid_or_internal},
       {hybrid},
       qr},
      // Or a recognized platform credential.
      {L,
       ga,
       {usb, internal, cable},
       {empty_al, has_plat, one_cred},
       {c(cred1), hybrid},
       hero},
      // Ignore the platform credential for conditional ui requests
      {L,
       ga,
       {usb, internal, cable},
       {c_ui, empty_al, has_plat, one_cred},
       {hybrid},
       qr},
      // If there is an allow-list containing USB, go to QR code as well.
      {L, ga, {usb, internal, cable}, {}, {hybrid}, qr},
      // Windows should also jump to the QR code first.
      // TODO: the expectation here (mss) doesn't match the comment.
      {L, ga, {cable}, {empty_al, has_winapi}, {hybrid, winapi}, mss},
      // Unless there is a recognized platform credential, in which case we
      // should jump directly to Windows.
      // Regression test for https://crbug.com/326508293.
      {L,
       ga,
       {cable},
       {empty_al, has_winapi, has_plat, one_cred},
       {c(wincred1), hybrid, winapi},
       plat_ui},
      // For <=Win 10, we can't tell if there is a credential or not. Show the
      // mechanism selection screen instead.
      {L,
       ga,
       {cable},
       {empty_al, has_winapi, maybe_plat},
       {winapi, hybrid},
       mss},
    #if BUILDFLAG(IS_MAC)
      // If there's a single enclave passkey, we should jump directly to
      // the enclave Touch ID sheet.
      {L,
       ga,
       {cable, internal},
       {only_hybrid_or_internal, empty_al, enclave_cred, uv_pref},
       {c(enclave_cred1), hybrid},
       enclave_touchid},
      // But not if Touch ID isn't available.
      {L,
       ga,
       {cable, internal},
       {only_hybrid_or_internal, empty_al, enclave_cred, no_touchid, uv_pref},
       {c(enclave_cred1), hybrid},
       hero},
      // And not if uv=discouraged
      {L,
       ga,
       {cable, internal},
       {only_hybrid_or_internal, empty_al, enclave_cred},
       {c(enclave_cred1), hybrid},
       hero},
    #endif
    #if !BUILDFLAG(IS_CHROMEOS)
      // If an enclave credential is in an allowlist, we should jump to UV
      // immediately.
      {L,
       ga,
       {cable, internal},
       {only_hybrid_or_internal, enclave_cred, uv_pref},
       {c(enclave_cred1), hybrid},
       kIsMac ? enclave_touchid : hero},
     #endif
      // But, again, not for uv=discouraged.
      {L,
       ga,
       {cable, internal},
       {only_hybrid_or_internal, enclave_cred},
       {c(enclave_cred1), hybrid},
       hero},
      // When the enclave needs to sign-in again, that should appear as a
      // mechanism and the MSS should be shown.
      {L,
       ga,
       {cable, usb},
       {enclave_cred, enclave_needs_sign_in},
       {sign_in_again, hybrid},
       mss},
      // Hinting "client-device" should not jump to the sign-in-again option.
      {L,
       mc,
       {cable, usb},
       {enclave_needs_sign_in, hint_plat},
       {sign_in_again, hybrid},
       mss},
      // Hinting "client-device" should not just to any other options, like
      // the profile authenticator, if GPM needs to sign in again.
      {L,
       mc,
       {cable, usb, internal},
       {enclave_needs_sign_in, hint_plat},
       {sign_in_again, hybrid, t(internal)},
       mss},

      // Tests for the mechanism selection screen mixing credential types.
      // Mac & Linux:
      // Internal credentials + qr code.
      {L,
       ga,
       {usb, cable, internal},
       {two_cred, has_plat, empty_al},
       {c(cred1), c(cred2), hybrid},
       mss},
      // qr code with ble disabled shows usb option.
      {L, ga, {usb, cable}, {ble_off}, {hybrid, t(usb)}, mss},
      // qr code with ble access denied shows usb option.
      {L, ga, {usb, cable}, {ble_denied}, {hybrid, t(usb)}, mss},
      // Internal credentials, no qr code.
      {L,
       ga,
       {usb, internal},
       {two_cred, has_plat, empty_al},
       {c(cred1), c(cred2), t(usb)},
       mss},
      // Single internal credential with empty allow list.
      {L,
       ga,
       {usb, cable, internal},
       {one_cred, has_plat, empty_al},
       {c(cred1), hybrid},
       hero,
     },
      // Single internal credential with non-empty allow list.
      {L,
       ga,
       {usb, cable, internal},
       {one_cred, has_plat},
       {c(cred1), hybrid},
#if BUILDFLAG(IS_MAC)
       plat_ui,
#else
       use_pk,
#endif
      },
      // Regression test for crbug.com/40282332.
      // A platform authenticator that reports the availability of credentials
      // but does not enumerate them should be listed.
      {L,
       ga,
       {usb, cable, internal},
       {has_plat},
       {hybrid, t(internal)},
       plat_ui},

#if BUILDFLAG(IS_MAC)
      // Even with iCloud Keychain present, we shouldn't jump to it without
      // additional flags set.
      {L, mc, {internal}, {rk, has_ickc}, {ickc, t(internal)}, create_pk},
      // iCloud Keychain should be the default if the request delegate
      // configured that.
      {L,
       mc,
       {internal},
       {rk, has_ickc, create_ickc},
       {ickc, t(internal)},
       plat_ui},
      // ... and also for attachment=any
      {L,
       mc,
       {internal},
       {rk, att_any, has_ickc, create_ickc},
       {ickc, t(internal)},
       plat_ui},
#endif

       // Tests for RP hints.
       //
       // create(): Security key hint should show security key UI.
       {L, mc, {usb, internal, cable}, {rk, hint_sk},
        {hybrid, t(internal), t(usb)}, usb_ui},
       // But not if USB isn't a valid transport.
       {L, mc, {internal, cable}, {rk, hint_sk}, {hybrid, t(internal)},
#if BUILDFLAG(IS_MAC)
         create_pk,
#else
         qr,
#endif
       },
       // If webauthn.dll is present, jump to it.
       {L, mc, {cable}, {has_winapi, rk, hint_sk}, {winapi, hybrid}, plat_ui},

       // create(): Hybrid hint should show QR.
       {L, mc, {usb, internal, cable}, {rk, hint_hybrid},
        {hybrid, t(internal), t(usb)}, qr},
       // But not if Hybrid isn't a valid transport.
       {L, mc, {usb, internal}, {rk, hint_hybrid}, {t(internal), t(usb)},
#if BUILDFLAG(IS_MAC)
         create_pk,
#else
         mss,
#endif
       },
       // If older webauthn.dll is present, don't jump to it since it doesn't do
       // hybrid.
       {L, mc, {cable}, {has_winapi, rk, hint_hybrid}, {winapi, hybrid}, qr},
#if BUILDFLAG(IS_WIN)
       // ... but do if it supports hybrid.
       {L, mc, {cable}, {has_winapi, win_hybrid, rk, hint_hybrid}, {winapi},
        plat_ui},
#endif

       // create(): Client device hint should jump to the platform
       // authenticator.
       {L, mc, {usb, internal, cable}, {rk, hint_plat}, {hybrid, t(internal)},
        kIsMac ? create_pk : plat_ui,
       },
       // But not if there isn't a platform authenticator.
       {L, mc, {usb, cable}, {rk, hint_plat}, {hybrid}, qr},
       // If webauthn.dll is present, jump to it.
       {L, mc, {cable}, {has_winapi, rk, hint_plat}, {winapi, hybrid},
        plat_ui},
       // Or if there's iCloud Keychain.
#if BUILDFLAG(IS_MAC)
       {L, mc, {cable}, {has_ickc, create_ickc, rk, hint_plat}, {ickc, hybrid},
        plat_ui},
#endif // BUILDFLAG(IS_MAC)

       // get(): Security key hint should show security key UI.
       {L, ga, {usb, internal, cable}, {rk, hint_sk}, {hybrid, t(usb)},
        usb_ui},
       // But not if USB isn't a valid transport.
       {L, ga, {internal, cable}, {rk, hint_sk}, {hybrid}, qr},
       // If credentials are found on a platform authenticator, they are still
       // shown.
       {L, ga, {usb, internal, cable}, {one_cred, rk, hint_sk},
        {c(cred1), hybrid, t(usb)}, mss},
       // If webauthn.dll is present, jump to it.
       {L, ga, {cable}, {has_winapi, rk, hint_sk}, {hybrid, winapi}, plat_ui},

       // get(): Hybrid hint should show QR.
       {L, ga, {usb, internal, cable}, {rk, hint_hybrid}, {hybrid, t(usb)}, qr},
       // But not if hybrid isn't available.
       {L, ga, {usb, internal}, {rk, hint_hybrid}, {t(usb)}, usb_ui},
       // If older webauthn.dll is present, don't jump to it since it doesn't do
       // hybrid.
       {L, ga, {cable}, {has_winapi, rk, hint_hybrid}, {hybrid, winapi}, qr},
#if BUILDFLAG(IS_WIN)
       // ... but do if it supports hybrid.
       {L, ga, {cable}, {has_winapi, win_hybrid, rk, hint_hybrid}, {winapi},
        plat_ui},
#endif
       // If credentials are found on a platform authenticator, they are still
       // shown.
       {L, ga, {usb, internal, cable}, {one_cred, rk, hint_hybrid},
        {c(cred1), hybrid, t(usb)}, mss},

       // get(): Client device hint should trigger webauthn.dll, if it exists.
       {L, ga, {cable}, {rk, has_winapi, hint_plat}, {hybrid, winapi},
        plat_ui},
       // And otherwise it doesn't do anything because we generally assume that
       // we can enumerate platform authenticators and do a good job.
       {L, ga, {usb, cable, internal}, {rk, hint_plat}, {hybrid}, qr},

#if BUILDFLAG(IS_WIN)
      // Windows tests.
      // Mix of internal credentials, but no USB/NFC.
      // This should jump to Windows, as there is a match with the local
      // authenticator.
      {L,
       ga,
       {cable},
       {two_cred, has_winapi, only_hybrid_or_internal,
        has_plat},
       {c(wincred1), c(wincred2), hybrid},
       plat_ui},
      // Mix of internal credentials, and USB/NFC (empty allow list).
      // This should default to Windows, and on cancel offer dispatching to the
      // Windows API for USB/NFC.
      {L,
       ga,
       {cable},
       {two_cred, has_winapi, empty_al, has_plat},
       {c(wincred1), c(wincred2), hybrid, winapi},
       plat_ui},

      // Tests where Windows handles hybrid with internal credentials only.
      // This should dispatch directly to the Windows API.
      {L,
       ga,
       {},
       {two_cred, has_winapi, win_hybrid, only_internal, has_plat},
       {c(wincred1), c(wincred2)},
       plat_ui},
#endif  // BUILDFLAG(IS_WIN)
  };
  // clang-format on
#undef L

#if BUILDFLAG(IS_WIN)
  device::FakeWinWebAuthnApi fake_win_webauthn_api;
  device::WinWebAuthnApi::ScopedOverride win_webauthn_api_override(
      &fake_win_webauthn_api);
#endif

  auto RunTest = [&](const Test& test) {
    SCOPED_TRACE(static_cast<int>(test.expected_first_step));
    SCOPED_TRACE(
        (SetToString<TransportAvailabilityParam,
                     TransportAvailabilityParamToString>(test.params)));
    SCOPED_TRACE((SetToString<device::FidoTransportProtocol, device::ToString>(
        test.transports)));
    SCOPED_TRACE(RequestTypeToString(test.request_type));
    SCOPED_TRACE(testing::Message() << "At line number: " << test.line_num);

#if BUILDFLAG(IS_WIN)
    bool has_win_hybrid =
        test.params.contains(TransportAvailabilityParam::kWindowsHandlesHybrid);
    fake_win_webauthn_api.set_version(has_win_hybrid ? 7 : 4);
#endif

    TransportAvailabilityInfo transports_info;
    if (test.params.contains(TransportAvailabilityParam::kBleDisabled)) {
      transports_info.ble_status = BleStatus::kOff;
    } else if (test.params.contains(
                   TransportAvailabilityParam::kBleAccessDenied)) {
      transports_info.ble_status = BleStatus::kPermissionDenied;
    } else {
      transports_info.ble_status = BleStatus::kOn;
    }
    transports_info.request_type = test.request_type;
    if (test.request_type == device::FidoRequestType::kMakeCredential) {
      transports_info.attestation_conveyance_preference =
          device::AttestationConveyancePreference::kNone;
    }
    transports_info.available_transports = test.transports;
    transports_info.user_verification_requirement =
        test.params.contains(TransportAvailabilityParam::kUVRequired)
            ? device::UserVerificationRequirement::kRequired
            : (test.params.contains(TransportAvailabilityParam::kUVPreferred)
                   ? device::UserVerificationRequirement::kPreferred
                   : device::UserVerificationRequirement::kDiscouraged);

    if (test.params.contains(
            TransportAvailabilityParam::kHasPlatformCredential)) {
      transports_info.has_platform_authenticator_credential =
          device::FidoRequestHandlerBase::RecognizedCredential::
              kHasRecognizedCredential;
    } else if (test.params.contains(
                   TransportAvailabilityParam::kMaybeHasPlatformCredential)) {
      transports_info.has_platform_authenticator_credential =
          device::FidoRequestHandlerBase::RecognizedCredential::kUnknown;
    } else {
      transports_info.has_platform_authenticator_credential = device::
          FidoRequestHandlerBase::RecognizedCredential::kNoRecognizedCredential;
    }

    device::DiscoverableCredentialMetadata cred1;
    device::DiscoverableCredentialMetadata cred2;
    if (test.params.contains(
            TransportAvailabilityParam::kHasWinNativeAuthenticator)) {
      cred1 = kWinCred1;
      cred2 = kWinCred2;
    } else {
      cred1 = kCred1;
      cred2 = kCred2;
    }
    device::DiscoverableCredentialMetadata touchid_cred1 = kTouchIDCred1;
    if (test.params.contains(
            TransportAvailabilityParam::kHasICloudKeychainCreds)) {
      transports_info.has_icloud_keychain_credential =
          device::FidoRequestHandlerBase::RecognizedCredential::
              kHasRecognizedCredential;
      transports_info.recognized_credentials.emplace_back(
          kCred1FromICloudKeychain);
    } else {
      transports_info.has_icloud_keychain_credential = device::
          FidoRequestHandlerBase::RecognizedCredential::kNoRecognizedCredential;
    }

    if (test.params.contains(TransportAvailabilityParam::kEnclaveCred)) {
      transports_info.recognized_credentials.emplace_back(kEnclaveCred1);
    }

    if (test.params.contains(TransportAvailabilityParam::kOneRecognizedCred)) {
      transports_info.recognized_credentials = {std::move(cred1)};
    } else if (test.params.contains(
                   TransportAvailabilityParam::kTwoRecognizedCreds)) {
      transports_info.recognized_credentials = {std::move(cred1),
                                                std::move(cred2)};
    } else if (test.params.contains(
                   TransportAvailabilityParam::kOneTouchIDRecognizedCred)) {
      transports_info.recognized_credentials = {std::move(touchid_cred1)};
    }

    transports_info.has_icloud_keychain =
        test.params.contains(TransportAvailabilityParam::kHasICloudKeychain);
    transports_info.has_empty_allow_list =
        test.params.contains(TransportAvailabilityParam::kEmptyAllowList);
    if (test.params.contains(TransportAvailabilityParam::kOnlyInternal)) {
      transports_info.request_is_internal_only = true;
      transports_info.transport_list_did_include_hybrid = false;
      transports_info.transport_list_did_include_security_key = false;
    } else if (test.params.contains(
                   TransportAvailabilityParam::kOnlyHybridOrInternal)) {
      transports_info.transport_list_did_include_hybrid = true;
      transports_info.transport_list_did_include_security_key = false;
    } else {
      transports_info.transport_list_did_include_hybrid = true;
      transports_info.transport_list_did_include_security_key = true;
    }
    transports_info.transport_list_did_include_internal = true;

    if (test.params.contains(
            TransportAvailabilityParam::kHasWinNativeAuthenticator)) {
      transports_info.has_win_native_api_authenticator = true;
      transports_info.win_native_ui_shows_resident_credential_notice = true;
      transports_info.win_is_uvpaa = true;
    }
    transports_info.resident_key_requirement =
        test.params.contains(TransportAvailabilityParam::kRequireResidentKey)
            ? device::ResidentKeyRequirement::kRequired
            : device::ResidentKeyRequirement::kDiscouraged;
    if (test.params.contains(TransportAvailabilityParam::kAttachmentAny)) {
      CHECK(transports_info.request_type == RequestType::kMakeCredential);
      transports_info.make_credential_attachment =
          device::AuthenticatorAttachment::kAny;
    }
    if (test.params.contains(
            TransportAvailabilityParam::kAttachmentCrossPlatform)) {
      CHECK(transports_info.request_type == RequestType::kMakeCredential);
      CHECK(!transports_info.make_credential_attachment.has_value());
      transports_info.make_credential_attachment =
          device::AuthenticatorAttachment::kCrossPlatform;
    }
    if (!transports_info.make_credential_attachment.has_value() &&
        transports_info.request_type == RequestType::kMakeCredential) {
      transports_info.make_credential_attachment =
          device::AuthenticatorAttachment::kPlatform;
    }

    auto model =
        base::MakeRefCounted<AuthenticatorRequestDialogModel>(main_rfh());
    AuthenticatorRequestDialogController controller(model.get(), main_rfh());
    FakeEnclaveController enclave_controller(model.get());

    controller.set_allow_icloud_keychain(transports_info.has_icloud_keychain);
    if (test.params.contains(
            TransportAvailabilityParam::kCreateInICloudKeychain)) {
      controller.set_should_create_in_icloud_keychain(true);
    }
#if BUILDFLAG(IS_MAC)
    transports_info.platform_has_biometrics =
        !test.params.contains(TransportAvailabilityParam::kNoTouchId);
#endif

    std::optional<device::FidoTransportProtocol> hint_transport;
    if (test.params.contains(TransportAvailabilityParam::kHintSecurityKeys)) {
      CHECK(!hint_transport.has_value());
      hint_transport = device::FidoTransportProtocol::kUsbHumanInterfaceDevice;
    }
    if (test.params.contains(TransportAvailabilityParam::kHintHybrid)) {
      CHECK(!hint_transport.has_value());
      hint_transport = device::FidoTransportProtocol::kHybrid;
    }
    if (test.params.contains(TransportAvailabilityParam::kHintClientDevice)) {
      CHECK(!hint_transport.has_value());
      hint_transport = device::FidoTransportProtocol::kInternal;
    }

    if (hint_transport.has_value()) {
      content::AuthenticatorRequestClientDelegate::Hints hints;
      hints.transport = hint_transport;
      controller.SetHints(hints);
    }

    if (test.params.contains(TransportAvailabilityParam::kEnclaveNeedsSignIn)) {
      controller.OnGPMEnclaveEnabledStatusChanged(
          EnclaveEnabledStatus::kEnabledAndReauthNeeded);
    } else if (test.params.contains(TransportAvailabilityParam::kEnclaveCred)) {
      controller.OnGPMEnclaveEnabledStatusChanged(
          EnclaveEnabledStatus::kEnabled);
    }

    controller.SetAccountPreselectedCallback(base::BindRepeating(
        [](device::DiscoverableCredentialMetadata cred) {}));

    if (test.transports.contains(device::FidoTransportProtocol::kHybrid)) {
      controller.set_cable_transport_info("fido:/1234");
    }

    const bool is_autofill =
        test.params.contains(TransportAvailabilityParam::kIsConditionalUI);
    controller.SetUIPresentation(is_autofill ? UIPresentation::kAutofill
                                             : UIPresentation::kModal);
    UpdateModelBeforeStartFlow(model.get(), transports_info,
                               /*is_off_the_record=*/false);
    controller.StartFlow(std::move(transports_info), {});
    if (is_autofill) {
      EXPECT_EQ(model->step(), Step::kPasskeyAutofill);
      controller.TransitionToModalWebAuthnRequest();
    }

    EXPECT_EQ(test.expected_first_step, model->step());

    std::vector<AuthenticatorRequestDialogModel::Mechanism::Type>
        mechanism_types;
    for (const auto& mech : model->mechanisms) {
      mechanism_types.push_back(mech.type);
    }
    EXPECT_EQ(test.expected_mechanisms, mechanism_types);

    if (!model->offer_try_again_in_ui) {
      return;
    }

    model->StartOver();
    EXPECT_EQ(Step::kMechanismSelection, model->step());
  };

  for (const auto& test : kTests) {
    RunTest(test);
  }
}

#if BUILDFLAG(IS_WIN)
TEST_F(AuthenticatorRequestDialogControllerTest, WinCancel) {
  // Simulate the user canceling the Windows native UI, both with and without
  // that UI being immediately triggered. If it was immediately triggered then
  // canceling it should show the mechanism selection UI.

  device::FakeWinWebAuthnApi fake_win_webauthn_api;
  device::WinWebAuthnApi::ScopedOverride win_webauthn_api_override(
      &fake_win_webauthn_api);

  for (const int win_webauthn_api_version : {4, 7}) {
    fake_win_webauthn_api.set_version(win_webauthn_api_version);
    for (const bool is_passkey_request : {false, true}) {
      SCOPED_TRACE(testing::Message() << "passkey req? " << is_passkey_request);
      SCOPED_TRACE(testing::Message() << "win v" << win_webauthn_api_version);

      TransportAvailabilityInfo tai;
      tai.make_credential_attachment =
          device::AuthenticatorAttachment::kCrossPlatform;
      tai.request_type = device::FidoRequestType::kMakeCredential;
      tai.attestation_conveyance_preference =
          device::AttestationConveyancePreference::kNone;
      tai.has_win_native_api_authenticator = true;
      tai.win_native_ui_shows_resident_credential_notice = true;
      tai.available_transports.insert(device::FidoTransportProtocol::kHybrid);
      tai.resident_key_requirement =
          is_passkey_request ? device::ResidentKeyRequirement::kRequired
                             : device::ResidentKeyRequirement::kDiscouraged;
      tai.ble_status = BleStatus::kOn;

      auto model =
          base::MakeRefCounted<AuthenticatorRequestDialogModel>(main_rfh());
      AuthenticatorRequestDialogController controller(model.get(), main_rfh());
      controller.saved_authenticators().emplace_back(
          "ID", AuthenticatorTransport::kInternal,
          device::AuthenticatorType::kWinNative);
      controller.set_cable_transport_info("fido:/1234");

      UpdateModelBeforeStartFlow(model.get(), tai, /*is_off_the_record=*/false);
      controller.StartFlow(std::move(tai), {});

      const bool win_ui_was_immediately_triggered =
          !is_passkey_request || win_webauthn_api_version == 7;
      if (!win_ui_was_immediately_triggered) {
        EXPECT_NE(model->step(), Step::kNotStarted);
        // Canceling the Windows UI ends the request because the user must have
        // selected the Windows option first.
        EXPECT_FALSE(controller.OnWinUserCancelled());
        continue;
      }

      EXPECT_EQ(model->step(), Step::kPlatformAuthenticator);

      if (win_webauthn_api_version >= 7) {
        // Windows handles hybrid itself starting with this version, so
        // canceling shouldn't try to show Chrome UI.
        EXPECT_FALSE(controller.OnWinUserCancelled());
        continue;
      }

      // Canceling the Windows native UI should be handled.
      EXPECT_TRUE(controller.OnWinUserCancelled());
      // The mechanism selection sheet should now be showing.
      EXPECT_EQ(model->step(), Step::kMechanismSelection);
      // Canceling the Windows UI ends the request because the user must have
      // selected the Windows option first.
      EXPECT_FALSE(controller.OnWinUserCancelled());
    }
  }
}

// Simulate the user cancelling the Windows native UI after it was automatically
// dispatched to because a matching credential for Windows Hello was found for
// an allow-list request.
// Regression test for crbug.com/40280770.
TEST_F(AuthenticatorRequestDialogControllerTest,
       WinCancel_AfterMatchingLocalCred) {
  device::FakeWinWebAuthnApi fake_win_webauthn_api;
  device::WinWebAuthnApi::ScopedOverride win_webauthn_api_override(
      &fake_win_webauthn_api);

  fake_win_webauthn_api.set_version(4);

  TransportAvailabilityInfo tai;
  tai.request_type = device::FidoRequestType::kGetAssertion;
  tai.has_win_native_api_authenticator = true;
  tai.has_empty_allow_list = false;
  tai.available_transports.insert(device::FidoTransportProtocol::kHybrid);
  tai.ble_status = BleStatus::kOn;
  tai.recognized_credentials = {kWinCred1};
  tai.has_platform_authenticator_credential = device::FidoRequestHandlerBase::
      RecognizedCredential::kHasRecognizedCredential;

  auto model =
      base::MakeRefCounted<AuthenticatorRequestDialogModel>(main_rfh());
  AuthenticatorRequestDialogController controller(model.get(), main_rfh());
  controller.saved_authenticators().emplace_back(
      "ID", AuthenticatorTransport::kInternal,
      device::AuthenticatorType::kWinNative);
  controller.set_cable_transport_info("fido:/1234");
  UpdateModelBeforeStartFlow(model.get(), tai, /*is_off_the_record=*/false);
  controller.StartFlow(std::move(tai), {});

  // The Windows native UI should have been triggered.
  EXPECT_EQ(model->step(), Step::kPlatformAuthenticator);

  // Canceling the Windows native UI should be handled.
  EXPECT_TRUE(controller.OnWinUserCancelled());

  // The mechanism selection sheet should now be showing.
  EXPECT_EQ(model->step(), Step::kMechanismSelection);

  // Canceling the Windows UI ends the request because the user must have
  // selected the Windows option first.
  EXPECT_FALSE(controller.OnWinUserCancelled());
}

TEST_F(AuthenticatorRequestDialogControllerTest, WinNoPlatformAuthenticator) {
  TransportAvailabilityInfo tai;
  tai.request_type = device::FidoRequestType::kMakeCredential;
  tai.attestation_conveyance_preference =
      device::AttestationConveyancePreference::kNone;
  tai.make_credential_attachment = device::AuthenticatorAttachment::kAny;
  tai.request_is_internal_only = true;
  tai.win_is_uvpaa = false;
  tai.has_win_native_api_authenticator = true;
  auto model =
      base::MakeRefCounted<AuthenticatorRequestDialogModel>(main_rfh());
  UpdateModelBeforeStartFlow(model.get(), tai, /*is_off_the_record=*/false);
  AuthenticatorRequestDialogController controller(model.get(), main_rfh());
  controller.StartFlow(std::move(tai), {});
  EXPECT_EQ(model->step(), Step::kErrorWindowsHelloNotEnabled);
  EXPECT_FALSE(model->offer_try_again_in_ui);
}

// Tests that if a WebAuthn request with an empty allow-list has a matching
// Windows Hello credential, the request is dispatched to Windows with an empty
// allow list (i.e. no filtering takes place).
// Regression test for https://crbug.com/448351425.
TEST_F(AuthenticatorRequestDialogControllerTest, WinCredMatchEmptyAllowList) {
  static constexpr char kWinAuthenticatorId[] = "win-authenticator";
  TransportAvailabilityInfo tai;
  tai.request_type = device::FidoRequestType::kGetAssertion;
  tai.win_is_uvpaa = true;
  tai.recognized_credentials = {kWinCred1};
  tai.has_win_native_api_authenticator = true;
  tai.has_empty_allow_list = true;
  auto model =
      base::MakeRefCounted<AuthenticatorRequestDialogModel>(main_rfh());
  UpdateModelBeforeStartFlow(model.get(), tai, /*is_off_the_record=*/false);
  AuthenticatorRequestDialogController controller(model.get(), main_rfh());
  controller.saved_authenticators().emplace_back(
      kWinAuthenticatorId, AuthenticatorTransport::kInternal,
      device::AuthenticatorType::kWinNative);
  base::test::TestFuture<std::string> request_future;
  controller.SetRequestCallback(
      request_future.GetRepeatingCallback<const std::string&>());
  controller.SetAccountPreselectedCallback(base::BindLambdaForTesting(
      [](device::DiscoverableCredentialMetadata cred) {
        FAIL() << "Should not have narrowed the allow list";
      }));
  controller.StartFlow(std::move(tai), {});
  EXPECT_EQ(model->step(), Step::kPlatformAuthenticator);
  EXPECT_EQ(request_future.Get(), kWinAuthenticatorId);
}
#endif

TEST_F(AuthenticatorRequestDialogControllerTest, NoAvailableTransports) {
  testing::StrictMock<MockDialogModelObserver> mock_observer;
  auto model =
      base::MakeRefCounted<AuthenticatorRequestDialogModel>(main_rfh());
  AuthenticatorRequestDialogController controller(model.get(), main_rfh());
  model->observers.AddObserver(&mock_observer);

  EXPECT_CALL(mock_observer, OnStepTransition());
  TransportAvailabilityInfo transports_info;
  transports_info.attestation_conveyance_preference =
      device::AttestationConveyancePreference::kNone;
  UpdateModelBeforeStartFlow(model.get(), transports_info,
                             /*is_off_the_record=*/false);
  controller.StartFlow(std::move(transports_info), {});
  EXPECT_EQ(Step::kErrorNoAvailableTransports, model->step());
  testing::Mock::VerifyAndClearExpectations(&mock_observer);

  EXPECT_CALL(mock_observer, OnCancelRequest());
  model->CancelAuthenticatorRequest();
  testing::Mock::VerifyAndClearExpectations(&mock_observer);

  EXPECT_CALL(mock_observer, OnStepTransition());
  model->OnRequestComplete();
  EXPECT_EQ(Step::kClosed, model->step());
  testing::Mock::VerifyAndClearExpectations(&mock_observer);

  EXPECT_CALL(mock_observer, OnModelDestroyed(model.get()));
}

TEST_F(AuthenticatorRequestDialogControllerTest, GpmDisabledForCreate) {
  for (bool security_keys_available : {false, true}) {
    SCOPED_TRACE(security_keys_available);
    auto model =
        base::MakeRefCounted<AuthenticatorRequestDialogModel>(main_rfh());
    AuthenticatorRequestDialogController controller(model.get(), main_rfh());
    model->gpm_create_available_but_disabled_by_policy = true;
    TransportAvailabilityInfo transports_info;
    if (security_keys_available) {
      transports_info.available_transports.insert(
          device::FidoTransportProtocol::kUsbHumanInterfaceDevice);
    }
    transports_info.attestation_conveyance_preference =
        device::AttestationConveyancePreference::kNone;
    transports_info.request_type = device::FidoRequestType::kMakeCredential;
    UpdateModelBeforeStartFlow(model.get(), transports_info,
                               /*is_off_the_record=*/false);
    controller.StartFlow(std::move(transports_info), {});
    if (security_keys_available) {
      EXPECT_EQ(Step::kUsbInsertAndActivate, model->step());
    } else {
      EXPECT_EQ(Step::kErrorGpmDisabled, model->step());
    }
  }
}

TEST_F(AuthenticatorRequestDialogControllerTest, Cable2ndFactorFlows) {
#if BUILDFLAG(IS_WIN)
  // TODO(crbug.com/41490900): Get test to pass in the webauthn supports
  // hybrid case.
  device::FakeWinWebAuthnApi fake_win_webauthn_api;
  device::WinWebAuthnApi::ScopedOverride win_webauthn_api_override(
      &fake_win_webauthn_api);
  fake_win_webauthn_api.set_version(4);
#endif  // BUILDFLAG(IS_WIN)

  enum class Profile {
    NORMAL,
    INCOGNITO,
  };

  const auto mc = RequestType::kMakeCredential;
  const auto ga = RequestType::kGetAssertion;
  const auto on_ = BleStatus::kOn;
  const auto off = BleStatus::kOff;
  const auto normal = Profile::NORMAL;
  const auto otr___ = Profile::INCOGNITO;
  const auto mss = Step::kMechanismSelection;
  const auto qr = Step::kCableV2QRCode;
  [[maybe_unused]] const auto interstitial = Step::kOffTheRecordInterstitial;
  const auto power = Step::kBlePowerOnAutomatic;

  const struct {
    RequestType request_type;
    BleStatus ble_power;
    Profile profile;
    std::vector<Step> steps;
  } kTests[] = {
      //               | Expected UI steps in order.
      {mc, on_, normal, {qr}},
      // TODO(crbug.com/424448497): this should show the interstitial.
      {mc, on_, otr___, {qr}},
      {mc, off, normal, {mss, power, qr}},
      // TODO(crbug.com/424448497): this should show the interstitial.
      {mc, off, otr___, {mss, power, qr}},
      {ga, on_, normal, {qr}},
      {ga, on_, otr___, {qr}},
      {ga, off, normal, {mss, power, qr}},
      {ga, off, otr___, {mss, power, qr}},
  };

  unsigned test_num = 0;
  for (const auto& test : kTests) {
    SCOPED_TRACE(test_num++);

    TransportAvailabilityInfo transports_info;
    transports_info.ble_status = test.ble_power;
    transports_info.can_power_on_ble_adapter = true;
    transports_info.request_type = test.request_type;
    if (transports_info.request_type == RequestType::kMakeCredential) {
      transports_info.make_credential_attachment =
          device::AuthenticatorAttachment::kAny;
      transports_info.attestation_conveyance_preference =
          device::AttestationConveyancePreference::kNone;
    }
    transports_info.available_transports = {
        AuthenticatorTransport::kHybrid,
        device::FidoTransportProtocol::kUsbHumanInterfaceDevice};

    auto model =
        base::MakeRefCounted<AuthenticatorRequestDialogModel>(main_rfh());
    AuthenticatorRequestDialogController controller(model.get(), main_rfh());

    controller.set_cable_transport_info("fido:/1234");
    UpdateModelBeforeStartFlow(
        model.get(), transports_info,
        /*is_off_the_record=*/test.profile == Profile::INCOGNITO);
    controller.StartFlow(std::move(transports_info), {});

    for (const auto step : test.steps) {
      ASSERT_EQ(step, model->step())
          << static_cast<int>(step)
          << " != " << static_cast<int>(model->step());

      switch (step) {
        case Step::kBlePowerOnAutomatic:
          controller.BluetoothAdapterStatusChanged(BleStatus::kOn);
          break;

        case Step::kOffTheRecordInterstitial:
          model->OnOffTheRecordInterstitialAccepted();
          break;

        case Step::kCableV2QRCode:
          break;

        case Step::kMechanismSelection:
          std::ranges::find_if(model->mechanisms, [](const auto& m) -> bool {
            return std::holds_alternative<
                AuthenticatorRequestDialogModel::Mechanism::Hybrid>(m.type);
          })->callback.Run();
          break;

        default:
          NOTREACHED();
      }
    }
  }
}

TEST_F(AuthenticatorRequestDialogControllerTest, AwaitingAcknowledgement) {
  const struct {
    void (AuthenticatorRequestDialogController::*event)();
    Step expected_sheet;
  } kTestCases[] = {
      {&AuthenticatorRequestDialogController::OnRequestTimeout,
       Step::kTimedOut},
      {&AuthenticatorRequestDialogController::OnActivatedKeyNotRegistered,
       Step::kKeyNotRegistered},
      {&AuthenticatorRequestDialogController::OnActivatedKeyAlreadyRegistered,
       Step::kKeyAlreadyRegistered},
  };

  for (const auto& test_case : kTestCases) {
    testing::StrictMock<MockDialogModelObserver> mock_observer;
    auto model =
        base::MakeRefCounted<AuthenticatorRequestDialogModel>(main_rfh());
    AuthenticatorRequestDialogController controller(model.get(), main_rfh());
    model->observers.AddObserver(&mock_observer);

    TransportAvailabilityInfo transports_info;
    transports_info.request_type = RequestType::kMakeCredential;
    transports_info.attestation_conveyance_preference =
        device::AttestationConveyancePreference::kNone;
    transports_info.make_credential_attachment =
        device::AuthenticatorAttachment::kAny;
    transports_info.available_transports = kAllTransportsWithoutCable;

    EXPECT_CALL(mock_observer, OnStepTransition());
    UpdateModelBeforeStartFlow(model.get(), transports_info,
                               /*is_off_the_record=*/false);
    controller.StartFlow(std::move(transports_info), {});
#if BUILDFLAG(IS_MAC)
    EXPECT_EQ(Step::kChromeProfileCreatePasskey, model->step());
#else
    EXPECT_EQ(Step::kMechanismSelection, model->step());
#endif
    testing::Mock::VerifyAndClearExpectations(&mock_observer);

    EXPECT_CALL(mock_observer, OnStepTransition());
    (controller.*test_case.event)();
    EXPECT_EQ(test_case.expected_sheet, model->step());
    testing::Mock::VerifyAndClearExpectations(&mock_observer);

    EXPECT_CALL(mock_observer, OnStepTransition());
    EXPECT_CALL(mock_observer, OnCancelRequest());
    controller.CancelAuthenticatorRequest();
    EXPECT_EQ(Step::kClosed, model->step());
    testing::Mock::VerifyAndClearExpectations(&mock_observer);

    EXPECT_CALL(mock_observer, OnModelDestroyed(model.get()));
  }
}

// Test fixture for tests that involve Bluetooth status and have different
// behaviour on Windows when the platform API is managing hybrid. It
// overrides the platform API in that case.
class AuthenticatorRequestDialogControllerBluetoothTest
    : public AuthenticatorRequestDialogControllerTest {
 public:
  AuthenticatorRequestDialogControllerBluetoothTest() {}

  void SetUp() override {
    AuthenticatorRequestDialogControllerTest::SetUp();
#if BUILDFLAG(IS_WIN)
    win_webauthn_api_override_ =
        std::make_unique<device::WinWebAuthnApi::ScopedOverride>(
            &fake_win_webauthn_api_);
    fake_win_webauthn_api_.set_version(4);
#endif  // BUILDFLAG(IS_WIN)
  }

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

 private:
#if BUILDFLAG(IS_WIN)
  device::FakeWinWebAuthnApi fake_win_webauthn_api_;
  std::unique_ptr<device::WinWebAuthnApi::ScopedOverride>
      win_webauthn_api_override_;
#endif  // BUILDFLAG(IS_WIN)
};

TEST_F(AuthenticatorRequestDialogControllerBluetoothTest,
       BleAdapterAlreadyPowered) {
  const struct {
    AuthenticatorTransport transport;
    Step expected_final_step;
  } kTestCases[] = {
      {AuthenticatorTransport::kHybrid, Step::kCableV2QRCode},
  };

  for (const auto test_case : kTestCases) {
    TransportAvailabilityInfo transports_info;
    transports_info.request_type = RequestType::kGetAssertion;
    transports_info.available_transports = {test_case.transport};
    transports_info.can_power_on_ble_adapter = true;
    transports_info.ble_status = BleStatus::kOn;

    BluetoothAdapterPowerOnCallbackReceiver power_receiver;
    auto model =
        base::MakeRefCounted<AuthenticatorRequestDialogModel>(main_rfh());
    AuthenticatorRequestDialogController controller(model.get(), main_rfh());
    controller.SetBluetoothAdapterPowerOnCallback(power_receiver.GetCallback());
    controller.set_cable_transport_info("fido:/1234");
    UpdateModelBeforeStartFlow(model.get(), transports_info,
                               /*is_off_the_record=*/false);
    controller.StartFlow(std::move(transports_info), {});
    EXPECT_EQ(test_case.expected_final_step, model->step());
    EXPECT_TRUE(model->ble_adapter_is_powered);
    EXPECT_FALSE(power_receiver.was_called());
  }
}

TEST_F(AuthenticatorRequestDialogControllerBluetoothTest,
       BleAdapterNeedToBeManuallyPowered) {
  const struct {
    AuthenticatorTransport transport;
    Step expected_final_step;
  } kTestCases[] = {
      {AuthenticatorTransport::kHybrid, Step::kCableV2QRCode},
  };

  for (const auto test_case : kTestCases) {
    TransportAvailabilityInfo transports_info;
    transports_info.request_type = RequestType::kGetAssertion;
    transports_info.available_transports = {test_case.transport};
    transports_info.can_power_on_ble_adapter = false;
    transports_info.ble_status = BleStatus::kOff;

    testing::NiceMock<MockDialogModelObserver> mock_observer;
    BluetoothAdapterPowerOnCallbackReceiver power_receiver;
    auto model =
        base::MakeRefCounted<AuthenticatorRequestDialogModel>(main_rfh());
    AuthenticatorRequestDialogController controller(model.get(), main_rfh());
    model->observers.AddObserver(&mock_observer);
    controller.SetBluetoothAdapterPowerOnCallback(power_receiver.GetCallback());
    controller.set_cable_transport_info("fido:/1234");
    UpdateModelBeforeStartFlow(model.get(), transports_info,
                               /*is_off_the_record=*/false);
    controller.StartFlow(std::move(transports_info), {});

    EXPECT_EQ(Step::kBlePowerOnManual, model->step());
    EXPECT_FALSE(model->ble_adapter_is_powered);

    EXPECT_CALL(mock_observer, OnBluetoothPoweredStateChanged());
    controller.BluetoothAdapterStatusChanged(BleStatus::kOn);

    EXPECT_EQ(Step::kBlePowerOnManual, model->step());
    EXPECT_TRUE(model->ble_adapter_is_powered);
    testing::Mock::VerifyAndClearExpectations(&mock_observer);

    controller.ContinueWithFlowAfterBleAdapterPowered();

    EXPECT_EQ(test_case.expected_final_step, model->step());
    EXPECT_FALSE(power_receiver.was_called());
  }
}

// Tests that if the bluetooth adapter needs action, the QR sheet and USB sheet
// are split.
TEST_F(AuthenticatorRequestDialogControllerBluetoothTest,
       BleAdapterNeedsActionSplitsUsbAndQrSheets) {
  for (BleStatus ble_status :
       {BleStatus::kPendingPermissionRequest, BleStatus::kPermissionDenied,
        BleStatus::kOff, BleStatus::kOn}) {
    SCOPED_TRACE(testing::Message() << static_cast<int>(ble_status));
    TransportAvailabilityInfo transports_info;
    transports_info.request_type = RequestType::kMakeCredential;
    transports_info.attestation_conveyance_preference =
        device::AttestationConveyancePreference::kNone;
    transports_info.available_transports = {
        AuthenticatorTransport::kUsbHumanInterfaceDevice,
        AuthenticatorTransport::kHybrid};
    transports_info.can_power_on_ble_adapter = false;
    transports_info.ble_status = ble_status;
    auto model =
        base::MakeRefCounted<AuthenticatorRequestDialogModel>(main_rfh());
    AuthenticatorRequestDialogController controller(model.get(), main_rfh());
    controller.set_cable_transport_info("fido:/1234");
    UpdateModelBeforeStartFlow(model.get(), transports_info,
                               /*is_off_the_record=*/false);
    controller.StartFlow(std::move(transports_info), {});
    EXPECT_EQ(model->show_security_key_on_qr_sheet,
              ble_status == BleStatus::kOn);
    EXPECT_TRUE(
        std::ranges::any_of(model->mechanisms, [](const auto& m) -> bool {
          return std::holds_alternative<
              AuthenticatorRequestDialogModel::Mechanism::Hybrid>(m.type);
        }));
    EXPECT_EQ(std::ranges::any_of(
                  model->mechanisms,
                  [](const auto& m) -> bool {
                    const auto* transport = std::get_if<
                        AuthenticatorRequestDialogModel::Mechanism::Transport>(
                        &m.type);
                    return transport &&
                           transport->value() ==
                               AuthenticatorTransport::kUsbHumanInterfaceDevice;
                  }),
              ble_status != BleStatus::kOn);
  }
}

TEST_F(AuthenticatorRequestDialogControllerBluetoothTest,
       BleAdapterCanBeAutomaticallyPowered) {
  const struct {
    AuthenticatorTransport transport;
    Step expected_final_step;
  } kTestCases[] = {
      {AuthenticatorTransport::kHybrid, Step::kCableV2QRCode},
  };

  for (const auto test_case : kTestCases) {
    TransportAvailabilityInfo transports_info;
    transports_info.request_type = RequestType::kGetAssertion;
    transports_info.available_transports = {test_case.transport};
    transports_info.can_power_on_ble_adapter = true;
    transports_info.ble_status = BleStatus::kOff;

    BluetoothAdapterPowerOnCallbackReceiver power_receiver;
    auto model =
        base::MakeRefCounted<AuthenticatorRequestDialogModel>(main_rfh());
    AuthenticatorRequestDialogController controller(model.get(), main_rfh());
    controller.SetBluetoothAdapterPowerOnCallback(power_receiver.GetCallback());
    controller.set_cable_transport_info("fido:/1234");
    UpdateModelBeforeStartFlow(model.get(), transports_info,
                               /*is_off_the_record=*/false);
    controller.StartFlow(std::move(transports_info), {});

    EXPECT_EQ(Step::kBlePowerOnAutomatic, model->step());

    controller.PowerOnBleAdapter();

    EXPECT_EQ(Step::kBlePowerOnAutomatic, model->step());
    EXPECT_TRUE(power_receiver.was_called());
    EXPECT_FALSE(model->ble_adapter_is_powered);

    controller.BluetoothAdapterStatusChanged(BleStatus::kOn);

    EXPECT_EQ(test_case.expected_final_step, model->step());
    EXPECT_TRUE(model->ble_adapter_is_powered);
  }
}

// Tests that Chrome will request Bluetooth permissions before attempting to
// power the adapter on if the adapter reports the status as pending permission.
TEST_F(AuthenticatorRequestDialogControllerBluetoothTest,
       BleAdapterPendingPermission) {
  for (BleStatus ble_status :
       {BleStatus::kOn, BleStatus::kOff, BleStatus::kPermissionDenied}) {
    SCOPED_TRACE(testing::Message() << static_cast<int>(ble_status));
    TransportAvailabilityInfo transports_info;
    transports_info.request_type = RequestType::kGetAssertion;
    transports_info.available_transports = {
        device::FidoTransportProtocol::kHybrid};
    transports_info.can_power_on_ble_adapter = true;
    transports_info.ble_status = BleStatus::kPendingPermissionRequest;

    RepeatingValueCallbackReceiver<
        device::FidoRequestHandlerBase::BlePermissionCallback>
        request_ble_permission_callback_receiver;
    auto model =
        base::MakeRefCounted<AuthenticatorRequestDialogModel>(main_rfh());
    AuthenticatorRequestDialogController controller(model.get(), main_rfh());
    controller.SetRequestBlePermissionCallback(
        request_ble_permission_callback_receiver.Callback());
    controller.set_cable_transport_info("fido:/1234");
    UpdateModelBeforeStartFlow(model.get(), transports_info,
                               /*is_off_the_record=*/false);
    controller.StartFlow(std::move(transports_info), {});

    device::FidoRequestHandlerBase::BlePermissionCallback
        ble_permission_callback =
            request_ble_permission_callback_receiver.WaitForResult();
    ASSERT_TRUE(ble_permission_callback);
    std::move(ble_permission_callback).Run(ble_status);

    if (ble_status == BleStatus::kOn) {
      EXPECT_TRUE(model->ble_adapter_is_powered);
      EXPECT_EQ(model->step(), Step::kCableV2QRCode);
    } else if (ble_status == BleStatus::kOff) {
      EXPECT_FALSE(model->ble_adapter_is_powered);
      EXPECT_EQ(model->step(), Step::kBlePowerOnAutomatic);
    } else {
      EXPECT_FALSE(model->ble_adapter_is_powered);
      EXPECT_EQ(model->step(), Step::kBlePermissionMac);
    }
  }
}

TEST_F(AuthenticatorRequestDialogControllerTest,
       ConditionalUINoRecognizedCredential) {
  auto model =
      base::MakeRefCounted<AuthenticatorRequestDialogModel>(main_rfh());
  AuthenticatorRequestDialogController controller(model.get(), main_rfh());

  int preselect_num_called = 0;
  controller.SetAccountPreselectedCallback(base::BindRepeating(
      [](int* i, device::DiscoverableCredentialMetadata cred) {
        EXPECT_EQ(cred.cred_id, std::vector<uint8_t>({1, 2, 3, 4}));
        ++(*i);
      },
      &preselect_num_called));
  int request_num_called = 0;
  controller.SetRequestCallback(base::BindRepeating(
      [](int* i, const std::string& authenticator_id) { ++(*i); },
      &request_num_called));
  controller.saved_authenticators().emplace_back(
      /*device_id=*/"authenticator",
      AuthenticatorTransport::kUsbHumanInterfaceDevice,
      device::AuthenticatorType::kOther);
  controller.saved_authenticators().emplace_back(
      /*device_id=*/"authenticator", AuthenticatorTransport::kInternal,
      device::AuthenticatorType::kOther);

  TransportAvailabilityInfo transports_info;
  transports_info.attestation_conveyance_preference =
      device::AttestationConveyancePreference::kNone;
  transports_info.available_transports = kAllTransports;
  transports_info.has_platform_authenticator_credential = device::
      FidoRequestHandlerBase::RecognizedCredential::kHasRecognizedCredential;
  controller.SetUIPresentation(UIPresentation::kAutofill);
  UpdateModelBeforeStartFlow(model.get(), transports_info,
                             /*is_off_the_record=*/false);
  controller.StartFlow(std::move(transports_info), {});
  task_environment()->RunUntilIdle();
  EXPECT_EQ(model->step(), Step::kPasskeyAutofill);
  EXPECT_TRUE(model->should_dialog_be_closed());
  EXPECT_EQ(preselect_num_called, 0);
  EXPECT_EQ(request_num_called, 0);
}

TEST_F(AuthenticatorRequestDialogControllerTest,
       ConditionalUIRecognizedCredential) {
  auto model =
      base::MakeRefCounted<AuthenticatorRequestDialogModel>(main_rfh());
  AuthenticatorRequestDialogController controller(model.get(), main_rfh());
  int preselect_num_called = 0;
  controller.SetAccountPreselectedCallback(base::BindRepeating(
      [](int* i, device::DiscoverableCredentialMetadata cred) {
        EXPECT_EQ(cred.cred_id, std::vector<uint8_t>({0}));
        ++(*i);
      },
      &preselect_num_called));
  int request_num_called = 0;
  controller.SetRequestCallback(base::BindRepeating(
      [](int* i, const std::string& authenticator_id) {
        EXPECT_EQ(authenticator_id, "internal");
        ++(*i);
      },
      &request_num_called));
  controller.saved_authenticators().emplace_back(
      /*device_id=*/"usb", AuthenticatorTransport::kUsbHumanInterfaceDevice,
      device::AuthenticatorType::kOther);
  controller.saved_authenticators().emplace_back(
      /*device_id=*/"internal", AuthenticatorTransport::kInternal,
      device::AuthenticatorType::kOther);

  TransportAvailabilityInfo transports_info;
  transports_info.attestation_conveyance_preference =
      device::AttestationConveyancePreference::kNone;
  transports_info.available_transports = kAllTransports;
  transports_info.has_platform_authenticator_credential = device::
      FidoRequestHandlerBase::RecognizedCredential::kHasRecognizedCredential;
  transports_info.recognized_credentials = {kCred1, kCred2};
  controller.SetUIPresentation(UIPresentation::kAutofill);
  UpdateModelBeforeStartFlow(model.get(), transports_info,
                             /*is_off_the_record=*/false);
  controller.StartFlow(std::move(transports_info), {});
  EXPECT_EQ(model->step(), Step::kPasskeyAutofill);
  EXPECT_TRUE(model->should_dialog_be_closed());
  EXPECT_EQ(request_num_called, 0);

  // After preselecting an account, the request should be dispatched to the
  // platform authenticator.
  controller.OnAccountPreselected(kCred1.cred_id);
  task_environment()->RunUntilIdle();
  EXPECT_EQ(preselect_num_called, 1);
  EXPECT_EQ(request_num_called, 1);
}

// Tests that cancelling a Conditional UI request that has completed restarts
// it.
TEST_F(AuthenticatorRequestDialogControllerTest, ConditionalUICancelRequest) {
  testing::StrictMock<MockDialogModelObserver> mock_observer;
  auto model =
      base::MakeRefCounted<AuthenticatorRequestDialogModel>(main_rfh());
  AuthenticatorRequestDialogController controller(model.get(), main_rfh());
  model->observers.AddObserver(&mock_observer);
  controller.saved_authenticators().emplace_back(
      /*device_id=*/"internal", AuthenticatorTransport::kInternal,
      device::AuthenticatorType::kOther);

  EXPECT_CALL(mock_observer, OnStepTransition());
  TransportAvailabilityInfo transports_info;
  transports_info.attestation_conveyance_preference =
      device::AttestationConveyancePreference::kNone;
  controller.SetUIPresentation(UIPresentation::kAutofill);
  UpdateModelBeforeStartFlow(model.get(), transports_info,
                             /*is_off_the_record=*/false);
  controller.StartFlow(std::move(transports_info), {});
  EXPECT_EQ(model->step(), Step::kPasskeyAutofill);
  testing::Mock::VerifyAndClearExpectations(&mock_observer);

  // Cancel an ongoing request (as if e.g. the user clicked the accept button).
  // The request should be restarted.
  EXPECT_CALL(mock_observer, OnStartOver());
  EXPECT_CALL(mock_observer, OnStepTransition()).Times(2);
  controller.SetCurrentStepForTesting(Step::kKeyAlreadyRegistered);
  controller.CancelAuthenticatorRequest();
  EXPECT_EQ(model->step(), Step::kPasskeyAutofill);
  testing::Mock::VerifyAndClearExpectations(&mock_observer);
  model->observers.RemoveObserver(&mock_observer);
}

#if BUILDFLAG(IS_WIN)
// Tests that cancelling the Windows Platform authenticator during a Conditional
// UI request restarts it.
TEST_F(AuthenticatorRequestDialogControllerTest, ConditionalUIWindowsCancel) {
  testing::StrictMock<MockDialogModelObserver> mock_observer;
  auto model =
      base::MakeRefCounted<AuthenticatorRequestDialogModel>(main_rfh());
  AuthenticatorRequestDialogController controller(model.get(), main_rfh());
  model->observers.AddObserver(&mock_observer);
  controller.saved_authenticators().emplace_back(
      /*device_id=*/"internal", AuthenticatorTransport::kInternal,
      device::AuthenticatorType::kOther);

  EXPECT_CALL(mock_observer, OnStepTransition());
  TransportAvailabilityInfo transports_info;
  transports_info.attestation_conveyance_preference =
      device::AttestationConveyancePreference::kNone;
  controller.SetUIPresentation(UIPresentation::kAutofill);
  UpdateModelBeforeStartFlow(model.get(), transports_info,
                             /*is_off_the_record=*/false);
  controller.StartFlow(std::move(transports_info), {});
  EXPECT_EQ(model->step(), Step::kPasskeyAutofill);
  testing::Mock::VerifyAndClearExpectations(&mock_observer);

  // Simulate the Windows authenticator cancelling.
  EXPECT_CALL(mock_observer, OnStepTransition());
  EXPECT_CALL(mock_observer, OnStartOver());
  controller.OnWinUserCancelled();
  EXPECT_EQ(model->step(), Step::kPasskeyAutofill);
  testing::Mock::VerifyAndClearExpectations(&mock_observer);
  model->observers.RemoveObserver(&mock_observer);
}
#endif  // BUILDFLAG(IS_WIN)

#if BUILDFLAG(IS_MAC)
// Tests that a transport = internal virtual authenticator can be dispatched to
// on Mac.
// Regression test for crbug.com/41493872.
TEST_F(AuthenticatorRequestDialogControllerTest, PlatformVirtualAuthenticator) {
  auto model =
      base::MakeRefCounted<AuthenticatorRequestDialogModel>(main_rfh());
  AuthenticatorRequestDialogController controller(model.get(), main_rfh());
  controller.saved_authenticators().emplace_back(
      /*device_id=*/"virtual-authenticator", AuthenticatorTransport::kInternal,
      device::AuthenticatorType::kOther);
  controller.SetAccountPreselectedCallback(base::DoNothing());
  base::test::TestFuture<std::string> request_future;
  controller.SetRequestCallback(
      request_future.GetRepeatingCallback<const std::string&>());
  TransportAvailabilityInfo transports_info;
  transports_info.user_verification_requirement =
      device::UserVerificationRequirement::kRequired;
  transports_info.request_type = device::FidoRequestType::kGetAssertion;
  transports_info.has_empty_allow_list = false;
  transports_info.recognized_credentials = {kCred2};
  UpdateModelBeforeStartFlow(model.get(), transports_info,
                             /*is_off_the_record=*/false);
  controller.StartFlow(std::move(transports_info), {});
  EXPECT_EQ(request_future.Get(), "virtual-authenticator");
}
#endif  // BUILDFLAG(IS_MAC)

TEST_F(AuthenticatorRequestDialogControllerTest, PreSelect) {
  for (const bool has_empty_allow_list : {false, true}) {
    SCOPED_TRACE(::testing::Message()
                 << "has_empty_allow_list=" << has_empty_allow_list);

    auto model =
        base::MakeRefCounted<AuthenticatorRequestDialogModel>(main_rfh());
    AuthenticatorRequestDialogController controller(model.get(), main_rfh());
    int preselect_num_called = 0;
    controller.SetAccountPreselectedCallback(base::BindLambdaForTesting(
        [&preselect_num_called](device::DiscoverableCredentialMetadata cred) {
          EXPECT_EQ(cred.cred_id, std::vector<uint8_t>({1}));
          ++preselect_num_called;
        }));
    int request_num_called = 0;
    controller.SetRequestCallback(base::BindLambdaForTesting(
        [&request_num_called](const std::string& authenticator_id) {
          EXPECT_EQ(authenticator_id, "internal-authenticator");
          ++request_num_called;
        }));

    controller.saved_authenticators().emplace_back(
        /*device_id=*/"usb-authenticator",
        AuthenticatorTransport::kUsbHumanInterfaceDevice,
        device::AuthenticatorType::kOther);
    controller.saved_authenticators().emplace_back(
        /*device_id=*/"internal-authenticator",
        AuthenticatorTransport::kInternal, device::AuthenticatorType::kOther);

    TransportAvailabilityInfo transports_info;
    transports_info.request_type = device::FidoRequestType::kGetAssertion;
    transports_info.available_transports = kAllTransports;
    transports_info.has_empty_allow_list = has_empty_allow_list;
    transports_info.user_verification_requirement =
        device::UserVerificationRequirement::kPreferred;
    transports_info.has_platform_authenticator_credential = device::
        FidoRequestHandlerBase::RecognizedCredential::kHasRecognizedCredential;
    transports_info.recognized_credentials = {kCred1FromICloudKeychain, kCred2};
    UpdateModelBeforeStartFlow(model.get(), transports_info,
                               /*is_off_the_record=*/false);
    controller.StartFlow(std::move(transports_info), {});

    if (has_empty_allow_list) {
      EXPECT_EQ(model->step(), Step::kSelectPriorityMechanism);
    } else {
      EXPECT_EQ(model->step(), Step::kPreSelectAccount);
    }
    task_environment()->RunUntilIdle();

    if (has_empty_allow_list) {
      EXPECT_EQ(preselect_num_called, 0);
      EXPECT_EQ(request_num_called, 0);
      // After preselecting an account, the request should be dispatched to the
      // platform authenticator.
      controller.OnAccountPreselected(kCred2.cred_id);
      task_environment()->RunUntilIdle();
      EXPECT_EQ(preselect_num_called, 1);
      EXPECT_EQ(request_num_called, 1);
    } else {
      EXPECT_EQ(request_num_called, 0);
      ASSERT_EQ(model->creds.size(), 1u);
      // `kCred1FromICloudKeychain` is an iCloud Keychain credential so,
      // even though it's in `recognized_credentials`, it shouldn't have been
      // used by the standard platform authenticator code.
      EXPECT_EQ(model->creds[0].cred_id, std::vector<uint8_t>({1}));
    }
  }
}

TEST_F(AuthenticatorRequestDialogControllerTest,
       PreSelectWithMultipleTransportsOnCredential) {
  auto cred = kCred2;
  cred.transports = {device::FidoTransportProtocol::kInternal,
                     device::FidoTransportProtocol::kHybrid};

  auto model =
      base::MakeRefCounted<AuthenticatorRequestDialogModel>(main_rfh());
  AuthenticatorRequestDialogController controller(model.get(), main_rfh());
  int preselect_num_called = 0;
  controller.SetAccountPreselectedCallback(base::BindLambdaForTesting(
      [&](device::DiscoverableCredentialMetadata selected_cred) {
        EXPECT_EQ(selected_cred.cred_id, cred.cred_id);
        EXPECT_THAT(selected_cred.transports,
                    testing::UnorderedElementsAre(
                        device::FidoTransportProtocol::kInternal,
                        device::FidoTransportProtocol::kHybrid));
        ++preselect_num_called;
      }));
  controller.SetRequestCallback(
      base::BindLambdaForTesting([](const std::string& authenticator_id) {}));

  controller.saved_authenticators().emplace_back(
      /*device_id=*/"internal-authenticator", AuthenticatorTransport::kInternal,
      device::AuthenticatorType::kOther);

  TransportAvailabilityInfo transports_info;
  transports_info.request_type = device::FidoRequestType::kGetAssertion;
  transports_info.available_transports = kAllTransports;
  transports_info.has_empty_allow_list = true;
  transports_info.user_verification_requirement =
      device::UserVerificationRequirement::kPreferred;
  transports_info.has_platform_authenticator_credential = device::
      FidoRequestHandlerBase::RecognizedCredential::kHasRecognizedCredential;
  transports_info.recognized_credentials = {cred};
  UpdateModelBeforeStartFlow(model.get(), transports_info,
                             /*is_off_the_record=*/false);
  controller.StartFlow(std::move(transports_info), {});

  controller.OnAccountPreselected(cred.cred_id);
  ASSERT_TRUE(base::test::RunUntil([&] { return preselect_num_called == 1; }));
}

#if BUILDFLAG(IS_WIN)
// Regression test for crbug.com/40280124.
TEST_F(AuthenticatorRequestDialogControllerTest, JumpToWindowsWithNewUI) {
  auto model =
      base::MakeRefCounted<AuthenticatorRequestDialogModel>(main_rfh());
  AuthenticatorRequestDialogController controller(model.get(), main_rfh());

  TransportAvailabilityInfo transports_info;
  transports_info.request_type = device::FidoRequestType::kGetAssertion;
  transports_info.available_transports = kAllTransports;
  transports_info.has_win_native_api_authenticator = true;
  transports_info.has_empty_allow_list = false;
  transports_info.has_platform_authenticator_credential = device::
      FidoRequestHandlerBase::RecognizedCredential::kHasRecognizedCredential;
  transports_info.recognized_credentials = {kWinCred1, kWinCred2};

  controller.saved_authenticators().emplace_back(
      /*device_id=*/"win", AuthenticatorTransport::kInternal,
      device::AuthenticatorType::kWinNative);

  RequestCallbackReceiver request_callback;
  controller.SetRequestCallback(request_callback.Callback());
  UpdateModelBeforeStartFlow(model.get(), transports_info,
                             /*is_off_the_record=*/false);
  controller.StartFlow(std::move(transports_info), {});
  EXPECT_EQ(request_callback.WaitForResult(), "win");
}
#endif  // BUILDFLAG(IS_WIN)

#if BUILDFLAG(IS_MAC)
TEST_F(AuthenticatorRequestDialogControllerTest, BluetoothPermissionPrompt) {
  // When BLE permission is denied on macOS, we should jump to the sheet that
  // explains that if the user tries to use a linked phone or tries to show the
  // QR code.
  for (const BleStatus ble_status :
       {BleStatus::kOn, BleStatus::kPermissionDenied}) {
      SCOPED_TRACE(::testing::Message()
                   << "ble_status=" << static_cast<int>(ble_status));

      auto model =
          base::MakeRefCounted<AuthenticatorRequestDialogModel>(main_rfh());
      AuthenticatorRequestDialogController controller(model.get(), main_rfh());
      controller.set_cable_transport_info("fido:/1234");
      TransportAvailabilityInfo transports_info;
      transports_info.ble_status = ble_status;
      transports_info.request_type = device::FidoRequestType::kGetAssertion;
      transports_info.available_transports = {
          AuthenticatorTransport::kHybrid,
          AuthenticatorTransport::kUsbHumanInterfaceDevice};
      UpdateModelBeforeStartFlow(model.get(), transports_info,
                                 /*is_off_the_record=*/false);
      controller.StartFlow(std::move(transports_info), {});

      std::ranges::find_if(model->mechanisms, [](const auto& m) -> bool {
        return std::holds_alternative<
            AuthenticatorRequestDialogModel::Mechanism::Hybrid>(m.type);
      })->callback.Run();

      if (ble_status == BleStatus::kPermissionDenied) {
        EXPECT_EQ(model->step(), Step::kBlePermissionMac);
      } else {
        EXPECT_EQ(model->step(), Step::kCableV2QRCode);
      }
  }
}
#endif

TEST_F(AuthenticatorRequestDialogControllerTest, AdvanceThroughCableV2States) {
  auto model =
      base::MakeRefCounted<AuthenticatorRequestDialogModel>(main_rfh());
  AuthenticatorRequestDialogController controller(model.get(), main_rfh());
  controller.set_cable_transport_info("fido:/1234");
  TransportAvailabilityInfo transports_info;
  transports_info.ble_status = BleStatus::kOn;
  transports_info.request_type = device::FidoRequestType::kGetAssertion;
  transports_info.available_transports = {AuthenticatorTransport::kHybrid};
  UpdateModelBeforeStartFlow(model.get(), transports_info,
                             /*is_off_the_record=*/false);
  controller.StartFlow(std::move(transports_info), {});

  controller.OnCableEvent(device::cablev2::Event::kPhoneConnected);
  EXPECT_EQ(model->step(), Step::kCableV2Connecting);
  controller.OnCableEvent(device::cablev2::Event::kBLEAdvertReceived);
  EXPECT_EQ(model->step(), Step::kCableV2Connecting);
  controller.OnCableEvent(device::cablev2::Event::kReady);
  // kCableV2Connecting won't flash by too quickly, so it'll still be showing.
  EXPECT_EQ(model->step(), Step::kCableV2Connecting);

  task_environment()->FastForwardBy(base::Seconds(2));
  EXPECT_EQ(model->step(), Step::kCableV2Connected);
}

TEST_F(AuthenticatorRequestDialogControllerTest,
       AdvanceThroughCableV2StatesStopTimer) {
  auto model =
      base::MakeRefCounted<AuthenticatorRequestDialogModel>(main_rfh());
  AuthenticatorRequestDialogController controller(model.get(), main_rfh());
  controller.set_cable_transport_info("fido:/1234");
  TransportAvailabilityInfo transports_info;
  transports_info.ble_status = BleStatus::kOn;
  transports_info.request_type = device::FidoRequestType::kGetAssertion;
  transports_info.available_transports = {AuthenticatorTransport::kHybrid};
  UpdateModelBeforeStartFlow(model.get(), transports_info,
                             /*is_off_the_record=*/false);
  controller.StartFlow(std::move(transports_info), {});

  controller.OnCableEvent(device::cablev2::Event::kPhoneConnected);
  EXPECT_EQ(model->step(), Step::kCableV2Connecting);
  controller.OnCableEvent(device::cablev2::Event::kBLEAdvertReceived);
  EXPECT_EQ(model->step(), Step::kCableV2Connecting);
  controller.OnCableEvent(device::cablev2::Event::kReady);
  // kCableV2Connecting won't flash by too quickly, so it'll still be showing.
  EXPECT_EQ(model->step(), Step::kCableV2Connecting);

  // Moving to a different step should stop the timer so that kCableV2Connected
  // never shows.
  controller.SetCurrentStepForTesting(Step::kClosed);

  task_environment()->FastForwardBy(base::Seconds(10));
  EXPECT_EQ(model->step(), Step::kClosed);
}

TEST_F(AuthenticatorRequestDialogControllerTest, Crbug1503187) {
  // This test reproduces the crash from crbug.com/40943613.
  TransportAvailabilityInfo transports_info;
  transports_info.request_type = device::FidoRequestType::kGetAssertion;
  transports_info.available_transports = {
      AuthenticatorTransport::kInternal,
      AuthenticatorTransport::kUsbHumanInterfaceDevice};
  transports_info.recognized_credentials = {kCred1FromChromeOS};
  transports_info.has_empty_allow_list = false;
  transports_info.has_platform_authenticator_credential = device::
      FidoRequestHandlerBase::RecognizedCredential::kHasRecognizedCredential;

  auto model =
      base::MakeRefCounted<AuthenticatorRequestDialogModel>(main_rfh());
  AuthenticatorRequestDialogController controller(model.get(), main_rfh());
  RepeatingValueCallbackReceiver<device::DiscoverableCredentialMetadata>
      account_preselected_callback;
  controller.SetAccountPreselectedCallback(
      account_preselected_callback.Callback());
  UpdateModelBeforeStartFlow(model.get(), transports_info,
                             /*is_off_the_record=*/false);
  controller.StartFlow(std::move(transports_info), {});
}

TEST_F(AuthenticatorRequestDialogControllerTest, DeduplicateAccounts) {
  using Mechanism = AuthenticatorRequestDialogModel::Mechanism;
  const struct {
    std::vector<device::DiscoverableCredentialMetadata> recognized_credentials;
    std::optional<Mechanism::Type> type_of_priority_mechanism;
  } kTests[] = {
      {{kCred1, kCred2}, std::nullopt},
      {{kCred1, kCred1FromICloudKeychain},
       Mechanism::Credential(CredentialInfoFrom(kCred1FromICloudKeychain))},
      {{kCred1FromICloudKeychain, kCred1},
       Mechanism::Credential(CredentialInfoFrom(kCred1FromICloudKeychain))},
  };

  for (const auto& test : kTests) {
    TransportAvailabilityInfo transports_info;
    transports_info.request_type = device::FidoRequestType::kGetAssertion;
    transports_info.available_transports = {AuthenticatorTransport::kInternal};
    transports_info.recognized_credentials = test.recognized_credentials;
    transports_info.has_empty_allow_list = true;

    auto model =
        base::MakeRefCounted<AuthenticatorRequestDialogModel>(main_rfh());
    AuthenticatorRequestDialogController controller(model.get(), main_rfh());
    controller.set_allow_icloud_keychain(true);
    RepeatingValueCallbackReceiver<device::DiscoverableCredentialMetadata>
        account_preselected_callback;
    controller.SetAccountPreselectedCallback(
        account_preselected_callback.Callback());
    UpdateModelBeforeStartFlow(model.get(), transports_info,
                               /*is_off_the_record=*/false);
    controller.StartFlow(std::move(transports_info), {});
    ASSERT_EQ(model->priority_mechanism_index.has_value(),
              test.type_of_priority_mechanism.has_value());
    if (!test.type_of_priority_mechanism.has_value()) {
      continue;
    }

    EXPECT_EQ(*test.type_of_priority_mechanism,
              model->mechanisms[*model->priority_mechanism_index].type);

    account_preselected_callback.WaitForResult();
  }
}

// Tests the text on the hybrid button label.
// Regression test for crbug.com/328698086.
TEST_F(AuthenticatorRequestDialogControllerTest, HybridButtonLabel) {
#if BUILDFLAG(IS_WIN)
  device::FakeWinWebAuthnApi fake_win_webauthn_api;
  device::WinWebAuthnApi::ScopedOverride win_webauthn_api_override(
      &fake_win_webauthn_api);
#endif
  constexpr int kPhoneOrSk =
      IDS_WEBAUTHN_PASSKEY_PHONE_TABLET_OR_SECURITY_KEY_LABEL;
  constexpr int kPhone = IDS_WEBAUTHN_PASSKEY_PHONE_OR_TABLET_LABEL;
  const auto usb = AuthenticatorTransport::kUsbHumanInterfaceDevice;
  const auto hybrid = AuthenticatorTransport::kHybrid;
  enum CanDoUSB : bool {
    kNoUSB = false,
    kUSB = true,
  };
  struct TestCase {
    CanDoUSB chrome_can_do_usb;
    BleStatus ble_status;
    std::optional<AuthenticatorTransport> transport_hint;
    int expected;
  } kTestCases[] = {
      {kUSB, BleStatus::kOn, std::nullopt, kPhoneOrSk},
      {kUSB, BleStatus::kOn, usb, kPhone},
      {kUSB, BleStatus::kOn, hybrid, kPhone},
      {kUSB, BleStatus::kOff, std::nullopt, kPhone},
      {kNoUSB, BleStatus::kOn, std::nullopt, kPhone},
  };
  for (const auto& test_case : kTestCases) {
    SCOPED_TRACE(test_case.transport_hint
                     ? static_cast<int>(*test_case.transport_hint)
                     : -1);
    SCOPED_TRACE(static_cast<int>(test_case.ble_status));
    SCOPED_TRACE(static_cast<int>(test_case.chrome_can_do_usb));
    auto model =
        base::MakeRefCounted<AuthenticatorRequestDialogModel>(main_rfh());
    AuthenticatorRequestDialogController controller(model.get(), main_rfh());
    TransportAvailabilityInfo transports_info;
    transports_info.request_type = device::FidoRequestType::kMakeCredential;
    transports_info.attestation_conveyance_preference =
        device::AttestationConveyancePreference::kNone;
    controller.set_cable_transport_info("fido:/1234");
    if (test_case.chrome_can_do_usb) {
      transports_info.available_transports = {
          device::FidoTransportProtocol::kHybrid,
          device::FidoTransportProtocol::kUsbHumanInterfaceDevice};
    } else {
      transports_info.available_transports = {
          device::FidoTransportProtocol::kHybrid};
    }
    transports_info.ble_status = test_case.ble_status;
    content::AuthenticatorRequestClientDelegate::Hints hints;
    hints.transport = test_case.transport_hint;
    controller.SetHints(std::move(hints));
    UpdateModelBeforeStartFlow(model.get(), transports_info,
                               /*is_off_the_record=*/false);
    controller.StartFlow(std::move(transports_info), {});
    auto hybrid_button_it =
        std::ranges::find_if(model->mechanisms, [](const auto& m) {
          return std::holds_alternative<
              AuthenticatorRequestDialogModel::Mechanism::Hybrid>(m.type);
        });
    ASSERT_NE(hybrid_button_it, model->mechanisms.end());
    EXPECT_EQ(hybrid_button_it->name,
              l10n_util::GetStringUTF16(test_case.expected));
  }
}

#if BUILDFLAG(IS_MAC)

TEST_F(AuthenticatorRequestDialogControllerTest, Dispatch) {
  for (const bool should_create_in_icloud_keychain : {false, true}) {
    for (const bool platform_attachment : {false, true}) {
      if (!platform_attachment && should_create_in_icloud_keychain) {
        // Without `platform_attachment`, `should_create_in_icloud_keychain` is
        // moot.
        continue;
      }

      SCOPED_TRACE(testing::Message() << "should_create_in_icloud_keychain: "
                                      << should_create_in_icloud_keychain);
      SCOPED_TRACE(testing::Message()
                   << "platform_attachment: " << platform_attachment);

      TransportAvailabilityInfo transports_info;
      transports_info.attestation_conveyance_preference =
          device::AttestationConveyancePreference::kNone;
      transports_info.has_icloud_keychain = true;
      transports_info.available_transports = {
          AuthenticatorTransport::kInternal,
          AuthenticatorTransport::kUsbHumanInterfaceDevice};
      transports_info.request_type = device::FidoRequestType::kMakeCredential;
      transports_info.resident_key_requirement =
          device::ResidentKeyRequirement::kRequired;
      transports_info.make_credential_attachment =
          platform_attachment ? device::AuthenticatorAttachment::kPlatform
                              : device::AuthenticatorAttachment::kAny;

      auto model =
          base::MakeRefCounted<AuthenticatorRequestDialogModel>(main_rfh());
      AuthenticatorRequestDialogController controller(model.get(), main_rfh());
      controller.set_allow_icloud_keychain(true);
      controller.set_should_create_in_icloud_keychain(
          should_create_in_icloud_keychain);

      RequestCallbackReceiver request_callback;
      controller.SetRequestCallback(request_callback.Callback());

      const std::string kProfileAuthenticatorId = "platauth";
      controller.saved_authenticators().emplace_back(
          kProfileAuthenticatorId, AuthenticatorTransport::kInternal,
          device::AuthenticatorType::kTouchID);
      const std::string kICloudKeychainId = "ickc";
      controller.saved_authenticators().emplace_back(
          kICloudKeychainId, AuthenticatorTransport::kInternal,
          device::AuthenticatorType::kICloudKeychain);
      UpdateModelBeforeStartFlow(model.get(), transports_info,
                                 /*is_off_the_record=*/false);
      controller.StartFlow(std::move(transports_info), {});
      if (should_create_in_icloud_keychain) {
        EXPECT_EQ(request_callback.WaitForResult(), kICloudKeychainId);
      } else {
        EXPECT_EQ(model->step(), Step::kChromeProfileCreatePasskey);
        controller.HideDialogAndDispatchToPlatformAuthenticator();
        EXPECT_EQ(request_callback.WaitForResult(), kProfileAuthenticatorId);
      }

      controller.OnUserConsentDenied();

      EXPECT_EQ(model->step(), should_create_in_icloud_keychain
                                   ? Step::kMechanismSelection
                                   : Step::kErrorInternalUnrecognized);

      controller.saved_authenticators().emplace_back(
          kProfileAuthenticatorId, AuthenticatorTransport::kInternal,
          device::AuthenticatorType::kTouchID);
      controller.saved_authenticators().emplace_back(
          kICloudKeychainId, AuthenticatorTransport::kInternal,
          device::AuthenticatorType::kICloudKeychain);

      // Dispatch and cancel again to confirm that canceling the non-automatic
      // dispatch cancels the whole request.
      controller.HideDialogAndDispatchToPlatformAuthenticator(
          device::AuthenticatorType::kICloudKeychain);
      controller.OnUserConsentDenied();

      EXPECT_EQ(model->step(), Step::kPlatformAuthenticator);
    }
  }
}

TEST_F(AuthenticatorRequestDialogControllerTest,
       OnlyShowConfirmationSheetForProfileAuthenticator) {
  for (const auto credential_source :
       {device::AuthenticatorType::kTouchID,
        device::AuthenticatorType::kICloudKeychain}) {
    SCOPED_TRACE(static_cast<int>(credential_source));

    TransportAvailabilityInfo transports_info;
    transports_info.has_icloud_keychain = true;
    transports_info.available_transports = {AuthenticatorTransport::kInternal};
    transports_info.request_type = device::FidoRequestType::kGetAssertion;
    transports_info.has_empty_allow_list = false;

    if (credential_source == device::AuthenticatorType::kTouchID) {
      transports_info.recognized_credentials = {kCred2};
      transports_info.has_platform_authenticator_credential =
          device::FidoRequestHandlerBase::RecognizedCredential::
              kHasRecognizedCredential;
    } else {
      transports_info.recognized_credentials = {kCred1FromICloudKeychain};
      transports_info.has_icloud_keychain_credential =
          device::FidoRequestHandlerBase::RecognizedCredential::
              kHasRecognizedCredential;
    }

    auto model =
        base::MakeRefCounted<AuthenticatorRequestDialogModel>(main_rfh());
    AuthenticatorRequestDialogController controller(model.get(), main_rfh());
    controller.set_allow_icloud_keychain(true);
    RepeatingValueCallbackReceiver<device::DiscoverableCredentialMetadata>
        account_preselected_callback;
    controller.SetAccountPreselectedCallback(
        account_preselected_callback.Callback());
    UpdateModelBeforeStartFlow(model.get(), transports_info,
                               /*is_off_the_record=*/false);
    controller.StartFlow(std::move(transports_info), {});

    EXPECT_EQ(model->step(), Step::kPlatformAuthenticator);
    device::DiscoverableCredentialMetadata descriptor =
        account_preselected_callback.WaitForResult();
    if (credential_source == device::AuthenticatorType::kTouchID) {
      EXPECT_EQ(descriptor.cred_id, kCred2.cred_id);
    } else {
      EXPECT_EQ(descriptor.cred_id, kCred1FromICloudKeychain.cred_id);
    }
  }
}

#endif

// Tests that iCloud Keychain passkeys are listed in conditional UI with the
// correct label.
// Regression test for crbug.com/409806800.
TEST_F(AuthenticatorRequestDialogControllerTest,
       ListICloudKeychainPasskeysInConditionalUI) {
  NavigateAndCommit(GURL("rp.com"));

  ChromeWebAuthnCredentialsDelegate* delegate =
      ChromeWebAuthnCredentialsDelegateFactory::GetFactory(web_contents())
          ->GetDelegateForFrame(web_contents()->GetPrimaryMainFrame());
  ASSERT_TRUE(delegate);

  TransportAvailabilityInfo transports_info;
  transports_info.request_type = device::FidoRequestType::kGetAssertion;
  transports_info.recognized_credentials = {kCred1From3p};
  auto model =
      base::MakeRefCounted<AuthenticatorRequestDialogModel>(main_rfh());
  AuthenticatorRequestDialogController controller(model.get(), main_rfh());
  controller.SetUIPresentation(UIPresentation::kAutofill);
  UpdateModelBeforeStartFlow(model.get(), transports_info,
                             /*is_off_the_record=*/false);
  controller.StartFlow(transports_info, {});

  const std::vector<password_manager::PasskeyCredential>* autofill_passkeys =
      delegate->GetPasskeys().value();
  ASSERT_TRUE(autofill_passkeys);
  EXPECT_EQ(autofill_passkeys->size(), 1u);
  EXPECT_THAT(
      base::UTF16ToUTF8(autofill_passkeys->at(0).GetAuthenticatorLabel()),
      testing::HasSubstr("Bitwarden"));
}

TEST_F(AuthenticatorRequestDialogControllerTest, MechanismsFromUserAccounts) {
  // Set up a model with two local passkeys and a GPM passkey.
  auto model =
      base::MakeRefCounted<AuthenticatorRequestDialogModel>(main_rfh());
  AuthenticatorRequestDialogController controller(model.get(), main_rfh());
  TransportAvailabilityInfo transports_info;
  transports_info.request_type = device::FidoRequestType::kGetAssertion;
  transports_info.available_transports = {AuthenticatorTransport::kInternal};
  transports_info.recognized_credentials = {kCred1, kCred2};
  transports_info.ble_status = BleStatus::kOn;

  controller.set_cable_transport_info("fido:/1234");
  RepeatingValueCallbackReceiver<device::DiscoverableCredentialMetadata>
      account_preselected_callback;
  controller.SetAccountPreselectedCallback(
      account_preselected_callback.Callback());

  RequestCallbackReceiver request_callback;
  controller.SetRequestCallback(request_callback.Callback());
  const std::string kLocalAuthenticatorId = "local-authenticator";
  controller.saved_authenticators().emplace_back(
      kLocalAuthenticatorId, AuthenticatorTransport::kInternal,
      device::AuthenticatorType::kOther);
  UpdateModelBeforeStartFlow(model.get(), transports_info,
                             /*is_off_the_record=*/false);
  controller.StartFlow(std::move(transports_info), {});

  // Entries will be sorted by username. So the first entry should correspond to
  // the first local passkey.
  const AuthenticatorRequestDialogModel::Mechanism& mech1 =
      model->mechanisms[0];
  EXPECT_EQ(mech1.name, base::UTF8ToUTF16(*kUser1.name));
  EXPECT_EQ(mech1.description,
            l10n_util::GetStringUTF16(IDS_WEBAUTHN_SOURCE_USB_SECURITY_KEY));
  EXPECT_EQ(mech1.icon, features::IsRoundedIconsEnabled()
                            ? vector_icons::kPasskeyIcon
                            : vector_icons::kPasskeyOldIcon);
  mech1.callback.Run();
  device::DiscoverableCredentialMetadata result =
      account_preselected_callback.WaitForResult();
  EXPECT_EQ(result.cred_id, kCred1.cred_id);
  EXPECT_EQ(result.source, device::AuthenticatorType::kOther);
  EXPECT_EQ(request_callback.WaitForResult(), kLocalAuthenticatorId);

  // Reset the model as if the user had cancelled out of the operation.
  controller.StartOver();
  controller.saved_authenticators().emplace_back(
      kLocalAuthenticatorId, AuthenticatorTransport::kInternal,
      device::AuthenticatorType::kOther);

  // The second entry will be `kCred2`.
  const AuthenticatorRequestDialogModel::Mechanism& mech2 =
      model->mechanisms[1];
  EXPECT_EQ(mech2.name, base::UTF8ToUTF16(*kUser2.name));
  EXPECT_EQ(mech2.description,
            l10n_util::GetStringUTF16(IDS_WEBAUTHN_SOURCE_USB_SECURITY_KEY));
  EXPECT_EQ(mech2.icon, features::IsRoundedIconsEnabled()
                            ? vector_icons::kPasskeyIcon
                            : vector_icons::kPasskeyOldIcon);
  mech2.callback.Run();
  result = account_preselected_callback.WaitForResult();
  EXPECT_EQ(result.cred_id, kCred2.cred_id);
  EXPECT_EQ(result.source, device::AuthenticatorType::kOther);
  EXPECT_EQ(request_callback.WaitForResult(), kLocalAuthenticatorId);
}

#if BUILDFLAG(IS_WIN)

using HasCreds = device::FidoRequestHandlerBase::RecognizedCredential;
constexpr int kNoWinButton = -1;
constexpr int kNoChromeUI = -2;
constexpr int kHelloOrSk = IDS_WEBAUTHN_TRANSPORT_WINDOWS_HELLO_OR_SECURITY_KEY;
constexpr int kHello = IDS_WEBAUTHN_TRANSPORT_WINDOWS_HELLO;
constexpr int kSk = IDS_WEBAUTHN_TRANSPORT_EXTERNAL_SECURITY_KEY;
constexpr int kPhoneOrSk =
    IDS_WEBAUTHN_PASSKEY_PHONE_TABLET_OR_SECURITY_KEY_LABEL;
constexpr int kPhone = IDS_WEBAUTHN_PASSKEY_PHONE_OR_TABLET_LABEL;
#define L __LINE__
struct {
  int line_num;
  bool has_sk;
  bool has_hybrid;
  bool has_internal;
  bool supports_hybrid;
  bool has_uvpaa;
  HasCreds has_creds;
  int expected_button;
} kWinHelloButtonGetAssertionTestCases[] = {
    // Windows v7+ with all transports.
    {L, true, true, true, true, true, HasCreds::kHasRecognizedCredential,
     kPhoneOrSk},

    // Windows v7+ with only security keys.
    {L, true, false, false, true, true, HasCreds::kNoRecognizedCredential, kSk},

    // Windows v7+ with only phones.
    {L, false, true, false, true, true, HasCreds::kNoRecognizedCredential,
     kPhone},

    // Windows v7+ with only internal creds.
    {L, false, false, true, true, true, HasCreds::kHasRecognizedCredential,
     kNoChromeUI},

    // Windows v7+ with empty allow-list.
    {L, false, false, false, true, true, HasCreds::kHasRecognizedCredential,
     kPhoneOrSk},

    // Windows v5+ with all transports.
    {L, true, true, true, false, true, HasCreds::kHasRecognizedCredential, kSk},

    // Windows v5+ with only security keys
    {L, true, false, false, false, true, HasCreds::kNoRecognizedCredential,
     kSk},

    // Windows v5+ with only phones.
    {L, false, true, false, false, true, HasCreds::kNoRecognizedCredential,
     kNoWinButton},

    // Windows v5+ with only internal creds.
    {L, false, false, true, false, true, HasCreds::kHasRecognizedCredential,
     kNoChromeUI},

    // Windows v5+ with empty allow-list.
    {L, false, false, false, false, true, HasCreds::kHasRecognizedCredential,
     kSk},

    // Windows <v4 with all transports.
    {L, true, true, true, false, true, HasCreds::kUnknown, kHelloOrSk},

    // Windows <v4 with only security keys.
    {L, true, false, false, false, true, HasCreds::kUnknown, kSk},

    // Windows <v4 with only phones.
    {L, false, true, false, false, true, HasCreds::kUnknown, kNoWinButton},

    // Windows <v4 with only internal creds.
    {L, false, false, true, false, true, HasCreds::kUnknown, kHello},

    // Windows <v4 with empty allow-list.
    {L, false, false, false, false, true, HasCreds::kUnknown, kHelloOrSk},

    // Windows <v4 with empty allow-list and no Win Hello.
    {L, false, false, false, false, false, HasCreds::kUnknown, kSk},
};
#undef L

TEST_F(AuthenticatorRequestDialogControllerTest,
       WindowsHelloButtonLabel_GetAssertion) {
  device::FakeWinWebAuthnApi fake_win_webauthn_api;
  device::WinWebAuthnApi::ScopedOverride win_webauthn_api_override(
      &fake_win_webauthn_api);
  for (const auto& test_case : kWinHelloButtonGetAssertionTestCases) {
    auto model =
        base::MakeRefCounted<AuthenticatorRequestDialogModel>(main_rfh());
    AuthenticatorRequestDialogController controller(model.get(), main_rfh());
    controller.SetAccountPreselectedCallback(base::DoNothing());

    TransportAvailabilityInfo transports_info;
    transports_info.has_win_native_api_authenticator = true;
    transports_info.request_type = device::FidoRequestType::kGetAssertion;
    transports_info.transport_list_did_include_security_key = test_case.has_sk;
    transports_info.transport_list_did_include_hybrid = test_case.has_hybrid;
    transports_info.transport_list_did_include_internal =
        test_case.has_internal;
    transports_info.has_platform_authenticator_credential = test_case.has_creds;
    transports_info.win_is_uvpaa = test_case.has_uvpaa;
    if (test_case.has_creds == HasCreds::kHasRecognizedCredential) {
      transports_info.recognized_credentials = {kCred1};
    }
    if (!test_case.has_sk && !test_case.has_hybrid && !test_case.has_internal) {
      transports_info.has_empty_allow_list = true;
    }
    fake_win_webauthn_api.set_version(test_case.supports_hybrid ? 7 : 4);
    SCOPED_TRACE(testing::Message() << "Line number: " << test_case.line_num);
    SCOPED_TRACE(testing::Message() << "SK: " << test_case.has_sk);
    SCOPED_TRACE(testing::Message() << "Hybrid: " << test_case.has_hybrid);
    SCOPED_TRACE(testing::Message() << "Internal: " << test_case.has_internal);
    SCOPED_TRACE(testing::Message() << "Win isUVPAA: " << test_case.has_uvpaa);
    SCOPED_TRACE(testing::Message()
                 << "Has creds: " << static_cast<int>(test_case.has_creds));
    SCOPED_TRACE(testing::Message()
                 << "Handles hybrid: " << test_case.supports_hybrid);
    UpdateModelBeforeStartFlow(model.get(), transports_info,
                               /*is_off_the_record=*/false);
    controller.StartFlow(std::move(transports_info), {});
    auto win_button_it =
        std::ranges::find_if(model->mechanisms, [](const auto& m) {
          return std::holds_alternative<
              AuthenticatorRequestDialogModel::Mechanism::WindowsAPI>(m.type);
        });
    if (test_case.expected_button == kNoWinButton) {
      EXPECT_EQ(win_button_it, model->mechanisms.end());
    } else if (test_case.expected_button == kNoChromeUI) {
      // In these cases, Chrome should have invoked the Windows UI immediately.
      EXPECT_EQ(model->step(), Step::kPlatformAuthenticator);
    } else {
      ASSERT_NE(win_button_it, model->mechanisms.end());
      EXPECT_EQ(win_button_it->name,
                l10n_util::GetStringUTF16(test_case.expected_button));
      switch (test_case.expected_button) {
        case kHelloOrSk:
        case kHello:
          EXPECT_EQ(win_button_it->icon, features::IsRoundedIconsEnabled()
                                             ? kLaptopWindowsIcon
                                             : kLaptopOldIcon);
          break;
        case kSk:
          EXPECT_EQ(win_button_it->icon, features::IsRoundedIconsEnabled()
                                             ? kSecurityKeyIcon
                                             : kUsbSecurityKeyOldIcon);
          break;
        case kPhoneOrSk:
        case kPhone:
          EXPECT_EQ(win_button_it->icon, features::IsRoundedIconsEnabled()
                                             ? kMobileIcon
                                             : kSmartphoneOldIcon);
          break;
        default:
          NOTREACHED();
      }
    }
  }
}

struct {
  device::AuthenticatorAttachment attachment;
  bool has_uvpaa;
  int expected_button;
} kWinHelloButtonMakeCredentialTestCases[] = {
    // For make credential, we will only show the authenticator picker when
    // Windows does not do hybrid. Therefore, there is no option for "Hello,
    // Security Key, or Phone".
    {device::AuthenticatorAttachment::kAny, true, kHelloOrSk},
    {device::AuthenticatorAttachment::kCrossPlatform, true, kSk},
    {device::AuthenticatorAttachment::kPlatform, true, kHello},
    {device::AuthenticatorAttachment::kAny, false, kSk},
};

TEST_F(AuthenticatorRequestDialogControllerTest,
       WindowsHelloButtonLabel_MakeCredential) {
  device::FakeWinWebAuthnApi fake_win_webauthn_api;
  device::WinWebAuthnApi::ScopedOverride win_webauthn_api_override(
      &fake_win_webauthn_api);
  for (const auto& test_case : kWinHelloButtonMakeCredentialTestCases) {
    auto model =
        base::MakeRefCounted<AuthenticatorRequestDialogModel>(main_rfh());
    AuthenticatorRequestDialogController controller(model.get(), main_rfh());
    TransportAvailabilityInfo transports_info;
    transports_info.has_win_native_api_authenticator = true;
    transports_info.request_type = device::FidoRequestType::kMakeCredential;
    transports_info.attestation_conveyance_preference =
        device::AttestationConveyancePreference::kNone;
    transports_info.make_credential_attachment = test_case.attachment;
    fake_win_webauthn_api.set_version(4);
    transports_info.win_is_uvpaa = test_case.has_uvpaa;
    SCOPED_TRACE(testing::Message()
                 << "Attachment: " << static_cast<int>(test_case.attachment));
    UpdateModelBeforeStartFlow(model.get(), transports_info,
                               /*is_off_the_record=*/false);
    controller.StartFlow(std::move(transports_info), {});
    auto win_button_it =
        std::ranges::find_if(model->mechanisms, [](const auto& m) {
          return std::holds_alternative<
              AuthenticatorRequestDialogModel::Mechanism::WindowsAPI>(m.type);
        });
    ASSERT_NE(win_button_it, model->mechanisms.end());
    EXPECT_EQ(win_button_it->name,
              l10n_util::GetStringUTF16(test_case.expected_button));
    switch (test_case.expected_button) {
      case kHelloOrSk:
      case kHello:
        EXPECT_EQ(win_button_it->icon, features::IsRoundedIconsEnabled()
                                           ? kLaptopWindowsIcon
                                           : kLaptopOldIcon);
        break;
      case kSk:
        EXPECT_EQ(win_button_it->icon, features::IsRoundedIconsEnabled()
                                           ? kSecurityKeyIcon
                                           : kUsbSecurityKeyOldIcon);
        break;
      default:
        NOTREACHED();
    }
  }
}

#endif  // BUILDFLAG(IS_WIN)

TEST_F(AuthenticatorRequestDialogControllerTest,
       NoICloudKeychainMechanism_ModalImmediate_UnknownCredStatus) {
  auto model =
      base::MakeRefCounted<AuthenticatorRequestDialogModel>(main_rfh());
  AuthenticatorRequestDialogController controller(model.get(), main_rfh());

  TransportAvailabilityInfo transports_info;
  transports_info.request_type = device::FidoRequestType::kGetAssertion;
  transports_info.has_icloud_keychain = true;
  transports_info.has_icloud_keychain_credential =
      device::FidoRequestHandlerBase::RecognizedCredential::kUnknown;
  transports_info.available_transports = {AuthenticatorTransport::kInternal};
  transports_info.transport_list_did_include_internal = true;

  // Ensure allow_icloud_keychain_ is true in the controller.
  // This would typically be set based on transports_info.has_icloud_keychain
  // during the full flow, but we set it explicitly here for clarity and
  // directness.
  controller.set_allow_icloud_keychain(true);

  // Set the UI presentation to kModalImmediate.
  controller.SetUIPresentation(UIPresentation::kModalImmediate);

  UpdateModelBeforeStartFlow(model.get(), transports_info,
                             /*is_off_the_record=*/false);
  controller.StartFlow(std::move(transports_info), {});

  // Verify that no iCloud Keychain mechanism was added.
  bool icloud_mechanism_found = false;
  for (const auto& mech : model->mechanisms) {
    if (std::holds_alternative<
            AuthenticatorRequestDialogModel::Mechanism::ICloudKeychain>(
            mech.type)) {
      icloud_mechanism_found = true;
      break;
    }
  }
  EXPECT_FALSE(icloud_mechanism_found);
}

#if BUILDFLAG(IS_MAC)

// Test that when iCloud Keychain is dispatched to automatically because of
// client hints, cancelling brings the user back to the mechanism selection
// screen.
// Regression test for crbug.com/413572038.
TEST_F(AuthenticatorRequestDialogControllerTest,
       CancelICloudKeychainWithHints) {
  // Set up a request with hints and iCloud Keychain.
  auto model =
      base::MakeRefCounted<AuthenticatorRequestDialogModel>(main_rfh());
  AuthenticatorRequestDialogController controller(model.get(), main_rfh());
  RequestCallbackReceiver request_callback;
  controller.SetRequestCallback(request_callback.Callback());
  const std::string kICloudKeychainId = "ickc";
  controller.saved_authenticators().emplace_back(
      kICloudKeychainId, AuthenticatorTransport::kInternal,
      device::AuthenticatorType::kICloudKeychain);
  controller.set_allow_icloud_keychain(true);
  controller.set_should_create_in_icloud_keychain(true);
  content::AuthenticatorRequestClientDelegate::Hints hints;
  hints.transport = device::FidoTransportProtocol::kInternal;
  controller.SetHints(std::move(hints));

  TransportAvailabilityInfo transports_info;
  transports_info.available_transports = {
      AuthenticatorTransport::kInternal,
      AuthenticatorTransport::kUsbHumanInterfaceDevice};
  transports_info.request_type = device::FidoRequestType::kMakeCredential;
  transports_info.attestation_conveyance_preference =
      device::AttestationConveyancePreference::kNone;
  transports_info.has_icloud_keychain = true;
  transports_info.available_transports = {AuthenticatorTransport::kInternal};
  transports_info.transport_list_did_include_internal = true;
  controller.StartFlow(std::move(transports_info), {});

  // Verify that the request automatically dispatched to iCloud Keychain.
  EXPECT_EQ(request_callback.WaitForResult(), kICloudKeychainId);

  // Simulate the user cancelling.
  controller.OnUserConsentDenied();
  EXPECT_EQ(model->step(), Step::kMechanismSelection);

  // Select iCloud Keychain again and cancel. This time, the request should be
  // cancelled.
  controller.saved_authenticators().emplace_back(
      kICloudKeychainId, AuthenticatorTransport::kInternal,
      device::AuthenticatorType::kICloudKeychain);
  controller.HideDialogAndDispatchToPlatformAuthenticator(
      device::AuthenticatorType::kICloudKeychain);
  controller.OnUserConsentDenied();
  EXPECT_EQ(model->step(), Step::kPlatformAuthenticator);
}

#endif  // BUILDFLAG(IS_MAC)

TEST_F(AuthenticatorRequestDialogControllerTest, PopulatePasswordsWithOrigin) {
  auto model =
      base::MakeRefCounted<AuthenticatorRequestDialogModel>(main_rfh());
  AuthenticatorRequestDialogController controller(model.get(), main_rfh());

  // Setup TransportAvailabilityInfo
  device::FidoRequestHandlerBase::TransportAvailabilityInfo tai;
  tai.request_type = device::FidoRequestType::kGetAssertion;
  tai.available_transports = {};

  // Create PasswordCredentials
  std::vector<std::unique_ptr<password_manager::PasswordForm>> passwords;

  // 1. Exact Match
  auto exact_match = std::make_unique<password_manager::PasswordForm>();
  exact_match->username_value = u"user_exact";
  exact_match->url = GURL("https://example.com/login");
  exact_match->signon_realm = "https://example.com/";
  exact_match->match_type = password_manager::PasswordForm::MatchType::kExact;
  passwords.push_back(std::move(exact_match));

  // 2. PSL Match
  auto psl_match = std::make_unique<password_manager::PasswordForm>();
  psl_match->username_value = u"user_psl";
  psl_match->url = GURL("https://sub.example.com/login");
  psl_match->signon_realm = "https://sub.example.com/";
  psl_match->match_type = password_manager::PasswordForm::MatchType::kPSL;
  passwords.push_back(std::move(psl_match));

  // Start Flow
  model->relying_party_id = "example.com";
  UpdateModelBeforeStartFlow(model.get(), tai, /*is_off_the_record=*/false);
  controller.StartFlow(std::move(tai), std::move(passwords));

  // Verify Mechanisms
  ASSERT_EQ(model->mechanisms.size(), 2u);

  // Helper to find mechanism by username
  auto find_mechanism = [&](const std::u16string& username)
      -> const AuthenticatorRequestDialogModel::Mechanism* {
    for (const auto& mech : model->mechanisms) {
      if (mech.name == username) {
        return &mech;
      }
    }
    return nullptr;
  };

  auto get_origin = [](const AuthenticatorRequestDialogModel::Mechanism* mech)
      -> std::optional<std::u16string> {
    if (const auto* password =
            std::get_if<AuthenticatorRequestDialogModel::Mechanism::Password>(
                &mech->type)) {
      return password->value().origin;
    }
    return std::nullopt;
  };

  // 1. Exact Match: Origin should be empty
  const auto* mech_exact = find_mechanism(u"user_exact");
  ASSERT_TRUE(mech_exact);
  EXPECT_FALSE(get_origin(mech_exact).has_value());

  // 2. PSL Match: Origin should be "https://sub.example.com/"
  const auto* mech_psl = find_mechanism(u"user_psl");
  ASSERT_TRUE(mech_psl);
  auto origin_psl = get_origin(mech_psl);
  EXPECT_TRUE(origin_psl.has_value());
  EXPECT_EQ(origin_psl.value(), u"sub.example.com");
}
