// Copyright 2024 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/ui/signin/signin_view_controller.h"

#include <string_view>

#include "base/scoped_observation.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/mock_callback.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "chrome/browser/enterprise/signin/managed_profile_required_navigation_throttle.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/signin/dice_tab_helper.h"
#include "chrome/browser/signin/logout_tab_helper.h"
#include "chrome/browser/signin/signin_browser_test_base.h"
#include "chrome/browser/signin/signin_ui_util.h"
#include "chrome/browser/sync/sync_service_factory.h"
#include "chrome/browser/ui/browser_element_identifiers.h"
#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/signin/chrome_signout_confirmation_prompt.h"
#include "chrome/browser/ui/signin/cross_device_signin_qr_bubble.h"
#include "chrome/browser/ui/signin/signin_qrcode_infobar.h"
#include "chrome/browser/ui/signin/signin_qrcode_model.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/frame/toolbar_button_provider.h"
#include "chrome/browser/ui/views/toolbar/avatar_toolbar_button_interface.h"
#include "chrome/browser/ui/webui/signin/signin_utils.h"
#include "chrome/browser/ui/webui/signin/signout_confirmation/signout_confirmation_ui.h"
#include "chrome/browser/ui/webui/signin/signout_confirmation/test_signout_confirmation_handler_waiter.h"
#include "chrome/common/webui_url_constants.h"
#include "chrome/test/base/ui_test_utils.h"
#include "chrome_signout_confirmation_prompt.h"
#include "components/infobars/content/content_infobar_manager.h"
#include "components/keyed_service/core/keyed_service.h"
#include "components/prefs/pref_service.h"
#include "components/signin/public/base/consent_level.h"
#include "components/signin/public/base/signin_buildflags.h"
#include "components/signin/public/base/signin_metrics.h"
#include "components/signin/public/base/signin_pref_names.h"
#include "components/signin/public/base/signin_switches.h"
#include "components/signin/public/identity_manager/account_capabilities_test_mutator.h"
#include "components/signin/public/identity_manager/account_info.h"
#include "components/signin/public/identity_manager/identity_manager.h"
#include "components/signin/public/identity_manager/identity_test_environment.h"
#include "components/signin/public/identity_manager/identity_test_utils.h"
#include "components/sync/base/data_type.h"
#include "components/sync/base/data_type_histogram.h"
#include "components/sync/test/test_sync_service.h"
#include "content/public/browser/context_menu_params.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/public/test/url_loader_interceptor.h"
#include "device/bluetooth/bluetooth_adapter_factory.h"
#include "device/bluetooth/test/mock_bluetooth_adapter.h"
#include "extensions/browser/extension_registry.h"
#include "google_apis/gaia/google_service_auth_error.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/views/controls/webview/webview.h"
#include "ui/views/interaction/element_tracker_views.h"
#include "ui/views/test/widget_test.h"
#include "ui/views/view_utils.h"
#include "ui/views/widget/any_widget_observer.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_delegate.h"
#include "ui/views/window/dialog_delegate.h"

#if BUILDFLAG(ENABLE_EXTENSIONS)
#include "base/path_service.h"
#include "chrome/browser/extensions/chrome_test_extension_loader.h"
#include "chrome/browser/extensions/scoped_test_mv2_enabler.h"
#include "chrome/browser/extensions/signin_test_util.h"
#include "chrome/browser/extensions/sync/extension_sync_util.h"
#include "chrome/browser/ui/webui/test_support/webui_interactive_test_mixin.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/test/interaction/interactive_browser_test.h"
#include "extensions/common/extension.h"
#endif  // BUILDFLAG(ENABLE_EXTENSIONS)

namespace {

constexpr char kTestEmail[] = "email@gmail.com";
constexpr signin_metrics::AccessPoint kTestAccessPoint =
    signin_metrics::AccessPoint::kProfileMenuSignoutConfirmationPrompt;

constexpr char kConfirmationNoUnsyncedHistogramName[] =
    "Signin.ChromeSignoutConfirmationPrompt.NoUnsynced";
constexpr char kConfirmationUnsyncedHistogramName[] =
    "Signin.ChromeSignoutConfirmationPrompt.Unsynced";
constexpr char kConfirmationUnsyncedReauthHistogramName[] =
    "Signin.ChromeSignoutConfirmationPrompt.UnsyncedReauth";
constexpr char kConfirmationSupervisedProfileHistogramName[] =
    "Signin.ChromeSignoutConfirmationPrompt.SupervisedProfile";
constexpr char kConfirmationTooManyBookmarksHistogramName[] =
    "Signin.ChromeSignoutConfirmationPrompt.TooManyBookmarks";
constexpr char16_t kTestExtensionName[] = u"Test extension";

constexpr char kAccountExtensionsSignoutChoiceHistogramName[] =
    "Signin.Extensions.AccountExtensionsSignoutChoice";

DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kWebContentsId);
DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kElementExists);
DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kChecked);

std::unique_ptr<KeyedService> CreateTestSyncService(content::BrowserContext*) {
  return std::make_unique<syncer::TestSyncService>();
}

void VerifySignoutPromptHistogram(
    const base::HistogramTester& histogram_tester,
    ChromeSignoutConfirmationPromptVariant variant,
    ChromeSignoutConfirmationChoice choice) {
  const char* confirmation_prompt_histogram_name;
  switch (variant) {
    case ChromeSignoutConfirmationPromptVariant::kNoUnsyncedData:
      confirmation_prompt_histogram_name = kConfirmationNoUnsyncedHistogramName;
      break;
    case ChromeSignoutConfirmationPromptVariant::kUnsyncedData:
      confirmation_prompt_histogram_name = kConfirmationUnsyncedHistogramName;
      break;
    case ChromeSignoutConfirmationPromptVariant::kUnsyncedDataWithReauthButton:
      confirmation_prompt_histogram_name =
          kConfirmationUnsyncedReauthHistogramName;
      break;
    case ChromeSignoutConfirmationPromptVariant::kProfileWithParentalControls:
      confirmation_prompt_histogram_name =
          kConfirmationSupervisedProfileHistogramName;
      break;
    case ChromeSignoutConfirmationPromptVariant::kTooManyBookmarks:
      confirmation_prompt_histogram_name =
          kConfirmationTooManyBookmarksHistogramName;
      break;
  }

  histogram_tester.ExpectUniqueSample(confirmation_prompt_histogram_name,
                                      choice, 1);
  base::HistogramTester::CountsMap expected_counts;
  expected_counts[confirmation_prompt_histogram_name] = 1;
  EXPECT_THAT(histogram_tester.GetTotalCountsForPrefix(
                  "Signin.ChromeSignoutConfirmationPrompt."),
              testing::ContainerEq(expected_counts));
}

void VerifyUnsyncedDataCountHistograms(
    const base::HistogramTester& histogram_tester,
    ChromeSignoutConfirmationPromptVariant variant) {
  // Unsynced data histograms.
  using syncer::UnsyncedDataRecordingEvent;
  // No records for extensions, because the unsynced data is a bookmark:
  histogram_tester.ExpectTotalCount(
      "Sync.DataTypeNumUnsyncedEntitiesOnModelReady.EXTENSION", 0);
  histogram_tester.ExpectTotalCount(
      "Sync.DataTypeNumUnsyncedEntitiesOnReauthFromPendingState.EXTENSION", 0);
  histogram_tester.ExpectTotalCount(
      "Sync.DataTypeNumUnsyncedEntitiesOnSignoutConfirmationFromPendingState."
      "EXTENSION",
      /*expected_count=*/0);
  histogram_tester.ExpectTotalCount(
      "Sync.DataTypeNumUnsyncedEntitiesOnSignoutConfirmation.EXTENSION", 0);
  // Records for bookmarks:
  histogram_tester.ExpectTotalCount(
      "Sync.DataTypeNumUnsyncedEntitiesOnModelReady.BOOKMARK", 0);
  histogram_tester.ExpectTotalCount(
      "Sync.DataTypeNumUnsyncedEntitiesOnReauthFromPendingState.BOOKMARK", 0);
  if (variant == ChromeSignoutConfirmationPromptVariant::kUnsyncedData) {
    histogram_tester.ExpectUniqueSample(
        "Sync.DataTypeNumUnsyncedEntitiesOnSignoutConfirmation.BOOKMARK",
        /*sample=*/1, /*expected_bucket_count=*/1);
  } else {
    histogram_tester.ExpectTotalCount(
        "Sync.DataTypeNumUnsyncedEntitiesOnSignoutConfirmation.BOOKMARK",
        /*expected_count=*/0);
  }
  if (variant ==
      ChromeSignoutConfirmationPromptVariant::kUnsyncedDataWithReauthButton) {
    histogram_tester.ExpectUniqueSample(
        "Sync.DataTypeNumUnsyncedEntitiesOnSignoutConfirmationFromPendingState."
        "BOOKMARK",
        /*sample=*/1, /*expected_bucket_count=*/1);
  } else {
    histogram_tester.ExpectTotalCount(
        "Sync.DataTypeNumUnsyncedEntitiesOnSignoutConfirmationFromPendingState."
        "BOOKMARK",
        /*expected_count=*/0);
  }
}

}  // namespace

