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

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

#import <set>
#import <string>

#import "base/apple/foundation_util.h"
#import "base/functional/bind.h"
#import "base/functional/callback_helpers.h"
#import "base/i18n/time_formatting.h"
#import "base/run_loop.h"
#import "base/strings/sys_string_conversions.h"
#import "base/task/sequenced_task_runner.h"
#import "base/time/time.h"
#import "google_apis/gaia/gaia_auth_util.h"
#import "ios/chrome/browser/signin/model/constants.h"
#import "ios/chrome/browser/signin/model/fake_account_details_view_controller.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_details.h"
#import "ios/chrome/browser/signin/model/fake_system_identity_interaction_manager.h"
#import "ios/chrome/browser/signin/model/fake_system_identity_manager_storage.h"
#import "ios/chrome/browser/signin/model/refresh_access_token_error.h"
#import "ios/chrome/browser/signin/model/system_identity.h"
#import "ios/public/provider/chrome/browser/signin/signin_resources_api.h"

namespace {

// Expiration duration of the fake access token returned by `GetAccessToken()`.
// Needs to be cover the execution of a typical test, as tests usually set up
// their mock to expect just one token request and rely on the tokens being
// served from cache.
constexpr base::TimeDelta kAccessTokenExpiration = base::Minutes(5);

// Returns a hosted domain for identity.
NSString* FakeGetHostedDomainForIdentity(id<SystemIdentity> identity) {
  NSString* domain = base::SysUTF8ToNSString(
      gaia::ExtractDomainName(base::SysNSStringToUTF8(identity.userEmail)));
  return [domain isEqualToString:@"gmail.com"] ? @"" : domain;
}

// Stores a pointer to the last created FakeSystemIdentityManager*. Used to
// check whether the conversion is possible in `FromSystemIdentityManager()`.
FakeSystemIdentityManager* gFakeSystemIdentityManager = nullptr;

}  // anonymous namespace

FakeSystemIdentityManager::FakeSystemIdentityManager()
    : FakeSystemIdentityManager(nil) {}

FakeSystemIdentityManager::FakeSystemIdentityManager(
    NSArray<FakeSystemIdentity*>* fake_identities)
    : storage_([[FakeSystemIdentityManagerStorage alloc] init]) {
  DCHECK(!gFakeSystemIdentityManager);
  gFakeSystemIdentityManager = this;

  for (FakeSystemIdentity* fake_identity in fake_identities) {
    [storage_ addFakeIdentity:fake_identity];
    // Set up capabilities to remove fetch delays during the authentication
    // flow.
    // TODO(b/327221052): verify if this should be replaced by a handler for
    // default capabilities.
    AccountCapabilitiesTestMutator* mutator =
        GetPendingCapabilitiesMutator(fake_identity);
    mutator->set_can_show_history_sync_opt_ins_without_minor_mode_restrictions(
        true);
    mutator->set_can_sign_in_to_chrome(true);
  }
}

FakeSystemIdentityManager::~FakeSystemIdentityManager() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK_EQ(gFakeSystemIdentityManager, this);
  gFakeSystemIdentityManager = nullptr;
}

// static
FakeSystemIdentityManager* FakeSystemIdentityManager::FromSystemIdentityManager(
    SystemIdentityManager* manager) {
  DCHECK_EQ(gFakeSystemIdentityManager, manager);
  return gFakeSystemIdentityManager;
}

void FakeSystemIdentityManager::AddIdentity(id<SystemIdentity> identity) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  GaiaId gaiaID = identity.gaiaId;
  DCHECK(![storage_ containsIdentityWithGaiaID:gaiaID]);
  gaia_ids_removed_by_user_.erase(gaiaID);
  FakeSystemIdentity* fake_identity =
      base::apple::ObjCCast<FakeSystemIdentity>(identity);
  [storage_ addFakeIdentity:fake_identity];
  FireIdentityListChanged();

  // Set up capabilities to remove fetch delays during the authentication flow.
  // TODO(b/327221052): verify if this should be replaced by a handler for
  // default capabilities.
  AccountCapabilitiesTestMutator* mutator =
      GetPendingCapabilitiesMutator(identity);
  mutator->set_can_show_history_sync_opt_ins_without_minor_mode_restrictions(
      true);
  mutator->set_can_sign_in_to_chrome(true);
}

