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

#import "ios/chrome/browser/signin/model/authentication_service.h"

#import <memory>

#import "base/apple/foundation_util.h"
#import "base/functional/bind.h"
#import "base/functional/callback_helpers.h"
#import "base/memory/raw_ptr.h"
#import "base/strings/sys_string_conversions.h"
#import "base/test/bind.h"
#import "base/test/gtest_util.h"
#import "base/test/metrics/histogram_tester.h"
#import "base/test/run_until.h"
#import "base/test/scoped_feature_list.h"
#import "base/test/test_future.h"
#import "components/keyed_service/core/service_access_type.h"
#import "components/policy/core/common/policy_loader_ios_constants.h"
#import "components/pref_registry/pref_registry_syncable.h"
#import "components/prefs/pref_registry_simple.h"
#import "components/signin/ios/browser/features.h"
#import "components/signin/public/base/signin_pref_names.h"
#import "components/signin/public/base/signin_switches.h"
#import "components/signin/public/identity_manager/device_accounts_synchronizer.h"
#import "components/signin/public/identity_manager/identity_manager.h"
#import "components/signin/public/identity_manager/identity_test_environment.h"
#import "components/signin/public/identity_manager/identity_test_utils.h"
#import "components/signin/public/identity_manager/test_identity_manager_observer.h"
#import "components/sync/test/mock_sync_service.h"
#import "components/sync_preferences/pref_service_mock_factory.h"
#import "components/sync_preferences/pref_service_syncable.h"
#import "ios/chrome/browser/content_settings/model/host_content_settings_map_factory.h"
#import "ios/chrome/browser/policy/model/policy_util.h"
#import "ios/chrome/browser/shared/model/application_context/application_context.h"
#import "ios/chrome/browser/shared/model/prefs/browser_prefs.h"
#import "ios/chrome/browser/shared/model/prefs/pref_names.h"
#import "ios/chrome/browser/shared/model/profile/features.h"
#import "ios/chrome/browser/shared/model/profile/test/test_profile_ios.h"
#import "ios/chrome/browser/shared/model/profile/test/test_profile_manager_ios.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/shared/public/features/system_flags.h"
#import "ios/chrome/browser/signin/model/authentication_service.h"
#import "ios/chrome/browser/signin/model/authentication_service_factory.h"
#import "ios/chrome/browser/signin/model/authentication_service_observer.h"
#import "ios/chrome/browser/signin/model/chrome_account_manager_service.h"
#import "ios/chrome/browser/signin/model/chrome_account_manager_service_factory.h"
#import "ios/chrome/browser/signin/model/fake_authentication_service_delegate.h"
#import "ios/chrome/browser/signin/model/fake_refresh_access_token_error.h"
#import "ios/chrome/browser/signin/model/fake_system_identity.h"
#import "ios/chrome/browser/signin/model/fake_system_identity_manager.h"
#import "ios/chrome/browser/signin/model/identity_manager_factory.h"
#import "ios/chrome/browser/signin/model/ios_device_management_error_details.h"
#import "ios/chrome/browser/signin/model/refresh_access_token_error.h"
#import "ios/chrome/browser/signin/model/system_identity.h"
#import "ios/chrome/browser/sync/model/mock_sync_service_utils.h"
#import "ios/chrome/browser/sync/model/sync_service_factory.h"
#import "ios/chrome/test/ios_chrome_scoped_testing_local_state.h"
#import "ios/web/public/test/web_task_environment.h"
#import "testing/gtest/include/gtest/gtest.h"
#import "testing/gtest_mac.h"
#import "testing/platform_test.h"
#import "third_party/ocmock/OCMock/OCMock.h"
#import "third_party/ocmock/gtest_support.h"

using testing::_;
using testing::Return;

using HandleMDMCallback = FakeSystemIdentityManager::HandleMDMCallback;
using HandleMDMNotificationCallback =
    FakeSystemIdentityManager::HandleMDMNotificationCallback;

namespace {

CoreAccountId GetAccountId(id<SystemIdentity> identity) {
  return CoreAccountId::FromGaiaId(identity.gaiaId);
}

}  // namespace

class AuthenticationServiceObserverTest : public AuthenticationServiceObserver {
 public:
  void OnPrimaryAccountRestricted() override {
    ++on_primary_account_restricted_counter_;
  }

  int GetOnPrimaryAccountRestrictedCounter() {
    return on_primary_account_restricted_counter_;
  }

  void OnServiceStatusChanged() override {
    ++on_service_status_changed_counter_;
  }

  int GetOnServiceStatusChangedCounter() {
    return on_service_status_changed_counter_;
  }

 private:
  int on_primary_account_restricted_counter_ = 0;
  int on_service_status_changed_counter_ = 0;
};

class AuthenticationServiceTest : public PlatformTest {
 protected:
  explicit AuthenticationServiceTest() {
    TestProfileIOS::Builder builder;
    builder.SetPrefService(CreatePrefService());
    builder.AddTestingFactory(SyncServiceFactory::GetInstance(),
                              base::BindRepeating(&CreateMockSyncService));
    builder.AddTestingFactory(
        AuthenticationServiceFactory::GetInstance(),
        AuthenticationServiceFactory::GetFactoryWithDelegateForTesting(
            std::make_unique<FakeAuthenticationServiceDelegate>()));
    profile_ = profile_manager_.AddProfileWithBuilder(std::move(builder));

    fake_system_identity1_ = [FakeSystemIdentity fakeIdentity1];
    fake_system_identity_manager()->AddIdentity(fake_system_identity1_);
    fake_system_identity2_ = [FakeSystemIdentity fakeIdentity2];
    fake_system_identity_manager()->AddIdentity(fake_system_identity2_);

    account_manager_ =
        ChromeAccountManagerServiceFactory::GetForProfile(profile_.get());

    // Force explicit instantiation of the AuthenticationService.
    std::ignore = authentication_service();
  }