class SigninViewControllerBrowserTestBase : public SigninBrowserTestBase {
 public:
  SigninViewControllerBrowserTestBase() = default;

  AccountInfo SetPrimaryAccount() {
    return identity_test_env()->MakePrimaryAccountAvailable(
        kTestEmail, signin::ConsentLevel::kSignin);
  }

  void AddUnsyncedData() {
    GetTestSyncService()->SetTypesWithUnsyncedData(
        syncer::DataTypeSet{syncer::DataType::BOOKMARKS});
  }

  SignoutConfirmationUI* TriggerSignoutAndWaitForConfirmationPrompt() {
    auto url = GURL(chrome::kChromeUISignoutConfirmationURL);
    content::TestNavigationObserver observer(url);
    observer.StartWatchingNewWebContents();

    auto* signin_view_controller =
        browser()->GetFeatures().signin_view_controller();
    signin_view_controller->SignoutOrReauthWithPrompt(
        kTestAccessPoint,
        signin_metrics::ProfileSignout::kUserClickedSignoutProfileMenu,
        signin_metrics::SourceForRefreshTokenOperation::
            kUserMenu_SignOutAllAccounts);

    observer.Wait();

    CHECK(signin_view_controller->ShowsModalDialog());
    SignoutConfirmationUI* signout_confirmation_ui =
        SignoutConfirmationUI::GetForTesting(
            signin_view_controller->GetModalDialogWebContentsForTesting());
    // TODO(crbug.com/469344442): Explore using a standard widget observer
    // checking for the widget's visibility, instead of custom ui observer.
    TestSignoutConfirmationHandlerWaiter handler_observer(
        signout_confirmation_ui);
    handler_observer.Wait();

    return signout_confirmation_ui;
  }

  bool IsSigninTab(
      content::WebContents* tab,
      signin_metrics::AccessPoint access_point = kTestAccessPoint) const {
    DiceTabHelper* dice_tab_helper = DiceTabHelper::FromWebContents(tab);
    if (!dice_tab_helper) {
      return false;
    }

    if (!dice_tab_helper->IsChromeSigninPage()) {
      ADD_FAILURE();
      return false;
    }
    if (dice_tab_helper->signin_access_point() != access_point) {
      ADD_FAILURE();
      return false;
    }
    return true;
  }

  bool IsSignoutTab(content::WebContents* tab) const {
    return LogoutTabHelper::FromWebContents(tab);
  }

 protected:
  syncer::TestSyncService* GetTestSyncService() {
    return static_cast<syncer::TestSyncService*>(
        SyncServiceFactory::GetForProfile(GetProfile()));
  }

 private:
  void OnWillCreateBrowserContextServices(
      content::BrowserContext* context) override {
    SigninBrowserTestBaseT::OnWillCreateBrowserContextServices(context);
    SyncServiceFactory::GetInstance()->SetTestingFactory(
        context, base::BindRepeating(&CreateTestSyncService));
  }
};

class SigninViewControllerBrowserTest
    : public SigninViewControllerBrowserTestBase {
 public:
  SigninViewControllerBrowserTest() = default;

  views::DialogDelegate* TriggerChromeSigninDialogForExtensionsPrompt(
      base::OnceClosure on_complete) {
    views::NamedWidgetShownWaiter widget_waiter(
        views::test::AnyWidgetTestPasskey{},
        "ChromeSigninChoiceForExtensionsPrompt");
    browser()
        ->GetFeatures()
        .signin_view_controller()
        ->MaybeShowChromeSigninDialogForExtensions(kTestExtensionName,
                                                   std::move(on_complete));

    // Confirmation prompt is shown.
    views::Widget* confirmation_prompt = widget_waiter.WaitIfNeededAndGet();
    return confirmation_prompt->widget_delegate()->AsDialogDelegate();
  }

 private:
  base::test::ScopedFeatureList feature_list_{
      features::kManagedProfileRequiredInterstitial};
};

IN_PROC_BROWSER_TEST_F(
    SigninViewControllerBrowserTest,
    SignoutOrReauthWithPromptForPersistentErrorState_Reauth) {
  // Setup a primary account in error state.
  AccountInfo primary_account_info = SetPrimaryAccount();
  identity_test_env()->UpdatePersistentErrorOfRefreshTokenForAccount(
      primary_account_info.GetAccountId(),
      GoogleServiceAuthError::FromInvalidGaiaCredentialsReason(
          GoogleServiceAuthError::InvalidGaiaCredentialsReason::
              CREDENTIALS_REJECTED_BY_SERVER));

  // Add pending sync data.
  AddUnsyncedData();

  base::HistogramTester histogram_tester;
  // Trigger the Chrome signout action.
  SignoutConfirmationUI* signout_confirmation_ui =
      TriggerSignoutAndWaitForConfirmationPrompt();
  ASSERT_TRUE(signout_confirmation_ui);

  // Click "Verify it's you".
  signout_confirmation_ui->CancelDialogAndReauthForTesting();
  VerifySignoutPromptHistogram(
      histogram_tester,
      ChromeSignoutConfirmationPromptVariant::kUnsyncedDataWithReauthButton,
      ChromeSignoutConfirmationChoice::kCancelSignoutAndReauth);
  VerifyUnsyncedDataCountHistograms(
      histogram_tester,
      ChromeSignoutConfirmationPromptVariant::kUnsyncedDataWithReauthButton);

  // The tab was navigated to the signin page.
  content::WebContents* tab =
      browser()->GetTabStripModel()->GetActiveWebContents();
  ASSERT_TRUE(tab);
  EXPECT_TRUE(IsSigninTab(tab));
}

IN_PROC_BROWSER_TEST_F(
    SigninViewControllerBrowserTest,
    SignoutOrReauthWithPromptForPersistentErrorState_SignOutWithUnsyncedData) {
  // Setup a primary account in error state.
  AccountInfo primary_account_info = SetPrimaryAccount();
  identity_test_env()->UpdatePersistentErrorOfRefreshTokenForAccount(
      primary_account_info.GetAccountId(),
      GoogleServiceAuthError::FromInvalidGaiaCredentialsReason(
          GoogleServiceAuthError::InvalidGaiaCredentialsReason::
              CREDENTIALS_REJECTED_BY_SERVER));

  // Add pending sync data.
  AddUnsyncedData();

  // Trigger the Chrome signout action.
  base::HistogramTester histogram_tester;
  SignoutConfirmationUI* signout_confirmation_ui =
      TriggerSignoutAndWaitForConfirmationPrompt();
  ASSERT_TRUE(signout_confirmation_ui);

  // Click "Sign Out Anyway".
  // Note: This is the accept action.
  signout_confirmation_ui->AcceptDialogForTesting();
  VerifySignoutPromptHistogram(
      histogram_tester,
      ChromeSignoutConfirmationPromptVariant::kUnsyncedDataWithReauthButton,
      ChromeSignoutConfirmationChoice::kSignout);
  VerifyUnsyncedDataCountHistograms(
      histogram_tester,
      ChromeSignoutConfirmationPromptVariant::kUnsyncedDataWithReauthButton);

  // User was signed out.
  EXPECT_FALSE(
      identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSignin));

  // The tab was navigated to the signout page.
  content::WebContents* tab =
      browser()->GetTabStripModel()->GetActiveWebContents();
  ASSERT_TRUE(tab);
  EXPECT_TRUE(IsSignoutTab(tab));
}

IN_PROC_BROWSER_TEST_F(SigninViewControllerBrowserTest,
                       SignoutOrReauthWithPrompt_Cancel) {
  // Setup a primary account.
  AccountInfo primary_account_info = SetPrimaryAccount();

  // Add pending sync data.
  AddUnsyncedData();

  // Trigger the Chrome signout action.
  base::HistogramTester histogram_tester;
  SignoutConfirmationUI* signout_confirmation_ui =
      TriggerSignoutAndWaitForConfirmationPrompt();
  ASSERT_TRUE(signout_confirmation_ui);

  // Click "Cancel".
  signout_confirmation_ui->CancelDialogForTesting();
  VerifySignoutPromptHistogram(
      histogram_tester, ChromeSignoutConfirmationPromptVariant::kUnsyncedData,
      ChromeSignoutConfirmationChoice::kCancelSignout);
  VerifyUnsyncedDataCountHistograms(
      histogram_tester, ChromeSignoutConfirmationPromptVariant::kUnsyncedData);

  // User is still signed in.
  EXPECT_EQ(
      primary_account_info.GetAccountId(),
      identity_manager()->GetPrimaryAccountId(signin::ConsentLevel::kSignin));
  // The tab was not navigated to the signin page or signout page.
  content::WebContents* tab =
      browser()->GetTabStripModel()->GetActiveWebContents();
  ASSERT_TRUE(tab);
  EXPECT_FALSE(IsSigninTab(tab));
  EXPECT_FALSE(IsSignoutTab(tab));
}