void FakeSystemIdentityManager::AddIdentityWithUnknownCapabilities(
    id<SystemIdentity> identity) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(![storage_ containsIdentityWithGaiaID:identity.gaiaId]);
  gaia_ids_removed_by_user_.erase(identity.gaiaId);
  FakeSystemIdentity* fake_identity =
      base::apple::ObjCCast<FakeSystemIdentity>(identity);
  [storage_ addFakeIdentity:fake_identity];
  FireIdentityListChanged();
}

void FakeSystemIdentityManager::AddIdentityWithCapabilities(
    id<SystemIdentity> identity,
    NSDictionary<NSString*, NSNumber*>* capabilities) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(![storage_ containsIdentityWithGaiaID:identity.gaiaId]);
  gaia_ids_removed_by_user_.erase(identity.gaiaId);
  FakeSystemIdentity* fake_identity =
      base::apple::ObjCCast<FakeSystemIdentity>(identity);
  [storage_ addFakeIdentity:fake_identity];
  AccountCapabilitiesTestMutator* mutator =
      GetPendingCapabilitiesMutator(identity);
  for (NSString* name in capabilities) {
    std::string stdString = base::SysNSStringToUTF8(name);
    bool value = capabilities[name].boolValue;
    mutator->SetCapability(stdString, value);
  }
  FireIdentityListChanged();
}

void FakeSystemIdentityManager::ForgetIdentityFromOtherApplication(
    id<SystemIdentity> identity) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (![storage_ containsIdentityWithGaiaID:identity.gaiaId]) {
    return;
  }

  ForgetIdentityAsync(identity, base::DoNothing(), /*removed_by_user=*/false);
}

void FakeSystemIdentityManager::UpdateSystemIdentityAvatar(
    const GaiaId& gaia_id,
    UIImage* avatar,
    bool send_notification) {
  FakeSystemIdentityDetails* details = [storage_ detailsForGaiaID:gaia_id];
  details.cachedAvatar = avatar;
  details.avatarUpdatedFromLastFetch = YES;
  if (send_notification) {
    FireIdentityUpdated(details.fakeIdentity);
  }
}

AccountCapabilitiesTestMutator*
FakeSystemIdentityManager::GetPendingCapabilitiesMutator(
    id<SystemIdentity> identity) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK([storage_ containsIdentityWithGaiaID:identity.gaiaId]);
  FakeSystemIdentityDetails* details =
      [storage_ detailsForGaiaID:identity.gaiaId];
  return details.pendingCapabilitiesMutator;
}

AccountCapabilities FakeSystemIdentityManager::GetVisibleCapabilities(
    id<SystemIdentity> identity) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK([storage_ containsIdentityWithGaiaID:identity.gaiaId]);
  FakeSystemIdentityDetails* details =
      [storage_ detailsForGaiaID:identity.gaiaId];
  return AccountCapabilities(details.visibleCapabilities);
}

void FakeSystemIdentityManager::SetInstantlyFillHostedDomainCache(
    bool instantly_fill) {
  instantly_fill_hosted_domain_cache_ = instantly_fill;
}

void FakeSystemIdentityManager::SetGetHostedDomainError(NSError* error) {
  get_hosted_domain_error_ = error;
}

size_t FakeSystemIdentityManager::GetNumHostedDomainErrorsReturned() const {
  return num_hosted_domain_errors_returned_;
}

void FakeSystemIdentityManager::FireSystemIdentityReloaded() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  FireIdentityListChanged();
}

void FakeSystemIdentityManager::FireIdentityUpdatedNotification(
    id<SystemIdentity> identity) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  FireIdentityUpdated(identity);
}

void FakeSystemIdentityManager::FireIdentityRefreshTokenUpdatedNotification(
    id<SystemIdentity> identity) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  FireIdentityRefreshTokenUpdated(identity);
}

void FakeSystemIdentityManager::WaitForServiceCallbacksToComplete() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  while (pending_callbacks_) {
    DCHECK(resume_closure_.is_null());

    base::RunLoop run_loop;
    resume_closure_ = run_loop.QuitClosure();
    run_loop.Run();
  }
}

