// 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 "components/autofill/ios/browser/autofill_agent.h"

#import <UIKit/UIKit.h>

#import <algorithm>
#import <cstdint>
#import <memory>
#import <optional>
#include <ranges>
#import <string>
#import <tuple>
#import <utility>
#import <variant>

#import "base/apple/foundation_util.h"
#import "base/check_op.h"
#import "base/containers/map_util.h"
#import "base/debug/crash_logging.h"
#import "base/feature_list.h"
#import "base/format_macros.h"
#import "base/functional/bind.h"
#import "base/json/json_reader.h"
#import "base/json/json_writer.h"
#import "base/memory/raw_ptr.h"
#import "base/memory/weak_ptr.h"
#import "base/metrics/field_trial.h"
#import "base/metrics/histogram_functions.h"
#import "base/strings/string_number_conversions.h"
#import "base/strings/sys_string_conversions.h"
#import "base/strings/utf_string_conversions.h"
#import "base/time/time.h"
#import "base/uuid.h"
#import "base/values.h"
#import "build/branding_buildflags.h"
#import "components/autofill/core/browser/at_memory/at_memory_enablement_utils.h"
#import "components/autofill/core/browser/autofill_field.h"
#import "components/autofill/core/browser/data_model/addresses/autofill_profile.h"
#import "components/autofill/core/browser/data_model/payments/credit_card.h"
#import "components/autofill/core/browser/filling/filling_product.h"
#import "components/autofill/core/browser/foundations/browser_autofill_manager.h"
#import "components/autofill/core/browser/metrics/autofill_metrics.h"
#import "components/autofill/core/browser/suggestions/suggestion.h"
#import "components/autofill/core/browser/suggestions/suggestion_type.h"
#import "components/autofill/core/common/autofill_constants.h"
#import "components/autofill/core/common/autofill_debug_features.h"
#import "components/autofill/core/common/autofill_features.h"
#import "components/autofill/core/common/autofill_payments_features.h"
#import "components/autofill/core/common/autofill_prefs.h"
#import "components/autofill/core/common/autofill_util.h"
#import "components/autofill/core/common/field_data_manager.h"
#import "components/autofill/core/common/form_data.h"
#import "components/autofill/core/common/form_data_predictions.h"
#import "components/autofill/core/common/form_field_data.h"
#import "components/autofill/core/common/mojom/autofill_types.mojom-shared.h"
#import "components/autofill/core/common/unique_ids.h"
#import "components/autofill/ios/browser/autofill_client_ios.h"
#import "components/autofill/ios/browser/autofill_driver_ios.h"
#import "components/autofill/ios/browser/autofill_driver_ios_bridge.h"
#import "components/autofill/ios/browser/autofill_java_script_feature.h"
#import "components/autofill/ios/browser/autofill_util.h"
#import "components/autofill/ios/browser/form_suggestion.h"
#import "components/autofill/ios/browser/form_suggestion_provider.h"
#import "components/autofill/ios/browser/password_autofill_agent.h"
#import "components/autofill/ios/common/features.h"
#import "components/autofill/ios/common/field_data_manager_factory_ios.h"
#import "components/autofill/ios/form_util/autofill_form_features_java_script_feature.h"
#import "components/autofill/ios/form_util/form_activity_observer_bridge.h"
#import "components/autofill/ios/form_util/form_activity_params.h"
#import "components/autofill/ios/form_util/form_handlers_java_script_feature.h"
#import "components/feature_engagement/public/feature_constants.h"
#import "components/grit/components_resources.h"
#import "components/prefs/ios/pref_observer_bridge.h"
#import "components/prefs/pref_change_registrar.h"
#import "components/prefs/pref_service.h"
#import "components/strings/grit/components_strings.h"
#import "components/ukm/ios/ukm_url_recorder.h"
#import "ios/web/common/url_scheme_util.h"
#import "ios/web/public/js_messaging/web_frame.h"
#import "ios/web/public/js_messaging/web_frames_manager.h"
#import "ios/web/public/js_messaging/web_frames_manager_observer_bridge.h"
#import "ios/web/public/navigation/navigation_context.h"
#import "ios/web/public/web_state.h"
#import "ios/web/public/web_state_observer_bridge.h"
#import "services/metrics/public/cpp/ukm_builders.h"
#import "third_party/abseil-cpp/absl/functional/overload.h"
#import "ui/base/l10n/l10n_util_mac.h"
#import "ui/base/resource/resource_bundle.h"
#import "ui/gfx/geometry/rect.h"
#import "ui/gfx/image/image.h"
#import "url/gurl.h"

using autofill::AtMemoryAction;
using autofill::AutofillFormFeaturesJavaScriptFeature;
using autofill::AutofillJavaScriptFeature;
using autofill::FieldDataManager;
using autofill::FieldDataManagerFactoryIOS;
using autofill::FieldGlobalId;
using autofill::FieldRendererId;
using autofill::FormData;
using autofill::FormFieldData;
using autofill::FormGlobalId;
using autofill::FormHandlersJavaScriptFeature;
using autofill::FormRendererId;
using autofill::MayPerformAtMemoryAction;
using autofill::Section;
using autofill::Suggestion;
using autofill::SuggestionType;
using autofill::FieldPropertiesFlags::kAutofilledOnUserTrigger;
using autofill::mojom::FieldActionType;
using ActivityType = autofill::FormActivityParams::ActivityType;
using base::NumberToString;
using base::SysNSStringToUTF16;
using base::SysNSStringToUTF8;
using base::SysUTF16ToNSString;
using base::SysUTF8ToNSString;

namespace {

using FormDataVector = std::vector<FormData>;
// Maps each field id to their respective host form id. This is needed as the
// information linking the fields to their host form is lost between the moment
// of filling and when receiving the filling response.
using FieldToFormLookupMap = std::map<FieldRendererId, FormRendererId>;

// Contains the data for doing filling.
struct AutofillData {
  std::string frameID;
  base::DictValue payload;
  FieldToFormLookupMap fieldToFormLookupMap;
  autofill::mojom::FormActionType actionType;
};

// Delay for setting an utterance to be queued, it is required to ensure that
// standard announcements have already been started and thus would not interrupt
// the enqueued utterance.
constexpr base::TimeDelta kA11yAnnouncementQueueDelay = base::Seconds(1);

// The correct icon size to use in suggestions. Used to ensure images are scaled
// appropriately.
constexpr CGFloat kSuggestionIconWidth = 32;

// Gets the icon that will be used for the specified suggestion.
SuggestionIconType GetSuggestionIconType(const Suggestion& suggestion,
                                         BOOL hasValue) {
  if (suggestion.icon == Suggestion::Icon::kUndo) {
    return SuggestionIconType::kUndoAutofill;
  } else if (suggestion.icon == Suggestion::Icon::kHome && hasValue) {
    return SuggestionIconType::kAccountHome;
  } else if (suggestion.icon == Suggestion::Icon::kWork && hasValue) {
    return SuggestionIconType::kAccountWork;
  }
  return SuggestionIconType::kNone;
}

bool ContainsFocusableField(const FormData& form, FieldRendererId field_id) {
  auto it =
      std::ranges::find(form.fields(), field_id, &FormFieldData::renderer_id);
  return it != form.fields().end() && it->is_focusable();
}

// Returns whether the FormSuggestion's payload contains a non empty guid.
bool HasGuid(const Suggestion::Payload& payload) {
  return std::visit(
      absl::Overload{
          // Duplicated logic for these two types
          [](const Suggestion::AutofillProfilePayload& p) {
            return !p.guid.value().empty();
          },
          [](const Suggestion::AutofillAiPayload& p) {
            return !p.guid.value().empty();
          },

          [](const Suggestion::Guid& p) { return !p.value().empty(); },
          [](const auto&) { return false; }  // Catch-all
      },
      payload);
}

}  // namespace

