// Copyright 2017 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/signin/dice_response_handler.h"

#include <memory>
#include <queue>
#include <string_view>
#include <utility>

#include "base/check.h"
#include "base/containers/to_vector.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/notreached.h"
#include "base/strings/stringprintf.h"
#include "base/test/gmock_move_support.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/mock_callback.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "components/prefs/pref_service.h"
#include "components/signin/core/browser/about_signin_internals.h"
#include "components/signin/core/browser/account_reconcilor.h"
#include "components/signin/core/browser/dice_account_reconcilor_delegate.h"
#include "components/signin/core/browser/dice_header_helper.h"
#include "components/signin/core/browser/signin_error_controller.h"
#include "components/signin/core/browser/signin_header_helper.h"
#include "components/signin/public/base/binding_key_registration_token_result.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/base/test_signin_client.h"
#include "components/signin/public/identity_manager/account_info.h"
#include "components/signin/public/identity_manager/identity_test_environment.h"
#include "components/signin/public/identity_manager/identity_test_utils.h"
#include "components/sync_preferences/testing_pref_service_syncable.h"
#include "components/unexportable_keys/fake_unexportable_key_service.h"
#include "components/unexportable_keys/unexportable_key_id.h"
#include "components/unexportable_keys/unexportable_key_service.h"
#include "components/unexportable_keys/unexportable_key_task_manager.h"
#include "crypto/signature_verifier.h"
#include "google_apis/gaia/core_account_id.h"
#include "google_apis/gaia/gaia_id.h"
#include "google_apis/gaia/gaia_urls.h"
#include "net/http/http_response_headers.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

using signin::DiceAction;
using signin::DiceResponseParams;
using testing::_;
using testing::Return;
using testing::StrictMock;
using testing::Unused;

namespace {

constexpr char kAuthorizationCode[] = "authorization_code";
constexpr char kEmail[] = "test@email.com";
constexpr int kSessionIndex = 42;
constexpr char kEligibleForTokenBinding[] = "ES256 RS256";

constexpr char kTokenBindingOutcomeHistogram[] =
    "Signin.DiceTokenBindingOutcome";

DiceResponseParams::AccountInfo GetDiceResponseParamsAccountInfo(
    const std::string& email) {
  DiceResponseParams::AccountInfo account_info;
  account_info.gaia_id = signin::GetTestGaiaIdForEmail(email);
  account_info.email = email;
  account_info.session_index = kSessionIndex;
  return account_info;
}

// TestSigninClient implementation that intercepts the GaiaAuthConsumer and
// replaces it by a dummy one.
class DiceTestSigninClient : public TestSigninClient, public GaiaAuthConsumer {
 public:
  explicit DiceTestSigninClient(PrefService* pref_service)
      : TestSigninClient(pref_service) {}

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

  ~DiceTestSigninClient() override = default;

  std::unique_ptr<GaiaAuthFetcher> CreateGaiaAuthFetcher(
      GaiaAuthConsumer* consumer,
      gaia::GaiaSource source) override {
    consumers_.push(consumer);

    // Pass |this| as a dummy consumer to CreateGaiaAuthFetcher().
    // Since DiceTestSigninClient does not overrides any consumer method,
    // everything will be dropped on the floor.
    return TestSigninClient::CreateGaiaAuthFetcher(this, source);
  }

  // Returns the next consumer in the queue (FIFO).
  GaiaAuthConsumer* GetAndClearConsumer() {
    if (consumers_.empty()) {
      return nullptr;
    }
    GaiaAuthConsumer* next_consumer = consumers_.front();
    consumers_.pop();
    return next_consumer;
  }

 private:
  std::queue<raw_ptr<GaiaAuthConsumer>> consumers_;
};

class DiceResponseHandlerTest : public testing::Test,
                                public AccountReconcilor::Observer {
 public:
  // Called after the refresh token was fetched and added in the token service.
  void HandleTokenExchangeSuccess(CoreAccountId account_id,
                                  bool is_new_account,
                                  signin::Tribool primary_is_connected) {
    token_exchange_account_id_ = account_id;
    token_exchange_is_new_account_ = is_new_account;
    token_exchange_primary_is_connected_ = primary_is_connected;
  }

  // Called after the refresh token was fetched and added in the token service.
  void CompleteChromeSignInAfterGaiaSignin(
      const CoreAccountInfo& account_info) {
    complete_profile_signin_account_info_ = account_info;
  }

  void HandleTokenExchangeFailure(const std::string& email,
                                  const GoogleServiceAuthError& error) {
    auth_error_email_ = email;
    auth_error_ = error;
  }

  void OnDiceSigninSessionComplete(
      std::vector<CoreAccountId> secondary_accounts) {
    session_complete_called_ = true;
    completed_secondary_accounts_ = std::move(secondary_accounts);
  }

 protected:
  DiceResponseHandlerTest()
      : task_environment_(
            base::test::SingleThreadTaskEnvironment::MainThreadType::IO,
            base::test::SingleThreadTaskEnvironment::TimeSource::
                MOCK_TIME),  // URLRequestContext requires IO.
        signin_client_(&pref_service_),
        identity_test_env_(/*test_url_loader_factory=*/nullptr,
                           &pref_service_,
                           &signin_client_),
        signin_error_controller_(
            SigninErrorController::AccountMode::PRIMARY_ACCOUNT,
            identity_test_env_.identity_manager()) {
    EXPECT_TRUE(temp_dir_.CreateUniqueTempDir());
    AboutSigninInternals::RegisterPrefs(pref_service_.registry());
    auto account_reconcilor_delegate =
        std::make_unique<signin::DiceAccountReconcilorDelegate>(
            identity_manager());
    account_reconcilor_ = std::make_unique<AccountReconcilor>(
        identity_test_env_.identity_manager(), &signin_client_,
        std::move(account_reconcilor_delegate));
    account_reconcilor_->AddObserver(this);

    about_signin_internals_ = std::make_unique<AboutSigninInternals>(
        identity_test_env_.identity_manager(), &signin_error_controller_,
        signin::AccountConsistencyMethod::kDice, &signin_client_,
        account_reconcilor_.get());

    dice_response_handler_ = std::make_unique<DiceResponseHandler>(
        &signin_client_, identity_test_env_.identity_manager(),
        account_reconcilor_.get(), about_signin_internals_.get());
  }

  ~DiceResponseHandlerTest() override {
    dice_response_handler_.reset();
    account_reconcilor_->RemoveObserver(this);
    account_reconcilor_->Shutdown();
    about_signin_internals_->Shutdown();
    signin_error_controller_.Shutdown();
  }

  DiceResponseParams MakeMultiSigninDiceParams(
      DiceAction action,
      int account_count,
      signin::Tribool primary_is_connected,
      bool eligible_for_token_binding = true,
      bool mtls_token_binding = false,
      int initiator_index = 0) {
    DiceResponseParams dice_params;
    DiceResponseParams::AccountInfo account_info =
        GetDiceResponseParamsAccountInfo(kEmail);
    switch (action) {
      case DiceAction::SIGNIN: {
        DiceResponseParams::SigninInfo* signin_info =
            &dice_params.data.emplace<DiceResponseParams::SigninInfo>();
        std::string binding_supported = eligible_for_token_binding
                                            ? kEligibleForTokenBinding
                                            : std::string();
        signin_info->AddAccount({account_info, kAuthorizationCode,
                                 /*no_authorization_code=*/false,
                                 binding_supported, mtls_token_binding});
        for (int i = 1; i < account_count; ++i) {
          std::string email = base::StringPrintf("other%d@gmail.com", i);
          signin_info->AddAccount({GetDiceResponseParamsAccountInfo(email),
                                   base::StringPrintf("other_code_%d", i),
                                   false, binding_supported,
                                   mtls_token_binding});
        }
        if (account_count > 0) {
          signin::DiceResponseParams::SigninInfo::LinkedAccountsMetadata
              metadata;
          CHECK_LT(initiator_index, account_count);
          metadata.initiator_id =
              signin_info->accounts()[initiator_index].account_info.gaia_id;
          metadata.primary_is_connected = primary_is_connected;
          signin_info->set_linked_accounts_metadata(std::move(metadata));
        }
        break;
      }
      case DiceAction::ENABLE_SYNC: {
        DiceResponseParams::EnableSyncInfo* enable_sync_info =
            &dice_params.data.emplace<DiceResponseParams::EnableSyncInfo>();
        enable_sync_info->account_info = account_info;
        break;
      }
      case DiceAction::SIGNOUT: {
        DiceResponseParams::SignoutInfo* signout_info =
            &dice_params.data.emplace<DiceResponseParams::SignoutInfo>();
        signout_info->account_infos.push_back(account_info);
        break;
      }
      case DiceAction::NONE:
        NOTREACHED();
    }
    return dice_params;
  }

  DiceResponseParams MakeDiceParams(DiceAction action,
                                    bool eligible_for_token_binding = true,
                                    bool mtls_token_binding = false) {
    return MakeMultiSigninDiceParams(
        action, /*account_count=*/1,
        /*primary_is_connected=*/signin::Tribool::kUnknown,
        eligible_for_token_binding, mtls_token_binding,
        /*initiator_index=*/0);
  }

  sync_preferences::TestingPrefServiceSyncable& pref_service() {
    return pref_service_;
  }

  void RunSignoutTest(
      DiceResponseParams dice_params,
      const std::vector<CoreAccountId>& secondary_with_valid_refresh_tokens,
      const CoreAccountId& primary_account,
      bool invalid_primary_account);

  void EnableTokenBindingRegistration() {
    identity_test_env_.EnableTokenBindingRegistration();
  }

  void SimulateRegistrationTokenHelperResult(
      const std::string& authorization_code,
      std::optional<signin::BindingKeyRegistrationTokenResult> result) {
    identity_test_env_.IssueTokenBindingRegistrationTokenForAuthCode(
        authorization_code, std::move(result));
  }

  // AccountReconcilor::Observer:
  void OnBlockReconcile() override { ++reconcilor_blocked_count_; }
  void OnUnblockReconcile() override { ++reconcilor_unblocked_count_; }

  signin::IdentityManager* identity_manager() {
    return identity_test_env_.identity_manager();
  }

  base::test::SingleThreadTaskEnvironment task_environment_;
  base::ScopedTempDir temp_dir_;
  sync_preferences::TestingPrefServiceSyncable pref_service_;
  DiceTestSigninClient signin_client_;
  signin::IdentityTestEnvironment identity_test_env_;
  SigninErrorController signin_error_controller_;
  std::unique_ptr<AboutSigninInternals> about_signin_internals_;
  std::unique_ptr<AccountReconcilor> account_reconcilor_;
  int reconcilor_blocked_count_ = 0;
  int reconcilor_unblocked_count_ = 0;
  CoreAccountId token_exchange_account_id_;
  bool token_exchange_is_new_account_ = false;
  signin::Tribool token_exchange_primary_is_connected_ =
      signin::Tribool::kUnknown;
  CoreAccountInfo complete_profile_signin_account_info_;
  GoogleServiceAuthError auth_error_;
  std::string auth_error_email_;
  base::test::ScopedFeatureList feature_list_{
      switches::kEnableChromeRefreshTokenBinding};
  base::HistogramTester histogram_tester_;
  bool session_complete_called_ = false;
  std::vector<CoreAccountId> completed_secondary_accounts_;
  std::unique_ptr<DiceResponseHandler> dice_response_handler_;
};

class TestProcessDiceHeaderDelegate : public ProcessDiceHeaderDelegate {
 public:
  explicit TestProcessDiceHeaderDelegate(DiceResponseHandlerTest* owner)
      : owner_(owner) {}

  ~TestProcessDiceHeaderDelegate() override = default;

  // Called after the refresh token was fetched and added in the token service.
  void HandleTokenExchangeSuccess(
      CoreAccountId account_id,
      bool is_new_account,
      signin::Tribool primary_is_connected) override {
    owner_->HandleTokenExchangeSuccess(account_id, is_new_account,
                                       primary_is_connected);
  }

  // Called after the refresh token was fetched and added in the token service.
  void CompleteChromeSignInAfterGaiaSignin(
      const CoreAccountInfo& account_info) override {
    owner_->CompleteChromeSignInAfterGaiaSignin(account_info);
  }

  void HandleTokenExchangeFailure(
      const std::string& email,
      const GoogleServiceAuthError& error) override {
    owner_->HandleTokenExchangeFailure(email, error);
  }

  signin_metrics::AccessPoint GetAccessPoint() override {
    return signin_metrics::AccessPoint::kSettings;
  }

  void OnDiceSigninHeaderReceived() override {}

  void OnDiceSigninSessionComplete(
      std::vector<CoreAccountId> secondary_accounts) override {
    owner_->OnDiceSigninSessionComplete(std::move(secondary_accounts));
  }