bool FakeSystemIdentityManager::ContainsIdentity(id<SystemIdentity> identity) {
  return [storage_ containsIdentityWithGaiaID:identity.gaiaId];
}

void FakeSystemIdentityManager::SetPersistentAuthErrorForAccount(
    const CoreAccountId& accountId) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  GaiaId gaia_id(accountId.ToString());
  CHECK([storage_ containsIdentityWithGaiaID:gaia_id]);
  FakeSystemIdentityDetails* details = [storage_ detailsForGaiaID:gaia_id];
  // TODO(crbug.com/502126003): Remove when AccessTokenCallback is removed.
  details.getAccessTokenCallback = base::BindRepeating(
      [](AccessTokenCallback callback) -> id<RefreshAccessTokenError> {
        NSInteger integer_error_code = static_cast<NSInteger>(
            SystemIdentityManagerErrorCode::kInvalidTokenIdentity);
        NSError* error =
            [NSError errorWithDomain:kSystemIdentityManagerErrorDomain
                                code:integer_error_code
                            userInfo:nil];
        std::move(callback).Run(std::nullopt, error);
        return nil;
      });
  details.getAccessTokenRequestCallback =
      base::BindRepeating([](AccessTokenRequestCallback callback) {
        std::move(callback).Run(base::unexpected(
            GoogleServiceAuthError::FromInvalidGaiaCredentialsReason(
                GoogleServiceAuthError::InvalidGaiaCredentialsReason::
                    UNKNOWN)));
      });
  FakeSystemIdentity* identity = details.fakeIdentity;
  identity.hasValidAuth = NO;
  FireIdentityRefreshTokenUpdated(identity);
}

void FakeSystemIdentityManager::ClearPersistentAuthErrorForAccount(
    const CoreAccountId& accountId) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  GaiaId gaia_id(accountId.ToString());
  CHECK([storage_ containsIdentityWithGaiaID:gaia_id]);
  FakeSystemIdentityDetails* details = [storage_ detailsForGaiaID:gaia_id];

  // Reset the custom callback to revert back to the default behavior.
  // TODO(crbug.com/502126003): Remove when AccessTokenCallback is removed.
  details.getAccessTokenCallback.Reset();
  details.getAccessTokenRequestCallback.Reset();
  FakeSystemIdentity* identity = details.fakeIdentity;
  identity.hasValidAuth = YES;
  FireIdentityRefreshTokenUpdated(identity);
}

void FakeSystemIdentityManager::SetGetAccessTokenCallback(
    const CoreAccountId& accountId,
    GetAccessTokenCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  GaiaId gaia_id(accountId.ToString());
  CHECK([storage_ containsIdentityWithGaiaID:gaia_id]);
  FakeSystemIdentityDetails* details = [storage_ detailsForGaiaID:gaia_id];
  details.getAccessTokenCallback = std::move(callback);
}

void FakeSystemIdentityManager::SetGetAccessTokenCallback(
    const CoreAccountId& accountId,
    GetAccessTokenRequestCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  GaiaId gaia_id(accountId.ToString());
  CHECK([storage_ containsIdentityWithGaiaID:gaia_id]);
  FakeSystemIdentityDetails* details = [storage_ detailsForGaiaID:gaia_id];
  details.getAccessTokenRequestCallback = std::move(callback);
}

id<RefreshAccessTokenError>
FakeSystemIdentityManager::CreateRefreshAccessTokenFailure(
    id<SystemIdentity> identity,
    HandleMDMNotificationCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK([storage_ containsIdentityWithGaiaID:identity.gaiaId]);
  FakeSystemIdentityDetails* details =
      [storage_ detailsForGaiaID:identity.gaiaId];
  details.error = [[FakeRefreshAccessTokenError alloc]
         initWithIdentity:identity
      isScopeLimitedError:NO
                 callback:std::move(callback)];
  return details.error;
}

bool FakeSystemIdentityManager::WasMDMNotificationDisplayed() const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return was_mdm_notification_displayed_;
}

void FakeSystemIdentityManager::ResetMDMNotificationDisplayed() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  was_mdm_notification_displayed_ = false;
}

bool FakeSystemIdentityManager::IsSigninSupported() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return true;
}