  // Wait until started tasks are executed and expects the absence of primary
  // identity.
  void WaitForPrimaryIdentityToBeRemoved() {
    base::RunLoop run_loop;
    task_environment_.GetMainThreadTaskRunner()->PostTask(
        FROM_HERE, run_loop.QuitClosure());
    run_loop.Run();
    EXPECT_FALSE(authentication_service()->HasPrimaryIdentity());
  }

  std::unique_ptr<sync_preferences::PrefServiceSyncable> CreatePrefService() {
    sync_preferences::PrefServiceMockFactory factory;
    scoped_refptr<user_prefs::PrefRegistrySyncable> registry(
        new user_prefs::PrefRegistrySyncable);
    std::unique_ptr<sync_preferences::PrefServiceSyncable> prefs =
        factory.CreateSyncable(registry.get());
    RegisterProfilePrefs(registry.get());
    return prefs;
  }

  void VerifyLastSigninTimestamp() {
    EXPECT_EQ(profile_.get()->GetPrefs()->GetTime(prefs::kLastSigninTimestamp),
              base::Time::Now());
  }

  void FireApplicationWillEnterForeground() {
    authentication_service()->OnApplicationWillEnterForeground();
  }

  void FireAccessTokenRefreshFailed(id<SystemIdentity> identity,
                                    id<RefreshAccessTokenError> error) {
    authentication_service()->OnAccessTokenRefreshFailed(
        identity, error, std::set<std::string>());
  }

  void MarkSignedinUserMigratedFromSyncing() {
    profile_->GetPrefs()->SetString(
        prefs::kGoogleServicesSyncingGaiaIdMigratedToSignedIn,
        authentication_service()->GetPrimaryIdentity().gaiaId.ToString());
    profile_->GetPrefs()->SetString(
        prefs::kGoogleServicesSyncingUsernameMigratedToSignedIn,
        base::SysNSStringToUTF8(
            authentication_service()->GetPrimaryIdentity().userEmail));
  }

  // Simulates that fetching access token for `identity` fails with a given
  // error identifier. Returns the MDM error information.
  //
  // `invocation_counter` will be incremented each time `HandleMDMNotification`
  // is invoked with the returned error object (unless a new error is created).
  // The pointer mush outlive the use of the returned error object. Using a
  // stack allocated value in a test case should be enough.
  //
  // The callback passed to `HandleMDMNotification` will be invoked with the
  // value of `is_identity_blocked`.
  id<RefreshAccessTokenError> CreateRefreshAccessTokenError(
      id<SystemIdentity> identity,
      uint32_t* invocation_counter = nullptr,
      bool is_identity_blocked = false,
      bool is_scope_limited_error = false) {
    auto mdm_callback = base::BindRepeating(
        [](uint32_t* counter, bool is_blocked, HandleMDMCallback callback) {
          if (counter) {
            ++*counter;
          }
          std::move(callback).Run(is_blocked);
        },
        invocation_counter, is_identity_blocked);
    id<RefreshAccessTokenError> mdm_error = [[FakeRefreshAccessTokenError alloc]
           initWithIdentity:identity
        isScopeLimitedError:is_scope_limited_error
                   callback:std::move(mdm_callback)];
    GetAccessTokenCallback callback = base::BindRepeating(
        [](id<RefreshAccessTokenError> mdm_error,
           SystemIdentityManager::AccessTokenCallback cb)
            -> id<RefreshAccessTokenError> {
          NSError* get_token_error =
              [NSError errorWithDomain:@"com.google.HTTPStatus"
                                  code:-1
                              userInfo:nil];
          std::move(cb).Run(std::nullopt, get_token_error);
          return mdm_error;
        },
        mdm_error);
    fake_system_identity_manager()->SetGetAccessTokenCallback(
        CoreAccountId::FromGaiaId(identity.gaiaId), callback);
    return mdm_error;
  }

  void SetCachedMDMInfo(id<SystemIdentity> identity,
                        id<RefreshAccessTokenError> mdm_error) {
    auto& cached_mdm_errors = authentication_service()->cached_mdm_errors_;
    cached_mdm_errors[GetAccountId(identity)] = mdm_error;
  }

  id<RefreshAccessTokenError> GetCachedMDMInfo(id<SystemIdentity> identity) {
    auto& cached_mdm_errors = authentication_service()->cached_mdm_errors_;
    auto iterator = cached_mdm_errors.find(GetAccountId(identity));
    return iterator == cached_mdm_errors.end() ? nil : iterator->second;
  }

  bool HasCachedMDMInfo(id<SystemIdentity> identity) {
    return GetCachedMDMInfo(identity) != nil;
  }

  int ClearBrowsingDataCount() {
    return authentication_service()->delegate_->clear_browsing_data_counter_;
  }

  int ClearBrowsingDataFromSigninCount() {
    return authentication_service()
        ->delegate_->clear_browsing_data_from_signin_counter_;
  }

  AuthenticationService* authentication_service() {
    return AuthenticationServiceFactory::GetForProfile(profile_.get());
  }

  signin::IdentityManager* identity_manager() {
    return IdentityManagerFactory::GetForProfile(profile_.get());
  }

  FakeSystemIdentityManager* fake_system_identity_manager() {
    return FakeSystemIdentityManager::FromSystemIdentityManager(
        GetApplicationContext()->GetSystemIdentityManager());
  }

  syncer::MockSyncService* mock_sync_service() {
    return static_cast<syncer::MockSyncService*>(
        SyncServiceFactory::GetForProfile(profile_.get()));
  }