@interface AutofillAgent () <CRWWebStateObserver,
                             CRWWebFramesManagerObserver,
                             FormActivityObserver,
                             PrefObserverDelegate> {
  // The WebState this instance is observing. Will be null after
  // -webStateDestroyed: has been called.
  raw_ptr<web::WebState> _webState;

  // Bridge to observe the web state from Objective-C.
  std::unique_ptr<web::WebStateObserverBridge> _webStateObserverBridge;

  // Bridge to observe the web frames manager from Objective-C.
  std::unique_ptr<web::WebFramesManagerObserverBridge>
      _webFramesManagerObserverBridge;

  // The pref service for which this agent was created.
  raw_ptr<PrefService> _prefService;

  // Suggestions state:
  // The most recent form suggestions.
  NSArray* _mostRecentSuggestions;

  // The completion to inform FormSuggestionController that a user selection
  // has been handled.
  SuggestionHandledCompletion _suggestionHandledCompletion;

  // The completion to inform FormSuggestionController that suggestions are
  // available for a given form and field.
  SuggestionsAvailableCompletion _suggestionsAvailableCompletion;

  // The text entered by the user into the active field.
  NSString* _typedValue;

  // Primary autofill suggestions carry their delegate directly in
  // `FormSuggestionMetadata` for stateless routing. `_lastReceivedDelegate` is
  // updated here as a passive fallback for unbound suggestions (e.g. manual
  // fill) targeting the currently active frame.
  base::WeakPtr<autofill::AutofillSuggestionDelegate> _lastReceivedDelegate;

  // The autofill data that needs to be sent when the |webState_| is shown.
  std::optional<AutofillData> _pendingFormData;

  // Bridge to listen to pref changes.
  std::unique_ptr<PrefObserverBridge> _prefObserverBridge;
  // Registrar for pref changes notifications.
  PrefChangeRegistrar _prefChangeRegistrar;

  // Bridge to observe form activity in |webState_|.
  std::unique_ptr<autofill::FormActivityObserverBridge>
      _formActivityObserverBridge;

  // ID of the last Autofill query made. Used to discard outdated suggestions.
  FieldGlobalId _lastQueriedFieldID;
}

@end

@implementation AutofillAgent

- (instancetype)initWithPrefService:(PrefService*)prefService
                           webState:(web::WebState*)webState {
  DCHECK(prefService);
  DCHECK(webState);
  self = [super init];
  if (self) {
    _webState = webState;
    _webStateObserverBridge =
        std::make_unique<web::WebStateObserverBridge>(self);
    _webState->AddObserver(_webStateObserverBridge.get());
    _webFramesManagerObserverBridge =
        std::make_unique<web::WebFramesManagerObserverBridge>(self);
    web::WebFramesManager* framesManager =
        AutofillJavaScriptFeature::GetInstance()->GetWebFramesManager(
            _webState);
    framesManager->AddObserver(_webFramesManagerObserverBridge.get());
    _formActivityObserverBridge =
        std::make_unique<autofill::FormActivityObserverBridge>(_webState, self);
    _prefService = prefService;
    _prefObserverBridge = std::make_unique<PrefObserverBridge>(self);
    _prefChangeRegistrar.Init(prefService);
    _prefObserverBridge->ObserveChangesForPreference(
        autofill::prefs::kAutofillCreditCardEnabled, &_prefChangeRegistrar);
    _prefObserverBridge->ObserveChangesForPreference(
        autofill::prefs::kAutofillProfileEnabled, &_prefChangeRegistrar);
  }
  return self;
}

- (void)dealloc {
  if (_webState) {
    [self webStateDestroyed:_webState];
  }
}

#pragma mark - FormSuggestionProvider

- (void)checkIfSuggestionsAvailableForForm:
            (FormSuggestionProviderQuery*)formQuery
                            hasUserGesture:(BOOL)hasUserGesture
                                  webState:(web::WebState*)webState
                         completionHandler:
                             (SuggestionsAvailableCompletion)completion {
  DCHECK_EQ(_webState, webState);

  if (![self isAutofillEnabled]) {
    completion(NO);
    return;
  }

  // Check for suggestions if the form activity is initiated by the user.
  if (!hasUserGesture) {
    completion(NO);
    return;
  }

  // kContentEditable is added for AtMemory. Suggestions are not available.
  // A user uses AtMemory UI to run a search and then fill data manually to it.
  if (formQuery.fieldType ==
          autofill::FormActivityParams::FieldType::kContentEditable &&
      base::FeatureList::IsEnabled(kAutofillSupportContentEditableIos)) {
    completion(NO);
    return;
  }

  web::WebFramesManager* frames_manager =
      AutofillJavaScriptFeature::GetInstance()->GetWebFramesManager(_webState);
  web::WebFrame* frame =
      frames_manager->GetFrameWithId(SysNSStringToUTF8(formQuery.frameID));
  if (!frame) {
    completion(NO);
    return;
  }

  auto* driver =
      autofill::AutofillDriverIOS::FromWebStateAndWebFrame(_webState, frame);
  if (!driver) {
    completion(NO);
    return;
  }

  const auto callback = [](AutofillAgent* agent,
                           FormSuggestionProviderQuery* formQuery,
                           base::WeakPtr<web::WebFrame> frame,
                           base::WeakPtr<web::WebState> webState,
                           SuggestionsAvailableCompletion completion,
                           std::optional<FormDataVector> forms) {
    if (forms && forms->size() == 1) {
      // Once the active form and field are extracted, send a query to the
      // BrowserAutofillManager for suggestions.
      [agent queryAutofillForForm:forms.value()[0]
                  fieldIdentifier:formQuery.fieldRendererID
                             type:formQuery.type
                       typedValue:formQuery.typedValue
                            frame:frame
                         webState:webState
                completionHandler:completion];
    }
  };

  // Re-extract the active form and field only. All forms with at least one
  // input element are considered because key/value suggestions are offered
  // even on short forms.
  driver->FetchFormsFilteredByName(
      SysNSStringToUTF16(formQuery.formName),
      base::BindOnce(callback, self, formQuery, frame->AsWeakPtr(),
                     webState->GetWeakPtr(), completion));
}

- (void)retrieveSuggestionsForForm:(FormSuggestionProviderQuery*)formQuery
                          webState:(web::WebState*)webState
                 completionHandler:(SuggestionsReadyCompletion)completion {
  DCHECK(_mostRecentSuggestions) << "Requestor should have called "
                                 << "|checkIfSuggestionsAvailableForForm:"
                                    "webState:completionHandler:|.";
  completion(_mostRecentSuggestions, self);
}