bool FakeSystemIdentityManager::HandleSessionOpenURLContexts(
    UIScene* scene,
    NSSet<UIOpenURLContext*>* url_contexts) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  // Ignored.
  return false;
}

void FakeSystemIdentityManager::ApplicationDidDiscardSceneSessions(
    NSSet<UISceneSession*>* scene_sessions) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  // Ignored.
}

void FakeSystemIdentityManager::DismissDialogs() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  // Ignored.
}

FakeSystemIdentityManager::DismissViewCallback
FakeSystemIdentityManager::PresentAccountDetailsController(
    PresentDialogConfiguration configuration) {
  ProceduralBlock dismissalCompletion = nil;
  if (configuration.dismissal_completion) {
    dismissalCompletion =
        base::CallbackToBlock(std::move(configuration.dismissal_completion));
  }
  FakeAccountDetailsViewController* account_details_view_controller =
      [[FakeAccountDetailsViewController alloc]
             initWithIdentity:configuration.identity
          dismissalCompletion:dismissalCompletion];
  [configuration.view_controller
      presentViewController:account_details_view_controller
                   animated:configuration.animated
                 completion:nil];
  return base::BindOnce(^(BOOL dismiss_animated) {
    [account_details_view_controller dismissAnimated:dismiss_animated];
  });
}

FakeSystemIdentityManager::DismissViewCallback
FakeSystemIdentityManager::PresentWebAndAppSettingDetailsController(
    PresentDialogConfiguration configuration) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return base::DoNothing();
}

FakeSystemIdentityManager::DismissViewCallback
FakeSystemIdentityManager::PresentLinkedServicesSettingsDetailsController(
    PresentDialogConfiguration configuration) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return base::DoNothing();
}

void FakeSystemIdentityManager::SetInteractionManagerFactory(
    base::RepeatingCallback<id<SystemIdentityInteractionManager>()> factory) {
  interaction_manager_factory_ = factory;
}

id<SystemIdentityInteractionManager>
FakeSystemIdentityManager::CreateInteractionManager() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (interaction_manager_factory_) {
    return interaction_manager_factory_.Run();
  }
  return [[FakeSystemIdentityInteractionManager alloc]
      initWithManager:GetWeakPtr()];
}

void FakeSystemIdentityManager::IterateOverIdentities(
    IdentityIteratorCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  for (FakeSystemIdentityDetails* details in storage_) {
    if (callback.Run(details.fakeIdentity) ==
        IteratorResult::kInterruptIteration) {
      break;
    }
  }
}

void FakeSystemIdentityManager::ForgetIdentity(
    id<SystemIdentity> identity,
    ForgetIdentityCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK([storage_ containsIdentityWithGaiaID:identity.gaiaId]);
  // Forgetting an identity is an asynchronous operation (as it requires some
  // network calls).
  PostClosure(FROM_HERE,
              base::BindOnce(&FakeSystemIdentityManager::ForgetIdentityAsync,
                             GetWeakPtr(), identity, std::move(callback),
                             /*removed_by_user=*/true));
}

bool FakeSystemIdentityManager::IdentityRemovedByUser(const GaiaId& gaia_id) {
  return gaia_ids_removed_by_user_.contains(gaia_id);
}

void FakeSystemIdentityManager::GetAccessToken(
    id<SystemIdentity> identity,
    const std::set<std::string>& scopes,
    AccessTokenCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  GetAccessToken(identity, /*client_id*/ {}, scopes, std::move(callback));
}

void FakeSystemIdentityManager::GetAccessToken(
    id<SystemIdentity> identity,
    const std::string& client_id,
    const std::set<std::string>& scopes,
    AccessTokenCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK([storage_ containsIdentityWithGaiaID:identity.gaiaId]);
  // Fetching the access token is an asynchronous operation (as it requires
  // some network calls).
  PostClosure(
      FROM_HERE,
      base::BindOnce(&FakeSystemIdentityManager::GetAccessTokenAsyncLegacy,
                     GetWeakPtr(), identity, std::move(callback)));
}

void FakeSystemIdentityManager::GetAccessToken(
    id<SystemIdentity> identity,
    const std::set<std::string>& scopes,
    AccessTokenRequestCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  GetAccessToken(identity, /*client_id*/ {}, scopes, std::move(callback));
}

