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

#import "components/password_manager/ios/password_suggestion_helper.h"

#import <algorithm>
#import <utility>

#import "base/feature_list.h"
#import "base/strings/sys_string_conversions.h"
#import "base/time/time.h"
#import "components/autofill/core/common/form_data.h"
#import "components/autofill/core/common/password_form_fill_data.h"
#import "components/autofill/core/common/unique_ids.h"
#import "components/autofill/ios/browser/autofill_driver_ios.h"
#import "components/autofill/ios/browser/form_suggestion.h"
#import "components/password_manager/core/browser/features/password_features.h"
#import "components/password_manager/core/browser/password_form.h"
#import "components/password_manager/core/browser/password_form_cache.h"
#import "components/password_manager/core/browser/password_manager_interface.h"
#import "components/password_manager/core/browser/password_ui_utils.h"
#import "components/password_manager/ios/account_select_fill_data.h"
#import "components/password_manager/ios/features.h"
#import "components/password_manager/ios/ios_password_manager_driver_factory.h"
#import "components/password_manager/ios/password_manager_ios_util.h"
#import "components/password_manager/ios/password_manager_java_script_feature.h"
#import "ios/web/public/js_messaging/web_frame.h"
#import "ios/web/public/js_messaging/web_frames_manager.h"
#import "ios/web/public/web_state.h"

using autofill::FieldRendererId;
using autofill::FormData;
using autofill::FormRendererId;
using autofill::PasswordFormFillData;
using base::SysNSStringToUTF16;
using base::SysNSStringToUTF8;
using base::SysUTF16ToNSString;
using base::SysUTF8ToNSString;
using password_manager::AccountSelectFillData;
using password_manager::FillData;
using password_manager::IsCrossOriginIframe;

namespace {

// Status of form extraction for a given frame.
enum class FormExtractionStatus {
  kNotRequested = 0,
  kRequested = 1,
  kCompleted = 2
};

// Gets the maximal period of time the form extraction queries can be hanging
// before timing out.
base::TimeDelta GetFormExtractionTimeoutMs() {
  return base::Milliseconds(
      password_manager::features::kIosPasswordFormExtractionRequestsTimeoutMs
          .Get());
}

// Gets the period of time between the scheduled cleanup tasks that completes
// the timed out form extraction queries. Set to be slightly more than
// GetFormExtractionTimeoutMs() to make sure that the hanging extraction query
// that originally triggered the cleanup task has the time to expire.
base::TimeDelta GetCleanupTaskPeriodMs() {
  return GetFormExtractionTimeoutMs() + base::Milliseconds(50);
}

// Determines if the form should be automatically submitted.
// The heuristic deems a form submittable if:
// 1. It has valid username and password fields.
// 2. There are no focusable fields (excluding checkboxes) between the
//    username and the password.
// 3. There are no focusable fields (excluding checkboxes) after the
//    password.
// 4. It does not likely contain a CAPTCHA.
// Note: Empty fields found before the username do not prevent submission.
bool ShouldTriggerSubmission(
    password_manager::PasswordManagerInterface* password_manager,
    password_manager::PasswordManagerDriver* driver,
    autofill::FormRendererId form_renderer_id) {
  if (!driver || !password_manager ||
      !base::FeatureList::IsEnabled(
          password_manager::features::kIOSPasswordAutoSubmission)) {
    return false;
  }
  const password_manager::PasswordForm* form =
      password_manager->GetPasswordFormCache()->GetPasswordForm(
          driver, form_renderer_id);
  if (!form) {
    return false;
  }
  autofill::FieldGlobalId username_field_id;
  autofill::FieldGlobalId password_field_id;
  for (const autofill::FormFieldData& field : form->form_data.fields()) {
    if (field.renderer_id() == form->username_element_renderer_id) {
      username_field_id = field.global_id();
    }
    if (field.renderer_id() == form->password_element_renderer_id) {
      password_field_id = field.global_id();
    }
  }
  password_manager::SubmissionReadinessState readiness =
      password_manager::CalculateSubmissionReadiness(
          form->form_data, username_field_id, password_field_id);
  return password_manager::CalculateTriggerSubmission(readiness);
}

}  // namespace