 private:
  raw_ptr<DiceResponseHandlerTest> owner_;
};

void DiceResponseHandlerTest::RunSignoutTest(
    DiceResponseParams dice_params,
    const std::vector<CoreAccountId>& secondary_with_valid_refresh_tokens,
    const CoreAccountId& primary_account,
    bool invalid_primary_account) {
  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));

  // Only the token corresponding the the Dice parameter has been removed, and
  // the user is still signed in.
  bool has_primary_account = !primary_account.empty();
  size_t expected_accounts_with_refresh_tokens =
      secondary_with_valid_refresh_tokens.size() +
      (has_primary_account ? 1U : 0U);
  EXPECT_EQ(identity_manager()->GetAccountsWithRefreshTokens().size(),
            expected_accounts_with_refresh_tokens);
  for (const CoreAccountId& account_id : secondary_with_valid_refresh_tokens) {
    SCOPED_TRACE(account_id.ToString());
    EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id));
    EXPECT_FALSE(
        identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState(
            account_id));
  }

  CHECK(!invalid_primary_account || has_primary_account);
  if (has_primary_account) {
    EXPECT_EQ(
        identity_manager()->GetPrimaryAccountId(signin::ConsentLevel::kSignin),
        primary_account);
    EXPECT_EQ(
        identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState(
            primary_account),
        invalid_primary_account);
  } else if (identity_manager()->HasPrimaryAccount(
                 signin::ConsentLevel::kSignin)) {
    // In the unittest `RemoveAccount()` will not lead to the primary account
    // being removed. Check there is no refresh token instead.
    EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken(
        identity_manager()->GetPrimaryAccountId(
            signin::ConsentLevel::kSignin)));
  }

  if (invalid_primary_account) {
    auto error = identity_manager()->GetErrorStateOfRefreshTokenForAccount(
        primary_account);
    EXPECT_EQ(error.state(), GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS);
    EXPECT_EQ(error.GetInvalidGaiaCredentialsReason(),
              GoogleServiceAuthError::InvalidGaiaCredentialsReason::
                  CREDENTIALS_REJECTED_BY_CLIENT);
  }
}

class DiceResponseHandlerParamTest
    : public DiceResponseHandlerTest,
      public testing::WithParamInterface<size_t> {
 public:
  size_t GetAccountCount() const { return GetParam(); }
};

INSTANTIATE_TEST_SUITE_P(All,
                         DiceResponseHandlerParamTest,
                         testing::Values(1, 2, 4));

TEST_P(DiceResponseHandlerParamTest, Signin_PrimaryConnected) {
  base::HistogramTester histogram_tester;
  identity_test_env_.MakePrimaryAccountAvailable("primary@example.com",
                                                 signin::ConsentLevel::kSignin);
  const size_t account_count = GetAccountCount();
  const int initiator_index = 0;
  DiceResponseParams dice_params = MakeMultiSigninDiceParams(
      DiceAction::SIGNIN, account_count,
      /*primary_is_connected=*/signin::Tribool::kTrue);

  std::vector<CoreAccountId> account_ids;
  for (const auto& account : dice_params.signin_info()->accounts()) {
    account_ids.push_back(identity_manager()->PickAccountIdForAccount(
        account.account_info.gaia_id, account.account_info.email));
  }

  for (const auto& id : account_ids) {
    EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken(id));
  }

  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));

  histogram_tester.ExpectUniqueSample(
      "Signin.Dice.InvalidPrimaryConnectedInUnsignedProfile", false, 1);

  // Check that GaiaAuthFetchers have been created and URL is correct.
  for (size_t i = 0; i < account_count; ++i) {
    EXPECT_EQ(signin_client_.GetTestURLLoaderFactory()
                  ->GetPendingRequest(i)
                  ->request.url,
              GaiaUrls::GetInstance()->oauth2_token_url());
  }

  EXPECT_EQ(1, reconcilor_blocked_count_);
  EXPECT_EQ(0, reconcilor_unblocked_count_);

  // Simulate GaiaAuthFetcher success for all accounts.
  for (size_t i = 0; i < account_count; ++i) {
    GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer();
    ASSERT_THAT(consumer, testing::NotNull());

    // Simulate GaiaAuthFetcher success.
    consumer->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
        base::StringPrintf("refresh_token_%zu", i), "access_token",
        /*expires_in_secs=*/10,
        /*is_under_advanced_protection=*/true, /*is_bound_to_key=*/false));
  }

  // Check that the tokens have been inserted in the token service.
  for (const auto& id : account_ids) {
    EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(id));
  }
  EXPECT_TRUE(auth_error_email_.empty());
  EXPECT_EQ(GoogleServiceAuthError::NONE, auth_error_.state());

  // Check HandleTokenExchangeSuccess parameters.
  // The implementation only calls it for the initiator.
  EXPECT_EQ(token_exchange_account_id_, account_ids[initiator_index]);
  EXPECT_TRUE(token_exchange_is_new_account_);
  EXPECT_EQ(token_exchange_primary_is_connected_, signin::Tribool::kTrue);

  // Check that the reconcilor was blocked and unblocked exactly once.
  EXPECT_EQ(1, reconcilor_blocked_count_);
  EXPECT_EQ(1, reconcilor_unblocked_count_);

  // Check that the AccountInfo::is_under_advanced_protection is set for all
  // accounts.
  for (const auto& id : account_ids) {
    AccountInfo extended_account_info =
        identity_manager()->FindExtendedAccountInfoByAccountId(id);
    EXPECT_TRUE(extended_account_info.IsUnderAdvancedProtection());
    // Check that the AccessPoint was propagated from the delegate.
    EXPECT_EQ(extended_account_info.GetLastAuthenticationAccessPoint(),
              signin_metrics::AccessPoint::kSettings);
  }
  EXPECT_EQ(
      identity_test_env_.GetNumCallsToPrepareForFetchingAccountCapabilities(),
      1);
  histogram_tester_.ExpectUniqueSample(
      kTokenBindingOutcomeHistogram,
      DiceResponseHandler::TokenBindingOutcome::kNotBoundNotSupported,
      /*expected_bucket_count=*/account_count);
}

TEST_P(DiceResponseHandlerParamTest, Signin_PrimaryNotConnected_Success) {
  const size_t account_count = GetAccountCount();
  if (account_count < 2) {
    GTEST_SKIP() << "This test requires at least 2 accounts to verify "
                    "sequential behavior.";
  }
  const int initiator_index =
      1;  // Use non-zero index to test that initiator doesn't have to be first.
  DiceResponseParams dice_params = MakeMultiSigninDiceParams(
      DiceAction::SIGNIN, account_count,
      /*primary_is_connected=*/signin::Tribool::kFalse,
      /*eligible_for_token_binding=*/true,
      /*mtls_token_binding=*/false, initiator_index);

  std::vector<CoreAccountId> account_ids;
  for (const auto& account : dice_params.signin_info()->accounts()) {
    account_ids.push_back(identity_manager()->PickAccountIdForAccount(
        account.account_info.gaia_id, account.account_info.email));
  }

  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));

  // Only the initiator fetcher should be created initially.
  EXPECT_EQ(signin_client_.GetTestURLLoaderFactory()->NumPending(), 1);

  // Simulate GaiaAuthFetcher success for initiator.
  GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer();
  ASSERT_THAT(consumer, testing::NotNull());
  consumer->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
      "refresh_token_1", "access_token",
      /*expires_in_secs=*/10,
      /*is_under_advanced_protection=*/true, /*is_bound_to_key=*/false));

  // Check that initiator token has been inserted before secondaries finish.
  EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(
      account_ids[initiator_index]));

  // Now the rest of the fetchers should be created.
  EXPECT_EQ(signin_client_.GetTestURLLoaderFactory()->NumPending(),
            account_count - 1);

  // Simulate success for the rest.
  for (size_t i = 0; i < account_count - 1; ++i) {
    consumer = signin_client_.GetAndClearConsumer();
    ASSERT_THAT(consumer, testing::NotNull());
    consumer->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
        "refresh_token", "access_token",
        /*expires_in_secs=*/10,
        /*is_under_advanced_protection=*/true, /*is_bound_to_key=*/false));
  }

  // Check that all tokens have been inserted.
  for (const auto& id : account_ids) {
    EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(id));
  }

  // Check HandleTokenExchangeSuccess parameters.
  EXPECT_EQ(token_exchange_account_id_, account_ids[initiator_index]);
  EXPECT_EQ(token_exchange_primary_is_connected_, signin::Tribool::kFalse);
}

TEST_P(DiceResponseHandlerParamTest,
       Signin_PrimaryNotConnected_InitiatorFailure) {
  const size_t account_count = GetAccountCount();
  if (account_count < 2) {
    GTEST_SKIP() << "This test requires at least 2 accounts to verify "
                    "sequential behavior.";
  }
  const int initiator_index = 1;  // Use non-zero index.
  DiceResponseParams dice_params = MakeMultiSigninDiceParams(
      DiceAction::SIGNIN, account_count,
      /*primary_is_connected=*/signin::Tribool::kFalse,
      /*eligible_for_token_binding=*/true,
      /*mtls_token_binding=*/false, initiator_index);

  std::vector<CoreAccountId> account_ids;
  for (const auto& account : dice_params.signin_info()->accounts()) {
    account_ids.push_back(identity_manager()->PickAccountIdForAccount(
        account.account_info.gaia_id, account.account_info.email));
  }

  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));

  // Only initiator fetcher created.
  EXPECT_EQ(signin_client_.GetTestURLLoaderFactory()->NumPending(), 1);

  // Simulate GaiaAuthFetcher failure for initiator.
  GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer();
  ASSERT_THAT(consumer, testing::NotNull());
  consumer->OnClientOAuthFailure(GoogleServiceAuthError::FromServiceError(""));

  // No more fetchers should be created.
  EXPECT_EQ(signin_client_.GetTestURLLoaderFactory()->NumPending(), 0);

  // Session should be deleted, count should be 0.
  EXPECT_EQ(
      0u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());

  // Tokens should not be inserted.
  for (const auto& id : account_ids) {
    EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken(id));
  }
}

TEST_P(DiceResponseHandlerParamTest,
       Signin_PrimaryNotConnected_PartialFailure) {
  const size_t account_count = GetAccountCount();
  if (account_count < 2) {
    GTEST_SKIP()
        << "This test requires at least 2 accounts to verify partial failure.";
  }
  const int initiator_index = 1;  // Use non-zero index.
  DiceResponseParams dice_params = MakeMultiSigninDiceParams(
      DiceAction::SIGNIN, account_count,
      /*primary_is_connected=*/signin::Tribool::kFalse,
      /*eligible_for_token_binding=*/true,
      /*mtls_token_binding=*/false, initiator_index);

  std::vector<CoreAccountId> account_ids;
  for (const auto& account : dice_params.signin_info()->accounts()) {
    account_ids.push_back(identity_manager()->PickAccountIdForAccount(
        account.account_info.gaia_id, account.account_info.email));
  }

  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));

  // Only initiator fetcher created.
  EXPECT_EQ(signin_client_.GetTestURLLoaderFactory()->NumPending(), 1);

  // Simulate GaiaAuthFetcher success for initiator (account 1).
  GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer();
  ASSERT_THAT(consumer, testing::NotNull());
  consumer->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
      "refresh_token_1", "access_token",
      /*expires_in_secs=*/10,
      /*is_under_advanced_protection=*/true, /*is_bound_to_key=*/false));

  // Now the rest of the fetchers should be created.
  EXPECT_EQ(signin_client_.GetTestURLLoaderFactory()->NumPending(),
            account_count - 1);

  // Simulate failure for the first secondary account (account 0).
  consumer = signin_client_.GetAndClearConsumer();
  ASSERT_THAT(consumer, testing::NotNull());
  consumer->OnClientOAuthFailure(GoogleServiceAuthError::FromServiceError(""));

  // Simulate success for the remaining secondaries (if any).
  for (size_t i = 0; i < account_count - 2; ++i) {
    consumer = signin_client_.GetAndClearConsumer();
    ASSERT_THAT(consumer, testing::NotNull());
    consumer->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
        "refresh_token", "access_token",
        /*expires_in_secs=*/10,
        /*is_under_advanced_protection=*/true, /*is_bound_to_key=*/false));
  }

  // Check that initiator token has been inserted.
  EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(
      account_ids[initiator_index]));

  // Check that failed secondary token has NOT been inserted.
  EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_ids[0]));

  // Check that other successful secondaries have been inserted.
  for (size_t i = 2; i < account_count; ++i) {
    EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_ids[i]));
  }

  // Check HandleTokenExchangeSuccess parameters for initiator.
  EXPECT_EQ(token_exchange_account_id_, account_ids[initiator_index]);
  EXPECT_EQ(token_exchange_primary_is_connected_, signin::Tribool::kFalse);

  // Session should be deleted.
  EXPECT_EQ(
      0u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());
}