void FakeSystemIdentityManager::GetAccessToken(
    id<SystemIdentity> identity,
    const std::string& client_id,
    const std::set<std::string>& scopes,
    AccessTokenRequestCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK([storage_ containsIdentityWithGaiaID:identity.gaiaId]);
  // Fetching the access token is an asynchronous operation (as it requires
  // some network calls).
  PostClosure(FROM_HERE,
              base::BindOnce(&FakeSystemIdentityManager::GetAccessTokenAsync,
                             GetWeakPtr(), identity, std::move(callback)));
}

void FakeSystemIdentityManager::FetchAvatarForIdentity(
    id<SystemIdentity> identity) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  // Fetching the avatar is an asynchronous operation (as it requires some
  // network calls).
  if (![storage_ containsIdentityWithGaiaID:identity.gaiaId]) {
    // The identity was removed before async method was called. There is
    // nothing to do.
    return;
  }
  FakeSystemIdentityDetails* details =
      [storage_ detailsForGaiaID:identity.gaiaId];
  if (!details.avatarUpdatedFromLastFetch) {
    // If the avatar was not updated, async fetch should not be triggered. This
    // will avoid to call FireIdentityUpdated() for no reason, and simulate the
    // real behavior.
    return;
  }
  PostClosure(
      FROM_HERE,
      base::BindOnce(&FakeSystemIdentityManager::FetchAvatarForIdentityAsync,
                     GetWeakPtr(), identity));
}

UIImage* FakeSystemIdentityManager::GetCachedAvatarForIdentity(
    id<SystemIdentity> identity) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (![storage_ containsIdentityWithGaiaID:identity.gaiaId]) {
    // The identity was removed before async method was called. There is
    // nothing to do.
    return nil;
  }
  FakeSystemIdentityDetails* details =
      [storage_ detailsForGaiaID:identity.gaiaId];
  return details.cachedAvatar;
}

void FakeSystemIdentityManager::GetHostedDomain(id<SystemIdentity> identity,
                                                HostedDomainCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK([storage_ containsIdentityWithGaiaID:identity.gaiaId]);
  // Fetching the hosted domain is an asynchronous operation (as it requires
  // some network calls).
  PostClosure(FROM_HERE,
              base::BindOnce(&FakeSystemIdentityManager::GetHostedDomainAsync,
                             GetWeakPtr(), identity, std::move(callback)));
}

NSString* FakeSystemIdentityManager::GetCachedHostedDomainForIdentity(
    id<SystemIdentity> identity) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  CHECK(identity);
  if (instantly_fill_hosted_domain_cache_ ||
      [hosted_domain_cache_ containsObject:identity]) {
    return FakeGetHostedDomainForIdentity(identity);
  }
  return nil;
}

void FakeSystemIdentityManager::FetchCapabilities(
    id<SystemIdentity> identity,
    const std::vector<std::string>& names,
    FetchCapabilitiesCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK([storage_ containsIdentityWithGaiaID:identity.gaiaId]);
  // Fetching the hosted domain is an asynchronous operation (as it requires
  // some network calls).
  PostClosure(
      FROM_HERE,
      base::BindOnce(&FakeSystemIdentityManager::FetchCapabilitiesAsync,
                     GetWeakPtr(), identity, names, std::move(callback)));
}

void FakeSystemIdentityManager::FetchCapabilitiesWithPartial(
    id<SystemIdentity> identity,
    const std::vector<std::string>& names,
    FetchCapabilitiesCompletion completion,
    FetchPartialCapabilitiesCallback partial_callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK([storage_ containsIdentityWithGaiaID:identity.gaiaId]);
  // Fetching the hosted domain is an asynchronous operation (as it requires
  // some network calls).
  PostClosure(FROM_HERE,
              base::BindOnce(
                  &FakeSystemIdentityManager::FetchCapabilitiesWithPartialAsync,
                  GetWeakPtr(), identity, names, std::move(completion),
                  std::move(partial_callback)));
}

void FakeSystemIdentityManager::RegisterExternalPrivacyContextProvider(
    id<ExternalPrivacyContextUIProvider> provider) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  // Do nothing.
}