@protocol FillDataProvider <NSObject>

// True if suggestions are available for the field in form.
- (bool)
    areSuggestionsAvailableForFrameId:(NSString*)frameId
                       formRendererId:(autofill::FormRendererId)formRendererId
                      fieldRendererId:(autofill::FieldRendererId)fieldRendererId
                      isPasswordField:(bool)isPasswordField;

@end

// Represents a pending form query to be completed later with
// -runCompletion.
@interface PendingFormQuery : NSObject

// ID of the frame targeted by the query.
@property(nonatomic, strong, readonly) NSString* frameId;

// Timestamp when the query was created.
@property(nonatomic, readonly) base::TimeTicks creationTimestamp;

// Initializes the object with a `query` to complete with `completion` for
// frame with id `frameId`.
- (instancetype)initWithQuery:(FormSuggestionProviderQuery*)query
                   completion:(SuggestionsAvailableCompletion)completion
             fillDataProvider:(id<FillDataProvider>)fillDataProvider
              isPasswordField:(BOOL)isPasswordField;

// Runs the completion callback with the available fill data. This can only be
// done once in the lifetime of the query object.
- (void)runCompletion;

// Returns YES if the query is expired.
- (BOOL)isExpired;

@end

@implementation PendingFormQuery {
  FormSuggestionProviderQuery* _query;
  SuggestionsAvailableCompletion _completion;
  id<FillDataProvider> _fillDataProvider;
  BOOL _isPasswordField;
}

- (instancetype)initWithQuery:(FormSuggestionProviderQuery*)query
                   completion:(SuggestionsAvailableCompletion)completion
             fillDataProvider:(id<FillDataProvider>)fillDataProvider
              isPasswordField:(BOOL)isPasswordField {
  self = [super init];
  if (self) {
    _query = query;
    _completion = completion;
    _fillDataProvider = fillDataProvider;
    _frameId = query.frameID;
    _isPasswordField = isPasswordField;
    _creationTimestamp = base::TimeTicks::Now();
  }
  return self;
}

- (void)runCompletion {
  // Check that the completion was never run as -runCompletion
  // can only be called once.
  CHECK(_completion);

  _completion([_fillDataProvider
      areSuggestionsAvailableForFrameId:self.frameId
                         formRendererId:_query.formRendererID
                        fieldRendererId:_query.fieldRendererID
                        isPasswordField:_isPasswordField]);
  _completion = nil;
}

- (BOOL)isExpired {
  return base::TimeTicks::Now() >=
         _creationTimestamp + GetFormExtractionTimeoutMs();
}

@end

@interface PasswordSuggestionHelper () <FillDataProvider>

@end

@implementation PasswordSuggestionHelper {
  base::WeakPtr<web::WebState> _webState;

  // Fill data keyed by frame id for the frames' forms in the webstate.
  base::flat_map<std::string, std::unique_ptr<AccountSelectFillData>>
      _fillDataMap;

  // Pending form queries that are waiting for forms extraction results.
  NSMutableArray<PendingFormQuery*>* _pendingFormQueries;

  // Map of frame ids to the form extraction status for that frame.
  std::map<std::string, FormExtractionStatus> _framesFormExtractionStatus;

  // YES if there is pending queries cleanup task that was already scheduled.
  BOOL _cleanupScheduled;

  // Password Manager tied to the same web state as this helper.
  raw_ptr<password_manager::PasswordManagerInterface, DanglingUntriaged>
      _passwordManager;
}

#pragma mark - Initialization

- (instancetype)initWithWebState:(web::WebState*)webState
                 passwordManager:(password_manager::PasswordManagerInterface*)
                                     passwordManager {
  self = [super init];
  if (self) {
    _webState = webState->GetWeakPtr();
    _pendingFormQueries = [NSMutableArray array];
    _cleanupScheduled = NO;
    _passwordManager = passwordManager;
  }
  return self;
}