  // Returns the n-th identity on the device, identified by `index`.
  id<SystemIdentity> identity(NSUInteger index) {
    std::vector<AccountInfo> accountInfos =
        identity_manager()->GetAccountsOnDevice();
    CHECK_LT(index, accountInfos.size());
    return account_manager_->GetIdentityOnDeviceWithGaiaID(
        accountInfos[index].GetGaiaId());
  }

  // Sets a restricted pattern.
  void SetPattern(const std::string& pattern) {
    base::ListValue allowed_patterns;
    allowed_patterns.Append(pattern);
    GetApplicationContext()->GetLocalState()->SetList(
        prefs::kRestrictAccountsToPatterns, std::move(allowed_patterns));
  }

  PrefService* local_state() {
    return GetApplicationContext()->GetLocalState();
  }

  web::WebTaskEnvironment task_environment_{
      base::test::TaskEnvironment::TimeSource::MOCK_TIME};
  IOSChromeScopedTestingLocalState scoped_testing_local_state_;
  signin::IdentityTestEnvironment identity_test_env_;
  TestProfileManagerIOS profile_manager_;
  raw_ptr<TestProfileIOS> profile_;
  raw_ptr<ChromeAccountManagerService> account_manager_;
  // Used to verify histogram logging.
  base::HistogramTester histogram_tester_;
  FakeSystemIdentity* fake_system_identity1_;
  FakeSystemIdentity* fake_system_identity2_;
};

TEST_F(AuthenticationServiceTest, TestDefaultGetPrimaryIdentity) {
  EXPECT_FALSE(authentication_service()->GetPrimaryIdentity());
  EXPECT_FALSE(authentication_service()->HasPrimaryIdentity());
}

TEST_F(AuthenticationServiceTest, TestSignInAndGetPrimaryIdentity) {
  // Sign in.
  authentication_service()->SignIn(
      identity(0), signin_metrics::AccessPoint::kFullscreenSigninPromo);
  VerifyLastSigninTimestamp();

  EXPECT_NSEQ(identity(0), authentication_service()->GetPrimaryIdentity());

  std::string user_email = base::SysNSStringToUTF8([identity(0) userEmail]);
  AccountInfo account_info =
      identity_manager()->FindExtendedAccountInfoByEmailAddress(user_email);
  EXPECT_EQ(user_email, account_info.GetEmail());
  EXPECT_EQ(identity(0).gaiaId, account_info.GetGaiaId());
  EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(
      account_info.GetAccountId()));
  EXPECT_TRUE(authentication_service()->HasPrimaryIdentity());
  histogram_tester_.ExpectUniqueSample(
      "Signin.SignIn.Completed",
      signin_metrics::AccessPoint::kFullscreenSigninPromo, 1);
}

// Tests that reauth prompt can be set and reset.
TEST_F(AuthenticationServiceTest, TestSetReauthPromptForSignInAndSync) {
  // Verify that the default value of this flag is off.
  EXPECT_FALSE(authentication_service()->ShouldReauthPromptForSignInAndSync());
  // Verify that prompt-flag setter and getter functions are working correctly.
  authentication_service()->SetReauthPromptForSignInAndSync();
  EXPECT_TRUE(authentication_service()->ShouldReauthPromptForSignInAndSync());
  authentication_service()->ResetReauthPromptForSignInAndSync();
  EXPECT_FALSE(authentication_service()->ShouldReauthPromptForSignInAndSync());
}

// Tests that reauth prompt is not set when the user signs out.
TEST_F(AuthenticationServiceTest, TestHandleForgottenIdentityNoPromptSignIn) {
  // Sign in.
  authentication_service()->SignIn(identity(0),
                                   signin_metrics::AccessPoint::kStartPage);
  VerifyLastSigninTimestamp();

  // Set the authentication service as "In Foreground", then remove the
  // identity.
  FireApplicationWillEnterForeground();
  base::test::TestFuture<NSError*> identity_forgotten;
  fake_system_identity_manager()->ForgetIdentity(
      identity(0), identity_forgotten.GetCallback());
  ASSERT_TRUE(identity_forgotten.Wait());
  WaitForPrimaryIdentityToBeRemoved();

  // User is signed out (no corresponding identity), but not prompted for sign
  // in (as the action was user initiated).
  EXPECT_FALSE(authentication_service()->HasPrimaryIdentity());
  EXPECT_FALSE(authentication_service()->ShouldReauthPromptForSignInAndSync());
}

// The reauth prompt should be shown if the primary identity is removed from an
// other app when the user was signed in.
TEST_F(AuthenticationServiceTest, TestHandleForgottenIdentityPromptSignIn) {
  // Sign in.
  authentication_service()->SignIn(identity(0),
                                   signin_metrics::AccessPoint::kStartPage);
  VerifyLastSigninTimestamp();

  // Set the authentication service as "In Background", then remove the
  // identity.
  fake_system_identity_manager()->ForgetIdentityFromOtherApplication(
      identity(0));
  WaitForPrimaryIdentityToBeRemoved();
  base::RunLoop run_loop;
  task_environment_.GetMainThreadTaskRunner()->PostTask(FROM_HERE,
                                                        run_loop.QuitClosure());
  run_loop.Run();
  EXPECT_FALSE(authentication_service()->HasPrimaryIdentity());

  // User is signed out (no corresponding identity), and reauth prompt is set.
  EXPECT_FALSE(authentication_service()->HasPrimaryIdentity());
  EXPECT_TRUE(authentication_service()->ShouldReauthPromptForSignInAndSync());
}