IN_PROC_BROWSER_TEST_F(SigninViewControllerBrowserTest,
                       SignoutOrReauthWithPrompt_SignOutWithUnsyncedData) {
  // Setup a primary account.
  AccountInfo primary_account_info = SetPrimaryAccount();

  // Add pending sync data.
  AddUnsyncedData();

  // Trigger the Chrome signout action.
  base::HistogramTester histogram_tester;
  SignoutConfirmationUI* signout_confirmation_ui =
      TriggerSignoutAndWaitForConfirmationPrompt();
  ASSERT_TRUE(signout_confirmation_ui);

  // Click "Sign Out Anyway".
  signout_confirmation_ui->AcceptDialogForTesting();
  VerifySignoutPromptHistogram(
      histogram_tester, ChromeSignoutConfirmationPromptVariant::kUnsyncedData,
      ChromeSignoutConfirmationChoice::kSignout);
  VerifyUnsyncedDataCountHistograms(
      histogram_tester, ChromeSignoutConfirmationPromptVariant::kUnsyncedData);

  // User was signed out.
  EXPECT_FALSE(
      identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSignin));

  // The tab was navigated to the signout page.
  content::WebContents* tab =
      browser()->GetTabStripModel()->GetActiveWebContents();
  ASSERT_TRUE(tab);
  EXPECT_TRUE(IsSignoutTab(tab));
}

IN_PROC_BROWSER_TEST_F(SigninViewControllerBrowserTest,
                       SignoutOrReauthWithPrompt_SignOut) {
  // Setup a primary account.
  AccountInfo primary_account_info = SetPrimaryAccount();

  // Trigger the Chrome signout action.
  base::HistogramTester histogram_tester;
  SignoutConfirmationUI* signout_confirmation_ui =
      TriggerSignoutAndWaitForConfirmationPrompt();
  ASSERT_TRUE(signout_confirmation_ui);

  // Click "Sign Out Anyway".
  signout_confirmation_ui->AcceptDialogForTesting();
  VerifySignoutPromptHistogram(
      histogram_tester, ChromeSignoutConfirmationPromptVariant::kNoUnsyncedData,
      ChromeSignoutConfirmationChoice::kSignout);
  VerifyUnsyncedDataCountHistograms(
      histogram_tester,
      ChromeSignoutConfirmationPromptVariant::kNoUnsyncedData);
  histogram_tester.ExpectUniqueSample(
      "Sync.BookmarksLimitExceededOnSignoutPrompt", false, 1);

  // User was signed out.
  EXPECT_FALSE(
      identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSignin));

  // The tab was navigated to the signout page.
  content::WebContents* tab =
      browser()->GetTabStripModel()->GetActiveWebContents();
  ASSERT_TRUE(tab);
  EXPECT_TRUE(IsSignoutTab(tab));
}

IN_PROC_BROWSER_TEST_F(SigninViewControllerBrowserTest,
                       SignoutOrReauthWithPrompt_NoPrompt) {
  // Setup a primary account in auth error.
  AccountInfo primary_account_info = SetPrimaryAccount();

  identity_test_env()->UpdatePersistentErrorOfRefreshTokenForAccount(
      primary_account_info.GetAccountId(),
      GoogleServiceAuthError::FromInvalidGaiaCredentialsReason(
          GoogleServiceAuthError::InvalidGaiaCredentialsReason::
              CREDENTIALS_REJECTED_BY_SERVER));

  // Trigger the Chrome signout action.
  browser()->GetFeatures().signin_view_controller()->SignoutOrReauthWithPrompt(
      kTestAccessPoint,
      signin_metrics::ProfileSignout::kUserClickedSignoutProfileMenu,
      signin_metrics::SourceForRefreshTokenOperation::
          kUserMenu_SignOutAllAccounts);

  // User was signed out.
  EXPECT_FALSE(
      identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSignin));

  // The tab was navigated to the signout page.
  content::WebContents* tab =
      browser()->GetTabStripModel()->GetActiveWebContents();
  ASSERT_TRUE(tab);
  EXPECT_TRUE(IsSignoutTab(tab));
}

IN_PROC_BROWSER_TEST_F(SigninViewControllerBrowserTest,
                       SignoutOrReauthWithPrompt_SignOutSupervisedUser) {
  // Setup a primary account for a supervised user.
  AccountInfo primary_account_info = SetPrimaryAccount();
  AccountCapabilitiesTestMutator mutator(&primary_account_info);
  mutator.set_is_subject_to_parental_controls(true);
  identity_test_env()->UpdateAccountInfoForAccount(primary_account_info);

  // Trigger the Chrome signout action.
  base::HistogramTester histogram_tester;
  SignoutConfirmationUI* signout_confirmation_ui =
      TriggerSignoutAndWaitForConfirmationPrompt();
  ASSERT_TRUE(signout_confirmation_ui);

  // Click "Sign Out Anyway".
  signout_confirmation_ui->AcceptDialogForTesting();
  VerifySignoutPromptHistogram(
      histogram_tester,
      ChromeSignoutConfirmationPromptVariant::kProfileWithParentalControls,
      ChromeSignoutConfirmationChoice::kSignout);
  VerifyUnsyncedDataCountHistograms(
      histogram_tester,
      ChromeSignoutConfirmationPromptVariant::kProfileWithParentalControls);

  // User was signed out.
  EXPECT_FALSE(
      identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSignin));

  // The tab was navigated to the signout page.
  content::WebContents* tab =
      browser()->GetTabStripModel()->GetActiveWebContents();
  ASSERT_TRUE(tab);
  EXPECT_TRUE(IsSignoutTab(tab));
}

IN_PROC_BROWSER_TEST_F(SigninViewControllerBrowserTest,
                       SignoutOrReauthWithPrompt_BookmarksLimitExceeded) {
  // Setup a primary account.
  AccountInfo primary_account_info = SetPrimaryAccount();

  // Set Bookmarks Limit Exceeded error.
  GetTestSyncService()->SetBookmarksLimitExceeded(true);

  {
    // Trigger the Chrome signout action.
    base::HistogramTester histogram_tester;
    SignoutConfirmationUI* signout_confirmation_ui =
        TriggerSignoutAndWaitForConfirmationPrompt();
    ASSERT_TRUE(signout_confirmation_ui);

    // Click "Cancel".
    signout_confirmation_ui->CancelDialogForTesting();
    VerifySignoutPromptHistogram(
        histogram_tester,
        ChromeSignoutConfirmationPromptVariant::kTooManyBookmarks,
        ChromeSignoutConfirmationChoice::kCancelSignout);
    VerifyUnsyncedDataCountHistograms(
        histogram_tester,
        ChromeSignoutConfirmationPromptVariant::kTooManyBookmarks);
    histogram_tester.ExpectUniqueSample(
        "Sync.BookmarksLimitExceededOnSignoutPrompt", true, 1);
  }

  // User is still signed in.
  EXPECT_TRUE(
      identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSignin));

  // The tab was not navigated to the signin page or signout page.
  content::WebContents* active_tab =
      browser()->GetTabStripModel()->GetActiveWebContents();
  ASSERT_TRUE(active_tab);
  EXPECT_FALSE(IsSigninTab(active_tab));
  EXPECT_FALSE(IsSignoutTab(active_tab));

  {
    // Trigger the Chrome signout action again.
    base::HistogramTester histogram_tester;
    SignoutConfirmationUI* signout_confirmation_ui =
        TriggerSignoutAndWaitForConfirmationPrompt();
    ASSERT_TRUE(signout_confirmation_ui);

    // Click "Sign Out Anyway".
    signout_confirmation_ui->AcceptDialogForTesting();
    VerifySignoutPromptHistogram(
        histogram_tester,
        ChromeSignoutConfirmationPromptVariant::kTooManyBookmarks,
        ChromeSignoutConfirmationChoice::kSignout);
    VerifyUnsyncedDataCountHistograms(
        histogram_tester,
        ChromeSignoutConfirmationPromptVariant::kTooManyBookmarks);
    histogram_tester.ExpectUniqueSample(
        "Sync.BookmarksLimitExceededOnSignoutPrompt", true, 1);
  }

  // User was signed out.
  EXPECT_FALSE(
      identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSignin));

  // The tab was navigated to the signout page.
  content::WebContents* tab =
      browser()->GetTabStripModel()->GetActiveWebContents();
  ASSERT_TRUE(tab);
  EXPECT_TRUE(IsSignoutTab(tab));
}