#pragma mark - Public methods

- (NSArray<FormSuggestion*>*)retrieveSuggestionsWithForm:
    (FormSuggestionProviderQuery*)formQuery {
  const std::string frameId = SysNSStringToUTF8(formQuery.frameID);
  AccountSelectFillData* fillData = [self fillDataForFrameId:frameId].first;

  BOOL isPasswordField =
      [self isPasswordFieldOnForm:formQuery
                         webFrame:[self frameWithId:frameId]];

  NSMutableArray<FormSuggestion*>* results = [NSMutableArray array];

  if (fillData->IsSuggestionsAvailable(formQuery.formRendererID,
                                       formQuery.fieldRendererID,
                                       isPasswordField)) {
    password_manager::FormInfoRetrievalResult formInfoResult =
        fillData->GetFormInfo(formQuery.formRendererID,
                              formQuery.fieldRendererID, isPasswordField);
    bool is_single_username_form =
        formInfoResult.has_value() &&
        formInfoResult.value()->username_element_id &&
        !formInfoResult.value()->password_element_id;

    std::vector<password_manager::UsernameAndRealm> usernameAndRealms =
        fillData->RetrieveSuggestions(formQuery.formRendererID,
                                      formQuery.fieldRendererID,
                                      isPasswordField);

    for (const auto& usernameAndRealm : usernameAndRealms) {
      NSString* username = SysUTF16ToNSString(usernameAndRealm.username);
      NSString* realm = nil;
      if (!usernameAndRealm.realm.empty()) {
        url::Origin origin = url::Origin::Create(GURL(usernameAndRealm.realm));
        realm = SysUTF8ToNSString(password_manager::GetShownOrigin(origin));
      }

      autofill::SuggestionType suggestionType =
          usernameAndRealm.is_backup_credential
              ? autofill::SuggestionType::kBackupPasswordEntry
              : autofill::SuggestionType::kPasswordEntry;

      IOSPasswordManagerDriver* driver =
          IOSPasswordManagerDriverFactory::FromWebStateAndWebFrame(
              _webState.get(), [self frameWithId:frameId]);
      bool should_trigger_submission = ShouldTriggerSubmission(
          _passwordManager, driver, formQuery.formRendererID);

      FormSuggestionMetadata metadata;
      metadata.is_single_username_form = is_single_username_form;
      metadata.likely_from_real_password_field = isPasswordField;
      metadata.should_trigger_submission = should_trigger_submission;

      [results
          addObject:[FormSuggestion suggestionWithValue:username
                                     displayDescription:realm
                                                   icon:nil
                                                   type:suggestionType
                                                payload:autofill::Suggestion::
                                                            Payload()
                                         requiresReauth:YES
                             acceptanceA11yAnnouncement:nil
                                               metadata:std::move(metadata)]];
    }
  }

  return [results copy];
}

// Adds a pending form extraction query and schedules a cleanup task in the case
// the query hangs.
- (void)addPendingQuery:(PendingFormQuery*)query {
  [_pendingFormQueries addObject:query];
  [self scheduleCleanupIfNeeded];
}