// Tests that AuthenticationService triggers the IdentityManager reloads when
// a secondary identity is added.
TEST_F(AuthenticationServiceTest, OnAddIdentity) {
  // Sign in.
  authentication_service()->SignIn(identity(0),
                                   signin_metrics::AccessPoint::kStartPage);
  VerifyLastSigninTimestamp();

  auto account_compare_func = [](const CoreAccountInfo& first,
                                 const CoreAccountInfo& second) {
    return first.account_id < second.account_id;
  };
  std::vector<CoreAccountInfo> accounts =
      identity_manager()->GetAccountsWithRefreshTokens();
  std::sort(accounts.begin(), accounts.end(), account_compare_func);
  ASSERT_EQ(2u, accounts.size());
  CoreAccountId gaiad_id_1 =
      CoreAccountId::FromGaiaId(fake_system_identity1_.gaiaId);
  EXPECT_EQ(gaiad_id_1, accounts[0].account_id);
  CoreAccountId gaiad_id_2 =
      CoreAccountId::FromGaiaId(fake_system_identity2_.gaiaId);
  EXPECT_EQ(gaiad_id_2, accounts[1].account_id);

  FakeSystemIdentity* fake_system_identity3 =
      [FakeSystemIdentity fakeIdentity3];
  fake_system_identity_manager()->AddIdentity(fake_system_identity3);

  // Accounts are reloaded, "foo3@gmail.com" is added as it is now in
  // IdentityManager.
  accounts = identity_manager()->GetAccountsWithRefreshTokens();
  std::sort(accounts.begin(), accounts.end(), account_compare_func);
  ASSERT_EQ(3u, accounts.size());
  EXPECT_EQ(gaiad_id_1, accounts[0].account_id);
  EXPECT_EQ(gaiad_id_2, accounts[1].account_id);
  CoreAccountId gaiad_id_3 =
      CoreAccountId::FromGaiaId(fake_system_identity3.gaiaId);
  EXPECT_EQ(gaiad_id_3, accounts[2].account_id);
}

TEST_F(AuthenticationServiceTest, HasPrimaryIdentityBackground) {
  // Sign in.
  authentication_service()->SignIn(identity(0),
                                   signin_metrics::AccessPoint::kStartPage);
  EXPECT_TRUE(authentication_service()->HasPrimaryIdentity());
  VerifyLastSigninTimestamp();

  // Remove the signed in identity while in background, and check that
  // HasPrimaryIdentity is up-to-date.
  base::test::TestFuture<NSError*> identity_forgotten;
  fake_system_identity_manager()->ForgetIdentity(
      identity(0), identity_forgotten.GetCallback());
  ASSERT_TRUE(identity_forgotten.Wait());
  WaitForPrimaryIdentityToBeRemoved();

  EXPECT_FALSE(authentication_service()->HasPrimaryIdentity());
}

// Tests that MDM errors are correctly cleared on foregrounding, sending
// notifications that the state of error has changed.
TEST_F(AuthenticationServiceTest, MDMErrorsClearedOnForeground) {
  authentication_service()->SignIn(identity(0),
                                   signin_metrics::AccessPoint::kStartPage);
  EXPECT_EQ(identity_manager()->GetAccountsWithRefreshTokens().size(), 2UL);
  VerifyLastSigninTimestamp();

  SetCachedMDMInfo(identity(0), CreateRefreshAccessTokenError(identity(0)));

  GoogleServiceAuthError error =
      GoogleServiceAuthError::FromInvalidGaiaCredentialsReason(
          GoogleServiceAuthError::InvalidGaiaCredentialsReason::UNKNOWN);
  signin::UpdatePersistentErrorOfRefreshTokenForAccount(
      identity_manager(), GetAccountId(identity(0)), error);

  // MDM error for `identity_` is being cleared and the error state of refresh
  // token will be updated.
  {
    bool notification_received = false;
    signin::TestIdentityManagerObserver observer(identity_manager());
    observer.SetOnErrorStateOfRefreshTokenUpdatedCallback(
        base::BindLambdaForTesting([&]() { notification_received = true; }));

    FireApplicationWillEnterForeground();
    EXPECT_TRUE(notification_received);
    EXPECT_EQ(
        identity(0).gaiaId,
        observer.AccountFromErrorStateOfRefreshTokenUpdatedCallback().gaia);
  }

  // MDM error has already been cleared, no notification will be sent.
  {
    bool notification_received = false;
    signin::TestIdentityManagerObserver observer(identity_manager());
    observer.SetOnErrorStateOfRefreshTokenUpdatedCallback(
        base::BindLambdaForTesting([&]() { notification_received = true; }));

    FireApplicationWillEnterForeground();
    EXPECT_FALSE(notification_received);
  }
}

// Tests that MDM errors are correctly cleared when signing out.
TEST_F(AuthenticationServiceTest, MDMErrorsClearedOnSignout) {
  authentication_service()->SignIn(identity(0),
                                   signin_metrics::AccessPoint::kStartPage);
  ASSERT_EQ(identity_manager()->GetAccountsWithRefreshTokens().size(), 2UL);
  VerifyLastSigninTimestamp();

  SetCachedMDMInfo(identity(0), CreateRefreshAccessTokenError(identity(0)));
  authentication_service()->SignOut(
      signin_metrics::ProfileSignout::kAbortSignin, nil);
  EXPECT_FALSE(HasCachedMDMInfo(identity(0)));
  EXPECT_EQ(identity_manager()->GetAccountsWithRefreshTokens().size(), 2UL);
}