IN_PROC_BROWSER_TEST_F(SigninViewControllerBrowserTest,
                       SignoutOrReauthWithPrompt_ReauthAndBookmarksLimit) {
  // Setup a primary account in error state.
  AccountInfo primary_account_info = SetPrimaryAccount();
  identity_test_env()->UpdatePersistentErrorOfRefreshTokenForAccount(
      primary_account_info.GetAccountId(),
      GoogleServiceAuthError::FromInvalidGaiaCredentialsReason(
          GoogleServiceAuthError::InvalidGaiaCredentialsReason::
              CREDENTIALS_REJECTED_BY_SERVER));

  // Set Bookmarks Limit Exceeded error.
  GetTestSyncService()->SetBookmarksLimitExceeded(true);

  // Trigger the Chrome signout action.
  base::HistogramTester histogram_tester;
  SignoutConfirmationUI* signout_confirmation_ui =
      TriggerSignoutAndWaitForConfirmationPrompt();
  ASSERT_TRUE(signout_confirmation_ui);

  // Click "Sign Out Anyway".
  // Note: This is the accept action.
  signout_confirmation_ui->AcceptDialogForTesting();
  VerifySignoutPromptHistogram(
      histogram_tester,
      ChromeSignoutConfirmationPromptVariant::kUnsyncedDataWithReauthButton,
      ChromeSignoutConfirmationChoice::kSignout);

  // If we are in a bookmark limit state, we should not have any unsynced data
  // of type BOOKMARK. The reason is that the BOOKMARK data type is disabled,
  // hence bookmark entities are not forwarded to the commit path.
  histogram_tester.ExpectTotalCount(
      "Sync.DataTypeNumUnsyncedEntitiesOnSignoutConfirmationFromPendingState."
      "BOOKMARK",
      /*expected_count=*/0);

  // User was signed out.
  EXPECT_FALSE(
      identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSignin));

  // The tab was navigated to the signout page.
  content::WebContents* tab =
      browser()->GetTabStripModel()->GetActiveWebContents();
  ASSERT_TRUE(tab);
  EXPECT_TRUE(IsSignoutTab(tab));
}

IN_PROC_BROWSER_TEST_F(SigninViewControllerBrowserTest,
                       ShowChromeSigninDialogForExtensionsPromptReuseOpenTab) {
  ASSERT_EQ(browser()->GetTabStripModel()->count(), 1);
  ASSERT_TRUE(SigninViewController::IsNTPTab(
      browser()->GetTabStripModel()->GetActiveWebContents()));

  identity_test_env()->MakeAccountAvailable(kTestEmail, {.set_cookie = true});
  ASSERT_FALSE(identity_manager()->GetAccountsWithRefreshTokens().empty());
  ASSERT_FALSE(
      identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSignin));

  base::test::TestFuture<void> future;
  views::DialogDelegate* dialog_delegate =
      TriggerChromeSigninDialogForExtensionsPrompt(future.GetCallback());
  ASSERT_TRUE(dialog_delegate);

  content::WebContents* tab =
      browser()->GetTabStripModel()->GetActiveWebContents();
  ASSERT_TRUE(tab);
  EXPECT_TRUE(SigninViewController::IsNTPTab(tab));

  ASSERT_FALSE(future.IsReady());
  dialog_delegate->AcceptDialog();

  EXPECT_TRUE(
      identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSignin));
  ASSERT_TRUE(future.Wait());
  ASSERT_EQ(browser()->GetTabStripModel()->count(), 1);
}

IN_PROC_BROWSER_TEST_F(
    SigninViewControllerBrowserTest,
    ShowChromeSigninDialogForExtensionsPromptReuseInactiveOpenTab) {
  ui_test_utils::NavigateToURLWithDisposition(
      browser(), GURL("https://www.google.com"),
      WindowOpenDisposition::NEW_FOREGROUND_TAB,
      ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);
  ASSERT_EQ(browser()->GetTabStripModel()->count(), 2);
  ASSERT_FALSE(SigninViewController::IsNTPTab(
      browser()->GetTabStripModel()->GetActiveWebContents()));

  identity_test_env()->MakeAccountAvailable(kTestEmail, {.set_cookie = true});
  ASSERT_FALSE(identity_manager()->GetAccountsWithRefreshTokens().empty());
  ASSERT_FALSE(
      identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSignin));

  base::test::TestFuture<void> future;
  views::DialogDelegate* dialog_delegate =
      TriggerChromeSigninDialogForExtensionsPrompt(future.GetCallback());
  ASSERT_TRUE(dialog_delegate);

  content::WebContents* tab =
      browser()->GetTabStripModel()->GetActiveWebContents();
  ASSERT_TRUE(tab);
  EXPECT_TRUE(SigninViewController::IsNTPTab(tab));

  ASSERT_FALSE(future.IsReady());
  dialog_delegate->AcceptDialog();

  EXPECT_TRUE(
      identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSignin));
  ASSERT_TRUE(future.Wait());
  ASSERT_EQ(browser()->GetTabStripModel()->count(), 2);
}

IN_PROC_BROWSER_TEST_F(SigninViewControllerBrowserTest,
                       ShowChromeSigninDialogForExtensionsPromptInNewTab) {
  ASSERT_TRUE(
      ui_test_utils::NavigateToURL(browser(), GURL("https://www.google.com")));
  ASSERT_EQ(browser()->GetTabStripModel()->count(), 1);
  ASSERT_FALSE(SigninViewController::IsNTPTab(
      browser()->GetTabStripModel()->GetActiveWebContents()));

  identity_test_env()->MakeAccountAvailable(kTestEmail, {.set_cookie = true});
  ASSERT_FALSE(identity_manager()->GetAccountsWithRefreshTokens().empty());
  ASSERT_FALSE(
      identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSignin));

  base::test::TestFuture<void> future;
  views::DialogDelegate* dialog_delegate =
      TriggerChromeSigninDialogForExtensionsPrompt(future.GetCallback());
  ASSERT_TRUE(dialog_delegate);
  ASSERT_EQ(browser()->GetTabStripModel()->count(), 2);

  content::WebContents* tab =
      browser()->GetTabStripModel()->GetActiveWebContents();
  ASSERT_TRUE(tab);
  EXPECT_TRUE(SigninViewController::IsNTPTab(tab));

  ASSERT_FALSE(future.IsReady());
  dialog_delegate->AcceptDialog();

  EXPECT_TRUE(
      identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSignin));
  ASSERT_TRUE(future.Wait());
}

IN_PROC_BROWSER_TEST_F(SigninViewControllerBrowserTest,
                       ShowChromeSigninDialogForExtensionsPromptCancel) {
  identity_test_env()->MakeAccountAvailable(kTestEmail, {.set_cookie = true});
  ASSERT_FALSE(identity_manager()->GetAccountsWithRefreshTokens().empty());
  ASSERT_FALSE(
      identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSignin));

  base::test::TestFuture<void> future;
  views::DialogDelegate* dialog_delegate =
      TriggerChromeSigninDialogForExtensionsPrompt(future.GetCallback());
  ASSERT_TRUE(dialog_delegate);

  ASSERT_FALSE(future.IsReady());
  dialog_delegate->CancelDialog();

  EXPECT_FALSE(
      identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSignin));
  ASSERT_TRUE(future.Wait());
}

IN_PROC_BROWSER_TEST_F(
    SigninViewControllerBrowserTest,
    ShowChromeSigninDialogForExtensionsPromptNotShownPrimaryAccountSet) {
  identity_test_env()->MakePrimaryAccountAvailable(
      kTestEmail, signin::ConsentLevel::kSignin);
  ASSERT_TRUE(
      identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSignin));

  base::test::TestFuture<void> future;
  browser()
      ->GetFeatures()
      .signin_view_controller()
      ->MaybeShowChromeSigninDialogForExtensions(kTestExtensionName,
                                                 future.GetCallback());
  EXPECT_TRUE(future.IsReady());
}

IN_PROC_BROWSER_TEST_F(
    SigninViewControllerBrowserTest,
    ShowChromeSigninDialogForExtensionsPromptNotShownNoAccounts) {
  base::test::TestFuture<void> future;
  browser()
      ->GetFeatures()
      .signin_view_controller()
      ->MaybeShowChromeSigninDialogForExtensions(kTestExtensionName,
                                                 future.GetCallback());
  EXPECT_TRUE(future.IsReady());
}