- (void)checkIfSuggestionsAvailableForForm:
            (FormSuggestionProviderQuery*)formQuery
                         completionHandler:
                             (SuggestionsAvailableCompletion)completion {
  // When password controller's -processWithPasswordFormFillData: is already
  // called, `completion` will be called immediately and `triggerFormExtraction`
  // will be skipped.
  // Otherwise, -suggestionHelperShouldTriggerFormExtraction: will be called
  // and `completion` will not be called until
  // -processWithPasswordFormFillData: is called.
  DCHECK(_webState.get());

  const std::string frame_id = SysNSStringToUTF8(formQuery.frameID);
  web::WebFrame* frame = [self frameWithId:frame_id];
  DCHECK(frame);

  BOOL isPasswordField = [self isPasswordFieldOnForm:formQuery webFrame:frame];
  PendingFormQuery* query =
      [[PendingFormQuery alloc] initWithQuery:formQuery
                                   completion:completion
                             fillDataProvider:self
                              isPasswordField:isPasswordField];

  AccountSelectFillData* fillData = [self fillDataForFrameId:frame_id].first;

  if (![formQuery hasFocusType] || !fillData->Empty() ||
      _framesFormExtractionStatus[frame_id] ==
          FormExtractionStatus::kCompleted) {
    // If the query isn't triggered by focusing on the form or there is fill
    // data available, complete the check immediately with the available fill
    // data. If there is fill data, it doesn't mean that there are suggestions
    // for the form targeted by the query, but at least there are some chances
    // that suggestions will be available. If the extraction status is complete,
    // it means we already know whether or not suggestions are available and
    // there's no point in attempting form extraction again, so we can run the
    // completion block right away and exit early.
    [query runCompletion];
    return;
  }

  // Queue the form query until the fill data is processed. The queue can handle
  // concurent calls to -checkIfSuggestionsAvailableForForm, which may happen
  // when there is more than one consumer of suggestions.
  [self addPendingQuery:query];

  // Try to extract password forms from the frame's renderer content
  // because there is no knowledge of any extraction done yet. If
  // -checkIfSuggestionsAvailableForForm is called before the first forms
  // are extracted, this may result in extracting the forms twice, which
  // is fine.
  //
  // It is important to always call -suggestionHelperShouldTriggerFormExtraction
  // when there is a new query queued to make sure that the pending query is
  // completed when processing the form extraction results. Leaving a query
  // uncompleted may result in the caller waiting forever for query results
  // (e.g. having the keyboard input accessory not showing any suggestion
  // because the pipeline is blocked by an hanging request).
  [self.delegate suggestionHelperShouldTriggerFormExtraction:self
                                                     inFrame:frame];
  _framesFormExtractionStatus[frame_id] = FormExtractionStatus::kRequested;
}

- (password_manager::FillDataRetrievalResult)
    passwordFillDataForUsername:(NSString*)username
             isBackupCredential:(BOOL)isBackupCredential
        likelyRealPasswordField:(bool)passwordField
                 formIdentifier:(autofill::FormRendererId)formId
                fieldIdentifier:(autofill::FieldRendererId)fieldId
                        frameId:(const std::string&)frameId {
  auto [fill_data, is_new] = [self fillDataForFrameId:frameId];
  if (is_new) {
    // If the AccountSelectFillData was freshly created, there is no way there
    // is going to be FillData matching the query. Return an error as the result
    // with the exact reason why the FillData wasn't available. This would
    // ultimately lead not being to match the form for FillData but we wouldn't
    // know exactly why.
    return base::unexpected(
        password_manager::FillDataRetrievalStatus::kNoFrame);
  }
  return fill_data->GetFillData(SysNSStringToUTF16(username),
                                isBackupCredential, formId, fieldId,
                                passwordField);
}

- (password_manager::FillDataRetrievalResult)
    passwordFillDataForUsername:(NSString*)username
             isBackupCredential:(BOOL)isBackupCredential
                     forFrameId:(const std::string&)frameId {
  auto [fill_data, is_new] = [self fillDataForFrameId:frameId];
  if (is_new) {
    // If the AccountSelectFillData was freshly created, there is no way there
    // is going to be FillData matching the query. Return an error as the result
    // with the exact reason why the FillData wasn't available. This would
    // ultimately lead not being to match the form for FillData but we wouldn't
    // know exactly why.
    return base::unexpected(
        password_manager::FillDataRetrievalStatus::kNoFrame);
  }
  return fill_data->GetFillData(SysNSStringToUTF16(username),
                                isBackupCredential);
}

- (void)resetForNewPage {
  _fillDataMap.clear();
  [_pendingFormQueries removeAllObjects];
  _framesFormExtractionStatus.clear();
}