TEST_P(DiceResponseHandlerParamTest,
       SigninWithMtlsTokenBinding_PrimaryConnected) {
  base::test::ScopedFeatureList scoped_feature_list(
      switches::kEnableMtlsTokenBinding);
  identity_test_env_.MakePrimaryAccountAvailable("primary@example.com",
                                                 signin::ConsentLevel::kSignin);

  const size_t account_count = GetAccountCount();
  const int initiator_index = 0;
  DiceResponseParams dice_params =
      MakeMultiSigninDiceParams(DiceAction::SIGNIN, account_count,
                                /*primary_is_connected=*/signin::Tribool::kTrue,
                                /*eligible_for_token_binding=*/true,
                                /*mtls_token_binding=*/true, initiator_index);

  std::vector<CoreAccountId> account_ids;
  for (const auto& account : dice_params.signin_info()->accounts()) {
    account_ids.push_back(identity_manager()->PickAccountIdForAccount(
        account.account_info.gaia_id, account.account_info.email));
  }

  for (const auto& id : account_ids) {
    EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken(id));
  }

  // Check that GaiaAuthFetchers have been created.
  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));

  EXPECT_EQ(1, reconcilor_blocked_count_);
  EXPECT_EQ(0, reconcilor_unblocked_count_);

  for (size_t i = 0; i < account_count; ++i) {
    GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer();
    ASSERT_THAT(consumer, testing::NotNull());

    EXPECT_EQ(signin_client_.GetTestURLLoaderFactory()
                  ->GetPendingRequest(i)
                  ->request.url,
              GaiaUrls::GetInstance()->mtls_oauth2_token_url());

    // Simulate GaiaAuthFetcher success.
    consumer->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
        base::StringPrintf("refresh_token_%zu", i), "access_token",
        /*expires_in_secs=*/10,
        /*is_under_advanced_protection=*/true, /*is_bound_to_key=*/false));
  }

  // Check that the tokens have been inserted in the token service.
  for (const auto& id : account_ids) {
    EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(id));
    EXPECT_TRUE(identity_manager()->HasAccountWithRefreshTokenBoundToMtls(id));
  }
}

// Checks that a SIGNIN action triggers a token exchange request.
TEST_P(DiceResponseHandlerParamTest, SigninWithBoundToken_PrimaryConnected) {
  identity_test_env_.MakePrimaryAccountAvailable("primary@example.com",
                                                 signin::ConsentLevel::kSignin);
  const size_t account_count = GetAccountCount();
  EnableTokenBindingRegistration();
  DiceResponseParams dice_params = MakeMultiSigninDiceParams(
      DiceAction::SIGNIN, account_count,
      /*primary_is_connected=*/signin::Tribool::kTrue);

  std::vector<CoreAccountId> account_ids;
  std::vector<std::string> authorization_codes;
  for (const auto& account : dice_params.signin_info()->accounts()) {
    account_ids.push_back(identity_manager()->PickAccountIdForAccount(
        account.account_info.gaia_id, account.account_info.email));
    authorization_codes.push_back(account.authorization_code);
  }

  for (const auto& id : account_ids) {
    EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken(id));
  }

  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));

  // Token fetch should be blocked on the binding registration token generation.
  ASSERT_THAT(signin_client_.GetAndClearConsumer(), testing::IsNull());

  // Simulate successful token generation for all accounts.
  const std::vector<uint8_t> kWrappedKey = {1, 2, 3};
  for (const auto& code : authorization_codes) {
    SimulateRegistrationTokenHelperResult(
        code, signin::BindingKeyRegistrationTokenResult(
                  unexportable_keys::UnexportableSigningKeyId(), kWrappedKey,
                  "test_registration_token"));
  }

  // Check that GaiaAuthFetchers have been created.
  for (size_t i = 0; i < account_count; ++i) {
    GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer();
    ASSERT_THAT(consumer, testing::NotNull());
    // Simulate GaiaAuthFetcher success.
    consumer->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
        base::StringPrintf("refresh_token_%zu", i), "access_token",
        /*expires_in_secs=*/10,
        /*is_under_advanced_protection=*/false, /*is_bound_to_key=*/true));
  }

  // Check that the tokens have been inserted in the token service.
  for (const auto& id : account_ids) {
    EXPECT_TRUE(identity_manager()->HasAccountWithBoundRefreshToken(id));
  }
  EXPECT_EQ(identity_manager()->GetWrappedBindingKey(), kWrappedKey);

  EXPECT_TRUE(auth_error_email_.empty());
  EXPECT_EQ(GoogleServiceAuthError::NONE, auth_error_.state());
  histogram_tester_.ExpectUniqueSample(
      kTokenBindingOutcomeHistogram,
      DiceResponseHandler::TokenBindingOutcome::kBound,
      /*expected_bucket_count=*/account_count);

  EXPECT_EQ(
      0u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());
}

// Checks that no token binding attempt is made when an account is ineligible
// for token binding.
TEST_F(DiceResponseHandlerTest, SigninIneligibleForTokenBinding) {
  EnableTokenBindingRegistration();
  DiceResponseParams dice_params;
  DiceResponseParams::SigninInfo* signin_info =
      &dice_params.data.emplace<DiceResponseParams::SigninInfo>();
  signin_info->AddAccount({GetDiceResponseParamsAccountInfo(kEmail),
                           kAuthorizationCode, false, std::string(), false});
  CoreAccountId account_id = identity_manager()->PickAccountIdForAccount(
      signin_info->GetInitiator()->account_info.gaia_id,
      signin_info->GetInitiator()->account_info.email);
  EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id));

  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));
  // Check that a GaiaAuthFetcher has been created immediately.
  GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer();
  ASSERT_THAT(consumer, testing::NotNull());
  // Simulate GaiaAuthFetcher success.
  consumer->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
      "refresh_token", "access_token", /*expires_in_secs=*/10,
      /*is_under_advanced_protection=*/true, /*is_bound_to_key=*/false));
  // Check that the token has been inserted in the token service and it is
  // unbound.
  EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id));
  EXPECT_FALSE(identity_manager()->HasAccountWithBoundRefreshToken(account_id));
  EXPECT_TRUE(auth_error_email_.empty());
  EXPECT_EQ(GoogleServiceAuthError::NONE, auth_error_.state());
  histogram_tester_.ExpectUniqueSample(
      kTokenBindingOutcomeHistogram,
      DiceResponseHandler::TokenBindingOutcome::kNotBoundNotEligible,
      /*expected_bucket_count=*/1);
}

// Checks that token binding is skipped if refresh tokens are not loaded yet.
TEST_F(DiceResponseHandlerTest, SigninWithUnloadedTokensDoesNotBind) {
  EnableTokenBindingRegistration();
  identity_test_env_.ResetToAccountsNotYetLoadedFromDiskState();
  DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNIN);
  CoreAccountId account_id = identity_manager()->PickAccountIdForAccount(
      dice_params.signin_info()->GetInitiator()->account_info.gaia_id,
      dice_params.signin_info()->GetInitiator()->account_info.email);
  EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id));

  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));
  // Check that a GaiaAuthFetcher has been created immediately.
  GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer();
  ASSERT_THAT(consumer, testing::NotNull());
  // Simulate GaiaAuthFetcher success.
  consumer->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
      "refresh_token", "access_token", /*expires_in_secs=*/10,
      /*is_under_advanced_protection=*/true, /*is_bound_to_key=*/false));
  // Check that the token has been inserted in the token service and it is
  // unbound.
  EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id));
  EXPECT_FALSE(identity_manager()->HasAccountWithBoundRefreshToken(account_id));
  EXPECT_TRUE(auth_error_email_.empty());
  EXPECT_EQ(GoogleServiceAuthError::NONE, auth_error_.state());
  histogram_tester_.ExpectUniqueSample(
      kTokenBindingOutcomeHistogram,
      DiceResponseHandler::TokenBindingOutcome::kNotBoundRefreshTokensNotLoaded,
      /*expected_bucket_count=*/1);
}

// Checks that Chrome will discard the binding key if the server didn't accept
// the binding key.
TEST_F(DiceResponseHandlerTest, SigninServerRejectedBinding) {
  EnableTokenBindingRegistration();
  DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNIN);
  const auto* account = dice_params.signin_info()->GetInitiator();
  ASSERT_TRUE(account);
  CoreAccountId account_id = identity_manager()->PickAccountIdForAccount(
      account->account_info.gaia_id, account->account_info.email);
  EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id));
  const std::string authorization_code = account->authorization_code;
  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));

  // Token fetch should be blocked on the binding registration token generation.
  ASSERT_THAT(signin_client_.GetAndClearConsumer(), testing::IsNull());
  // Simulate successful token generation.
  const std::vector<uint8_t> kWrappedKey = {1, 2, 3};
  SimulateRegistrationTokenHelperResult(
      authorization_code, signin::BindingKeyRegistrationTokenResult(
                              unexportable_keys::UnexportableSigningKeyId(),
                              kWrappedKey, "test_registration_token"));

  // Check that a GaiaAuthFetcher has been created.
  GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer();
  ASSERT_THAT(consumer, testing::NotNull());
  // Simulate GaiaAuthFetcher success with an unbound token.
  consumer->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
      "refresh_token", "access_token", /*expires_in_secs=*/10,
      /*is_under_advanced_protection=*/false, /*is_bound_to_key=*/false));
  // Check that the token has been inserted in the token service.
  EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id));
  EXPECT_FALSE(identity_manager()->HasAccountWithBoundRefreshToken(account_id));
  EXPECT_TRUE(auth_error_email_.empty());
  EXPECT_EQ(GoogleServiceAuthError::NONE, auth_error_.state());
  histogram_tester_.ExpectUniqueSample(
      kTokenBindingOutcomeHistogram,
      DiceResponseHandler::TokenBindingOutcome::kNotBoundServerRejectedKey,
      /*expected_bucket_count=*/1);
}

TEST_F(DiceResponseHandlerTest, ReuseBindingKeyOtherTokenIsBound) {
  EnableTokenBindingRegistration();
  const std::vector<uint8_t> kWrappedKey = {1, 2, 3};
  identity_test_env_.MakeAccountAvailable(
      signin::AccountAvailabilityOptionsBuilder()
          .WithRefreshTokenBindingInfo(signin::TokenBindingInfo(
              kWrappedKey, /*mtls_token_binding=*/false))
          .Build("other@email.com"));

  DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNIN);
  const auto* account = dice_params.signin_info()->GetInitiator();
  ASSERT_TRUE(account);
  CoreAccountId account_id = identity_manager()->PickAccountIdForAccount(
      account->account_info.gaia_id, account->account_info.email);
  const std::string authorization_code = account->authorization_code;
  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));

  // Verify that the next step can complete with the reused token.
  // Token fetch should be blocked on the binding registration token generation.
  ASSERT_THAT(signin_client_.GetAndClearConsumer(), testing::IsNull());
  // Simulate successful token generation.
  SimulateRegistrationTokenHelperResult(
      authorization_code, signin::BindingKeyRegistrationTokenResult(
                              unexportable_keys::UnexportableSigningKeyId(),
                              kWrappedKey, "test_registration_token"));
  // Check that a GaiaAuthFetcher has been created.
  GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer();
  ASSERT_THAT(consumer, testing::NotNull());
  // Simulate GaiaAuthFetcher success.
  consumer->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
      "refresh_token", "access_token", /*expires_in_secs=*/10,
      /*is_under_advanced_protection=*/false, /*is_bound_to_key=*/true));
  // Check that the token has been inserted in the token service.
  EXPECT_TRUE(identity_manager()->HasAccountWithBoundRefreshToken(account_id));
  EXPECT_EQ(identity_manager()->GetWrappedBindingKey(), kWrappedKey);
}

TEST_F(DiceResponseHandlerTest, ReuseBindingKeyOneTokenBoundOneNonBound) {
  EnableTokenBindingRegistration();
  const std::vector<uint8_t> kWrappedKey = {1, 2, 3};
  identity_test_env_.MakeAccountAvailable("nonbound@gmail.com");
  identity_test_env_.MakeAccountAvailable(
      signin::AccountAvailabilityOptionsBuilder()
          .WithRefreshTokenBindingInfo(signin::TokenBindingInfo(
              kWrappedKey, /*mtls_token_binding=*/false))
          .Build("bound@email.com"));

  DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNIN);
  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));
}

TEST_F(DiceResponseHandlerTest, NewBindingKeyOtherTokenIsNotBound) {
  EnableTokenBindingRegistration();
  identity_test_env_.MakeAccountAvailable("other@email.com");

  DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNIN);
  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));
}