// Tests that (a) MDM errors are cleared, and (b) local data *only from the
// signed-in period* are cleared, when signing out of a managed account.
TEST_F(AuthenticationServiceTest, ManagedAccountSignOut_ClearDataFromSignin) {
  FakeSystemIdentity* fake_system_identity =
      [FakeSystemIdentity fakeManagedIdentity];
  fake_system_identity_manager()->AddIdentity(fake_system_identity);

  // The managed identity is assigned to a separate profile now.
  ASSERT_EQ([account_manager_->GetAllIdentities() count], 2UL);
  ASSERT_EQ(identity_manager()->GetAccountsWithRefreshTokens().size(), 2UL);
  // Move the managed identity into the personal profile, to mimic the
  // situation where the managed identity was already there before
  // kSeparateProfilesForManagedAccounts was enabled.
  GetApplicationContext()
      ->GetAccountProfileMapper()
      ->MoveManagedAccountToPersonalProfileForTesting(identity(2).gaiaId);

  ASSERT_EQ([account_manager_->GetAllIdentities() count], 3UL);
  ASSERT_EQ(identity_manager()->GetAccountsWithRefreshTokens().size(), 3UL);

  authentication_service()->SignIn(identity(2),
                                   signin_metrics::AccessPoint::kStartPage);
  ASSERT_TRUE(authentication_service()->HasPrimaryIdentityManaged());
  VerifyLastSigninTimestamp();

  SetCachedMDMInfo(identity(2), CreateRefreshAccessTokenError(identity(2)));
  authentication_service()->SignOut(
      signin_metrics::ProfileSignout::kUserClickedSignoutSettings, nil);
  EXPECT_FALSE(HasCachedMDMInfo(identity(2)));
  EXPECT_EQ(identity_manager()->GetAccountsWithRefreshTokens().size(), 3UL);
  EXPECT_EQ(ClearBrowsingDataCount(), 0);
  EXPECT_EQ(ClearBrowsingDataFromSigninCount(), 1);
}

// Tests that (a) MDM errors are cleared, and (b) local data is *not* cleared,
// when signing out of a managed account while the browser is managed.
TEST_F(AuthenticationServiceTest,
       ManagedAccountSignOut_DontClearIfManagedBrowser) {
  // Add managed configuration so the browser is managed.
  NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
  NSDictionary* dict = @{@"key" : @"value"};
  [userDefaults setObject:dict forKey:kPolicyLoaderIOSConfigurationKey];
  FakeSystemIdentity* fake_system_identity =
      [FakeSystemIdentity fakeManagedIdentity];
  fake_system_identity_manager()->AddIdentity(fake_system_identity);

  // The managed identity is assigned to a separate profile now.
  ASSERT_EQ([account_manager_->GetAllIdentities() count], 2UL);
  ASSERT_EQ(identity_manager()->GetAccountsWithRefreshTokens().size(), 2UL);
  // Move the managed identity into the personal profile, to mimic the
  // situation where the managed identity was already there before
  // kSeparateProfilesForManagedAccounts was enabled.
  GetApplicationContext()
      ->GetAccountProfileMapper()
      ->MoveManagedAccountToPersonalProfileForTesting(identity(2).gaiaId);

  ASSERT_EQ([account_manager_->GetAllIdentities() count], 3UL);
  ASSERT_EQ(identity_manager()->GetAccountsWithRefreshTokens().size(), 3UL);

  authentication_service()->SignIn(identity(2),
                                   signin_metrics::AccessPoint::kStartPage);
  ASSERT_TRUE(authentication_service()->HasPrimaryIdentityManaged());
  VerifyLastSigninTimestamp();

  SetCachedMDMInfo(identity(2), CreateRefreshAccessTokenError(identity(2)));
  // Data should not be cleared if the browser is managed.
  authentication_service()->SignOut(
      signin_metrics::ProfileSignout::kAbortSignin, nil);
  ASSERT_FALSE(HasCachedMDMInfo(identity(2)));
  ASSERT_EQ(identity_manager()->GetAccountsWithRefreshTokens().size(), 3UL);
  EXPECT_EQ(ClearBrowsingDataCount(), 0);
  EXPECT_EQ(ClearBrowsingDataFromSigninCount(), 0);
  [userDefaults removeObjectForKey:kPolicyLoaderIOSConfigurationKey];
}

// Tests that MDM errors do not lead to seeding empty account ids.
// Regression test for root cause of crbug.com/1482236
TEST_F(AuthenticationServiceTest, MDMErrorsDontSeedEmptyAccountIds) {
  authentication_service()->SignIn(identity(0),
                                   signin_metrics::AccessPoint::kStartPage);
  ASSERT_EQ(identity_manager()->GetAccountsWithRefreshTokens().size(), 2UL);
  VerifyLastSigninTimestamp();

  SetCachedMDMInfo(identity(0), CreateRefreshAccessTokenError(identity(0)));
  ASSERT_EQ(identity_manager()->GetAccountsWithRefreshTokens().size(), 2u);

  // Fake an mdm error for an identity that is not loaded in IdentityManager.
  FakeSystemIdentity* fake_system_identity3 =
      [FakeSystemIdentity fakeIdentity3];
  id<RefreshAccessTokenError> mdm_error_mock =
      OCMStrictProtocolMock(@protocol(RefreshAccessTokenError));
  SetCachedMDMInfo(fake_system_identity3, mdm_error_mock);

  // Make sure we still have 2 identities and `fake_system_identity3` is still
  // not loaded.
  ASSERT_EQ(identity_manager()->GetAccountsWithRefreshTokens().size(), 2u);

  GoogleServiceAuthError error =
      GoogleServiceAuthError::FromInvalidGaiaCredentialsReason(
          GoogleServiceAuthError::InvalidGaiaCredentialsReason::UNKNOWN);
  signin::UpdatePersistentErrorOfRefreshTokenForAccount(
      identity_manager(), GetAccountId(identity(0)), error);
  FireApplicationWillEnterForeground();

  ASSERT_EQ(identity_manager()->GetAccountsWithRefreshTokens().size(), 2u);
  EXPECT_FALSE(
      identity_manager()->GetAccountsWithRefreshTokens()[0].account_id.empty());
  EXPECT_FALSE(
      identity_manager()->GetAccountsWithRefreshTokens()[1].account_id.empty());
  EXPECT_OCMOCK_VERIFY((id)mdm_error_mock);
}