- (void)cleanupForFrameId:(const std::string&)frameId {
  _fillDataMap.erase(frameId);
  _framesFormExtractionStatus.erase(frameId);

  NSString* nsFrameId = SysUTF8ToNSString(frameId);
  NSMutableArray<PendingFormQuery*>* remainingQueries = [NSMutableArray array];
  for (PendingFormQuery* query in _pendingFormQueries) {
    if ([query.frameId isEqualToString:nsFrameId]) {
      // Complete the query even if the frame is gone so any task waiting on
      // this can be completed. Worst case: the task will be completed with
      // no suggestions available.
      [query runCompletion];
    } else {
      [remainingQueries addObject:query];
    }
  }
  _pendingFormQueries = remainingQueries;
}

- (void)processWithPasswordFormFillData:(const PasswordFormFillData&)formData
                             forFrameId:(const std::string&)frameId
                            isMainFrame:(BOOL)isMainFrame
                      forSecurityOrigin:(const url::Origin&)origin {
  DCHECK(_webState.get());
  [self fillDataForFrameId:frameId].first->Add(
      formData, [self shouldAlwaysPopulateRealmForFrame:frameId
                                            isMainFrame:isMainFrame
                                      forSecurityOrigin:origin]);

  // "attachListenersForBottomSheet" is used to add event listeners
  // to fields which must trigger a specific behavior. In this case,
  // the username and password fields' renderer ids are sent through
  // "attachListenersForBottomSheet" so that they may trigger the
  // credential bottom sheet on focus events for these specific fields.
  std::vector<autofill::FieldRendererId> rendererIds(2);
  rendererIds[0] = formData.username_element_renderer_id;
  rendererIds[1] = formData.password_element_renderer_id;
  [self.delegate attachListenersForBottomSheet:rendererIds forFrameId:frameId];

  [self completePendingFormQueriesForFrameId:frameId];
}

- (void)processWithNoSavedCredentialsWithFrameId:(const std::string&)frameId {
  [self completePendingFormQueriesForFrameId:frameId];
}

- (BOOL)isPasswordFieldOnForm:(FormSuggestionProviderQuery*)formQuery
                     webFrame:(web::WebFrame*)webFrame {
  if (formQuery.fieldType != FieldType::kObfuscated) {
    return NO;
  }

  if (!_webState.get() || !webFrame) {
    // Return YES if the objects needed to prove that the field is a password
    // aren't available.
    return YES;
  }

  // If the new approach to detect password fields is enabled, first
  // check if the Password Manager thinks it is a password field, then fallback
  // to using Autofill if the Password Manager verdict is negative.
  return [self isPasswordFieldFromPasswordManagerPerspective:formQuery
                                                    webFrame:webFrame] ||
         [self isPasswordFieldFromAutofillPerspective:formQuery
                                             webFrame:webFrame];
}