TEST_F(DiceResponseHandlerTest, TwoFetchersReuseRegistrationTokenHelper) {
  EnableTokenBindingRegistration();
  auto account_id = [&](const DiceResponseParams& dice_params) {
    const auto* initiator = dice_params.signin_info()->GetInitiator();
    return identity_manager()->PickAccountIdForAccount(
        initiator->account_info.gaia_id, initiator->account_info.email);
  };
  auto authorization_code = [&](const DiceResponseParams& dice_params) {
    return dice_params.signin_info()->GetInitiator()->authorization_code;
  };

  DiceResponseParams dice_params_1 = MakeDiceParams(DiceAction::SIGNIN);
  DiceResponseParams dice_params_2;
  DiceResponseParams::SigninInfo* signin_info_2 =
      &dice_params_2.data.emplace<DiceResponseParams::SigninInfo>();
  signin_info_2->AddAccount(
      {GetDiceResponseParamsAccountInfo("other@email.com"),
       "other_authorization_code", false, kEligibleForTokenBinding, false});
  std::string code_1 = authorization_code(dice_params_1);
  std::string code_2 = authorization_code(dice_params_2);
  CoreAccountId id_1 = account_id(dice_params_1);
  CoreAccountId id_2 = account_id(dice_params_2);
  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params_1),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));
  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params_2),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));

  // Token fetch should be blocked on the binding registration token generation.
  ASSERT_THAT(signin_client_.GetAndClearConsumer(), testing::IsNull());
  // Simulate successful token generation and check that GaiaAuthFetchers have
  // been created.
  const std::vector<uint8_t> kWrappedKey = {1, 2, 3};
  SimulateRegistrationTokenHelperResult(
      code_2, signin::BindingKeyRegistrationTokenResult(
                  unexportable_keys::UnexportableSigningKeyId(), kWrappedKey,
                  "test_registration_token"));
  GaiaAuthConsumer* consumer_2 = signin_client_.GetAndClearConsumer();
  ASSERT_THAT(consumer_2, testing::NotNull());
  SimulateRegistrationTokenHelperResult(
      code_1, signin::BindingKeyRegistrationTokenResult(
                  unexportable_keys::UnexportableSigningKeyId(), kWrappedKey,
                  "other_registration_token"));
  GaiaAuthConsumer* consumer_1 = signin_client_.GetAndClearConsumer();
  ASSERT_THAT(consumer_1, testing::NotNull());

  // Simulate GaiaAuthFetchers successes and check that tokens have been
  // inserted in the token service.
  consumer_1->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
      "refresh_token", "access_token", /*expires_in_secs=*/10,
      /*is_under_advanced_protection=*/false, /*is_bound_to_key=*/true));
  EXPECT_TRUE(identity_manager()->HasAccountWithBoundRefreshToken(id_1));
  consumer_2->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
      "refresh_token", "access_token", /*expires_in_secs=*/10,
      /*is_under_advanced_protection=*/false, /*is_bound_to_key=*/true));
  EXPECT_TRUE(identity_manager()->HasAccountWithBoundRefreshToken(id_2));
  EXPECT_EQ(identity_manager()->GetWrappedBindingKey(), kWrappedKey);
  histogram_tester_.ExpectUniqueSample(
      kTokenBindingOutcomeHistogram,
      DiceResponseHandler::TokenBindingOutcome::kBound,
      /*expected_bucket_count=*/2);
}

TEST_F(DiceResponseHandlerTest, TwoFetchersOneEligible) {
  EnableTokenBindingRegistration();
  auto authorization_code = [&](const DiceResponseParams& dice_params) {
    return dice_params.signin_info()->GetInitiator()->authorization_code;
  };

  DiceResponseParams eligible_dice_params_ = MakeDiceParams(DiceAction::SIGNIN);
  DiceResponseParams ineligible_dice_params;
  DiceResponseParams::SigninInfo* signin_info =
      &ineligible_dice_params.data.emplace<DiceResponseParams::SigninInfo>();
  signin_info->AddAccount({GetDiceResponseParamsAccountInfo("other@email.com"),
                           "other_authorization_code", false, std::string(),
                           false});
  std::string eligible_code = authorization_code(eligible_dice_params_);

  dice_response_handler_->ProcessDiceHeader(
      std::move(eligible_dice_params_),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));
  // Token fetch should be blocked on the binding registration token generation.
  ASSERT_THAT(signin_client_.GetAndClearConsumer(), testing::IsNull());

  dice_response_handler_->ProcessDiceHeader(
      std::move(ineligible_dice_params),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));
  // Token fetch should start immediately for ineligible account.
  ASSERT_THAT(signin_client_.GetAndClearConsumer(), testing::NotNull());

  // Simulate successful token generation and check that GaiaAuthFetcher has
  // been created.
  const std::vector<uint8_t> kWrappedKey = {1, 2, 3};
  SimulateRegistrationTokenHelperResult(
      eligible_code, signin::BindingKeyRegistrationTokenResult(
                         unexportable_keys::UnexportableSigningKeyId(),
                         kWrappedKey, "test_registration_token"));
  ASSERT_THAT(signin_client_.GetAndClearConsumer(), testing::NotNull());
}

TEST_F(DiceResponseHandlerTest,
       NewRegistrationTokenHelperCreatedForConsecutiveFetchers) {
  EnableTokenBindingRegistration();
  auto account_id = [&](const DiceResponseParams& dice_params) {
    const auto* initiator = dice_params.signin_info()->GetInitiator();
    return identity_manager()->PickAccountIdForAccount(
        initiator->account_info.gaia_id, initiator->account_info.email);
  };
  auto authorization_code = [&](const DiceResponseParams& dice_params) {
    return dice_params.signin_info()->GetInitiator()->authorization_code;
  };

  DiceResponseParams dice_params_1 = MakeDiceParams(DiceAction::SIGNIN);
  std::string code_1 = authorization_code(dice_params_1);
  CoreAccountId id_1 = account_id(dice_params_1);
  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params_1),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));

  // Token fetch should be blocked on the binding registration token generation.
  ASSERT_THAT(signin_client_.GetAndClearConsumer(), testing::IsNull());
  const std::vector<uint8_t> kWrappedKey = {1, 2, 3};
  SimulateRegistrationTokenHelperResult(
      code_1, signin::BindingKeyRegistrationTokenResult(
                  unexportable_keys::UnexportableSigningKeyId(), kWrappedKey,
                  "test_registration_token"));
  GaiaAuthConsumer* consumer_1 = signin_client_.GetAndClearConsumer();
  ASSERT_THAT(consumer_1, testing::NotNull());

  // Simulate GaiaAuthFetcher success with the binding key being rejected.
  consumer_1->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
      "refresh_token", "access_token", /*expires_in_secs=*/10,
      /*is_under_advanced_protection=*/false, /*is_bound_to_key=*/false));
  EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(id_1));
  EXPECT_FALSE(identity_manager()->HasAccountWithBoundRefreshToken(id_1));

  // Next request should create a new RegistrationTokenHelper with a new binding
  // key as none of the existing tokens are bound.
  DiceResponseParams dice_params_2;
  DiceResponseParams::SigninInfo* signin_info_2 =
      &dice_params_2.data.emplace<DiceResponseParams::SigninInfo>();
  signin_info_2->AddAccount(
      {GetDiceResponseParamsAccountInfo("other@email.com"),
       "other_authorization_code", false, kEligibleForTokenBinding, false});
  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params_2),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));
}

TEST_F(DiceResponseHandlerTest, SigninWithFailedBoundTokenAttempt) {
  EnableTokenBindingRegistration();
  DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNIN);
  const auto* signin_info = dice_params.signin_info();
  const auto* account = signin_info->GetInitiator();
  ASSERT_TRUE(account);
  CoreAccountId account_id = identity_manager()->PickAccountIdForAccount(
      account->account_info.gaia_id, account->account_info.email);
  EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id));
  const std::string authorization_code = account->authorization_code;
  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));

  // Token fetch should be blocked on the binding registration token generation.
  ASSERT_THAT(signin_client_.GetAndClearConsumer(), testing::IsNull());
  // Simulate failed token generation.
  SimulateRegistrationTokenHelperResult(authorization_code, std::nullopt);

  // Check that a GaiaAuthFetcher has been created.
  GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer();
  ASSERT_THAT(consumer, testing::NotNull());
  // Simulate GaiaAuthFetcher success.
  consumer->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
      "refresh_token", "access_token", /*expires_in_secs=*/10,
      /*is_under_advanced_protection=*/false, /*is_bound_to_key=*/false));
  // Check that the token has been inserted in the token service.
  EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id));
  EXPECT_FALSE(identity_manager()->HasAccountWithBoundRefreshToken(account_id));
  EXPECT_TRUE(auth_error_email_.empty());
  EXPECT_EQ(GoogleServiceAuthError::NONE, auth_error_.state());
  histogram_tester_.ExpectUniqueSample(
      kTokenBindingOutcomeHistogram,
      DiceResponseHandler::TokenBindingOutcome::
          kNotBoundRegistrationTokenGenerationFailed,
      /*expected_bucket_count=*/1);
}

// Checks that the account reconcilor is blocked when where was OAuth
// outage in Dice, and unblocked after the timeout.
TEST_F(DiceResponseHandlerTest, SupportOAuthOutageInDice) {
  DiceResponseParams dice_params;
  DiceResponseParams::SigninInfo* signin_info =
      &dice_params.data.emplace<DiceResponseParams::SigninInfo>();
  signin_info->AddAccount({GetDiceResponseParamsAccountInfo(kEmail),
                           std::string(), true, std::string(), false});
  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));
  // Check that the reconcilor was blocked and not unblocked before timeout.
  EXPECT_EQ(1, reconcilor_blocked_count_);
  EXPECT_EQ(0, reconcilor_unblocked_count_);
  task_environment_.FastForwardBy(
      base::Hours(kLockAccountReconcilorTimeoutHours + 1));
  // Check that the reconcilor was unblocked.
  EXPECT_EQ(1, reconcilor_unblocked_count_);
  EXPECT_EQ(1, reconcilor_blocked_count_);
}

// Check that after receiving two headers with no authorization code,
// timeout still restarts.
TEST_F(DiceResponseHandlerTest, CheckTimersDuringOutageinDice) {
  ASSERT_GT(kLockAccountReconcilorTimeoutHours, 3);
  // Create params for the first header with no authorization code.
  DiceResponseParams dice_params_1;
  DiceResponseParams::SigninInfo* signin_info_1 =
      &dice_params_1.data.emplace<DiceResponseParams::SigninInfo>();
  signin_info_1->AddAccount({GetDiceResponseParamsAccountInfo(kEmail),
                             std::string(), true, std::string(), false});
  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params_1),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));
  // Check that the reconcilor was blocked and not unblocked before timeout.
  EXPECT_EQ(1, reconcilor_blocked_count_);
  EXPECT_EQ(0, reconcilor_unblocked_count_);
  // Wait half of the timeout.
  task_environment_.FastForwardBy(
      base::Hours(kLockAccountReconcilorTimeoutHours / 2));
  // Create params for the second header with no authorization code.
  DiceResponseParams dice_params_2;
  DiceResponseParams::SigninInfo* signin_info_2 =
      &dice_params_2.data.emplace<DiceResponseParams::SigninInfo>();
  signin_info_2->AddAccount({GetDiceResponseParamsAccountInfo(kEmail),
                             std::string(), true, std::string(), false});
  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params_2),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));
  task_environment_.FastForwardBy(
      base::Hours((kLockAccountReconcilorTimeoutHours + 1) / 2 + 1));
  // Check that the reconcilor was not unblocked after the first timeout
  // passed, timer should be restarted after getting the second header.
  EXPECT_EQ(1, reconcilor_blocked_count_);
  EXPECT_EQ(0, reconcilor_unblocked_count_);
  task_environment_.FastForwardBy(
      base::Hours((kLockAccountReconcilorTimeoutHours + 1) / 2));
  // Check that the reconcilor was unblocked.
  EXPECT_EQ(1, reconcilor_blocked_count_);
  EXPECT_EQ(1, reconcilor_unblocked_count_);
}