IN_PROC_BROWSER_TEST_F(SigninViewControllerBrowserTest,
                       UpdateAccessPointOfSignInTab) {
  // Request a sign in tab, which will open a new tab.
  browser()->GetFeatures().signin_view_controller()->ShowDiceAddAccountTab(
      signin_metrics::AccessPoint::kPasswordBubble, std::string());
  EXPECT_TRUE(IsSigninTab(browser()->GetTabStripModel()->GetActiveWebContents(),
                          signin_metrics::AccessPoint::kPasswordBubble));

  // Request a sign in tab with a different access point, which will update the
  // existing sign in tab's access point.
  browser()->GetFeatures().signin_view_controller()->ShowDiceAddAccountTab(
      signin_metrics::AccessPoint::kAddressBubble, std::string());
  EXPECT_TRUE(IsSigninTab(browser()->GetTabStripModel()->GetActiveWebContents(),
                          signin_metrics::AccessPoint::kAddressBubble));

  EXPECT_TRUE(signin_ui_util::GetSignInTabWithAccessPoint(
      browser(), signin_metrics::AccessPoint::kAddressBubble));
  EXPECT_FALSE(signin_ui_util::GetSignInTabWithAccessPoint(
      browser(), signin_metrics::AccessPoint::kPasswordBubble));
}

#if BUILDFLAG(ENABLE_DICE_SUPPORT)
class AsyncMockBluetoothAdapter : public device::MockBluetoothAdapter {
 public:
  AsyncMockBluetoothAdapter() = default;

  bool IsInitialized() const override { return is_initialized_; }
  void SetInitialized(bool initialized) {
    is_initialized_ = initialized;
    if (is_initialized_ && init_callback_) {
      std::move(init_callback_).Run();
    }
  }

  void Initialize(base::OnceClosure callback) override {
    init_callback_ = std::move(callback);
  }

 private:
  bool is_initialized_ = false;
  base::OnceClosure init_callback_;
  ~AsyncMockBluetoothAdapter() override = default;
};

class SigninViewControllerSignInBanner
    : public SigninViewControllerBrowserTestBase {
 public:
  SigninViewControllerSignInBanner() {
    feature_list_.InitAndEnableFeatureWithParameters(
        switches::kMagiChromePasskeySignIn, {{"flow_type", "banner"}});
  }

  void SetUpOnMainThread() override {
    SigninViewControllerBrowserTestBase::SetUpOnMainThread();
    mock_bluetooth_adapter_ = base::MakeRefCounted<AsyncMockBluetoothAdapter>();
    ON_CALL(*mock_bluetooth_adapter_, IsPresent())
        .WillByDefault(testing::Return(true));
    ON_CALL(*mock_bluetooth_adapter_, IsPowered())
        .WillByDefault(testing::Return(true));
    ON_CALL(*mock_bluetooth_adapter_, GetOsPermissionStatus())
        .WillByDefault(testing::Return(
            device::BluetoothAdapter::PermissionStatus::kAllowed));
    device::BluetoothAdapterFactory::SetAdapterForTesting(
        mock_bluetooth_adapter_);
    // Other parts of Chrome may keep a reference to the bluetooth adapter.
    testing::Mock::AllowLeak(mock_bluetooth_adapter_.get());

    bluetooth_override_values_ =
        device::BluetoothAdapterFactory::Get()->InitGlobalOverrideValues();
    bluetooth_override_values_->SetLESupported(true);

    url_loader_interceptor_ =
        std::make_unique<content::URLLoaderInterceptor>(base::BindRepeating(
            [](content::URLLoaderInterceptor::RequestParams* params) {
              if (params->url_request.url.path() == "/signin/chrome/sync") {
                content::URLLoaderInterceptor::WriteResponse(
                    "HTTP/1.1 200 OK\nContent-Type: text/html\n\n",
                    "<html><body>Fake Sign-in Page</body></html>",
                    params->client.get());
                return true;
              }
              return false;
            }));
  }

  void TearDownOnMainThread() override {
    url_loader_interceptor_.reset();
    SigninViewControllerBrowserTestBase::TearDownOnMainThread();
  }

 protected:
  scoped_refptr<AsyncMockBluetoothAdapter> mock_bluetooth_adapter_;
  std::unique_ptr<device::BluetoothAdapterFactory::GlobalOverrideValues>
      bluetooth_override_values_;
  std::unique_ptr<content::URLLoaderInterceptor> url_loader_interceptor_;

 private:
  base::test::ScopedFeatureList feature_list_;
};

IN_PROC_BROWSER_TEST_F(SigninViewControllerSignInBanner, Visibility) {
  browser()->GetFeatures().signin_view_controller()->ShowDiceAddAccountTab(
      signin_metrics::AccessPoint::kSettings, std::string());

  mock_bluetooth_adapter_->SetInitialized(true);

  content::WebContents* active_contents =
      browser()->GetTabStripModel()->GetActiveWebContents();
  ASSERT_TRUE(active_contents);
  content::WaitForLoadStop(active_contents);

  // Check that the infobar is shown.
  infobars::ContentInfoBarManager* infobar_manager =
      infobars::ContentInfoBarManager::FromWebContents(active_contents);
  ASSERT_TRUE(infobar_manager);
  EXPECT_EQ(1u, infobar_manager->infobars().size());

  // Navigate away.
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GURL("about:blank")));

  // Check that it is no longer shown.
  EXPECT_EQ(0u, infobar_manager->infobars().size());
}

IN_PROC_BROWSER_TEST_F(SigninViewControllerSignInBanner,
                       NavigateAwayBeforeBluetoothResolved) {
  browser()->GetFeatures().signin_view_controller()->ShowDiceAddAccountTab(
      signin_metrics::AccessPoint::kSettings, std::string());

  content::WebContents* active_contents =
      browser()->GetTabStripModel()->GetActiveWebContents();
  ASSERT_TRUE(active_contents);

  // Navigate away immediately before resolving the initialization.
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GURL("about:blank")));

  // Resolve the bluetooth check. Since we are no longer on the signin page,
  // it should not add the banner.
  mock_bluetooth_adapter_->SetInitialized(true);

  infobars::ContentInfoBarManager* infobar_manager =
      infobars::ContentInfoBarManager::FromWebContents(active_contents);
  ASSERT_TRUE(infobar_manager);
  EXPECT_EQ(0u, infobar_manager->infobars().size());
}

IN_PROC_BROWSER_TEST_F(SigninViewControllerSignInBanner,
                       BluetoothResolvedAfterNavigationCommitted) {
  browser()->GetFeatures().signin_view_controller()->ShowDiceAddAccountTab(
      signin_metrics::AccessPoint::kSettings, std::string());

  content::WebContents* active_contents =
      browser()->GetTabStripModel()->GetActiveWebContents();
  ASSERT_TRUE(active_contents);

  // Wait for navigation to complete before resolving Bluetooth initialization.
  content::WaitForLoadStop(active_contents);

  infobars::ContentInfoBarManager* infobar_manager =
      infobars::ContentInfoBarManager::FromWebContents(active_contents);
  ASSERT_TRUE(infobar_manager);
  EXPECT_EQ(0u, infobar_manager->infobars().size());

  // Now resolve the Bluetooth check. Since the navigation already committed and
  // we are on the signin page, the banner should be added now.
  mock_bluetooth_adapter_->SetInitialized(true);
  EXPECT_EQ(1u, infobar_manager->infobars().size());
}

class SigninViewControllerSignInBannerNoBluetooth
    : public SigninViewControllerBrowserTestBase {
 public:
  SigninViewControllerSignInBannerNoBluetooth() {
    feature_list_.InitAndEnableFeatureWithParameters(
        switches::kMagiChromePasskeySignIn, {{"flow_type", "banner"}});
  }

  void SetUpOnMainThread() override {
    SigninViewControllerBrowserTestBase::SetUpOnMainThread();
    mock_bluetooth_adapter_ =
        base::MakeRefCounted<testing::NiceMock<device::MockBluetoothAdapter>>();
    // Bluetooth is supported (LE is true) but adapter is NOT present.
    ON_CALL(*mock_bluetooth_adapter_, IsPresent())
        .WillByDefault(testing::Return(false));
    device::BluetoothAdapterFactory::SetAdapterForTesting(
        mock_bluetooth_adapter_);

    bluetooth_override_values_ =
        device::BluetoothAdapterFactory::Get()->InitGlobalOverrideValues();
    bluetooth_override_values_->SetLESupported(true);
  }

 protected:
  scoped_refptr<testing::NiceMock<device::MockBluetoothAdapter>>
      mock_bluetooth_adapter_;
  std::unique_ptr<device::BluetoothAdapterFactory::GlobalOverrideValues>
      bluetooth_override_values_;

 private:
  base::test::ScopedFeatureList feature_list_;
};