- (void)didSelectSuggestion:(FormSuggestion*)suggestion
                    atIndex:(NSInteger)index
                       form:(NSString*)formName
             formRendererID:(FormRendererId)formRendererID
            fieldIdentifier:(NSString*)fieldIdentifier
            fieldRendererID:(FieldRendererId)fieldRendererID
                    frameID:(NSString*)frameID
          completionHandler:(SuggestionHandledCompletion)completion {
  [[UIDevice currentDevice] playInputClick];
  DCHECK(completion);
  // TODO(crbug.com/366247033): This double-checks the assumption that this
  // crash is caused by an unexpected suggestion type, and not a nil suggestion.
  // It can be removed once a root cause for the issue is known.
  CHECK(suggestion);

  _suggestionHandledCompletion = [completion copy];

  if (suggestion.acceptanceA11yAnnouncement != nil) {
    // The announcement is done asyncronously with certain delay to make sure
    // it is not interrupted by (almost) immediate standard announcements.
    dispatch_after(
        dispatch_time(DISPATCH_TIME_NOW,
                      kA11yAnnouncementQueueDelay.InNanoseconds()),
        dispatch_get_main_queue(), ^{
          // Queueing flag allows to preserve standard announcements,
          // they are conveyed first and then announce this message.
          // This is a tradeoff as there is no control over the
          // standard utterances (they are interrupting) and it is
          // not desirable to interrupt them. Hence acceptance
          // announcement is done after standard ones (which takes
          // seconds).
          NSAttributedString* message = [[NSAttributedString alloc]
              initWithString:suggestion.acceptanceA11yAnnouncement
                  attributes:@{
                    UIAccessibilitySpeechAttributeQueueAnnouncement : @YES
                  }];
          UIAccessibilityPostNotification(
              UIAccessibilityAnnouncementNotification, message);
        });
  }

  if (suggestion.type == SuggestionType::kAutocompleteAtMemoryButton) {
    [self.delegate showAtMemory];
    if (SuggestionHandledCompletion c =
            std::exchange(_suggestionHandledCompletion, nil)) {
      c();
    }
    return;
  }

  if (suggestion.type == SuggestionType::kAutocompleteEntry ||
      suggestion.type == SuggestionType::kAddressEntry ||
      suggestion.type == SuggestionType::kCreditCardEntry ||
      suggestion.type == SuggestionType::kVirtualCreditCardEntry ||
      suggestion.type == SuggestionType::kAddressFieldByFieldFilling ||
      suggestion.type == SuggestionType::kFillAutofillAi ||
      suggestion.type == SuggestionType::kAtMemorySearchAffordance ||
      suggestion.type == SuggestionType::kUndo ||
      (base::FeatureList::IsEnabled(
           autofill::features::kAutofillEnableBottomSheetScanCardAndFill) &&
       suggestion.type == SuggestionType::kSaveAndFillCreditCardEntry)) {
    // Pick the delegate bound to the suggestion if available.
    // Otherwise, fall back to the last received delegate (e.g. for manual
    // fill).
    base::WeakPtr<autofill::AutofillSuggestionDelegate> delegate =
        suggestion.metadata.suggestion_delegate
            ? suggestion.metadata.suggestion_delegate
            : _lastReceivedDelegate;

    if (delegate) {
      Suggestion autofill_suggestion(suggestion.type);
      autofill_suggestion.main_text.value =
          SysNSStringToUTF16(suggestion.value);
      autofill_suggestion.field_by_field_filling_type_used =
          suggestion.fieldByFieldFillingTypeUsed;
      autofill_suggestion.payload =
          base::FeatureList::IsEnabled(autofill::features::kAutofillUseOriginalPayloadIos) ||
                  HasGuid(suggestion.payload)
              ? suggestion.payload
              : Suggestion::Payload();

      CHECK_GE(index, 0);
      delegate->DidAcceptSuggestion(
          autofill_suggestion, {.multi_index = {static_cast<size_t>(index)}});
    }
    return;
  }

  NOTREACHED();
}

- (SuggestionProviderType)type {
  return SuggestionProviderTypeAutofill;
}

- (autofill::FillingProduct)mainFillingProduct {
  // All suggestions in the list are currently tied to the same delegate,
  // so we can pick the delegate from any suggestion to determine the
  // main filling product.
  for (FormSuggestion* suggestion in _mostRecentSuggestions) {
    if (suggestion.metadata.suggestion_delegate) {
      return suggestion.metadata.suggestion_delegate->GetMainFillingProduct();
    }
  }
  // Use _lastReceivedDelegate as a fallback (e.g. for manual fill suggestions).
  return _lastReceivedDelegate ? _lastReceivedDelegate->GetMainFillingProduct()
                               : autofill::FillingProduct::kNone;
}

#pragma mark - AutofillDriverIOSBridge

- (void)fillData:(const std::vector<autofill::FormFieldData::FillData>&)fields
           inFrame:(web::WebFrame*)frame
    withActionType:(autofill::mojom::FormActionType)actionType {
  base::DictValue fieldsData;
  FieldToFormLookupMap fieldToFormLookupMap;

  for (const auto& field : fields) {
    base::DictValue fieldData;
    fieldData.Set("value", field.value);
    fieldData.Set("hostFormId", static_cast<int>(*field.host_form_id));
    fieldData.Set("isAutofilled", field.is_autofilled);
    fieldsData.Set(NumberToString(*field.renderer_id), std::move(fieldData));

    fieldToFormLookupMap[field.renderer_id] = field.host_form_id;
  }
  auto payload = base::DictValue().Set("fields", std::move(fieldsData));
  AutofillData autofillData = {
      .frameID = frame ? frame->GetFrameId() : "",
      .payload = std::move(payload),
      .fieldToFormLookupMap = std::move(fieldToFormLookupMap),
      .actionType = actionType};

  // Store the form data when WebState is not visible, to send it as soon as it
  // becomes visible again, e.g., when the CVC unmask prompt is showing.
  if (!_webState->IsVisible()) {
    _pendingFormData = std::move(autofillData);
  } else {
    [self sendData:std::move(autofillData) toFrame:frame];
  }
}

// Similar to `fillField`, but does not rely on `FillActiveFormField`, opting
// instead to find and fill a specific field in `frame` with `value`. In other
// words, `field` need not be `document.activeElement`.
- (void)fillSpecificFormField:(const FieldRendererId&)field
                    withValue:(const std::u16string)value
                   actionType:(FieldActionType)actionType
                      inFrame:(web::WebFrame*)frame {
  CHECK(actionType == FieldActionType::kReplaceAll ||
        actionType == FieldActionType::kReplaceSelectionForAtMemory)
      << "Unexpected action type: " << std::to_underlying(actionType);
  base::DictValue data;
  data.Set("renderer_id", static_cast<int>(field.value()));
  data.Set("value", value);
  data.Set("should_insert_at_cursor",
           actionType == FieldActionType::kReplaceSelectionForAtMemory);

  const auto callback =
      [](__weak AutofillAgent* agent, SuggestionHandledCompletion completion,
         FieldRendererId fieldId, base::WeakPtr<web::WebFrame> frame,
         const std::u16string& value, BOOL success) {
        if (success && frame) {
          [agent onDidFillField:fieldId
                           form:std::nullopt
                          frame:frame.get()
                          value:value];
        }
        // Only run the completion if set as it isn't impossible that the
        // provided completion is nil.
        if (completion) {
          completion();
        }
      };

  __weak __typeof(self) weakSelf = self;
  AutofillJavaScriptFeature::GetInstance()->FillSpecificFormField(
      frame, std::move(data),
      base::BindOnce(callback, weakSelf,
                     std::exchange(_suggestionHandledCompletion, nil), field,
                     frame->AsWeakPtr(), value));
}

- (void)handleParsedForms:
            (const std::vector<raw_ref<const autofill::FormStructure>>&)forms
                  inFrame:(web::WebFrame*)frame {
}