// Check that signin works normally (the token is fetched and added to chrome)
// on valid headers after getting a no_authorization_code header.
TEST_F(DiceResponseHandlerTest, CheckSigninAfterOutageInDice) {
  // Create params for the header with no authorization code.
  DiceResponseParams dice_params_1;
  DiceResponseParams::SigninInfo* signin_info_1 =
      &dice_params_1.data.emplace<DiceResponseParams::SigninInfo>();
  signin_info_1->AddAccount({GetDiceResponseParamsAccountInfo(kEmail),
                             std::string(), true, std::string(), false});
  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params_1),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));
  // Create params for the valid header with an authorization code.
  DiceResponseParams dice_params_2 = MakeDiceParams(DiceAction::SIGNIN);
  const auto account_info_2 =
      dice_params_2.signin_info()->GetInitiator()->account_info;
  CoreAccountId account_id_2 = identity_manager()->PickAccountIdForAccount(
      account_info_2.gaia_id, account_info_2.email);
  EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id_2));
  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params_2),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));
  // Check that the reconcilor was blocked and not unblocked before timeout.
  EXPECT_EQ(1, reconcilor_blocked_count_);
  EXPECT_EQ(0, reconcilor_unblocked_count_);
  // Check that a GaiaAuthFetcher has been created.
  GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer();
  ASSERT_THAT(consumer, testing::NotNull());
  // Simulate GaiaAuthFetcher success.
  consumer->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
      "refresh_token", "access_token", /*expires_in_secs=*/10,
      /*is_under_advanced_protection=*/true, /*is_bound_to_key=*/false));
  // Check that the token has been inserted in the token service.
  EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id_2));
  EXPECT_TRUE(auth_error_email_.empty());
  EXPECT_EQ(GoogleServiceAuthError::NONE, auth_error_.state());
  // Check HandleTokenExchangeSuccess parameters.
  EXPECT_EQ(token_exchange_account_id_, account_id_2);
  EXPECT_TRUE(token_exchange_is_new_account_);
  EXPECT_EQ(token_exchange_primary_is_connected_, signin::Tribool::kUnknown);
  EXPECT_EQ(1, reconcilor_blocked_count_);
  EXPECT_EQ(0, reconcilor_unblocked_count_);
  // Check that the AccountInfo::is_under_advanced_protection is set.
  EXPECT_TRUE(identity_manager()
                  ->FindExtendedAccountInfoByAccountId(account_id_2)
                  .IsUnderAdvancedProtection());
  task_environment_.FastForwardBy(
      base::Hours(kLockAccountReconcilorTimeoutHours + 1));
  // Check that the reconcilor was unblocked.
  EXPECT_EQ(1, reconcilor_unblocked_count_);
  EXPECT_EQ(1, reconcilor_blocked_count_);
}

TEST_P(DiceResponseHandlerParamTest, Reauth) {
  const size_t account_count = GetAccountCount();
  DiceResponseParams dice_params = MakeMultiSigninDiceParams(
      DiceAction::SIGNIN, account_count,
      /*primary_is_connected=*/signin::Tribool::kTrue);
  const auto* initiator_account = dice_params.signin_info()->GetInitiator();

  // Only the primary (initiator) account should exist and have error.
  AccountInfo initiator_account_info =
      identity_test_env_.MakePrimaryAccountAvailable(
          initiator_account->account_info.email, signin::ConsentLevel::kSignin);
  CoreAccountId initiator_account_id = initiator_account_info.GetAccountId();

  identity_test_env_.UpdatePersistentErrorOfRefreshTokenForAccount(
      initiator_account_id,
      GoogleServiceAuthError::FromInvalidGaiaCredentialsReason(
          GoogleServiceAuthError::InvalidGaiaCredentialsReason::UNKNOWN));

  EXPECT_TRUE(
      identity_manager()->HasAccountWithRefreshToken(initiator_account_id));
  EXPECT_TRUE(
      identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState(
          initiator_account_id));

  // For secondary accounts, verify they don't exist yet.
  std::vector<CoreAccountId> account_ids;
  for (const auto& account : dice_params.signin_info()->accounts()) {
    CoreAccountId id = identity_manager()->PickAccountIdForAccount(
        account.account_info.gaia_id, account.account_info.email);
    account_ids.push_back(id);
    if (id != initiator_account_id) {
      EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken(id));
    }
  }

  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));

  EXPECT_EQ(1, reconcilor_blocked_count_);
  EXPECT_EQ(0, reconcilor_unblocked_count_);

  // Check that GaiaAuthFetchers have been created for all accounts.
  for (size_t i = 0; i < account_count; ++i) {
    GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer();
    ASSERT_THAT(consumer, testing::NotNull());

    // Simulate GaiaAuthFetcher success.
    consumer->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
        base::StringPrintf("refresh_token_%zu", i), "access_token",
        /*expires_in_secs=*/10,
        /*is_under_advanced_protection=*/true, /*is_bound_to_key=*/false));
  }

  // Check that the tokens are no longer in error state (for initiator) and
  // added (for secondaries).
  EXPECT_TRUE(
      identity_manager()->HasAccountWithRefreshToken(initiator_account_id));
  EXPECT_FALSE(
      identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState(
          initiator_account_id));

  for (const auto& id : account_ids) {
    EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(id));
  }

  // Check HandleTokenExchangeSuccess parameters.
  EXPECT_EQ(token_exchange_account_id_, initiator_account_id);
  EXPECT_FALSE(token_exchange_is_new_account_);

  EXPECT_EQ(1, reconcilor_unblocked_count_);
}

// Checks that a GaiaAuthFetcher failure is handled correctly.
TEST_P(DiceResponseHandlerParamTest, SigninFailure) {
  identity_test_env_.MakePrimaryAccountAvailable("primary@example.com",
                                                 signin::ConsentLevel::kSignin);
  const size_t account_count = GetAccountCount();
  const int initiator_index = 0;
  DiceResponseParams dice_params = MakeMultiSigninDiceParams(
      DiceAction::SIGNIN, account_count,
      /*primary_is_connected=*/signin::Tribool::kTrue);

  std::vector<CoreAccountId> account_ids;
  std::vector<std::string> emails;
  for (const auto& account : dice_params.signin_info()->accounts()) {
    emails.push_back(account.account_info.email);
    account_ids.push_back(identity_manager()->PickAccountIdForAccount(
        account.account_info.gaia_id, account.account_info.email));
  }

  for (const auto& id : account_ids) {
    EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken(id));
  }

  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));

  EXPECT_EQ(
      account_count,
      dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());

  // Simulate GaiaAuthFetcher failure for all accounts.
  for (int i = 0; i < account_count; ++i) {
    GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer();
    ASSERT_THAT(consumer, testing::NotNull());

    consumer->OnClientOAuthFailure(
        GoogleServiceAuthError::FromServiceUnavailable(""));
  }

  EXPECT_EQ(
      0u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());

  // Check that the tokens have not been inserted in the token service.
  for (const auto& id : account_ids) {
    EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken(id));
  }
  // The mock stores the last error.
  EXPECT_EQ(emails[initiator_index], auth_error_email_);
  EXPECT_EQ(GoogleServiceAuthError::SERVICE_UNAVAILABLE, auth_error_.state());
}

// Checks that failure of initiator account doesn't prevent secondary accounts
// from being added.
TEST_F(DiceResponseHandlerTest,
       MultipleAccounts_InitiatorFails_SecondarySucceeds) {
  identity_test_env_.MakePrimaryAccountAvailable("primary@example.com",
                                                 signin::ConsentLevel::kSignin);
  const int account_count = 2;
  const int initiator_index = 0;
  DiceResponseParams dice_params =
      MakeMultiSigninDiceParams(DiceAction::SIGNIN, account_count,
                                /*primary_is_connected=*/signin::Tribool::kTrue,
                                /*eligible_for_token_binding=*/true,
                                /*mtls_token_binding=*/false, initiator_index);

  auto* signin_info = dice_params.signin_info();
  CoreAccountId initiator_account_id;
  CoreAccountId secondary_account_id;
  std::string initiator_email = signin_info->GetInitiator()->account_info.email;

  const auto& accounts = signin_info->accounts();
  initiator_account_id = identity_manager()->PickAccountIdForAccount(
      accounts[initiator_index].account_info.gaia_id,
      accounts[initiator_index].account_info.email);
  secondary_account_id = identity_manager()->PickAccountIdForAccount(
      accounts[1].account_info.gaia_id, accounts[1].account_info.email);

  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));

  // Complete initiator fetcher with failure.
  GaiaAuthConsumer* consumer_init = signin_client_.GetAndClearConsumer();
  ASSERT_THAT(consumer_init, testing::NotNull());
  consumer_init->OnClientOAuthFailure(
      GoogleServiceAuthError::FromServiceUnavailable(""));

  // Complete secondary fetcher with success.
  GaiaAuthConsumer* consumer_sec = signin_client_.GetAndClearConsumer();
  ASSERT_THAT(consumer_sec, testing::NotNull());
  consumer_sec->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
      "refresh_token_sec", "access_token", /*expires_in_secs=*/10,
      /*is_under_advanced_protection=*/true, /*is_bound_to_key=*/false));

  // Verify initiator account NOT added.
  EXPECT_FALSE(
      identity_manager()->HasAccountWithRefreshToken(initiator_account_id));
  // Verify secondary account ADDED.
  EXPECT_TRUE(
      identity_manager()->HasAccountWithRefreshToken(secondary_account_id));

  // Verify delegate calls.
  EXPECT_EQ(auth_error_email_, initiator_email);

  // Check there is no pending fetchers.
  EXPECT_EQ(
      0u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());
  EXPECT_EQ(GoogleServiceAuthError::SERVICE_UNAVAILABLE, auth_error_.state());
  EXPECT_TRUE(token_exchange_account_id_.empty());

  // Verify completion signal contains expected secondary.
  EXPECT_TRUE(session_complete_called_);
  EXPECT_THAT(completed_secondary_accounts_,
              testing::UnorderedElementsAre(secondary_account_id));
}

// Checks that failure of secondary account doesn't prevent initiator account
// from being added.
TEST_F(DiceResponseHandlerTest,
       MultipleAccounts_InitiatorSucceeds_SecondaryFails) {
  const int account_count = 2;
  const int initiator_index = 0;
  DiceResponseParams dice_params =
      MakeMultiSigninDiceParams(DiceAction::SIGNIN, account_count,
                                /*primary_is_connected=*/signin::Tribool::kTrue,
                                /*eligible_for_token_binding=*/true,
                                /*mtls_token_binding=*/false, initiator_index);

  auto* signin_info = dice_params.signin_info();
  CoreAccountId initiator_account_id;
  CoreAccountId secondary_account_id;

  const auto& accounts = signin_info->accounts();
  initiator_account_id = identity_manager()->PickAccountIdForAccount(
      accounts[initiator_index].account_info.gaia_id,
      accounts[initiator_index].account_info.email);
  secondary_account_id = identity_manager()->PickAccountIdForAccount(
      accounts[1].account_info.gaia_id, accounts[1].account_info.email);

  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));

  // Complete initiator fetcher with success.
  GaiaAuthConsumer* consumer_init = signin_client_.GetAndClearConsumer();
  ASSERT_THAT(consumer_init, testing::NotNull());
  consumer_init->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
      "refresh_token_init", "access_token", /*expires_in_secs=*/10,
      /*is_under_advanced_protection=*/true, /*is_bound_to_key=*/false));

  // Complete secondary fetcher with failure.
  GaiaAuthConsumer* consumer_sec = signin_client_.GetAndClearConsumer();
  ASSERT_THAT(consumer_sec, testing::NotNull());
  consumer_sec->OnClientOAuthFailure(
      GoogleServiceAuthError::FromServiceUnavailable(""));

  // Verify initiator account ADDED.
  EXPECT_TRUE(
      identity_manager()->HasAccountWithRefreshToken(initiator_account_id));
  // Verify secondary account NOT added.
  EXPECT_FALSE(
      identity_manager()->HasAccountWithRefreshToken(secondary_account_id));

  // Verify delegate calls.
  EXPECT_EQ(token_exchange_account_id_, initiator_account_id);
  EXPECT_TRUE(token_exchange_is_new_account_);
  EXPECT_TRUE(auth_error_email_.empty());

  // Check there is no pending fetchers.
  EXPECT_EQ(
      0u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());

  // Verify completion signal contains expected secondary.
  EXPECT_TRUE(session_complete_called_);
  EXPECT_THAT(completed_secondary_accounts_,
              testing::UnorderedElementsAre(secondary_account_id));
}

// Checks that timeout of one fetcher doesn't prevent others from succeeding.
TEST_F(DiceResponseHandlerTest, MultipleAccounts_PartialTimeout) {
  const int account_count = 2;
  DiceResponseParams dice_params = MakeMultiSigninDiceParams(
      DiceAction::SIGNIN, account_count,
      /*primary_is_connected=*/signin::Tribool::kTrue);

  auto* signin_info = dice_params.signin_info();
  std::vector<CoreAccountId> account_ids;
  for (const auto& account : signin_info->accounts()) {
    account_ids.push_back(identity_manager()->PickAccountIdForAccount(
        account.account_info.gaia_id, account.account_info.email));
  }

  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));

  // Complete initiator fetcher with success.
  GaiaAuthConsumer* consumer_init = signin_client_.GetAndClearConsumer();
  ASSERT_THAT(consumer_init, testing::NotNull());
  consumer_init->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
      "refresh_token_init", "access_token", /*expires_in_secs=*/10,
      /*is_under_advanced_protection=*/true, /*is_bound_to_key=*/false));

  // Do not complete the secondary fetcher.
  // Get it from the client to avoid dangling pointer in the vector when it
  // times out.
  GaiaAuthConsumer* consumer_sec = signin_client_.GetAndClearConsumer();
  ASSERT_THAT(consumer_sec, testing::NotNull());

  // Fast forward time to trigger timeout.
  task_environment_.FastForwardBy(
      base::Seconds(kDiceTokenFetchTimeoutSeconds + 1));

  // Verify initiator account ADDED.
  EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_ids[0]));
  // Verify secondary account NOT added (timed out).
  EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_ids[1]));

  // Verify delegate calls for initiator.
  EXPECT_EQ(token_exchange_account_id_, account_ids[0]);
  EXPECT_TRUE(token_exchange_is_new_account_);

  // The failure for secondary account is ignored and NOT reported to delegate.
  EXPECT_TRUE(auth_error_email_.empty());
  EXPECT_EQ(GoogleServiceAuthError::NONE, auth_error_.state());
}