IN_PROC_BROWSER_TEST_F(SigninViewControllerSignInBannerNoBluetooth,
                       BluetoothUnavailable) {
  browser()->GetFeatures().signin_view_controller()->ShowDiceAddAccountTab(
      signin_metrics::AccessPoint::kSettings, std::string());

  content::WebContents* active_contents =
      browser()->GetTabStripModel()->GetActiveWebContents();
  ASSERT_TRUE(active_contents);

  // Check that the infobar is NOT shown because bluetooth is unavailable.
  infobars::ContentInfoBarManager* infobar_manager =
      infobars::ContentInfoBarManager::FromWebContents(active_contents);
  ASSERT_TRUE(infobar_manager);
  EXPECT_EQ(0u, infobar_manager->infobars().size());
}

IN_PROC_BROWSER_TEST_F(SigninViewControllerSignInBanner, EndToEndFlow) {
  // 1. Open the sign-in page, which triggers the tab helper and starts the
  // flow.
  signin_ui_util::SignInFromSingleAccountPromo(
      browser()->GetProfile(), CoreAccountInfo(),
      signin_metrics::AccessPoint::kPasswordBubble);

  content::WebContents* sign_in_tab =
      signin_ui_util::GetSignInTabWithAccessPoint(
          browser(), signin_metrics::AccessPoint::kPasswordBubble);
  ASSERT_TRUE(sign_in_tab);

  mock_bluetooth_adapter_->SetInitialized(true);

  // Wait for the sign-in tab to finish loading to ensure asynchronous
  // loader and Bluetooth check callbacks have completed.
  content::WaitForLoadStop(sign_in_tab);

  // Verify that the InfoBar is created and added to the manager.
  auto* infobar_manager =
      infobars::ContentInfoBarManager::FromWebContents(sign_in_tab);
  ASSERT_TRUE(infobar_manager);

  // The infobar should be added instantly.
  ASSERT_EQ(1u, infobar_manager->infobars().size());

  infobars::InfoBar* infobar = infobar_manager->infobars()[0];
  ASSERT_TRUE(infobar);

  // Verify it is our QR code infobar.
  EXPECT_EQ(infobars::InfoBarDelegate::SIGNIN_QRCODE_INFOBAR_DELEGATE,
            infobar->delegate()->GetIdentifier());

  SigninQRCodeInfoBar* qr_infobar = static_cast<SigninQRCodeInfoBar*>(infobar);

  // 2. Initially, the QR code is NOT ready, so it must show the throbber.
  EXPECT_FALSE(qr_infobar->IsShowingQrCodeForTesting());

  // Get the model and set a dummy QR code payload.
  SigninQRCodeModel* model = SigninQRCodeModel::FromWebContents(sign_in_tab);
  ASSERT_TRUE(model);

  // Setting the QR code should trigger the observer and swap to the QR view.
  model->SetQrCode("dummy_qr_code_payload_for_testing");

  // Verify that the InfoBar is now showing the QR code!
  EXPECT_TRUE(qr_infobar->IsShowingQrCodeForTesting());

  // 3. Navigate away from the sign-in page.
  // This should trigger the DiceTabHelper to notify that it's no longer the
  // sign-in page, and the InfoBar should be automatically dismissed.
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GURL("about:blank")));

  // The infobar should have been automatically dismissed!
  EXPECT_EQ(0u, infobar_manager->infobars().size());
}

class SigninViewControllerCrossDeviceSigninBrowserTest
    : public SigninViewControllerBrowserTestBase {
 public:
  SigninViewControllerCrossDeviceSigninBrowserTest() {
    feature_list_.InitAndEnableFeature(switches::kCrossDeviceSigninFromDesktop);
  }

 private:
  base::test::ScopedFeatureList feature_list_;
};

IN_PROC_BROWSER_TEST_F(SigninViewControllerCrossDeviceSigninBrowserTest,
                       ShowCrossDeviceSigninQrBubble) {
  SetPrimaryAccount();

  views::AnyWidgetObserver observer(views::test::AnyWidgetTestPasskey{});
  base::test::TestFuture<views::Widget*> widget_future;
  observer.set_shown_callback(widget_future.GetRepeatingCallback());

  base::MockCallback<base::OnceClosure> closing_callback;
  EXPECT_CALL(closing_callback, Run()).Times(1);
  BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
  AvatarToolbarButtonInterface* avatar_button =
      browser_view->toolbar_button_provider()
          ->GetAvatarToolbarButtonInterface();
  ASSERT_TRUE(avatar_button);
  // Before showing, there should be no explicit state.
  EXPECT_FALSE(avatar_button->HasExplicitButtonState());

  browser()
      ->GetFeatures()
      .signin_view_controller()
      ->ShowCrossDeviceSigninQrBubble(closing_callback.Get());

  views::Widget* bubble_widget = widget_future.Get();
  ASSERT_TRUE(bubble_widget);

  views::WidgetDelegate* delegate = bubble_widget->widget_delegate();
  ASSERT_TRUE(delegate);
  EXPECT_TRUE(delegate->ShouldShowCloseButton());
  if (auto* bubble_delegate = delegate->AsBubbleDialogDelegate()) {
    EXPECT_FALSE(bubble_delegate->ShouldCloseOnDeactivate());
  }

  views::WebView* web_view = views::AsViewClass<views::WebView>(
      views::ElementTrackerViews::GetInstance()->GetUniqueView(
          kCrossDeviceSigninQrBubbleWebViewElementId,
          views::ElementTrackerViews::GetContextForWidget(bubble_widget)));
  ASSERT_TRUE(web_view);

  content::WebContents* web_contents = web_view->GetWebContents();
  ASSERT_TRUE(web_contents);

  // Note: This observer is attached after the WebContents has started loading,
  // so it won't reliably catch load-time JS errors (like missing imports),
  // but it will successfully catch post-load runtime errors or unhandled
  // exceptions.
  content::WebContentsConsoleObserver console_observer(web_contents);
  if (web_contents->IsLoading()) {
    content::WaitForLoadStop(web_contents);
  }
  for (const auto& message : console_observer.messages()) {
    LOG(INFO) << "Console message: " << message.message;
    EXPECT_NE(message.log_level, blink::mojom::ConsoleMessageLevel::kError)
        << "JS Error on WebUI: " << message.message;
  }

  // After showing, the explicit state should be set.
  EXPECT_TRUE(avatar_button->HasExplicitButtonState());

  // Verify that the WebUI URL loaded successfully.
  EXPECT_EQ(web_contents->GetVisibleURL(),
            GURL(chrome::kChromeUICrossDeviceSigninQrBubbleURL));

  // Verify that right-click context menu is disabled in this bubble.
  content::ContextMenuParams params;
  EXPECT_TRUE(web_contents->GetDelegate()->HandleContextMenu(
      *web_contents->GetPrimaryMainFrame(), params));

  views::test::WidgetDestroyedWaiter waiter(bubble_widget);
  // Simulating a click on the avatar button should close the bubble because of
  // the explicit action.
  avatar_button->ButtonPressed(/*is_source_accelerator=*/false);
  waiter.Wait();

  // After closing, wait until the explicit state is cleared (reverted).
  EXPECT_FALSE(avatar_button->HasExplicitButtonState());
}
IN_PROC_BROWSER_TEST_F(SigninViewControllerCrossDeviceSigninBrowserTest,
                       ClosesOnSignOut) {
  AccountInfo account_info = SetPrimaryAccount();

  views::AnyWidgetObserver observer(views::test::AnyWidgetTestPasskey{});
  base::test::TestFuture<views::Widget*> widget_future;
  observer.set_shown_callback(widget_future.GetRepeatingCallback());

  base::MockCallback<base::OnceClosure> closing_callback;
  browser()
      ->GetFeatures()
      .signin_view_controller()
      ->ShowCrossDeviceSigninQrBubble(closing_callback.Get());
  views::Widget* bubble_widget = widget_future.Get();

  ASSERT_TRUE(bubble_widget);
  EXPECT_TRUE(bubble_widget->IsVisible());

  views::test::WidgetDestroyedWaiter waiter(bubble_widget);
  identity_test_env()->ClearPrimaryAccount();
  waiter.Wait();
}