// Tests that (a) MDM errors are cleared and (b) all browsing data is cleared
// (not just from the signed-in period), when signing out of a managed account
// that was migrated from sync consent.
TEST_F(AuthenticationServiceTest, ManagedAccountSignOut_MigratedFromSyncing) {
  FakeSystemIdentity* fake_system_identity =
      [FakeSystemIdentity fakeManagedIdentity];
  fake_system_identity_manager()->AddIdentity(fake_system_identity);

  // The managed identity is assigned to a separate profile now.
  ASSERT_EQ([account_manager_->GetAllIdentities() count], 2UL);
  ASSERT_EQ(identity_manager()->GetAccountsWithRefreshTokens().size(), 2UL);
  // Move the managed identity into the personal profile, to mimic the
  // situation where the managed identity was already there before
  // kSeparateProfilesForManagedAccounts was enabled.
  GetApplicationContext()
      ->GetAccountProfileMapper()
      ->MoveManagedAccountToPersonalProfileForTesting(identity(2).gaiaId);

  ASSERT_EQ([account_manager_->GetAllIdentities() count], 3UL);
  ASSERT_EQ(identity_manager()->GetAccountsWithRefreshTokens().size(), 3UL);

  authentication_service()->SignIn(identity(2),
                                   signin_metrics::AccessPoint::kStartPage);
  ASSERT_TRUE(authentication_service()->HasPrimaryIdentityManaged());
  VerifyLastSigninTimestamp();

  // Mark the signed-in user as "migrated from previously syncing".
  MarkSignedinUserMigratedFromSyncing();

  authentication_service()->SignOut(
      signin_metrics::ProfileSignout::kAbortSignin, nil);
  EXPECT_FALSE(HasCachedMDMInfo(identity(2)));
  EXPECT_EQ(identity_manager()->GetAccountsWithRefreshTokens().size(), 3UL);
  // Because the account was migrated from Sync-the-feature, all browsing data
  // should be cleared, not just from the signed-in period.
  EXPECT_EQ(ClearBrowsingDataCount(), 1);
  EXPECT_EQ(ClearBrowsingDataFromSigninCount(), 0);
}

// Tests that potential MDM notifications are correctly handled and dispatched
// to MDM service when necessary.
TEST_F(AuthenticationServiceTest, HandleMDMNotification) {
  authentication_service()->SignIn(identity(0),
                                   signin_metrics::AccessPoint::kStartPage);
  VerifyLastSigninTimestamp();

  GoogleServiceAuthError error =
      GoogleServiceAuthError::FromInvalidGaiaCredentialsReason(
          GoogleServiceAuthError::InvalidGaiaCredentialsReason::UNKNOWN);
  signin::UpdatePersistentErrorOfRefreshTokenForAccount(
      identity_manager(), GetAccountId(identity(0)), error);

  uint32_t invocation_counter1 = 0;
  id<RefreshAccessTokenError> mdm_error1 =
      CreateRefreshAccessTokenError(identity(0), &invocation_counter1);
  ASSERT_TRUE(mdm_error1);

  // Notification will show the MDM dialog the first time.
  FireAccessTokenRefreshFailed(identity(0), mdm_error1);
  fake_system_identity_manager()->WaitForServiceCallbacksToComplete();
  EXPECT_EQ(invocation_counter1, 1u);

  // Same notification won't show the MDM dialog the second time.
  FireAccessTokenRefreshFailed(identity(0), mdm_error1);
  fake_system_identity_manager()->WaitForServiceCallbacksToComplete();
  EXPECT_EQ(invocation_counter1, 1u);

  uint32_t invocation_counter2 = 0;
  id<RefreshAccessTokenError> mdm_error2 =
      CreateRefreshAccessTokenError(identity(0), &invocation_counter2);
  ASSERT_TRUE(mdm_error2);

  // New notification will show the MDM dialog on the same identity.
  FireAccessTokenRefreshFailed(identity(0), mdm_error2);
  fake_system_identity_manager()->WaitForServiceCallbacksToComplete();
  EXPECT_EQ(invocation_counter1, 1u);
  EXPECT_EQ(invocation_counter2, 1u);
}

// Tests that MDM notification is suppressed for scope limited errors.
TEST_F(AuthenticationServiceTest, HandleMDMNotificationSuppressed) {
  base::HistogramTester histogram_tester;
  authentication_service()->SignIn(identity(0),
                                   signin_metrics::AccessPoint::kStartPage);
  VerifyLastSigninTimestamp();

  GoogleServiceAuthError error =
      GoogleServiceAuthError::FromScopeLimitedUnrecoverableErrorReason(
          GoogleServiceAuthError::ScopeLimitedUnrecoverableErrorReason::
              kInvalidScope);
  signin::UpdatePersistentErrorOfRefreshTokenForAccount(
      identity_manager(), GetAccountId(identity(0)), error);

  uint32_t invocation_counter = 0;
  id<RefreshAccessTokenError> mdm_error = CreateRefreshAccessTokenError(
      identity(0), &invocation_counter, /*is_identity_blocked*/ false,
      /*is_scope_limited_error*/ true);
  ASSERT_TRUE(mdm_error);

  // MDM notification handling will be suppressed.
  FireAccessTokenRefreshFailed(identity(0), mdm_error);
  fake_system_identity_manager()->WaitForServiceCallbacksToComplete();
  EXPECT_EQ(invocation_counter, 0u);
  histogram_tester.ExpectBucketCount("Signin.ScopeLimitedErrorSuppressed", true,
                                     1);
}