- (void)fillFormDataPredictions:
            (const std::vector<autofill::FormDataPredictions>&)forms
                        inFrame:(web::WebFrame*)frame {
  CHECK(base::FeatureList::IsEnabled(
      autofill::features::debug::kAutofillShowTypePredictions));

  base::DictValue predictionData;
  for (const auto& form : forms) {
    base::DictValue fieldData;
    for (const auto [field, field_prediction] :
         std::views::zip(form.data.fields(), form.fields)) {
      fieldData.Set(NumberToString(field.renderer_id().value()),
                    base::Value(field_prediction.overall_type));
    }
    predictionData.Set(base::UTF16ToUTF8(form.data.name()),
                       std::move(fieldData));
  }
  AutofillJavaScriptFeature::GetInstance()->FillPredictionData(
      frame, std::move(predictionData));
}

- (void)scrollFieldIntoView:(const FieldRendererId&)field
                    inFrame:(web::WebFrame*)frame {
  AutofillJavaScriptFeature::GetInstance()->ScrollFieldIntoView(frame, field);
}

#pragma mark - AutofillClientIOSBridge

- (void)showAutofillPopup:(const std::vector<Suggestion>&)popup_suggestions
       suggestionDelegate:
           (const base::WeakPtr<autofill::AutofillSuggestionDelegate>&)
               delegate {
  if (popup_suggestions.empty() &&
      base::FeatureList::IsEnabled(
          autofill::features::
              kAutofillAndroidKeyboardAccessoryDynamicPositioning)) {
    [self hideAutofillPopup];
  }

  // Convert the suggestions into an NSArray for the keyboard.
  NSMutableArray<FormSuggestion*>* suggestions = [[NSMutableArray alloc] init];
  for (const Suggestion& popup_suggestion : popup_suggestions) {
    // Convert Autofill popup suggestions into keyboard accessory suggestions
    // (`FormSuggestion`). Only fillable or actionable types (e.g. address,
    // credit card, autocomplete, undo) are processed; non-fillable items like
    // headers or separators are omitted.
    NSString* value = nil;
    NSString* minorValue = nil;
    NSString* displayDescription = nil;
    UIImage* icon = nil;

    switch (popup_suggestion.type) {
      case SuggestionType::kAddressEntry:
      case SuggestionType::kAddressFieldByFieldFilling:
      case SuggestionType::kAutocompleteEntry:
      case SuggestionType::kCreditCardEntry:
      case SuggestionType::kFillAutofillAi:
      case SuggestionType::kSaveAndFillCreditCardEntry:
      case SuggestionType::kVirtualCreditCardEntry:
        if ((!base::FeatureList::IsEnabled(
                 autofill::features::
                     kAutofillEnableBottomSheetScanCardAndFill) &&
             popup_suggestion.type ==
                 SuggestionType::kSaveAndFillCreditCardEntry)) {
          break;
        }

        // `value` will contain the text to be filled in the selected element
        // while `displayDescription` will contain a summary of the data to be
        // filled in the other elements.
        value = SysUTF16ToNSString(popup_suggestion.main_text.value);
        if (!popup_suggestion.labels.empty() &&
            !popup_suggestion.labels.front().empty()) {
          DCHECK_EQ(popup_suggestion.labels.size(), 1U);
          DCHECK_EQ(popup_suggestion.labels[0].size(), 1U);
          displayDescription =
              SysUTF16ToNSString(popup_suggestion.labels[0][0].value);
        }

        if (!popup_suggestion.minor_texts.empty()) {
          // For Virtual Cards, the `main_text` is just "Virtual card" so we
          // need to include the `minor_text` (which is the card name + last 4
          // digits || card holder's name) as the `minorValue`.
          minorValue =
              SysUTF16ToNSString(popup_suggestion.minor_texts[0].value);
        }

        // Only show icon for credit card suggestions.
        if (delegate && delegate->GetMainFillingProduct() ==
                            autofill::FillingProduct::kCreditCard) {
          icon = [self createIcon:popup_suggestion];
        }
        break;

      case SuggestionType::kUndo:
        // There's no information to set, but this will not be discarded because
        // `suggestionIconType` will be set below.
        break;

      case SuggestionType::kFetchingAmbientData:
        value = SysUTF16ToNSString(popup_suggestion.main_text.value);
        break;

      case SuggestionType::kAccountStoragePasswordEntry:
      case SuggestionType::kAddressEntryOnTyping:
      case SuggestionType::kAllLoyaltyCardsEntry:
      case SuggestionType::kAllSavedPasswordsEntry:
      case SuggestionType::kAtMemoryAiDisclosure:
      case SuggestionType::kAtMemoryFetching:
      case SuggestionType::kAtMemoryGenericError:
      case SuggestionType::kAtMemoryInactivityNudge:
      case SuggestionType::kAtMemoryNoConnection:
      case SuggestionType::kAtMemoryOpenGemini:
      case SuggestionType::kAtMemorySearchAffordance:
      case SuggestionType::kAtMemorySearchResult:
      case SuggestionType::kAtMemorySourceAttribution:
      case SuggestionType::kAutocompleteAtMemoryButton:
      case SuggestionType::kAutofillAiOtherOrders:
      case SuggestionType::kAutofillAiOtherShipments:
      case SuggestionType::kAutofillAiPrivateInferenceNotice:
      case SuggestionType::kAutofillAiSourceAttribution:
      case SuggestionType::kBackupPasswordEntry:
      case SuggestionType::kBnplEntry:
      case SuggestionType::kBnplFootnote:
      case SuggestionType::kComposeDisable:
      case SuggestionType::kComposeGoToSettings:
      case SuggestionType::kComposeNeverShowOnThisSiteAgain:
      case SuggestionType::kComposeProactiveNudge:
      case SuggestionType::kComposeResumeNudge:
      case SuggestionType::kComposeSavedStateNotification:
      case SuggestionType::kDatalistEntry:
      case SuggestionType::kDevtoolsTestAddressByCountry:
      case SuggestionType::kDevtoolsTestAddressEntry:
      case SuggestionType::kDevtoolsTestAddresses:
      case SuggestionType::kFillPassword:
      case SuggestionType::kFreeformFooter:
      case SuggestionType::kGeneratePasswordEntry:
      case SuggestionType::kIbanEntry:
      case SuggestionType::kIdentityCredential:
      case SuggestionType::kInsecureContextPaymentDisabledMessage:
      case SuggestionType::kLoadingThrobber:
      case SuggestionType::kLoyaltyCardEntry:
      case SuggestionType::kManageAddress:
      case SuggestionType::kManageAutofillAi:
      case SuggestionType::kManageAutofillAiIdentityDocs:
      case SuggestionType::kManageAutofillAiShopping:
      case SuggestionType::kManageAutofillAiTravel:
      case SuggestionType::kManageCreditCard:
      case SuggestionType::kManageIban:
      case SuggestionType::kManageLoyaltyCard:
      case SuggestionType::kManageEnhancedAutofill:
      case SuggestionType::kMaximizeCreditCardBenefitsEntry:
      case SuggestionType::kMerchantPromoCodeEntry:
      case SuggestionType::kOneTimePasswordEntry:
      case SuggestionType::kPasswordEntry:
      case SuggestionType::kPasswordFieldByFieldFilling:
      case SuggestionType::kPendingStateSignin:
      case SuggestionType::kPersonalContextNotice:
      case SuggestionType::kRemoveAutofillAi:
      case SuggestionType::kScanCreditCard:
      case SuggestionType::kSeePromoCodeDetails:
      case SuggestionType::kSeparator:
      case SuggestionType::kTitle:
      case SuggestionType::kTroubleSigningInEntry:
      case SuggestionType::kViewPasswordDetails:
      case SuggestionType::kWebauthnCredential:
      case SuggestionType::kWebauthnPasskeyQrCode:
      case SuggestionType::kWebauthnSignInWithAnotherDevice:
        break;
    }

    SuggestionIconType suggestionIconType =
        GetSuggestionIconType(popup_suggestion, value.length > 0);

    // Skip if there is no text or icon to show.
    if (!value && !icon && suggestionIconType == SuggestionIconType::kNone) {
      continue;
    }

    NSString* acceptanceA11yAnnouncement =
        popup_suggestion.acceptance_a11y_announcement.has_value()
            ? SysUTF16ToNSString(*popup_suggestion.acceptance_a11y_announcement)
            : nil;

    autofill::FieldType fieldByFieldFillingTypeUsed =
        (popup_suggestion.field_by_field_filling_type_used
             ? *popup_suggestion.field_by_field_filling_type_used
             : autofill::FieldType::EMPTY_TYPE);

    FormSuggestionMetadata metadata;
    metadata.suggestion_delegate = delegate;

    FormSuggestion* suggestion =
        [FormSuggestion suggestionWithValue:value
                                 minorValue:minorValue
                         displayDescription:displayDescription
                                       icon:icon
                                       type:popup_suggestion.type
                                    payload:popup_suggestion.payload
                fieldByFieldFillingTypeUsed:fieldByFieldFillingTypeUsed
                             requiresReauth:NO
                 acceptanceA11yAnnouncement:acceptanceA11yAnnouncement
                                   metadata:metadata];

    suggestion.featureForIPH = SuggestionFeatureForIPH::kUnknown;
    suggestion.suggestionIconType = suggestionIconType;
    if (popup_suggestion.iph_metadata.feature ==
        &feature_engagement::
            kIPHAutofillExternalAccountProfileSuggestionFeature) {
      suggestion.featureForIPH =
          SuggestionFeatureForIPH::kAutofillExternalAccountProfile;
    } else if (popup_suggestion.iph_metadata.feature ==
               &feature_engagement::
                   kIPHAutofillHomeWorkProfileSuggestionFeature) {
      suggestion.featureForIPH =
          SuggestionFeatureForIPH::kHomeAndWorkAddressSuggestion;
    } else if (popup_suggestion.iph_metadata.feature ==
               &feature_engagement::
                   kIPHAutofillAccountNameEmailSuggestionFeature) {
      suggestion.featureForIPH =
          SuggestionFeatureForIPH::kAccountNameEmailSuggestion;
    }

    // Put the Undo suggestion at the front of the suggestions.
    if (popup_suggestion.type == SuggestionType::kUndo) {
      [suggestions insertObject:suggestion atIndex:0];
    } else {
      [suggestions addObject:suggestion];
    }
  }

  if (suggestions.count > 0 && _webState) {
    autofill::AutofillClientIOS* client =
        autofill::AutofillClientIOS::FromWebState(_webState);
    if (client &&
        MayPerformAtMemoryAction(AtMemoryAction::kTriggerSearchUI, *client)) {
      FormSuggestionMetadata metadata;
      metadata.suggestion_delegate = delegate;
      FormSuggestion* atMemorySuggestion = [FormSuggestion
                  suggestionWithValue:
                      l10n_util::GetNSString(
                          IDS_AUTOFILL_AT_MEMORY_SEARCH_AFFORDANCE_TITLE)
                           minorValue:nil
                   displayDescription:nil
                                 icon:nil
                                 type:autofill::SuggestionType::
                                          kAutocompleteAtMemoryButton
                              payload:autofill::Suggestion::Payload()
          fieldByFieldFillingTypeUsed:autofill::FieldType::EMPTY_TYPE
                       requiresReauth:NO
           acceptanceA11yAnnouncement:nil
                             metadata:metadata];
      [suggestions addObject:atMemorySuggestion];
    }
  }

  // Primary autofill suggestions carry their delegate directly in
  // `FormSuggestionMetadata` for stateless routing. `_lastReceivedDelegate` is
  // updated here as a passive fallback for unbound suggestions (e.g. manual
  // fill) targeting the currently active frame.
  _lastReceivedDelegate = delegate;

  [self onSuggestionsReady:suggestions];

  // TODO(crbug.com/363958046): Pass the actually shown suggestions instead of
  // `popup_suggestions`.
  if (delegate) {
    delegate->OnSuggestionsShown(popup_suggestions, std::nullopt);
  }
}