IN_PROC_BROWSER_TEST_F(SigninViewControllerCrossDeviceSigninBrowserTest,
                       ClosesOnRefreshTokenRemoved) {
  AccountInfo account_info = SetPrimaryAccount();

  views::AnyWidgetObserver observer(views::test::AnyWidgetTestPasskey{});
  base::test::TestFuture<views::Widget*> widget_future;
  observer.set_shown_callback(widget_future.GetRepeatingCallback());

  base::MockCallback<base::OnceClosure> closing_callback;
  browser()
      ->GetFeatures()
      .signin_view_controller()
      ->ShowCrossDeviceSigninQrBubble(closing_callback.Get());
  views::Widget* bubble_widget = widget_future.Get();

  ASSERT_TRUE(bubble_widget);
  EXPECT_TRUE(bubble_widget->IsVisible());

  views::test::WidgetDestroyedWaiter waiter(bubble_widget);
  identity_test_env()->RemoveRefreshTokenForAccount(
      account_info.GetAccountId());
  waiter.Wait();
}
IN_PROC_BROWSER_TEST_F(SigninViewControllerCrossDeviceSigninBrowserTest,
                       ClosesOnRefreshTokenError) {
  AccountInfo account_info = SetPrimaryAccount();

  views::AnyWidgetObserver observer(views::test::AnyWidgetTestPasskey{});
  base::test::TestFuture<views::Widget*> widget_future;
  observer.set_shown_callback(widget_future.GetRepeatingCallback());

  base::MockCallback<base::OnceClosure> closing_callback;
  browser()
      ->GetFeatures()
      .signin_view_controller()
      ->ShowCrossDeviceSigninQrBubble(closing_callback.Get());
  views::Widget* bubble_widget = widget_future.Get();

  ASSERT_TRUE(bubble_widget);
  EXPECT_TRUE(bubble_widget->IsVisible());

  views::test::WidgetDestroyedWaiter waiter(bubble_widget);
  identity_test_env()->UpdatePersistentErrorOfRefreshTokenForAccount(
      account_info.GetAccountId(),
      GoogleServiceAuthError::FromInvalidGaiaCredentialsReason(
          GoogleServiceAuthError::InvalidGaiaCredentialsReason::UNKNOWN));
  waiter.Wait();
}

#endif  // BUILDFLAG(ENABLE_DICE_SUPPORT)

IN_PROC_BROWSER_TEST_F(SigninViewControllerBrowserTest,
                       ShowModalManagedUserNoticeDialog) {
  AccountInfo account_info =
      AccountInfo::Builder(GaiaId("gaia_id"), "email@example.com").Build();
  base::MockCallback<signin::SigninChoiceCallback>
      mock_process_user_choice_callback;
  base::MockCallback<base::OnceClosure> mock_done_callback;
  browser()
      ->GetFeatures()
      .signin_view_controller()
      ->ShowModalManagedUserNoticeDialog(
          std::make_unique<signin::EnterpriseProfileCreationDialogParams>(
              account_info,
              /*is_oidc_account=*/false,
              /*user_already_signed_in=*/false,
              /*profile_creation_required_by_policy=*/false,
              /*show_link_data_option=*/false,
              /*process_user_choice_callback=*/
              mock_process_user_choice_callback.Get(),
              mock_done_callback.Get()));
  EXPECT_FALSE(ManagedProfileRequiredNavigationThrottle::IsBlockingNavigations(
      browser()->GetProfile()));
  browser()->GetFeatures().signin_view_controller()->CloseModalSignin();
  EXPECT_FALSE(ManagedProfileRequiredNavigationThrottle::IsBlockingNavigations(
      browser()->GetProfile()));

  browser()
      ->GetFeatures()
      .signin_view_controller()
      ->ShowModalManagedUserNoticeDialog(
          std::make_unique<signin::EnterpriseProfileCreationDialogParams>(
              account_info,
              /*is_oidc_account=*/false,
              /*user_already_signed_in=*/false,
              /*profile_creation_required_by_policy=*/true,
              /*show_link_data_option=*/false,
              /*process_user_choice_callback=*/
              mock_process_user_choice_callback.Get(),
              mock_done_callback.Get()));
  EXPECT_TRUE(ManagedProfileRequiredNavigationThrottle::IsBlockingNavigations(
      browser()->GetProfile()));
  browser()->GetFeatures().signin_view_controller()->CloseModalSignin();
  EXPECT_FALSE(ManagedProfileRequiredNavigationThrottle::IsBlockingNavigations(
      browser()->GetProfile()));
}

#if BUILDFLAG(ENABLE_EXTENSIONS)

// A browser test with interactive steps used to test the signout confirmation
// dialog.
class SigninViewControllerInteractiveBrowserTest
    : public SigninBrowserTestBaseT<
          WebUiInteractiveTestMixin<InteractiveBrowserTest>>,
      public testing::WithParamInterface<bool> {
 public:
  SigninViewControllerInteractiveBrowserTest() {
    base::FilePath test_data_dir;
    if (!base::PathService::Get(chrome::DIR_TEST_DATA, &test_data_dir)) {
      ADD_FAILURE();
      return;
    }
    extension_data_dir_ = test_data_dir.AppendASCII("extensions");
  }

 protected:
  const DeepQuery kExtensionsSectionExpandButton = {
      "signout-confirmation-app", "extensions-section", "#expandButton"};
  const DeepQuery kExtensionsSectionCollapse = {
      "signout-confirmation-app", "extensions-section", "#collapse"};
  const DeepQuery kExtensionsSectionAccountExtensions = {
      "signout-confirmation-app", "extensions-section",
      "#account-extensions-list"};

  bool uninstall_account_extensions() const { return GetParam(); }

  const base::FilePath& extension_data_dir() const {
    return extension_data_dir_;
  }

  extensions::ExtensionRegistry* extension_registry() {
    return extensions::ExtensionRegistry::Get(GetProfile());
  }

  AccountInfo SetPrimaryAccount() {
    return identity_test_env()->MakePrimaryAccountAvailable(
        kTestEmail, signin::ConsentLevel::kSignin);
  }

  auto WaitForElementExists(const ui::ElementIdentifier& contents_id,
                            const DeepQuery& element) {
    StateChange element_exists;
    element_exists.type =
        WebContentsInteractionTestUtil::StateChange::Type::kExists;
    element_exists.event = kElementExists;
    element_exists.where = element;
    return WaitForStateChange(contents_id, element_exists);
  }

  // Show the signout confirmation dialog and instrument its internal
  // WebContents.
  auto ShowAndInstrumentSignoutConfirmationDialog() {
    return Steps(
        Do([&] {
          browser()
              ->GetFeatures()
              .signin_view_controller()
              ->SignoutOrReauthWithPrompt(
                  kTestAccessPoint,
                  signin_metrics::ProfileSignout::
                      kUserClickedSignoutProfileMenu,
                  signin_metrics::SourceForRefreshTokenOperation::
                      kUserMenu_SignOutAllAccounts);
        }),
        WaitForShow(
            SigninViewController::kSignoutConfirmationDialogViewElementId),
        Check([&] {
          return browser()
              ->GetFeatures()
              .signin_view_controller()
              ->ShowsModalDialog();
        }),
        InstrumentNonTabWebView(
            kWebContentsId,
            SigninViewController::kSignoutConfirmationDialogViewElementId));
  }

  // Waits for the dialog to be ready to uninstall account extensions.
  auto WaitForUninstallExtensionsChecked(
      const ui::ElementIdentifier& contents_id) {
    StateChange uninstall_extensions_checked;
    uninstall_extensions_checked.type = WebContentsInteractionTestUtil::
        StateChange::Type::kExistsAndConditionTrue;
    uninstall_extensions_checked.event = kChecked;
    uninstall_extensions_checked.test_function =
        "el => { return el.uninstallExtensionsOnSignoutForTesting(); }";
    uninstall_extensions_checked.where = {"signout-confirmation-app"};
    return WaitForStateChange(contents_id, uninstall_extensions_checked);
  }

  // Accept the dialog, which signs the user out. Optionally, check the checkbox
  // at `kCheckbox` which will specify that account extensions should be
  // uninstalled after signing out.
  auto AcceptDialogAndSignout() {
    const DeepQuery kAcceptButton = {"signout-confirmation-app",
                                     "#acceptButton"};
    const DeepQuery kCheckbox = {"signout-confirmation-app",
                                 "extensions-section", "#checkbox"};

    auto steps = Steps(
        ExecuteJsAt(kWebContentsId, kAcceptButton, "(el) => { el.click(); }"),
        // Verify that the dialog closes correctly.
        WaitForHide(
            SigninViewController::kSignoutConfirmationDialogViewElementId),
        CheckResult(
            [&] {
              return browser()
                  ->GetFeatures()
                  .signin_view_controller()
                  ->ShowsModalDialog();
            },
            false),
        // Verify that the user has signed out.
        CheckResult(
            [&] {
              return identity_manager()->HasPrimaryAccount(
                  signin::ConsentLevel::kSignin);
            },
            false));

    // Check the checkbox for uninstalling account extensions in the dialog and
    // wait for the proper state to propagate.
    if (uninstall_account_extensions()) {
      auto steps_plus_click_checkbox = Steps(
          ExecuteJsAt(kWebContentsId, kCheckbox, "(el) => { el.click(); }"),
          WaitForUninstallExtensionsChecked(kWebContentsId));
      steps_plus_click_checkbox += std::move(steps);
      return steps_plus_click_checkbox;
    }

    return steps;
  }

  // Checks if the extension with the given `id` is installed.
  auto CheckExtensionInstalled(const extensions::ExtensionId& id,
                               bool installed) {
    return CheckResult(
        [&]() {
          return extension_registry()->GetInstalledExtension(id) != nullptr;
        },
        installed);
  }

  // Loads the extension from `extension_path`.
  auto LoadExtension(const std::string& extension_path) {
    extensions::ChromeTestExtensionLoader extension_loader(GetProfile());
    extension_loader.set_pack_extension(true);
    return extension_loader.LoadExtension(
        extension_data_dir().AppendASCII(extension_path));
  }

 private:
  // chrome/test/data/extensions/
  base::FilePath extension_data_dir_;
};