// Returns YES if the field of the `formQuery` is in the Autofill form cache and
// has its type associated to a password type. Still returns YES if the type of
// the field couldn't be verified (e.g. when the field can't be found in the
// cache). Returns NO if the field type could be determined and its type isn't
// associated to a password.
- (BOOL)isPasswordFieldFromAutofillPerspective:
            (FormSuggestionProviderQuery*)formQuery
                                      webFrame:(web::WebFrame*)webFrame {
  auto* driver = autofill::AutofillDriverIOS::FromWebStateAndWebFrame(
      _webState.get(), webFrame);
  if (!driver) {
    return YES;
  }

  const autofill::FormStructure* form_structure =
      driver->GetAutofillManager().FindCachedFormById(
          {driver->GetFrameToken(), formQuery.formRendererID});
  if (!form_structure) {
    return YES;
  }

  const auto& fields = form_structure->fields();
  auto itEnd = fields.end();
  auto it = std::find_if(fields.begin(), itEnd, [&](auto& field) {
    return formQuery.fieldRendererID == field->renderer_id();
  });
  if (it == itEnd) {
    return YES;
  }

  return std::ranges::any_of(
             (*it)->Type().GetTypes(),
             [](autofill::FieldType fieldType) {
               switch (GroupTypeOfFieldType(fieldType)) {
                 case autofill::FieldTypeGroup::kPasswordField:
                 case autofill::FieldTypeGroup::kNoGroup:
                   return true;  // May be a password field.
                 case autofill::FieldTypeGroup::kName:
                 case autofill::FieldTypeGroup::kEmail:
                 case autofill::FieldTypeGroup::kCompany:
                 case autofill::FieldTypeGroup::kAddress:
                 case autofill::FieldTypeGroup::kPhone:
                 case autofill::FieldTypeGroup::kCreditCard:
                 case autofill::FieldTypeGroup::kTransaction:
                 case autofill::FieldTypeGroup::kUsernameField:
                 case autofill::FieldTypeGroup::kUnfillable:
                 case autofill::FieldTypeGroup::kIban:
                 case autofill::FieldTypeGroup::kStandaloneCvcField:
                 case autofill::FieldTypeGroup::kAutofillAi:
                 case autofill::FieldTypeGroup::kLoyaltyCard:
                 case autofill::FieldTypeGroup::kOneTimePassword:
                   return false;
               }
               NOTREACHED();
             })
             ? YES
             : NO;
}

#pragma mark - FillDataProvider

- (bool)
    areSuggestionsAvailableForFrameId:(NSString*)frameId
                       formRendererId:(autofill::FormRendererId)formRendererId
                      fieldRendererId:(autofill::FieldRendererId)fieldRendererId
                      isPasswordField:(bool)isPasswordField {
  return [self fillDataForFrameId:SysNSStringToUTF8(frameId)]
      .first->IsSuggestionsAvailable(formRendererId, fieldRendererId,
                                     isPasswordField);
}

#pragma mark - Private

- (web::WebFrame*)frameWithId:(const std::string&)frameId {
  password_manager::PasswordManagerJavaScriptFeature* feature =
      password_manager::PasswordManagerJavaScriptFeature::GetInstance();
  return feature->GetWebFramesManager(_webState.get())->GetFrameWithId(frameId);
}

// Returns whether to add the form's url as the Credential's realm if the realm
// is not specified.
- (bool)shouldAlwaysPopulateRealmForFrame:(const std::string&)frameId
                              isMainFrame:(BOOL)isMainFrame
                        forSecurityOrigin:(const url::Origin&)origin {
  CHECK(_webState.get());
  if (IsCrossOriginIframe(_webState.get(), isMainFrame, origin)) {
    return true;
  }

  web::WebFrame* frame = [self frameWithId:frameId];
  if (!frame) {
    return false;
  }

  auto* driver = autofill::AutofillDriverIOS::FromWebStateAndWebFrame(
      _webState.get(), frame);
  if (!driver) {
    return false;
  }
  return driver->GetAutofillClient().ShouldFormatForLargeKeyboardAccessory();
}

// Completes all the pending form queries that were queued for the frame that
// corresponds to `frameId`. The fill data may not be the freshest if there are
// still other outgoing forms extractions queries pending for the frame, but at
// least something will be provided and the queries completed (avoiding the
// query caller waiting indefinitely for a callback).
- (void)completePendingFormQueriesForFrameId:(const std::string&)frameId {
  NSMutableArray<PendingFormQuery*>* remainingQueries = [NSMutableArray array];
  for (PendingFormQuery* query in _pendingFormQueries) {
    if ([query.frameId isEqualToString:SysUTF8ToNSString(frameId)]) {
      [query runCompletion];
    } else {
      [remainingQueries addObject:query];
    }
  }
  _pendingFormQueries = remainingQueries;

  // Only if the form extraction request has been made from
  // PasswordSuggestionHelper do we set the extraction status' value to
  // completed. Otherwise, the request could have happened too early and not yet
  // contain the information we are interested in.
  if (_framesFormExtractionStatus[frameId] ==
      FormExtractionStatus::kRequested) {
    _framesFormExtractionStatus[frameId] = FormExtractionStatus::kCompleted;
  }
}