// Checks that a header with mixed authorization codes (one valid, one missing
// due to outage) handles both correctly.
TEST_F(DiceResponseHandlerTest, MultipleAccounts_NoAuthCode_Mixed) {
  DiceResponseParams dice_params;
  DiceResponseParams::SigninInfo* signin_info =
      &dice_params.data.emplace<DiceResponseParams::SigninInfo>();

  DiceResponseParams::AccountInfo account_info1 =
      GetDiceResponseParamsAccountInfo("email1@gmail.com");
  DiceResponseParams::AccountInfo account_info2 =
      GetDiceResponseParamsAccountInfo("email2@gmail.com");

  // Account 1 has auth code.
  signin_info->AddAccount({account_info1, kAuthorizationCode,
                           /*no_authorization_code=*/false, std::string(),
                           /*mtls_token_binding=*/false});
  // Account 2 has NO auth code (outage).
  signin_info->AddAccount({account_info2, std::string(),
                           /*no_authorization_code=*/true, std::string(),
                           /*mtls_token_binding=*/false});

  signin::DiceResponseParams::SigninInfo::LinkedAccountsMetadata metadata;
  metadata.initiator_id = account_info1.gaia_id;
  metadata.primary_is_connected =
      /*primary_is_connected=*/signin::Tribool::kFalse;
  signin_info->set_linked_accounts_metadata(std::move(metadata));

  CoreAccountId account_id1 = identity_manager()->PickAccountIdForAccount(
      account_info1.gaia_id, account_info1.email);
  CoreAccountId account_id2 = identity_manager()->PickAccountIdForAccount(
      account_info2.gaia_id, account_info2.email);

  EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id1));
  EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id2));

  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));

  // Check there is 1 pending fetcher (for account 1).
  EXPECT_EQ(
      1u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());

  // Account 1 should trigger token fetch.
  GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer();
  ASSERT_THAT(consumer, testing::NotNull());

  // Account 2 should trigger OnNoAuthorizationCode and lock reconcilor.
  EXPECT_EQ(1, reconcilor_blocked_count_);
  EXPECT_EQ(0, reconcilor_unblocked_count_);

  // Complete token fetch for Account 1.
  consumer->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
      "refresh_token", "access_token", /*expires_in_secs=*/10,
      /*is_under_advanced_protection=*/true, /*is_bound_to_key=*/false));

  // Check there is no pending fetchers after consumer completes.
  EXPECT_EQ(
      0u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());

  EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id1));
  EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id2));
  EXPECT_EQ(token_exchange_primary_is_connected_, signin::Tribool::kFalse);

  // Reconcilor should still be blocked because of Account 2!
  EXPECT_EQ(1, reconcilor_blocked_count_);
  EXPECT_EQ(0, reconcilor_unblocked_count_);

  // Fast forward time to trigger timeout for the outage flow!
  task_environment_.FastForwardBy(
      base::Hours(kLockAccountReconcilorTimeoutHours));

  // Reconcilor should be unblocked now!
  EXPECT_EQ(1, reconcilor_unblocked_count_);
}

// Checks that a second token for the same account is requested when a
// request is already in flight. The first request should be cancelled.
TEST_F(DiceResponseHandlerTest, SigninRepeatedWithSameAccount) {
  DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNIN);
  CoreAccountId account_id = identity_manager()->PickAccountIdForAccount(
      dice_params.signin_info()->GetInitiator()->account_info.gaia_id,
      dice_params.signin_info()->GetInitiator()->account_info.email);
  ASSERT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id));

  // Start first request.
  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));
  GaiaAuthConsumer* consumer_1 = signin_client_.GetAndClearConsumer();
  ASSERT_THAT(consumer_1, testing::NotNull());

  // Start a second request for the same account.
  dice_params = MakeDiceParams(DiceAction::SIGNIN);
  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));
  // Check that a NEW request IS created.
  GaiaAuthConsumer* consumer_2 = signin_client_.GetAndClearConsumer();
  ASSERT_THAT(consumer_2, testing::NotNull());

  // The first fetcher is cancelled and only the second one is pending.
  EXPECT_EQ(
      1u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());

  // Simulate GaiaAuthFetcher success for the second request.
  consumer_2->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
      "refresh_token_2", "access_token_2", /*expires_in_secs=*/10,
      /*is_under_advanced_protection=*/true, /*is_bound_to_key=*/false));
  EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id));
  EXPECT_TRUE(identity_manager()
                  ->FindExtendedAccountInfoByAccountId(account_id)
                  .IsUnderAdvancedProtection());

  EXPECT_EQ(
      0u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());
}

// Checks that when a DICE SIGNIN header contains duplicate entries for the
// same account, `DiceHeaderHelper` deduplicates them so `DiceSigninSession`
// never receives duplicate accounts. When a second concurrent request is made
// for the same account, the first in-flight request is cleanly cancelled
// without any use-after-free.
TEST_F(DiceResponseHandlerTest, SigninRepeatedWithDuplicateAccountInHeader) {
  std::string gaia_id_str = signin::GetTestGaiaIdForEmail(kEmail).ToString();
  std::string header_value = base::StringPrintf(
      "action=SIGNIN;id=%s;email=%s;authuser=1;authorization_code=code1,"
      "action=SIGNIN;id=%s;email=%s;authuser=2;authorization_code=code2",
      gaia_id_str.c_str(), kEmail, gaia_id_str.c_str(), kEmail);
  scoped_refptr<net::HttpResponseHeaders> headers =
      base::MakeRefCounted<net::HttpResponseHeaders>("HTTP/1.1 200 OK\r\n");
  headers->AddHeader(signin::kDiceResponseHeader, header_value);
  headers->AddHeader(
      signin::kDiceLinkedAccountsMetaHeader,
      base::StringPrintf("initiator_id=%s;primary_is_connected=1",
                         gaia_id_str.c_str()));
  DiceResponseParams dice_params =
      signin::DiceHeaderHelper::CreateDiceResponseParams(headers.get());
  ASSERT_EQ(1U, dice_params.signin_info()->accounts().size());

  CoreAccountId account_id = identity_manager()->PickAccountIdForAccount(
      dice_params.signin_info()->GetInitiator()->account_info.gaia_id,
      dice_params.signin_info()->GetInitiator()->account_info.email);
  ASSERT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id));

  // Start first request from the deduplicated header params.
  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));
  GaiaAuthConsumer* consumer_1 = signin_client_.GetAndClearConsumer();
  ASSERT_THAT(consumer_1, testing::NotNull());

  // Start a second request for the same account while the first is in flight.
  DiceResponseParams dice_params_2 = MakeDiceParams(DiceAction::SIGNIN);
  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params_2),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));
  GaiaAuthConsumer* consumer_2 = signin_client_.GetAndClearConsumer();
  ASSERT_THAT(consumer_2, testing::NotNull());

  // The first fetcher is cancelled and only the second one is pending.
  EXPECT_EQ(
      1u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());

  // Simulate GaiaAuthFetcher success for the second request.
  consumer_2->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
      "refresh_token_2", "access_token_2", /*expires_in_secs=*/10,
      /*is_under_advanced_protection=*/true, /*is_bound_to_key=*/false));
  EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id));
  EXPECT_TRUE(identity_manager()
                  ->FindExtendedAccountInfoByAccountId(account_id)
                  .IsUnderAdvancedProtection());

  EXPECT_EQ(
      0u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());
}

// Checks that cancelling an in-flight token fetcher while registration token
// generation is pending does not cause a dangling pointer crash when the
// asynchronous token generation finishes.
// Regression test for https://crbug.com/548923019.
TEST_F(DiceResponseHandlerTest,
       SigninRepeatedWhileBindingKeyGenerationInFlight) {
  EnableTokenBindingRegistration();
  DiceResponseParams dice_params_1 = MakeDiceParams(DiceAction::SIGNIN);
  const std::string code_1 =
      dice_params_1.signin_info()->GetInitiator()->authorization_code;

  // Start first request. Token fetch is blocked on registration token
  // generation.
  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params_1),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));
  EXPECT_EQ(
      1u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());
  ASSERT_THAT(signin_client_.GetAndClearConsumer(), testing::IsNull());

  // Start a second request for the same account while token generation for the
  // first is still in flight.
  DiceResponseParams dice_params_2;
  DiceResponseParams::SigninInfo* signin_info_2 =
      &dice_params_2.data.emplace<DiceResponseParams::SigninInfo>();
  signin_info_2->AddAccount({GetDiceResponseParamsAccountInfo(kEmail),
                             "authorization_code_2", false,
                             kEligibleForTokenBinding, false});
  const std::string code_2 = "authorization_code_2";

  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params_2),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));

  // The first fetcher is cancelled and only the second one is pending.
  EXPECT_EQ(
      1u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());
  ASSERT_THAT(signin_client_.GetAndClearConsumer(), testing::IsNull());

  // Simulate completion of registration token generation for the first
  // request. The first fetcher was already destroyed, so the callback should be
  // safely dropped by WeakPtr without crashing.
  const std::vector<uint8_t> kWrappedKey = {1, 2, 3};
  SimulateRegistrationTokenHelperResult(
      code_1, signin::BindingKeyRegistrationTokenResult(
                  unexportable_keys::UnexportableSigningKeyId(), kWrappedKey,
                  "test_registration_token_1"));

  // Token fetch for the second request is still waiting for its own token
  // generation.
  ASSERT_THAT(signin_client_.GetAndClearConsumer(), testing::IsNull());

  // Complete token generation for the second request.
  SimulateRegistrationTokenHelperResult(
      code_2, signin::BindingKeyRegistrationTokenResult(
                  unexportable_keys::UnexportableSigningKeyId(), kWrappedKey,
                  "test_registration_token_2"));
  GaiaAuthConsumer* consumer_2 = signin_client_.GetAndClearConsumer();
  ASSERT_THAT(consumer_2, testing::NotNull());

  // Complete token fetch for the second request.
  consumer_2->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
      "refresh_token_2", "access_token_2", /*expires_in_secs=*/10,
      /*is_under_advanced_protection=*/true, /*is_bound_to_key=*/true));

  EXPECT_EQ(
      0u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());
}

// Checks that when an initiator account fetch in initiator-first mode is
// cancelled by a concurrent sign-in request for the initiator account, the
// entire session (including any waiting secondary accounts) is cancelled and
// deleted. We intentionally do not attempt to start secondary account fetches
// upon initiator cancellation because doing so would bypass the profile
// interception flow (which relies on delegate_->HandleTokenExchangeSuccess
// from the initiator account before moving accounts to a new profile).
TEST_F(DiceResponseHandlerTest,
       SigninRepeated_InitiatorCancelled_SecondariesCancelled) {
  const int account_count = 2;
  const int initiator_index = 0;
  // Create multi-account params in initiator-first mode (primary not
  // connected).
  DiceResponseParams dice_params_1 = MakeMultiSigninDiceParams(
      DiceAction::SIGNIN, account_count,
      /*primary_is_connected=*/signin::Tribool::kFalse,
      /*eligible_for_token_binding=*/true,
      /*mtls_token_binding=*/false, initiator_index);

  auto* signin_info_1 = dice_params_1.signin_info();
  const auto& accounts_1 = signin_info_1->accounts();
  CoreAccountId initiator_account_id =
      identity_manager()->PickAccountIdForAccount(
          accounts_1[0].account_info.gaia_id, accounts_1[0].account_info.email);
  CoreAccountId secondary_account_id =
      identity_manager()->PickAccountIdForAccount(
          accounts_1[1].account_info.gaia_id, accounts_1[1].account_info.email);

  ASSERT_FALSE(
      identity_manager()->HasAccountWithRefreshToken(initiator_account_id));
  ASSERT_FALSE(
      identity_manager()->HasAccountWithRefreshToken(secondary_account_id));

  // Start first request. Because it is in initiator-first mode, only the
  // initiator fetcher should start.
  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params_1),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));
  EXPECT_EQ(
      1u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());
  GaiaAuthConsumer* consumer_init_1 = signin_client_.GetAndClearConsumer();
  ASSERT_THAT(consumer_init_1, testing::NotNull());

  // Start a second concurrent sign-in request for ONLY the initiator account.
  DiceResponseParams dice_params_2 = MakeDiceParams(DiceAction::SIGNIN);
  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params_2),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));

  // Cancelling the initiator in Session 1 deletes Session 1. The waiting
  // secondary account fetch is cancelled along with the session. Only Session
  // 2's new fetcher for the initiator account should be pending.
  EXPECT_EQ(
      1u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());

  GaiaAuthConsumer* consumer_init_2 = signin_client_.GetAndClearConsumer();
  ASSERT_THAT(consumer_init_2, testing::NotNull());

  consumer_init_2->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
      "refresh_token_init", "access_token_init", /*expires_in_secs=*/10,
      /*is_under_advanced_protection=*/false, /*is_bound_to_key=*/false));

  EXPECT_TRUE(
      identity_manager()->HasAccountWithRefreshToken(initiator_account_id));
  EXPECT_FALSE(
      identity_manager()->HasAccountWithRefreshToken(secondary_account_id));
  EXPECT_EQ(
      0u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());
}