// Tests that MDM blocked notifications are correctly signing out the user if
// the primary account is blocked.
TEST_F(AuthenticationServiceTest, HandleMDMBlockedNotification) {
  authentication_service()->SignIn(identity(0),
                                   signin_metrics::AccessPoint::kStartPage);
  GoogleServiceAuthError error =
      GoogleServiceAuthError::FromInvalidGaiaCredentialsReason(
          GoogleServiceAuthError::InvalidGaiaCredentialsReason::UNKNOWN);
  signin::UpdatePersistentErrorOfRefreshTokenForAccount(
      identity_manager(), GetAccountId(identity(0)), error);
  VerifyLastSigninTimestamp();

  uint32_t invocation_counter = 0;
  id<RefreshAccessTokenError> mdm_error = CreateRefreshAccessTokenError(
      identity(0), &invocation_counter, /*is_identity_blocked*/ true);

  // User not signed out as `identity(1)` isn't the primary account.
  FireAccessTokenRefreshFailed(identity(1), mdm_error);
  fake_system_identity_manager()->WaitForServiceCallbacksToComplete();
  EXPECT_TRUE(authentication_service()->HasPrimaryIdentity());
  EXPECT_EQ(invocation_counter, 0u);

  // User signed out as `identity_` is the primary account.
  FireAccessTokenRefreshFailed(identity(0), mdm_error);
  fake_system_identity_manager()->WaitForServiceCallbacksToComplete();
  EXPECT_FALSE(authentication_service()->HasPrimaryIdentity());
  EXPECT_EQ(invocation_counter, 1u);
}

// Tests that MDM dialog isn't shown when there is no cached MDM error.
TEST_F(AuthenticationServiceTest, ShowMDMErrorDialogNoCachedError) {
  EXPECT_FALSE(
      authentication_service()->ShowMDMErrorDialogForIdentity(identity(0)));
}

// Tests that MDM dialog isn't shown when there is a cached MDM error but no
// corresponding error for the account.
TEST_F(AuthenticationServiceTest, ShowMDMErrorDialogInvalidCachedError) {
  uint32_t invocation_counter = 0;
  SetCachedMDMInfo(identity(0), CreateRefreshAccessTokenError(
                                    identity(0), &invocation_counter));

  EXPECT_FALSE(
      authentication_service()->ShowMDMErrorDialogForIdentity(identity(0)));
  fake_system_identity_manager()->WaitForServiceCallbacksToComplete();
  EXPECT_EQ(invocation_counter, 0u);
}

// Tests that MDM dialog is shown when there is a cached error and a
// corresponding error for the account.
TEST_F(AuthenticationServiceTest, ShowMDMErrorDialog) {
  authentication_service()->SignIn(identity(0),
                                   signin_metrics::AccessPoint::kStartPage);
  GoogleServiceAuthError error =
      GoogleServiceAuthError::FromInvalidGaiaCredentialsReason(
          GoogleServiceAuthError::InvalidGaiaCredentialsReason::UNKNOWN);
  signin::UpdatePersistentErrorOfRefreshTokenForAccount(
      identity_manager(), GetAccountId(identity(0)), error);
  VerifyLastSigninTimestamp();

  uint32_t invocation_counter = 0;
  SetCachedMDMInfo(identity(0), CreateRefreshAccessTokenError(
                                    identity(0), &invocation_counter));

  EXPECT_TRUE(
      authentication_service()->ShowMDMErrorDialogForIdentity(identity(0)));
  fake_system_identity_manager()->WaitForServiceCallbacksToComplete();
  EXPECT_EQ(invocation_counter, 1u);
}

TEST_F(AuthenticationServiceTest, SigninDisallowedCrash) {
  // Disable sign-in.
  local_state()->SetBoolean(prefs::kSigninAllowedOnDevice, false);
  EXPECT_EQ(profile_->GetPrefs()->GetBoolean(prefs::kSigninAllowed), false);

  // Attempt to sign in, and verify there is a crash.
  EXPECT_CHECK_DEATH(authentication_service()->SignIn(
      identity(0), signin_metrics::AccessPoint::kStartPage));
}

// Tests that reauth prompt is not set if the primary identity is restricted and
// `OnPrimaryAccountRestricted` is forwarded.
TEST_F(AuthenticationServiceTest, TestHandleRestrictedIdentityPromptSignIn) {
  AuthenticationServiceObserverTest observer_test;
  authentication_service()->AddObserver(&observer_test);
  // Sign in.
  authentication_service()->SignIn(identity(0),
                                   signin_metrics::AccessPoint::kStartPage);
  VerifyLastSigninTimestamp();

  // Set the account restriction.
  SetPattern("foo");
  WaitForPrimaryIdentityToBeRemoved();
  base::RunLoop run_loop;
  task_environment_.GetMainThreadTaskRunner()->PostTask(FROM_HERE,
                                                        run_loop.QuitClosure());
  run_loop.Run();
  EXPECT_FALSE(account_manager_->HasIdentities());

  // User is signed out (no corresponding identity), and reauth prompt is set.
  EXPECT_FALSE(authentication_service()->HasPrimaryIdentity());
  EXPECT_EQ(1, observer_test.GetOnPrimaryAccountRestrictedCounter());
  authentication_service()->RemoveObserver(&observer_test);
}