- (void)hideAutofillPopup {
  _lastReceivedDelegate.reset();
  [self onSuggestionsReady:@[]];
}

- (bool)isLastQueriedField:(FieldGlobalId)fieldID {
  return fieldID == _lastQueriedFieldID;
}

#pragma mark - CRWWebStateObserver

- (void)webStateWasShown:(web::WebState*)webState {
  DCHECK_EQ(_webState, webState);
  if (!_pendingFormData) {
    return;
  }

  // The frameID cannot be empty.
  const std::string& frameID = _pendingFormData->frameID;
  CHECK(!frameID.empty());
  web::WebFramesManager* frames_manager =
      AutofillJavaScriptFeature::GetInstance()->GetWebFramesManager(_webState);
  web::WebFrame* frame = frames_manager->GetFrameWithId(frameID);
  [self sendData:std::move(*_pendingFormData) toFrame:frame];
  _pendingFormData.reset();
}

- (void)webState:(web::WebState*)webState didLoadPageWithSuccess:(BOOL)success {
  DCHECK_EQ(_webState, webState);
  if (![self isAutofillEnabled]) {
    return;
  }

  [self processPage:webState];
}

- (void)webStateDestroyed:(web::WebState*)webState {
  DCHECK_EQ(_webState, webState);
  if (_webState) {
    _formActivityObserverBridge.reset();
    _webState->RemoveObserver(_webStateObserverBridge.get());
    _webStateObserverBridge.reset();
    web::WebFramesManager* framesManager =
        AutofillJavaScriptFeature::GetInstance()->GetWebFramesManager(
            _webState);
    framesManager->RemoveObserver(_webFramesManagerObserverBridge.get());
    _webFramesManagerObserverBridge.reset();
    _webState = nullptr;
  }
  // Do not wait for deallocation. Remove all observers here.
  _prefChangeRegistrar.RemoveAll();
}

#pragma mark - CRWWebFramesManagerObserver

- (void)webFramesManager:(web::WebFramesManager*)webFramesManager
    frameBecameAvailable:(web::WebFrame*)webFrame {
  DCHECK(_webState);
  DCHECK(webFrame);
  if (![self isAutofillEnabled] || _webState->IsLoading()) {
    return;
  }
  if (webFrame->IsMainFrame()) {
    [self processPage:_webState];
    return;
  }
  // Check that the main frame has already been processed.
  if (!webFramesManager->GetMainWebFrame()) {
    return;
  }
  auto* main_driver = autofill::AutofillDriverIOS::FromWebStateAndWebFrame(
      _webState, webFramesManager->GetMainWebFrame());
  DLOG_IF(WARNING, !main_driver) << "No AutofillDriverIOS found for WebFrame";
  if (!main_driver || !main_driver->is_processed()) {
    return;
  }
  [self processFrame:webFrame inWebState:_webState];
}