// Checks that two SIGNIN requests can happen concurrently.
TEST_F(DiceResponseHandlerTest, SigninWithTwoAccounts) {
  DiceResponseParams dice_params_1 = MakeDiceParams(DiceAction::SIGNIN);
  const auto account_info_1 =
      dice_params_1.signin_info()->GetInitiator()->account_info;
  DiceResponseParams dice_params_2;
  DiceResponseParams::SigninInfo* signin_info_2 =
      &dice_params_2.data.emplace<DiceResponseParams::SigninInfo>();
  signin_info_2->AddAccount({{GaiaId("other_gaia_id"), "other_email", 0},
                             kAuthorizationCode,
                             false,
                             kEligibleForTokenBinding,
                             false});
  const auto account_info_2 = signin_info_2->GetInitiator()->account_info;
  CoreAccountId account_id_1 = identity_manager()->PickAccountIdForAccount(
      account_info_1.gaia_id, account_info_1.email);
  CoreAccountId account_id_2 = identity_manager()->PickAccountIdForAccount(
      account_info_2.gaia_id, account_info_2.email);
  ASSERT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id_1));
  ASSERT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id_2));
  // Start first request.
  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params_1),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));
  // Check that a GaiaAuthFetcher has been created.
  GaiaAuthConsumer* consumer_1 = signin_client_.GetAndClearConsumer();
  ASSERT_THAT(consumer_1, testing::NotNull());
  EXPECT_EQ(1, reconcilor_blocked_count_);
  EXPECT_EQ(0, reconcilor_unblocked_count_);
  // Start second request.
  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params_2),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));
  GaiaAuthConsumer* consumer_2 = signin_client_.GetAndClearConsumer();
  ASSERT_THAT(consumer_2, testing::NotNull());
  // Simulate GaiaAuthFetcher success for the first request.
  consumer_1->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
      "refresh_token", "access_token", /*expires_in_secs=*/10,
      /*is_under_advanced_protection=*/true, /*is_bound_to_key=*/false));
  // Check that the token has been inserted in the token service.
  EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id_1));
  EXPECT_TRUE(identity_manager()
                  ->FindExtendedAccountInfoByAccountId(account_id_1)
                  .IsUnderAdvancedProtection());
  // Simulate GaiaAuthFetcher success for the second request.
  consumer_2->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
      "refresh_token", "access_token", /*expires_in_secs=*/10,
      /*is_under_advanced_protection=*/false, /*is_bound_to_key=*/false));
  // Check that the token has been inserted in the token service.
  EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id_2));
  EXPECT_FALSE(identity_manager()
                   ->FindExtendedAccountInfoByAccountId(account_id_2)
                   .IsUnderAdvancedProtection());
  // Check that the reconcilor was blocked and unblocked exactly once.
  EXPECT_EQ(1, reconcilor_blocked_count_);
  EXPECT_EQ(1, reconcilor_unblocked_count_);
}

// Checks that a ENABLE_SYNC action received after the refresh token is added
// to the token service, triggers a call to enable sync on the delegate.
TEST_F(DiceResponseHandlerTest,
       SigninEnableSyncDiceHeaderAfterRefreshTokenFetched) {
  DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNIN);
  auto gaia_id =
      dice_params.signin_info()->GetInitiator()->account_info.gaia_id;
  auto email = dice_params.signin_info()->GetInitiator()->account_info.email;
  CoreAccountId account_id =
      identity_manager()->PickAccountIdForAccount(gaia_id, email);
  ASSERT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id));
  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));
  // Check that a GaiaAuthFetcher has been created.
  GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer();
  ASSERT_THAT(consumer, testing::NotNull());
  // Simulate GaiaAuthFetcher success.
  consumer->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
      "refresh_token", "access_token", /*expires_in_secs=*/10,
      /*is_under_advanced_protection=*/false, /*is_bound_to_key=*/false));
  // Check that the token has been inserted in the token service.
  EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id));
  // Check HandleTokenExchangeSuccess parameters.
  EXPECT_EQ(token_exchange_account_id_, account_id);
  EXPECT_TRUE(token_exchange_is_new_account_);
  EXPECT_EQ(token_exchange_primary_is_connected_, signin::Tribool::kUnknown);
  // Check that delegate was not called to enable sync.
  EXPECT_TRUE(complete_profile_signin_account_info_.IsEmpty());

  // Enable sync.
  dice_response_handler_->ProcessDiceHeader(
      MakeDiceParams(DiceAction::ENABLE_SYNC),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));
  // Check that delegate was called to enable sync.
  EXPECT_EQ(gaia_id, complete_profile_signin_account_info_.gaia);
  EXPECT_EQ(email, complete_profile_signin_account_info_.email);
}

// Checks that a ENABLE_SYNC action received before the refresh token is added
// to the token service, is schedules a call to enable sync on the delegate
// once the refresh token is received.
TEST_F(DiceResponseHandlerTest,
       SigninEnableSyncDiceHeaderBeforeRefreshTokenFetched) {
  DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNIN);
  auto gaia_id =
      dice_params.signin_info()->GetInitiator()->account_info.gaia_id;
  auto email = dice_params.signin_info()->GetInitiator()->account_info.email;
  CoreAccountId account_id =
      identity_manager()->PickAccountIdForAccount(gaia_id, email);
  ASSERT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id));
  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));
  // Check that a GaiaAuthFetcher has been created.
  GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer();
  ASSERT_THAT(consumer, testing::NotNull());

  // Enable sync.
  dice_response_handler_->ProcessDiceHeader(
      MakeDiceParams(DiceAction::ENABLE_SYNC),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));
  // Check that delegate was not called to enable sync.
  EXPECT_TRUE(complete_profile_signin_account_info_.IsEmpty());

  // Simulate GaiaAuthFetcher success.
  consumer->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
      "refresh_token", "access_token", /*expires_in_secs=*/10,
      /*is_under_advanced_protection=*/false, /*is_bound_to_key=*/false));
  // Check that the token has been inserted in the token service.
  EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id));
  // Check HandleTokenExchangeSuccess parameters.
  EXPECT_EQ(token_exchange_account_id_, account_id);
  EXPECT_TRUE(token_exchange_is_new_account_);
  EXPECT_EQ(token_exchange_primary_is_connected_, signin::Tribool::kUnknown);
  // Check that delegate was called to enable sync.
  EXPECT_EQ(gaia_id, complete_profile_signin_account_info_.gaia);
  EXPECT_EQ(email, complete_profile_signin_account_info_.email);
}

// Checks that a ENABLE_SYNC action is ignored when the account info is missing.
TEST_F(DiceResponseHandlerTest,
       SigninEnableSyncDiceHeaderWithMissingAccountInfo) {
  DiceResponseParams dice_params = MakeDiceParams(DiceAction::ENABLE_SYNC);
  const auto& account_info = dice_params.enable_sync_info()->account_info;
  CoreAccountId account_id = identity_manager()->PickAccountIdForAccount(
      account_info.gaia_id, account_info.email);

  ASSERT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id));

  auto extended_account_info =
      identity_manager()->FindExtendedAccountInfoByGaiaId(account_info.gaia_id);
  ASSERT_TRUE(extended_account_info.IsEmpty());

  base::HistogramTester histogram_tester;
  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));

  // Check that delegate was NOT called to enable sync.
  EXPECT_TRUE(complete_profile_signin_account_info_.IsEmpty());
  histogram_tester.ExpectUniqueSample(
      "Signin.DiceEnableSyncHeaderAccountInfoIsPresent", /*sample=*/0,
      /*expected_bucket_count=*/1);
}

TEST_F(DiceResponseHandlerTest, Timeout) {
  DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNIN);
  CoreAccountId account_id = identity_manager()->PickAccountIdForAccount(
      dice_params.signin_info()->GetInitiator()->account_info.gaia_id,
      dice_params.signin_info()->GetInitiator()->account_info.email);
  ASSERT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id));
  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));
  // Check that a GaiaAuthFetcher has been created.
  GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer();
  ASSERT_THAT(consumer, testing::NotNull());
  EXPECT_EQ(
      1u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());
  // Force a timeout.
  task_environment_.FastForwardBy(
      base::Seconds(kDiceTokenFetchTimeoutSeconds + 1));
  EXPECT_EQ(
      0u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());
  // Check that the token has not been inserted in the token service.
  EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id));
  // Check that the reconcilor was blocked and unblocked exactly once.
  EXPECT_EQ(1, reconcilor_blocked_count_);
  EXPECT_EQ(1, reconcilor_unblocked_count_);
}

// Checks that there is no crash if the DiceResponseHandler is deleted before
// the timeout expires. Tests the scenario from https://crbug.com/40058574
TEST_F(DiceResponseHandlerTest, DeleteBeforeTimeout) {
  DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNIN);
  CoreAccountId account_id = identity_manager()->PickAccountIdForAccount(
      dice_params.signin_info()->GetInitiator()->account_info.gaia_id,
      dice_params.signin_info()->GetInitiator()->account_info.email);
  ASSERT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id));
  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));
  // Check that a GaiaAuthFetcher has been created.
  GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer();
  ASSERT_THAT(consumer, testing::NotNull());
  EXPECT_EQ(
      1u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());

  // Delete the handler.
  dice_response_handler_.reset();

  // Force a timeout, this should not crash.
  task_environment_.FastForwardBy(
      base::Seconds(kDiceTokenFetchTimeoutSeconds + 1));

  // Check that the token has not been inserted in the token service.
  EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id));
  // Check that the reconcilor was blocked and unblocked exactly once.
  EXPECT_EQ(1, reconcilor_blocked_count_);
  EXPECT_EQ(1, reconcilor_unblocked_count_);
}

TEST_F(DiceResponseHandlerTest, SignoutSigninPrimaryAccount) {
  // Setup.
  // Configure Dice params.
  DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNOUT);
  const char kSecondarySignedOutEmail[] = "secondary_signed_out@gmail.com";
  dice_params.signout_info()->account_infos.push_back(
      GetDiceResponseParamsAccountInfo(kSecondarySignedOutEmail));
  const std::string dice_primary_account_email =
      dice_params.signout_info()->account_infos[0].email;
  // Configure Chrome.
  AccountInfo primary_account = identity_test_env_.MakePrimaryAccountAvailable(
      dice_primary_account_email, signin::ConsentLevel::kSignin);
  AccountInfo secondary_signed_out =
      identity_test_env_.MakeAccountAvailable(kSecondarySignedOutEmail);
  AccountInfo secondary_not_signed_out =
      identity_test_env_.MakeAccountAvailable("other@gmail.com");
  EXPECT_EQ(identity_manager()->GetAccountsWithRefreshTokens().size(), 3U);
  EXPECT_TRUE(
      identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSignin));

  // Receive signout response including primary and secondary account.
  RunSignoutTest(std::move(dice_params),
                 {secondary_not_signed_out.GetAccountId()},
                 primary_account.GetAccountId(),
                 /*invalid_primary_account=*/true);
}

TEST_F(DiceResponseHandlerTest, SignoutSecondaryAccount) {
  const char kPrimaryAccount[] = "main@gmail.com";
  DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNOUT);
  const std::string secondary_account_email =
      dice_params.signout_info()->account_infos[0].email;
  // User is signed in to Chrome, and has some refresh token for a secondary
  // account.
  AccountInfo primary_account_info =
      identity_test_env_.MakePrimaryAccountAvailable(
          kPrimaryAccount, signin::ConsentLevel::kSignin);
  AccountInfo secondary_account_info =
      identity_test_env_.MakeAccountAvailable(secondary_account_email);
  EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(
      secondary_account_info.GetAccountId()));
  EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(
      primary_account_info.GetAccountId()));
  EXPECT_TRUE(
      identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSignin));
  // Receive signout response for the secondary account.
  RunSignoutTest(std::move(dice_params), {},
                 primary_account_info.GetAccountId(),
                 /*invalid_primary_account=*/false);
}