// Tests AuthenticationService::GetServiceStatus() using
// prefs::kBrowserSigninPolicy.
TEST_F(AuthenticationServiceTest, TestGetServiceStatus) {
  AuthenticationServiceObserverTest observer_test;
  authentication_service()->AddObserver(&observer_test);

  // Expect sign-in allowed by default.
  EXPECT_EQ(AuthenticationService::ServiceStatus::SigninAllowed,
            authentication_service()->GetServiceStatus());

  local_state()->SetBoolean(prefs::kSigninAllowedOnDevice, false);
  EXPECT_EQ(profile_->GetPrefs()->GetBoolean(prefs::kSigninAllowed), false);
  // Expect sign-in disabled by user.
  EXPECT_EQ(AuthenticationService::ServiceStatus::SigninDisabledByUser,
            authentication_service()->GetServiceStatus());
  // Expect onServiceStatus notification called.
  EXPECT_EQ(1, observer_test.GetOnServiceStatusChangedCounter());

  // Set sign-in disabled by policy.
  local_state()->SetInteger(prefs::kBrowserSigninPolicy,
                            static_cast<int>(BrowserSigninMode::kDisabled));
  // Expect sign-in to be disabled by policy.
  EXPECT_EQ(AuthenticationService::ServiceStatus::SigninDisabledByPolicy,
            authentication_service()->GetServiceStatus());
  // Expect onServiceStatus notification called.
  EXPECT_EQ(2, observer_test.GetOnServiceStatusChangedCounter());

  // Set sign-in forced by policy.
  local_state()->SetInteger(prefs::kBrowserSigninPolicy,
                            static_cast<int>(BrowserSigninMode::kForced));
  // Expect sign-in to be forced by policy.
  EXPECT_EQ(AuthenticationService::ServiceStatus::SigninForcedByPolicy,
            authentication_service()->GetServiceStatus());
  // Expect onServiceStatus notification called.
  EXPECT_EQ(3, observer_test.GetOnServiceStatusChangedCounter());

  local_state()->SetBoolean(prefs::kSigninAllowedOnDevice, true);
  EXPECT_EQ(profile_->GetPrefs()->GetBoolean(prefs::kSigninAllowed), true);
  // Expect sign-in to be still forced by policy.
  EXPECT_EQ(AuthenticationService::ServiceStatus::SigninForcedByPolicy,
            authentication_service()->GetServiceStatus());
  // Expect onServiceStatus notification called.
  EXPECT_EQ(4, observer_test.GetOnServiceStatusChangedCounter());
  authentication_service()->RemoveObserver(&observer_test);
}

// Tests that identity manager loads identities while being signed out.
// And also tests that an identity being added is loaded by identity manager.
TEST_F(AuthenticationServiceTest,
       TestAccountsLoadedByIdentityManagerWhenSignedOut) {
  // `fakeIdentity1` and `fakeIdentity2` are already loaded.
  std::vector<AccountInfo> account_info_vector =
      identity_manager()->GetExtendedAccountInfoForAccountsWithRefreshToken();
  EXPECT_EQ(2ul, account_info_vector.size());
  // Let's load `fakeIdentity3`.
  id<SystemIdentity> fake_identity3 = [FakeSystemIdentity fakeIdentity3];
  fake_system_identity_manager()->AddIdentity(fake_identity3);
  account_info_vector =
      identity_manager()->GetExtendedAccountInfoForAccountsWithRefreshToken();
  EXPECT_EQ(3ul, account_info_vector.size());
}

// Tests that identity manager loads identities while being signed out.
// And also tests that an identity being removed is forgotten by identity
// manager.
TEST_F(AuthenticationServiceTest, TestAccountsForgetIdentityWhenSignedOut) {
  std::vector<AccountInfo> account_info_vector =
      identity_manager()->GetExtendedAccountInfoForAccountsWithRefreshToken();
  // `fakeIdentity1` and `fakeIdentity2` are already loaded.
  EXPECT_EQ(2ul, account_info_vector.size());
  // Let's forget `fakeIdentity2`.
  id<SystemIdentity> fake_identity2 = [FakeSystemIdentity fakeIdentity2];
  base::test::TestFuture<NSError*> identity_forgotten;
  fake_system_identity_manager()->ForgetIdentity(
      fake_identity2, identity_forgotten.GetCallback());
  ASSERT_TRUE(identity_forgotten.Wait());
  account_info_vector =
      identity_manager()->GetExtendedAccountInfoForAccountsWithRefreshToken();
  EXPECT_EQ(1ul, account_info_vector.size());
}

// Tests that ShowMDMErrorDialogForIdentity works correctly for cached
// GoogleServiceAuthError MDM errors.
TEST_F(AuthenticationServiceTest,
       ShowMDMErrorDialogWithHandleMdmErrorsForDasherAccountsEnabled) {
  base::test::ScopedFeatureList feature_list;
  feature_list.InitAndEnableFeature(
      switches::kHandleMdmErrorsForDasherAccounts);

  authentication_service()->SignIn(identity(0),
                                   signin_metrics::AccessPoint::kStartPage);

  // ShowMDMErrorDialogForIdentity should do nothing as there is no error in
  // IdentityManager.
  EXPECT_FALSE(
      authentication_service()->ShowMDMErrorDialogForIdentity(identity(0)));

  auto details = std::make_unique<gaia::IOSDeviceManagementErrorDetails>(
      @{@"info" : @"test"}, /*is_user_actionable=*/true);
  GoogleServiceAuthError error =
      GoogleServiceAuthError::FromDeviceManagementError(std::move(details));

  // Set the error on the account in IdentityManager.
  signin::UpdatePersistentErrorOfRefreshTokenForAccount(
      identity_manager(), GetAccountId(identity(0)), error);

  // ShowMDMErrorDialogForIdentity should show the dialog using the error from
  // IdentityManager.
  EXPECT_TRUE(
      authentication_service()->ShowMDMErrorDialogForIdentity(identity(0)));
}