void FakeSystemIdentityManager::UnregisterExternalPrivacyContextProvider(
    id<ExternalPrivacyContextUIProvider> provider) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  // Do nothing.
}

void FakeSystemIdentityManager::ExternalPrivacyContextProviderReady(
    id<ExternalPrivacyContextUIProvider> provider) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  // Do nothing.
}

bool FakeSystemIdentityManager::HandleMDMNotification(
    id<SystemIdentity> identity,
    NSArray<id<SystemIdentity>>* active_identities,
    id<RefreshAccessTokenError> error,
    HandleMDMCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK([storage_ containsIdentityWithGaiaID:identity.gaiaId]);
  FakeRefreshAccessTokenError* fake_refresh_access_token_error =
      base::apple::ObjCCast<FakeRefreshAccessTokenError>(error);
  if (![fake_refresh_access_token_error.identity isEqual:identity]) {
    return false;
  }
  // Handling MDM error is asynchronous operation (as it requires some
  // network calls).
  PostClosure(FROM_HERE,
              base::BindOnce(fake_refresh_access_token_error.callback,
                             std::move(callback)));
  return true;
}

bool FakeSystemIdentityManager::DisplayMDMNotification(
    id<SystemIdentity> identity,
    const GoogleServiceAuthError& error,
    HandleMDMCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK([storage_ containsIdentityWithGaiaID:identity.gaiaId]);
  was_mdm_notification_displayed_ = true;
  base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, base::BindOnce(std::move(callback), false));
  return true;
}

bool FakeSystemIdentityManager::IsScopeLimitedError(
    id<RefreshAccessTokenError> error) {
  return base::apple::ObjCCastStrict<FakeRefreshAccessTokenError>(error)
      .isScopeLimitedError;
}

bool FakeSystemIdentityManager::IsMDMError(id<SystemIdentity> identity,
                                           NSError* error) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return false;
}

void FakeSystemIdentityManager::FetchTokenAuthURL(
    id<SystemIdentity> identity,
    NSURL* target_url,
    AuthenticatedURLCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  std::move(callback).Run(/*url=*/target_url, /*error=*/nil);
}

base::WeakPtr<FakeSystemIdentityManager>
FakeSystemIdentityManager::GetWeakPtr() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return weak_ptr_factory_.GetWeakPtr();
}

void FakeSystemIdentityManager::ForgetIdentityAsync(
    id<SystemIdentity> identity,
    ForgetIdentityCallback callback,
    bool removed_by_user) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (![storage_ containsIdentityWithGaiaID:identity.gaiaId]) {
    // The identity was removed before async method was called. There is
    // nothing to do.
    return;
  }
  GaiaId gaiaID = identity.gaiaId;
  if (removed_by_user) {
    gaia_ids_removed_by_user_.insert(gaiaID);
  }
  [storage_ removeIdentityWithGaiaID:identity.gaiaId];

  FireIdentityListChanged();

  std::move(callback).Run(/*error*/ nil);
}

void FakeSystemIdentityManager::GetAccessTokenAsyncLegacy(
    id<SystemIdentity> identity,
    AccessTokenCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (![storage_ containsIdentityWithGaiaID:identity.gaiaId]) {
    // The identity was removed before async method was called. There is
    // nothing to do.
    return;
  }
  FakeSystemIdentityDetails* details =
      [storage_ detailsForGaiaID:identity.gaiaId];
  if (details.getAccessTokenCallback) {
    id<RefreshAccessTokenError> error =
        details.getAccessTokenCallback.Run(std::move(callback));
    if (error) {
      FireIdentityAccessTokenRefreshFailed(identity, error,
                                           std::set<std::string>());
    }
    return;
  } else {
    const base::Time valid_until = base::Time::Now() + kAccessTokenExpiration;
    AccessTokenInfo info{TimeFormatHTTP(valid_until), valid_until};
    std::move(callback).Run(std::move(info), nil);
  }
}