// Test that the user's installed account extensions are shown in the signout
// confirmation prompt, then test accepting the dialog with two outcomes based
// on the test variant:
// - UninstallAccountExtensions: account extensions are uninstalled after
//   signing out.
// - KeepAccountExtensions: account extensions are kept after signing out.
IN_PROC_BROWSER_TEST_P(SigninViewControllerInteractiveBrowserTest,
                       ShowAccountExtensionsInSignoutPrompt) {
  // TODO(https://crbug.com/40804030): Remove this when updated to use MV3.
  extensions::ScopedTestMV2Enabler mv2_enabler;

  // Install a local extension; it should not be shown in the list of account
  // extensions in the dialog.
  scoped_refptr<const extensions::Extension> local_extension =
      LoadExtension("simple_with_file");
  ASSERT_TRUE(local_extension);
  auto local_extension_id = local_extension->id();

  // Setup a primary account.
  extensions::signin_test_util::SimulateExplicitSignIn(
      GetProfile(), identity_test_env(), kTestEmail);

  // Verify that the user can sync extensions while in transport mode.
  ASSERT_TRUE(
      extensions::sync_util::IsSyncingExtensionsInTransportMode(GetProfile()));

  // Install two account extensions: both should eventually be shown in the
  // dialog.
  scoped_refptr<const extensions::Extension> first_account_extension =
      LoadExtension("simple_with_host");
  ASSERT_TRUE(first_account_extension);
  auto first_account_extension_id = first_account_extension->id();

  scoped_refptr<const extensions::Extension> second_account_extension =
      LoadExtension("simple_with_icon");
  ASSERT_TRUE(second_account_extension);
  auto second_account_extension_id = second_account_extension->id();

  const int expected_num_account_extensions = 2;

  const char* get_num_shown_account_extensions = R"((el) => {
    if (!el.opened) { return -1; }
    return el.querySelectorAll('.account-extension').length;
  })";

  base::HistogramTester histogram_tester;

  // Test sequence setup:
  // - User is signed in and is about to sign out via confirmation prompt.
  // - User has two account extensions installed while signed in.
  RunTestSequence(
      // Show the dialog and verify that it has shown.
      ShowAndInstrumentSignoutConfirmationDialog(),

      // Within the dialog, verify that the extensions section is visible but
      // the list of account extensions is collapsed.
      WaitForElementExists(kWebContentsId, kExtensionsSectionExpandButton),
      CheckJsResultAt(kWebContentsId, kExtensionsSectionCollapse,
                      "el => el.opened", false),

      // Click the expand button to open the list of account extensions.
      ExecuteJsAt(kWebContentsId, kExtensionsSectionExpandButton,
                  "(el) => { el.click(); }"),
      WaitForElementExists(kWebContentsId, kExtensionsSectionAccountExtensions),

      // There should be `expected_num_account_extensions` shown in the list.
      CheckJsResultAt(kWebContentsId, kExtensionsSectionCollapse,
                      get_num_shown_account_extensions,
                      expected_num_account_extensions),

      // Now accept the dialog and sign out.
      AcceptDialogAndSignout(),

      // The local extension should always still be installed.
      CheckExtensionInstalled(local_extension_id, true),

      // The account extensions should be uninstalled if the user chose to
      // uninstall them from the dialog based on uninstall_account_extensions().
      CheckExtensionInstalled(first_account_extension_id,
                              !uninstall_account_extensions()),
      CheckExtensionInstalled(second_account_extension_id,
                              !uninstall_account_extensions()));

  AccountExtensionsSignoutChoice choice =
      uninstall_account_extensions()
          ? AccountExtensionsSignoutChoice::kSignoutAccountExtensionsUninstalled
          : AccountExtensionsSignoutChoice::kSignoutAccountExtensionsKept;
  histogram_tester.ExpectUniqueSample(
      kAccountExtensionsSignoutChoiceHistogramName, choice, 1);
}

// Test that the signout confirmation dialog will show account extensions if the
// user has disabled extensions syncing and that the user can choose to
// uninstall them on signout.
IN_PROC_BROWSER_TEST_P(SigninViewControllerInteractiveBrowserTest,
                       ShowAccountExtensionsSyncDisabled) {
  // TODO(https://crbug.com/40804030): Remove this when updated to use MV3.
  extensions::ScopedTestMV2Enabler mv2_enabler;

  // Setup a primary account.
  extensions::signin_test_util::SimulateExplicitSignIn(
      GetProfile(), identity_test_env(), kTestEmail);

  // Verify that the user can sync extensions while in transport mode.
  ASSERT_TRUE(
      extensions::sync_util::IsSyncingExtensionsInTransportMode(GetProfile()));

  // Install an account extension.
  scoped_refptr<const extensions::Extension> account_extension =
      LoadExtension("simple_with_host");
  ASSERT_TRUE(account_extension);
  auto account_extension_id = account_extension->id();

  // Disable extension syncing for this user.
  syncer::SyncService* sync_service =
      SyncServiceFactory::GetForProfile(GetProfile());
  sync_service->GetUserSettings()->SetSelectedType(
      syncer::UserSelectableType::kExtensions, false);

  // Install another extension; it should not be treated as an account
  // extension.
  scoped_refptr<const extensions::Extension> non_account_extension =
      LoadExtension("simple_with_icon");
  ASSERT_TRUE(non_account_extension);
  auto non_account_extension_id = non_account_extension->id();

  // Test sequence setup:
  // - User is signed in and is about to sign out via confirmation prompt.
  // - User has one account extension and one non-account extension installed
  //   while signed in.
  RunTestSequence(
      // Show the dialog and verify that it has shown.
      ShowAndInstrumentSignoutConfirmationDialog(),

      // Within the dialog, verify that the extensions section is visible
      // despite extensions not currently being synced.
      WaitForElementExists(kWebContentsId, kExtensionsSectionExpandButton),

      // Now accept the dialog and sign out.
      AcceptDialogAndSignout(),

      // The local extension should always still be installed.
      CheckExtensionInstalled(non_account_extension_id, true),

      // The account extension should be uninstalled if the user chose to
      // uninstall them from the dialog based on uninstall_account_extensions().
      CheckExtensionInstalled(account_extension_id,
                              !uninstall_account_extensions()));
}

INSTANTIATE_TEST_SUITE_P(,
                         SigninViewControllerInteractiveBrowserTest,
                         testing::Bool(),
                         [](const ::testing::TestParamInfo<bool>& info) {
                           return info.param ? "UninstallAccountExtensions"
                                             : "KeepAccountExtensions";
                         });

#endif  // BUILDFLAG(ENABLE_EXTENSIONS)

class SigninViewControllerBrowserCookieParamTest
    : public SigninViewControllerBrowserTest,
      public testing::WithParamInterface<bool> {
 public:
  bool with_cookies() const { return GetParam(); }
};

IN_PROC_BROWSER_TEST_P(SigninViewControllerBrowserCookieParamTest, SignOut) {
  // Setup a primary account, and cookie if requested.
  AccountInfo primary_account_info = SetPrimaryAccount();
  if (with_cookies()) {
    identity_test_env()->SetCookieAccounts(
        {{.email = kTestEmail,
          .gaia_id = signin::GetTestGaiaIdForEmail(kTestEmail),
          .signed_out = false}});
  }
  identity_test_env()->SetFreshnessOfAccountsInGaiaCookie(true);

  // Trigger the Chrome signout action, and confirm the prompt.
  SignoutConfirmationUI* signout_confirmation_ui =
      TriggerSignoutAndWaitForConfirmationPrompt();
  ASSERT_TRUE(signout_confirmation_ui);
  signout_confirmation_ui->AcceptDialogForTesting();

  // User was signed out.
  EXPECT_FALSE(
      identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSignin));

  // Signout tab was opened only if cookies there were cookies for the account.
  content::WebContents* tab =
      browser()->GetTabStripModel()->GetActiveWebContents();
  ASSERT_TRUE(tab);
  EXPECT_EQ(IsSignoutTab(tab), with_cookies());
  EXPECT_FALSE(IsSigninTab(tab));
}

INSTANTIATE_TEST_SUITE_P(,
                         SigninViewControllerBrowserCookieParamTest,
                         testing::Bool(),
                         [](const ::testing::TestParamInfo<bool>& info) {
                           return info.param ? "WithCookie" : "NoCookie";
                         });