// Returns a pair where the first element is the AccountSelectFillData for the
// corresponding `frameId` and the second element a bool that is true if the
// AccountSelectFillData had to be lazily created (i.e. didn't exist before).
- (std::pair<AccountSelectFillData*, bool>)fillDataForFrameId:
    (const std::string&)frameId {
  // Create empty AccountSelectFillData for the frame if it doesn't exist.
  auto insert_result = _fillDataMap.insert(
      std::make_pair(frameId, std::make_unique<AccountSelectFillData>()));
  return std::make_pair(insert_result.first->second.get(),
                        insert_result.second);
}

// Completes, if needed, frame extraction for `frameId`.
- (void)completeFormExtractionForFrame:(const std::string&)frameId {
  if (const auto it = _framesFormExtractionStatus.find(frameId);
      it != _framesFormExtractionStatus.end() &&
      it->second == FormExtractionStatus::kRequested) {
    _framesFormExtractionStatus[frameId] = FormExtractionStatus::kCompleted;
  }
}

// Schedules a cleanup task to clean up the expired queries for which no
// response was ever received within the time limit. Only schedules the task if
// there isn't already a task scheduled and there are still pending form queries
// that are subject to hanging. Is no op if the feature isn't enabled.
- (void)scheduleCleanupIfNeeded {
  if (_cleanupScheduled || _pendingFormQueries.count == 0 ||
      !base::FeatureList::IsEnabled(
          password_manager::features::
              kIosCleanupHangingPasswordFormExtractionRequests)) {
    return;
  }

  _cleanupScheduled = YES;

  __weak __typeof(self) weakSelf = self;
  base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
      FROM_HERE,
      base::BindOnce(
          [](__typeof(self) strongSelf) {
            [strongSelf completeExpiredQueries];
          },
          weakSelf),
      GetCleanupTaskPeriodMs());
}

// Completes all expired queries, across frames. Called by the scheduled cleanup
// task. Automatically reassesses if a follow-up cleanup task is required after
// this cleanup in the case there as still pending not yet expired queries.
- (void)completeExpiredQueries {
  _cleanupScheduled = NO;
  NSMutableArray<PendingFormQuery*>* remainingQueries = [NSMutableArray array];
  for (PendingFormQuery* query in _pendingFormQueries) {
    if ([query isExpired]) {
      [query runCompletion];
      // Complete the extraction for the frame targeted by this expired
      // `query`.
      std::string frameId = SysNSStringToUTF8(query.frameId);
      [self completeFormExtractionForFrame:frameId];
    } else {
      [remainingQueries addObject:query];
    }
  }
  _pendingFormQueries = remainingQueries;
  [self scheduleCleanupIfNeeded];
}

// Returns YES if Password Manager thinks the field in the `formQuery` is a
// password field or returns NO otherwise.
- (BOOL)isPasswordFieldFromPasswordManagerPerspective:
            (FormSuggestionProviderQuery*)formQuery
                                             webFrame:(web::WebFrame*)webFrame {
  FieldRendererId field_id = formQuery.fieldRendererID;

  if (!_passwordManager || !field_id) {
    return NO;
  }

  password_manager::PasswordManagerDriver* driver =
      IOSPasswordManagerDriverFactory::FromWebStateAndWebFrame(_webState.get(),
                                                               webFrame);
  // There must be a driver if there is a frame.
  CHECK(driver);

  const password_manager::PasswordForm* form =
      _passwordManager->GetPasswordFormCache()->GetPasswordForm(
          driver, formQuery.formRendererID);

  return form != nullptr &&
         (form->password_element_renderer_id == field_id ||
          form->new_password_element_renderer_id == field_id ||
          form->confirmation_password_element_renderer_id == field_id);
}

@end