TEST_F(DiceResponseHandlerTest, SignoutWebOnly) {
  DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNOUT);
  const auto& dice_account_info = dice_params.signout_info()->account_infos[0];
  // User is NOT signed in to Chrome, and has some refresh tokens for two
  // accounts.
  AccountInfo account_info =
      identity_test_env_.MakeAccountAvailable(dice_account_info.email);
  AccountInfo secondary_account_info =
      identity_test_env_.MakeAccountAvailable("other@gmail.com");
  EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(
      account_info.GetAccountId()));
  EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(
      secondary_account_info.GetAccountId()));
  EXPECT_FALSE(
      identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSignin));
  // Receive signout response.
  RunSignoutTest(std::move(dice_params),
                 {secondary_account_info.GetAccountId()},
                 /*primary_account=*/CoreAccountId(),
                 /*invalid_primary_account=*/false);
}

// Checks that signin in progress is canceled by a signout.
TEST_F(DiceResponseHandlerTest, SigninSignoutSameAccount) {
  DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNOUT);
  const auto& dice_account_info = dice_params.signout_info()->account_infos[0];

  // User is signed in to Chrome.
  AccountInfo account_info = identity_test_env_.MakePrimaryAccountAvailable(
      dice_account_info.email, signin::ConsentLevel::kSignin);
  EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(
      account_info.GetAccountId()));
  EXPECT_FALSE(
      identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState(
          account_info.GetAccountId()));
  // Start Dice signin (reauth).
  DiceResponseParams dice_params_2 = MakeDiceParams(DiceAction::SIGNIN);
  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params_2),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));
  // Check that a GaiaAuthFetcher has been created and is pending.
  ASSERT_THAT(signin_client_.GetAndClearConsumer(), testing::NotNull());
  EXPECT_EQ(
      1u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());
  // Signout while signin is in flight.
  RunSignoutTest(std::move(dice_params), {}, account_info.GetAccountId(),
                 /*invalid_primary_account=*/true);
  // Check that the token fetcher has been canceled and the token is invalid.
  EXPECT_EQ(
      0u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());
}

// Checks that signin in progress is not canceled by a signout for a different
// account.
TEST_F(DiceResponseHandlerTest, SigninSignoutDifferentAccount) {
  // User starts signin in the web with two accounts.
  DiceResponseParams signout_params_1 = MakeDiceParams(DiceAction::SIGNOUT);
  DiceResponseParams signin_params_1 = MakeDiceParams(DiceAction::SIGNIN);
  DiceResponseParams signin_params_2;
  DiceResponseParams::SigninInfo* signin_info_2 =
      &signin_params_2.data.emplace<DiceResponseParams::SigninInfo>();
  signin_info_2->AddAccount({{GaiaId("other_gaia_id"), "other_email", 0},
                             kAuthorizationCode,
                             false,
                             kEligibleForTokenBinding,
                             false});
  const auto& signin_account_info_1 =
      signin_params_1.signin_info()->GetInitiator()->account_info;
  const auto& signin_account_info_2 =
      signin_info_2->GetInitiator()->account_info;
  CoreAccountId account_id_1 = identity_manager()->PickAccountIdForAccount(
      signin_account_info_1.gaia_id, signin_account_info_1.email);
  CoreAccountId account_id_2 = identity_manager()->PickAccountIdForAccount(
      signin_account_info_2.gaia_id, signin_account_info_2.email);
  dice_response_handler_->ProcessDiceHeader(
      std::move(signin_params_1),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));

  GaiaAuthConsumer* consumer_1 = signin_client_.GetAndClearConsumer();
  ASSERT_THAT(consumer_1, testing::NotNull());
  dice_response_handler_->ProcessDiceHeader(
      std::move(signin_params_2),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));
  GaiaAuthConsumer* consumer_2 = signin_client_.GetAndClearConsumer();
  ASSERT_THAT(consumer_2, testing::NotNull());
  EXPECT_EQ(
      2u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());
  ASSERT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id_1));
  ASSERT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id_2));
  ASSERT_FALSE(
      identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState(
          account_id_1));
  ASSERT_FALSE(
      identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState(
          account_id_2));
  // Signout from one of the accounts while signin is in flight.
  dice_response_handler_->ProcessDiceHeader(
      std::move(signout_params_1),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));
  // Check that one of the fetchers is cancelled.
  EXPECT_EQ(
      1u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());
  // Allow the remaining fetcher to complete.
  consumer_2->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
      "refresh_token", "access_token", /*expires_in_secs=*/10,
      /*is_under_advanced_protection=*/false, /*is_bound_to_key=*/false));
  EXPECT_EQ(
      0u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());
  // Check that the right token is available.
  EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id_1));
  EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id_2));
  EXPECT_FALSE(
      identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState(
          account_id_2));
}

TEST_F(DiceResponseHandlerTest, SignoutPrimaryAccountWithSignoutRestrictions) {
  signin_client_.set_is_clear_primary_account_allowed_for_testing(
      SigninClient::SignoutDecision::CLEAR_PRIMARY_ACCOUNT_DISALLOWED);
  const char kSecondaryEmail[] = "other@gmail.com";
  DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNOUT);
  dice_params.signout_info()->account_infos.push_back(
      GetDiceResponseParamsAccountInfo(kSecondaryEmail));
  const auto& dice_account_info = dice_params.signout_info()->account_infos[0];
  AccountInfo primary_account = identity_test_env_.MakePrimaryAccountAvailable(
      dice_account_info.email, signin::ConsentLevel::kSignin);
  AccountInfo secondary_account_info =
      identity_test_env_.MakeAccountAvailable(kSecondaryEmail);
  EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(
      primary_account.GetAccountId()));
  EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(
      secondary_account_info.GetAccountId()));
  EXPECT_TRUE(
      identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSignin));
  // Receive signout response.
  RunSignoutTest(std::move(dice_params), {}, primary_account.GetAccountId(),
                 /*invalid_primary_account=*/true);

  // Check that the reconcilor was not blocked.
  EXPECT_EQ(0, reconcilor_blocked_count_);
  EXPECT_EQ(0, reconcilor_unblocked_count_);
}

// Verifies that if a background token fetch hangs, the session is still
// guaranteed to complete and fire OnDiceSigninSessionComplete within the
// timeout limit (10s + 1s scheduling leeway).
TEST_F(DiceResponseHandlerTest,
       MultipleAccounts_TokenFetchTimeoutFiresSessionComplete) {
  const int account_count = 2;
  const int initiator_index = 0;
  DiceResponseParams dice_params =
      MakeMultiSigninDiceParams(DiceAction::SIGNIN, account_count,
                                /*primary_is_connected=*/signin::Tribool::kTrue,
                                /*eligible_for_token_binding=*/true,
                                /*mtls_token_binding=*/false, initiator_index);

  auto* signin_info = dice_params.signin_info();
  CoreAccountId initiator_account_id;
  CoreAccountId secondary_account_id;

  const auto& accounts = signin_info->accounts();
  initiator_account_id = identity_manager()->PickAccountIdForAccount(
      accounts[initiator_index].account_info.gaia_id,
      accounts[initiator_index].account_info.email);
  secondary_account_id = identity_manager()->PickAccountIdForAccount(
      accounts[1].account_info.gaia_id, accounts[1].account_info.email);

  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));

  // Complete initiator fetcher successfully.
  GaiaAuthConsumer* consumer_init = signin_client_.GetAndClearConsumer();
  ASSERT_THAT(consumer_init, testing::NotNull());
  consumer_init->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
      "refresh_token_init", "access_token", /*expires_in_secs=*/10,
      /*is_under_advanced_protection=*/false, /*is_bound_to_key=*/false));

  // Now secondary fetcher (Bob) is running. We simulate it hanging (never
  // returning). Retrieve and clear the secondary consumer from signin_client_
  // to prevent a dangling pointer warning when the fetcher is destroyed on
  // timeout.
  GaiaAuthConsumer* consumer_sec = signin_client_.GetAndClearConsumer();
  ASSERT_THAT(consumer_sec, testing::NotNull());

  EXPECT_EQ(
      1u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());
  EXPECT_FALSE(session_complete_called_);

  // Fast forward time by 9 seconds.
  // The 10-second fetcher timeout has NOT fired yet!
  task_environment_.FastForwardBy(base::Seconds(9));
  EXPECT_FALSE(session_complete_called_);
  EXPECT_EQ(
      1u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());

  // Fast forward time by 2 more seconds (11 seconds total).
  // The 10-second timeout has fired!
  task_environment_.FastForwardBy(base::Seconds(2));

  // The fetcher timed out, so the session completed and fired the terminal
  // signal.
  EXPECT_TRUE(session_complete_called_);
  EXPECT_EQ(
      0u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());

  // The secondary failed (timed out), but it is still passed in the completed
  // list to be filtered out by the profile creator.
  EXPECT_THAT(completed_secondary_accounts_,
              testing::UnorderedElementsAre(secondary_account_id));
}

TEST_F(DiceResponseHandlerTest, InvalidPrimaryConnectedInUnsignedProfile) {
  base::HistogramTester histogram_tester;

  ASSERT_FALSE(
      identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSignin));

  DiceResponseParams dice_params = MakeMultiSigninDiceParams(
      DiceAction::SIGNIN, /*account_count=*/2,
      /*primary_is_connected=*/signin::Tribool::kTrue,
      /*eligible_for_token_binding=*/false, /*mtls_token_binding=*/false,
      /*initiator_index=*/0);

  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));

  histogram_tester.ExpectUniqueSample(
      "Signin.Dice.InvalidPrimaryConnectedInUnsignedProfile", true, 1);

  // Because primary_is_connected was normalized from kTrue to kFalse,
  // FetchMode::kInitiatorFirst is used instead of FetchMode::kAll. Therefore,
  // only 1 token fetcher (for the initiator) should be pending initially.
  EXPECT_EQ(1u, signin_client_.GetTestURLLoaderFactory()->NumPending());

  // Complete the initiator fetcher to trigger the secondary fetcher.
  GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer();
  ASSERT_THAT(consumer, testing::NotNull());
  consumer->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
      "refresh_token_0", "access_token", /*expires_in_secs=*/10,
      /*is_under_advanced_protection=*/false, /*is_bound_to_key=*/false));

  // Now the secondary fetcher should be created.
  EXPECT_EQ(1u, signin_client_.GetTestURLLoaderFactory()->NumPending());
  consumer = signin_client_.GetAndClearConsumer();
  ASSERT_THAT(consumer, testing::NotNull());
  consumer->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult(
      "refresh_token_1", "access_token", /*expires_in_secs=*/10,
      /*is_under_advanced_protection=*/false, /*is_bound_to_key=*/false));

  EXPECT_EQ(
      0u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());
}

TEST_F(DiceResponseHandlerTest, SessionCompleteFiredOnCancellation) {
  DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNIN);
  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));

  EXPECT_EQ(
      1u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());
  EXPECT_FALSE(session_complete_called_);

  // Clear the consumer from signin_client_ to prevent a dangling pointer
  // warning when the token fetcher is destroyed during cancellation.
  signin_client_.GetAndClearConsumer();

  // A second concurrent sign-in request for the same account cancels the first
  // session's fetchers. This must reliably trigger OnDiceSigninSessionComplete
  // on the first session's delegate.
  dice_response_handler_->ProcessDiceHeader(
      MakeDiceParams(DiceAction::SIGNIN),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));
  EXPECT_TRUE(session_complete_called_);

  // Clear the new consumer created by the second sign-in request before test
  // fixture teardown deletes dice_response_handler_.
  signin_client_.GetAndClearConsumer();
}

TEST_F(DiceResponseHandlerTest, SessionCompleteFiredOnHandlerDestruction) {
  DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNIN);
  dice_response_handler_->ProcessDiceHeader(
      std::move(dice_params),
      std::make_unique<TestProcessDiceHeaderDelegate>(this));

  EXPECT_EQ(
      1u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting());
  EXPECT_FALSE(session_complete_called_);

  // Clear the consumer from signin_client_ to prevent a dangling pointer
  // warning when the token fetcher is destroyed during shutdown.
  signin_client_.GetAndClearConsumer();

  // Destroying the handler (e.g. during profile shutdown or teardown) while
  // token fetches are still pending must guaranteed fire
  // OnDiceSigninSessionComplete.
  dice_response_handler_.reset();
  EXPECT_TRUE(session_complete_called_);
}

}  // namespace