void FakeSystemIdentityManager::GetAccessTokenAsync(
    id<SystemIdentity> identity,
    AccessTokenRequestCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (![storage_ containsIdentityWithGaiaID:identity.gaiaId]) {
    // The identity was removed before async method was called. There is
    // nothing to do.
    return;
  }
  FakeSystemIdentityDetails* details =
      [storage_ detailsForGaiaID:identity.gaiaId];
  if (details.getAccessTokenRequestCallback) {
    details.getAccessTokenRequestCallback.Run(std::move(callback));
    return;
  } else {
    const base::Time valid_until = base::Time::Now() + kAccessTokenExpiration;
    AccessTokenInfo info{TimeFormatHTTP(valid_until), valid_until};
    std::move(callback).Run(std::move(info));
  }
}

void FakeSystemIdentityManager::FetchAvatarForIdentityAsync(
    id<SystemIdentity> identity) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (![storage_ containsIdentityWithGaiaID:identity.gaiaId]) {
    // The identity was removed before async method was called. There is
    // nothing to do.
    return;
  }
  FakeSystemIdentityDetails* details =
      [storage_ detailsForGaiaID:identity.gaiaId];
  if (!details.cachedAvatar) {
    details.cachedAvatar = ios::provider::GetSigninDefaultAvatar();
  }
  details.avatarUpdatedFromLastFetch = NO;

  FireIdentityUpdated(identity);
}

void FakeSystemIdentityManager::GetHostedDomainAsync(
    id<SystemIdentity> identity,
    HostedDomainCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (![storage_ containsIdentityWithGaiaID:identity.gaiaId]) {
    // The identity was removed before the async method was called. There is
    // nothing to do.
    return;
  }
  // If a GetHostedDomain error has been set up, return that.
  if (get_hosted_domain_error_) {
    ++num_hosted_domain_errors_returned_;
    std::move(callback).Run(nil, get_hosted_domain_error_);
    return;
  }
  // No error -> success! The hosted domain is now also available via
  // GetCachedHostedDomainForIdentity().
  [hosted_domain_cache_ addObject:identity];
  std::move(callback).Run(FakeGetHostedDomainForIdentity(identity), nil);
}

void FakeSystemIdentityManager::FetchCapabilitiesAsync(
    id<SystemIdentity> identity,
    const std::vector<std::string>& names,
    FetchCapabilitiesCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (![storage_ containsIdentityWithGaiaID:identity.gaiaId]) {
    // The identity was removed before async method was called. There is
    // nothing to do.
    return;
  }
  FakeSystemIdentityDetails* details =
      [storage_ detailsForGaiaID:identity.gaiaId];

  // Simulates the action to refresh the internal capability state with
  // the pending changes fetched from the server.
  [details updateVisibleCapabilities];
  const FakeSystemIdentityCapabilitiesMap& capabilities =
      details.visibleCapabilities;

  std::map<std::string, CapabilityResult> result;
  for (const std::string& name : names) {
    const auto& iter = capabilities.find(name);
    if (iter == capabilities.end()) {
      result.insert({name, CapabilityResult::kUnknown});
    } else {
      result.insert({name, iter->second ? CapabilityResult::kTrue
                                        : CapabilityResult::kFalse});
    }
  }

  std::move(callback).Run(result);
}

void FakeSystemIdentityManager::FetchCapabilitiesWithPartialAsync(
    id<SystemIdentity> identity,
    const std::vector<std::string>& names,
    FetchCapabilitiesCompletion completion,
    FetchPartialCapabilitiesCallback partial_callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  auto callback = base::BindOnce(
      [](FetchPartialCapabilitiesCallback partial_callback,
         FetchCapabilitiesCompletion completion,
         std::map<std::string, CapabilityResult> result) {
        partial_callback.Run(result);
        std::move(completion).Run();
      },
      std::move(partial_callback), std::move(completion));

  FetchCapabilitiesAsync(identity, names, std::move(callback));
}

void FakeSystemIdentityManager::PostClosure(base::Location from_here,
                                            base::OnceClosure closure) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  ++pending_callbacks_;
  base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
      from_here, base::BindOnce(&FakeSystemIdentityManager::ExecuteClosure,
                                GetWeakPtr(), std::move(closure)));
}

void FakeSystemIdentityManager::ExecuteClosure(base::OnceClosure closure) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK_GT(pending_callbacks_, 0u);
  if (--pending_callbacks_ == 0u) {
    if (!resume_closure_.is_null()) {
      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, std::move(resume_closure_));
    }
  }

  if (!closure.is_null()) {
    std::move(closure).Run();
  }
}