#pragma mark - FormActivityObserver

- (void)webState:(web::WebState*)webState
    didRegisterFormActivity:(const autofill::FormActivityParams&)params
                    inFrame:(web::WebFrame*)frame {
  DCHECK_EQ(_webState, webState);
  if (![self isAutofillEnabled]) {
    return;
  }

  if (!frame) {
    return;
  }

  // Return early if the page is not processed yet.
  auto* driver =
      autofill::AutofillDriverIOS::FromWebStateAndWebFrame(webState, frame);
  DLOG_IF(WARNING, !driver) << "No AutofillDriverIOS found for WebFrame";
  if (!driver || !driver->is_processed()) {
    return;
  }

  // Return early if |params| is not complete.
  if (params.input_missing) {
    return;
  }

  // If the event is a form_changed, then the event concerns the whole page and
  // not a particular form. The whole document's forms need to be extracted to
  // find the new forms.
  if (params.type == ActivityType::kFormChanged) {
    driver->ScanForms();
    return;
  }

  // We are only interested in 'input' events in order to notify the autofill
  // manager for metrics purposes.
  if (params.type != ActivityType::kInput ||
      (params.field_type != autofill::FormActivityParams::FieldType::kText &&
       params.field_type !=
           autofill::FormActivityParams::FieldType::kObfuscated)) {
    return;
  }

  // The completion block is executed asynchronously, thus it cannot refer
  // directly to `params.field_identifier` (as params is passed by reference
  // and may have been destroyed by the point the block is executed).
  __weak __typeof(self) weakSelf = self;
  const auto callback =
      [](__weak AutofillAgent* agent, base::WeakPtr<web::WebFrame> frame,
         FieldRendererId fieldId, std::optional<FormDataVector> forms) {
        if (!forms) {
          return;
        }
        [agent onFormsFetched:*forms webFrame:frame fieldIdentifier:fieldId];
      };

  // Extract the active form and field only.
  driver->FetchFormsFilteredByName(
      base::UTF8ToUTF16(params.form_name),
      base::BindOnce(callback, weakSelf, frame->AsWeakPtr(),
                     params.field_renderer_id));
}

- (void)webState:(web::WebState*)webState
    didSubmitDocumentWithFormData:(const FormData&)formData
                   hasUserGesture:(BOOL)hasUserGesture
                          inFrame:(web::WebFrame*)frame
                   perfectFilling:(BOOL)perfectFilling {
  if (![self isAutofillEnabled]) {
    base::UmaHistogramEnumeration(
        "Autofill.iOS.FormSubmission.BlockedReason",
        autofill::SubmissionBlockedReason::kAutofillDisabled);
    return;
  }
  if (!frame) {
    base::UmaHistogramEnumeration("Autofill.iOS.FormSubmission.BlockedReason",
                                  autofill::SubmissionBlockedReason::kNoFrame);
    return;
  }
  if (!hasUserGesture &&
      base::FeatureList::IsEnabled(
          kAutofillRejectFormSubmissionsWithoutUserGesture)) {
    base::UmaHistogramEnumeration(
        "Autofill.iOS.FormSubmission.BlockedReason",
        autofill::SubmissionBlockedReason::kNoUserGesture);
    return;
  }

  auto* driver =
      autofill::AutofillDriverIOS::FromWebStateAndWebFrame(webState, frame);
  if (!driver) {
    base::UmaHistogramEnumeration("Autofill.iOS.FormSubmission.BlockedReason",
                                  autofill::SubmissionBlockedReason::kNoDriver);
    return;
  }

  driver->FormSubmitted(formData,
                        autofill::mojom::SubmissionSource::FORM_SUBMISSION);
}

- (void)webState:(web::WebState*)webState
    didRegisterFormRemoval:(const autofill::FormRemovalParams&)params
                   inFrame:(web::WebFrame*)frame {
  CHECK_EQ(_webState, webState);
  CHECK(!params.removed_forms.empty() || !params.removed_unowned_fields.empty())
      << "Invalid params. Form removal events with missing input should have "
         "been filtered out by FormActivityTabHelper.";

  autofill::AutofillDriverIOS* autofillDriver =
      autofill::AutofillDriverIOS::FromWebStateAndWebFrame(webState, frame);
  if (!autofillDriver) {
    return;
  }

  autofillDriver->FormsRemoved(params.removed_forms,
                               params.removed_unowned_fields);
}

#pragma mark - PrefObserverDelegate

- (void)onPreferenceChanged:(const std::string&)preferenceName {
  // Processing the page can be needed here if Autofill is enabled in settings
  // when the page is already loaded.
  if ([self isAutofillEnabled]) {
    [self processPage:_webState];
  }
}

#pragma mark - Private methods

// Returns whether Autofill is enabled by checking if Autofill is turned on and
// if the current URL has a web scheme and the page content is HTML.
- (BOOL)isAutofillEnabled {
  if (!autofill::prefs::IsAutofillProfileEnabled(_prefService) &&
      !autofill::prefs::IsAutofillPaymentMethodsEnabled(_prefService)) {
    return NO;
  }

  // Only web URLs are supported by Autofill.
  return web::UrlHasWebScheme(_webState->GetLastCommittedURL()) &&
         _webState->ContentIsHTML();
}

// Fills a field identified with |fieldIdentifier| on the form named
// |formName| in |frame| using |value| then move the cursor.
// TODO(crbug.com/41284261): |dataString| ends up at fillFormField() in
// autofill_controller.js. fillFormField() expects an AutofillFormFieldData
// object, which |dataString| is not because 'form' is not a specified member of
// AutofillFormFieldData. fillFormField() also expects members 'max_length' and
// 'is_checked' to exist.
- (void)fillField:(const std::string&)fieldIdentifier
    fieldRendererID:(FieldRendererId)fieldRendererID
     formRendererID:(FormRendererId)formRendererID
           formName:(const std::string&)formName
              value:(const std::u16string)value
            inFrame:(web::WebFrame*)frame {
  base::DictValue data;
  data.Set("renderer_id", static_cast<int>(fieldRendererID.value()));
  data.Set("identifier", fieldIdentifier);
  data.Set("form", formName);
  data.Set("value", value);

  DCHECK(_suggestionHandledCompletion);

  const auto callback = [](__weak AutofillAgent* agent,
                           SuggestionHandledCompletion completion,
                           FieldRendererId fieldId,
                           std::optional<FormRendererId> formId,
                           base::WeakPtr<web::WebFrame> frame,
                           const std::u16string& value, BOOL success) {
    if (success && frame) {
      [agent onDidFillField:fieldId form:formId frame:frame.get() value:value];
    }
    // Only run the completion if set as it isn't impossible that the provided
    // completion is nil.
    if (completion) {
      completion();
    }
  };

  __weak __typeof(self) weakSelf = self;
  AutofillJavaScriptFeature::GetInstance()->FillActiveFormField(
      frame, std::move(data),
      base::BindOnce(
          callback, weakSelf, std::exchange(_suggestionHandledCompletion, nil),
          fieldRendererID, formRendererID, frame->AsWeakPtr(), value));
}

// Called when did fill a specific field.
- (void)onDidFillField:(FieldRendererId)fieldID
                  form:(std::optional<FormRendererId>)formID
                 frame:(web::WebFrame*)frame
                 value:(const std::u16string&)value {
  [self updateFieldManagerForSpecificField:fieldID
                                   inFrame:frame
                                 withValue:value];
  [self notifyAboutValueChangeOnField:fieldID
                               inForm:formID
                                frame:frame
                            withValue:value];
}

// Called when did fill multiple fields and received results serialized in a
// JSON string.
- (void)onDidFillWithResults:(NSString*)resultsAsJsonStr
                     inFrame:(web::WebFrame*)frame
        fieldToFormLookupMap:(const FieldToFormLookupMap&)fieldToFormLookupMap
              withActionType:(autofill::mojom::FormActionType)actionType {
  std::map<uint32_t, std::u16string> fillingResults;
  if (autofill::ExtractFillingResults(resultsAsJsonStr, &fillingResults)) {
    [self updateFieldManagerWithFillingResults:fillingResults inFrame:frame];
    [self notifyAboutFormFillingResults:fillingResults
                                inFrame:frame
                   fieldToFormLookupMap:fieldToFormLookupMap];
  }

  auto* driver =
      autofill::AutofillDriverIOS::FromWebStateAndWebFrame(_webState, frame);
  if (driver && driver->is_processed()) {
    driver->ScanForms();
  }

  if (actionType == autofill::mojom::FormActionType::kFill) {
    [self recordFormFillingSuccessMetrics:!fillingResults.empty()];
  }
}

// Updates field managers with filling results.
- (void)updateFieldManagerWithFillingResults:
            (const std::map<uint32_t, std::u16string>&)fillingResults
                                     inFrame:(web::WebFrame*)frame {
  for (auto& fillData : fillingResults) {
    [self updateFieldManagerForSpecificField:FieldRendererId(fillData.first)
                                     inFrame:frame
                                   withValue:fillData.second];
  }
}

- (void)updateFieldManagerForSpecificField:(FieldRendererId)fieldRendererID
                                   inFrame:(web::WebFrame*)frame
                                 withValue:(const std::u16string&)value {
  FieldDataManagerFactoryIOS::FromWebFrame(frame)->UpdateFieldDataMap(
      fieldRendererID, value, kAutofilledOnUserTrigger);
}

// Notifies the PasswordAutofillAgent that the value of a field has changed.
- (void)notifyAboutValueChangeOnField:(FieldRendererId)fieldID
                               inForm:(std::optional<FormRendererId>)formID
                                frame:(web::WebFrame*)frame
                            withValue:(const std::u16string&)value {
  CHECK(frame);

  autofill::PasswordAutofillAgent* agent =
      autofill::PasswordAutofillAgent::FromWebState(_webState);
  agent->DidFillField(frame, formID, fieldID, value);
}

// Notifies that form filling results were received.
- (void)notifyAboutFormFillingResults:
            (const std::map<uint32_t, std::u16string>&)fillingResults
                              inFrame:(web::WebFrame*)frame
                 fieldToFormLookupMap:
                     (const FieldToFormLookupMap&)fieldToFormLookupMap {
  CHECK(frame);

  for (auto& fillData : fillingResults) {
    FieldRendererId fieldID = FieldRendererId(fillData.first);
    if (const FormRendererId* formID =
            base::FindOrNull(fieldToFormLookupMap, fieldID)) {
      [self notifyAboutValueChangeOnField:fieldID
                                   inForm:*formID
                                    frame:frame
                                withValue:fillData.second];
    }
  }
}

// Sends the the |data| to |frame| to actually fill the data.
- (void)sendData:(AutofillData)data toFrame:(web::WebFrame*)frame {
  DCHECK(_webState->IsVisible());

  // `frame` may come from a frame ID that was previously cached; the frame
  // could have been destroyed since then. See crbug.com/425991572.
  if (!frame) {
    return;
  }

  __weak __typeof(self) weakSelf = self;
  const auto callback =
      [](__weak AutofillAgent* agent, base::WeakPtr<web::WebFrame> frame,
         SuggestionHandledCompletion completion,
         const FieldToFormLookupMap& map,
         autofill::mojom::FormActionType actionType, NSString* jsonString) {
        if (frame) {
          [agent onDidFillWithResults:jsonString
                              inFrame:frame.get()
                 fieldToFormLookupMap:map
                       withActionType:actionType];
        }

        // Only run the completion if set as it isn't impossible that the
        // provided completion is nil.
        if (completion) {
          completion();
        }
      };
  AutofillJavaScriptFeature::GetInstance()->FillForm(
      frame, std::move(data.payload),
      base::BindOnce(callback, weakSelf, frame->AsWeakPtr(),
                     std::exchange(_suggestionHandledCompletion, nil),
                     std::move(data.fieldToFormLookupMap), data.actionType));
}

// Helper method used to implement the aynchronous completion block of
// -webState:didRegisterFormActivity:inFrame:. Due to the asynchronous
// invocation, WebState* and WebFrame* may both have been destroyed, so
// the method needs to check for those edge cases.
- (void)onFormsFetched:(const FormDataVector&)forms
              webFrame:(base::WeakPtr<web::WebFrame>)webFrame
       fieldIdentifier:(FieldRendererId)fieldIdentifier {
  if (forms.size() != 1 || !_webState || !webFrame) {
    return;
  }

  auto* driver = autofill::AutofillDriverIOS::FromWebStateAndWebFrame(
      _webState, webFrame.get());
  if (!driver) {
    return;
  }
  const FormData& form = forms[0];
  if (!ContainsFocusableField(form, fieldIdentifier)) {
    return;
  }
  driver->TextFieldValueChanged(form, {form.host_frame(), fieldIdentifier},
                                base::TimeTicks::Now());
}

// Helper method to create icons for payment cards.
- (UIImage*)createIcon:(Suggestion)popup_suggestion {
  // If available, the custom icon for the card is preferred over the
  // generic network icon. The network icon may also be missing, in
  // which case we do not set an icon at all.
  if (auto* custom_icon =
          std::get_if<gfx::Image>(&popup_suggestion.custom_icon);
      custom_icon && !custom_icon->IsEmpty()) {
    UIImage* icon = custom_icon->ToUIImage();

    // On iOS, the keyboard accessory wants smaller icons than the default
    // 40x24 size, so we resize them to 32x20, if the provided icon is
    // larger than that.
    if (icon && (icon.size.width > kSuggestionIconWidth)) {
      // For a simple image resize, we can keep the same underlying image
      // and only adjust the ratio.
      CGFloat ratio = icon.size.width / kSuggestionIconWidth;
      return [UIImage imageWithCGImage:[icon CGImage]
                                 scale:icon.scale * ratio
                           orientation:icon.imageOrientation];
    }
    return icon;
  } else if (popup_suggestion.icon != Suggestion::Icon::kNoIcon) {
    const int resourceID =
        autofill::CreditCard::IconResourceId(popup_suggestion.icon);
    return ui::ResourceBundle::GetSharedInstance()
        .GetNativeImageNamed(resourceID)
        .ToUIImage();
  }
  return nil;
}

// Returns the autofill manager associated with a web::WebState instance.
// Returns nullptr if there is no autofill manager associated anymore, this can
// happen when |close| has been called on the |webState|. Also returns nullptr
// if -webStateDestroyed: has been called.
- (autofill::BrowserAutofillManager*)
    autofillManagerFromWebState:(web::WebState*)webState
                       webFrame:(web::WebFrame*)webFrame {
  if (!webState || !_webStateObserverBridge) {
    return nullptr;
  }
  auto* driver =
      autofill::AutofillDriverIOS::FromWebStateAndWebFrame(webState, webFrame);
  DLOG_IF(WARNING, !driver) << "No AutofillDriverIOS found for WebFrame";
  if (!driver) {
    return nullptr;
  }
  return &driver->GetAutofillManager();
}

// Notifies the autofill manager when forms are detected on a page.
- (void)notifyFormsSeen:(FormDataVector)updatedForms
                inFrame:(web::WebFrame*)frame {
  auto* driver =
      autofill::AutofillDriverIOS::FromWebStateAndWebFrame(_webState, frame);
  if (!driver) {
    return;
  }

  DCHECK(!updatedForms.empty());

  driver->FormsSeen(/*updated_forms=*/std::move(updatedForms),
                    /*removed_forms=*/{});
}

// Invokes the form extraction script in |frame| and loads the output into the
// format expected by the BrowserAutofillManager.
// If `formNameFilter` is `std::nullopt`, all forms are extracted.
// Otherwise,
//   - if `*formNameFilter` is non-empty, only forms of that name are extracted.
//   - if `*formNameFilter` is empty, unowned fields are extracted.
// Only forms with at least `requiredFieldsCount` fields are extracted.
// Calls `completionHandler` with a success BOOL of YES and the form data that
// was extracted.
// Calls `completionHandler` with NO if the forms could not be extracted.
// |completionHandler| cannot be nil.
- (void)fetchFormsFiltered:(std::optional<std::u16string>)formNameFilter
                   inFrame:(web::WebFrame*)frame
         completionHandler:(FormFetchCompletion)completionHandler {
  DCHECK(completionHandler);

  // Necessary so the values can be used inside a block.
  GURL pageURL = _webState->GetLastCommittedURL();
  url::Origin frameOrigin =
      frame ? frame->GetSecurityOrigin() : url::Origin::Create(pageURL);
  GURL frameURL = frame ? frame->GetUrl() : pageURL;

  if (auto* driver = autofill::AutofillDriverIOS::FromWebStateAndWebFrame(
          _webState, frame)) {
    driver->OnDidTriggerFormFetch();
  }

  const scoped_refptr<FieldDataManager> fieldDataManager =
      FieldDataManagerFactoryIOS::GetRetainable(frame);
  auto extractForms = base::BindOnce(
      [](std::optional<std::u16string> formNameFilter, const GURL& pageURL,
         const url::Origin& frameOrigin, const GURL& frameURL,
         scoped_refptr<FieldDataManager> fieldDataManager,
         const std::string& frame_id, NSString* formJSON) {
        return autofill::ExtractFormsData(formJSON, formNameFilter, pageURL,
                                          frameOrigin, frameURL,
                                          *fieldDataManager, frame_id);
      },
      std::move(formNameFilter), pageURL, frameOrigin, frameURL,
      fieldDataManager, frame->GetFrameId());
  AutofillJavaScriptFeature::GetInstance()->FetchForms(
      frame, std::move(extractForms).Then(std::move(completionHandler)));
}

- (void)onSuggestionsReady:(NSArray<FormSuggestion*>*)suggestions {
  _mostRecentSuggestions = suggestions;
  if (SuggestionsAvailableCompletion completion =
          std::exchange(_suggestionsAvailableCompletion, nil)) {
    completion([_mostRecentSuggestions count] > 0);
  }
}

// Sends a request to BrowserAutofillManager to retrieve suggestions for the
// specified form and field.
- (void)queryAutofillForForm:(const FormData&)form
             fieldIdentifier:(FieldRendererId)fieldIdentifier
                        type:(ActivityType)type
                  typedValue:(NSString*)typedValue
                       frame:(base::WeakPtr<web::WebFrame>)frame
                    webState:(base::WeakPtr<web::WebState>)webState
           completionHandler:(SuggestionsAvailableCompletion)completion {
  // If a query was already in flight, complete the previous completion callback
  // with NO so it doesn't remain unresolved.
  if (SuggestionsAvailableCompletion previousCompletion =
          std::exchange(_suggestionsAvailableCompletion, nil)) {
    previousCompletion(NO);
  }

  if (!frame || !webState) {
    if (completion) {
      completion(NO);
    }
    return;
  }

  if (!ContainsFocusableField(form, fieldIdentifier)) {
    if (completion) {
      completion(NO);
    }
    return;
  }

  auto* driver = autofill::AutofillDriverIOS::FromWebStateAndWebFrame(
      _webState, frame.get());
  DLOG_IF(WARNING, !driver) << "No AutofillDriverIOS found for WebFrame";
  if (!driver) {
    if (completion) {
      completion(NO);
    }
    return;
  }

  // Save the completion and go look for suggestions.
  _suggestionsAvailableCompletion = [completion copy];
  _typedValue = typedValue;
  _lastQueriedFieldID = {form.host_frame(), fieldIdentifier};

  // Query the BrowserAutofillManager for suggestions. Results will arrive in
  // -showAutofillPopup:suggestionDelegate:.
  driver->AskForValuesToFill(form, _lastQueriedFieldID);
}

- (void)processPage:(web::WebState*)webState {
  web::WebFramesManager* frames_manager =
      AutofillJavaScriptFeature::GetInstance()->GetWebFramesManager(webState);
  if (!frames_manager->GetMainWebFrame()) {
    return;
  }
  [self processFrame:frames_manager->GetMainWebFrame() inWebState:webState];
  for (auto* frame : frames_manager->GetAllWebFrames()) {
    if (frame->IsMainFrame()) {
      continue;
    }
    [self processFrame:frame inWebState:webState];
  }
}

- (void)processFrame:(web::WebFrame*)frame inWebState:(web::WebState*)webState {
  if (!frame) {
    return;
  }

  autofill::AutofillDriverIOS* driver =
      autofill::AutofillDriverIOS::FromWebStateAndWebFrame(webState, frame);
  DLOG_IF(WARNING, !driver) << "No AutofillDriverIOS found for WebFrame";
  // This process is only done once.
  if (!driver || driver->is_processed()) {
    return;
  }
  driver->set_processed(true);

  if (frame->IsMainFrame()) {
    _lastReceivedDelegate.reset();
    _suggestionsAvailableCompletion = nil;
    _suggestionHandledCompletion = nil;
    _mostRecentSuggestions = nil;
    _typedValue = nil;
  }

  FormHandlersJavaScriptFeature* formHandlerFeature =
      FormHandlersJavaScriptFeature::GetInstance();

  // Use a delay of 200ms when tracking form mutations to reduce the
  // communication overhead (as mutations are likely to come in batch).
  constexpr int kMutationTrackingEnabledDelayInMs = 200;
  formHandlerFeature->TrackFormMutations(frame,
                                         kMutationTrackingEnabledDelayInMs);

  driver->ScanForms(/*immediately=*/base::FeatureList::IsEnabled(
      kAutofillThrottleDocumentFormScanForceFirstScanIos));
}

// Records if the renderer was able to fill the Autofill-provided values in a
// form or formless fields.
- (void)recordFormFillingSuccessMetrics:(BOOL)success {
  base::UmaHistogramBoolean(/*name=*/"Autofill.FormFillSuccessIOS",
                            /*sample=*/success);

  ukm::SourceId source_id = ukm::GetSourceIdForWebStateDocument(_webState);
  ukm::builders::Autofill_FormFillSuccessIOS(source_id)
      .SetFormFillSuccess(success)
      .Record(ukm::UkmRecorder::Get());
}

@end
