// 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 <memory>
#import <string_view>

#import "base/ios/ios_util.h"
#import "base/strings/strcat.h"
#import "base/strings/string_util.h"
#import "base/strings/sys_string_conversions.h"
#import "base/strings/utf_string_conversions.h"
#import "base/test/ios/wait_util.h"
#import "base/time/time.h"
#import "components/autofill/core/browser/field_types.h"
#import "components/autofill/core/common/autofill_debug_features.h"
#import "components/autofill/core/common/autofill_features.h"
#import "components/autofill/ios/common/constants.h"
#import "components/autofill/ios/common/features.h"
#import "components/strings/grit/components_strings.h"
#import "ios/chrome/browser/authentication/test/signin_earl_grey.h"
#import "ios/chrome/browser/authentication/test/signin_earl_grey_ui_test_util.h"
#import "ios/chrome/browser/autofill/ui_bundled/address_editor/autofill_constants.h"
#import "ios/chrome/browser/autofill/ui_bundled/autofill_app_interface.h"
#import "ios/chrome/browser/autofill/ui_bundled/bottom_sheet/bottom_sheet_constants.h"
#import "ios/chrome/browser/badges/model/features.h"
#import "ios/chrome/browser/badges/ui_bundled/badge_constants.h"
#import "ios/chrome/browser/infobars/ui_bundled/banners/infobar_banner_constants.h"
#import "ios/chrome/browser/infobars/ui_bundled/infobar_earl_grey_ui_test_util.h"
#import "ios/chrome/browser/infobars/ui_bundled/modals/infobar_address_profile_modal_constants.h"
#import "ios/chrome/browser/intelligence/features/features.h"
#import "ios/chrome/browser/metrics/model/metrics_app_interface.h"
#import "ios/chrome/browser/signin/model/fake_system_identity.h"
#import "ios/chrome/grit/ios_branded_strings.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ios/chrome/test/earl_grey/chrome_actions.h"
#import "ios/chrome/test/earl_grey/chrome_earl_grey.h"
#import "ios/chrome/test/earl_grey/chrome_earl_grey_ui.h"
#import "ios/chrome/test/earl_grey/chrome_matchers.h"
#import "ios/chrome/test/earl_grey/chrome_test_case.h"
#import "ios/testing/earl_grey/earl_grey_test.h"
#import "net/test/embedded_test_server/embedded_test_server.h"
#import "testing/gtest/include/gtest/gtest.h"
#import "ui/base/l10n/l10n_util.h"
#import "ui/strings/grit/ui_strings.h"

namespace {

// URLs of the test pages.
constexpr char kProfileForm[] = "/autofill_smoke_test.html";
constexpr char kXframeFormPage[] = "/xhr_xframe_submit.html";
constexpr char kFullAddressFormPage[] = "/full_address_form.html";

// Ids of fields in the form.
constexpr char kFormElementName[] = "form_name";
constexpr char kFormElementEmail[] = "form_email";
constexpr char kFormElementSubmit[] = "submit_profile";

// Minimal cooldown period to wait for between typing characters. The bare
// minimum should be the frame rate, something aroung 17ms (1/60hz), but we
// prefer giving extra buffer as there are probably other latencies.
constexpr base::TimeDelta kTypingCoolDownPeriod = base::Milliseconds(50);

// Email value used by the tests.
constexpr std::string_view kEmail = "missing_names@gmail.com";

struct FullAddressFormPageParams {
  // True if the submission should be default prevented.
  bool default_prevented = false;
  // True if there should be redirection done after submitting with
  // a parameter that can stop the submit event from being handled by Autofill.
  bool redirect = false;
  // True if the submission should be prevented from propagating to any other
  // listener regardless of their positioning.
  bool stop_immediate_propagation = false;
  // True if multiple submissions should be scheduled.
  bool multiple_submissions = false;
  // True if when using `multiple_submissions` the last programmatic submission
  // should be skipped.
  bool multiple_submissions_skip_programmatic = false;
};

// Matcher for the banner button.
id<GREYMatcher> BannerButtonMatcher() {
  return chrome_test_util::ButtonWithAccessibilityLabel(l10n_util::GetNSString(
      IDS_IOS_AUTOFILL_SAVE_ADDRESS_MESSAGE_PRIMARY_ACTION));
}

// Matcher for the update banner button.
id<GREYMatcher> UpdateBannerButtonMatcher() {
  return chrome_test_util::ButtonWithAccessibilityLabel(l10n_util::GetNSString(
      IDS_IOS_AUTOFILL_UPDATE_ADDRESS_MESSAGE_PRIMARY_ACTION));
}

// Matcher for the "Save Address" modal button.
id<GREYMatcher> ModalButtonMatcher() {
  return chrome_test_util::ButtonWithAccessibilityLabel(
      l10n_util::GetNSString(IDS_AUTOFILL_SAVE_ADDRESS_PROMPT_OK_BUTTON_LABEL));
}

// Matcher for the "Update Address" modal button.
id<GREYMatcher> UpdateModalButtonMatcher() {
  return chrome_test_util::ButtonWithAccessibilityLabel(l10n_util::GetNSString(
      IDS_AUTOFILL_UPDATE_ADDRESS_ADD_NEW_INFO_PROMPT_OK_BUTTON_LABEL));
}

// Matcher for the modal button.
id<GREYMatcher> ModalEditButtonMatcher() {
  return grey_allOf(grey_accessibilityID(kInfobarSaveAddressModalEditButton),
                    grey_accessibilityTrait(UIAccessibilityTraitButton), nil);
}

// Matcher for the migration button in modal view.
id<GREYMatcher> ModalMigrationButtonMatcher() {
  return chrome_test_util::ButtonWithAccessibilityLabel(l10n_util::GetNSString(
      IDS_AUTOFILL_ADDRESS_MIGRATION_TO_ACCOUNT_PROMPT_OK_BUTTON_LABEL));
}

// Matcher for a country entry with the given accessibility label.
id<GREYMatcher> CountryEntry(NSString* label) {
  return grey_allOf(chrome_test_util::ButtonWithAccessibilityLabel(label),
                    grey_userInteractionEnabled(), grey_sufficientlyVisible(),
                    nil);
}

// Matcher for the search bar.
id<GREYMatcher> SearchBar() {
  // Match using the accessibility trait for a search field.
  return grey_allOf(grey_accessibilityTrait(UIAccessibilityTraitSearchField),
                    grey_sufficientlyVisible(), nil);
}

// Matcher for the search bar's scrim.
id<GREYMatcher> SearchBarScrim() {
  return grey_accessibilityID(kAutofillCountrySelectionSearchScrimId);
}

id<GREYMatcher> TextFieldWithLabel(NSString* textFieldLabel) {
  return grey_allOf(grey_accessibilityID(
                        [textFieldLabel stringByAppendingString:@"_textField"]),
                    grey_kindOfClass([UITextField class]), nil);
}

id<GREYMatcher> EditProfileBottomSheet() {
  return grey_accessibilityID(kEditProfileBottomSheetViewIdentfier);
}

// Slowly type characters using the keyboard by waiting between each tap.
void SlowlyTypeText(NSString* text) {
  for (NSUInteger i = 0; i < [text length]; ++i) {
    // Wait some time before typing the character.
    base::test::ios::SpinRunLoopWithMinDelay(kTypingCoolDownPeriod);
    // Type a single character so the user input can be effective.
    [ChromeEarlGrey
        simulatePhysicalKeyboardEvent:[text
                                          substringWithRange:NSMakeRange(i, 1)]
                                flags:0];
  }
  // Give some cooldown period so the character has the time to be typed
  // before doing something else on the page.
  base::test::ios::SpinRunLoopWithMinDelay(kTypingCoolDownPeriod);
}

void TypeTextInXframeField(NSString* fieldID, NSString* text) {
  // Focus on the field that corresponds to `fieldID` to pop up the keyboard.
  NSString* script = [NSString
      stringWithFormat:@"document.querySelector('iframe')"
                        ".contentDocument.getElementById('%@').focus()",
                       fieldID];
  [ChromeEarlGrey evaluateJavaScriptForSideEffect:script];

  // Type the `text` on the field.
  SlowlyTypeText(text);
}

}  // namespace

@interface SaveProfileEGTest : ChromeTestCase

@end

@implementation SaveProfileEGTest

- (void)setUp {
  [super setUp];
  chrome_test_util::GREYAssertErrorNil(
      [MetricsAppInterface setupHistogramTester]);
}

- (void)tearDownHelper {
  chrome_test_util::GREYAssertErrorNil(
      [MetricsAppInterface releaseHistogramTester]);

  // Clear existing profile.
  [AutofillAppInterface clearProfilesStore];

  [super tearDownHelper];
}

// TODO(crbug.com/391826905): Re-enable this test on simulator.
#if TARGET_OS_SIMULATOR
#define MAYBE_testEditBottomSheetAlertBySwipingDown \
  FLAKY_testEditBottomSheetAlertBySwipingDown
#else
#define MAYBE_testEditBottomSheetAlertBySwipingDown \
  testEditBottomSheetAlertBySwipingDown
#endif
- (AppLaunchConfiguration)appConfigurationForTestCase {
  AppLaunchConfiguration config;

  if ([self isRunningTest:@selector(testUserData_AccountSave)] ||
      [self
          isRunningTest:@selector(testUserData_LocalHideBottomSheetOnCancel)]) {
    // These test cases need a badge.
    config.features_disabled.push_back(kAutofillBadgeRemoval);
    // TODO(crbug.com/467331873): Re-enable this when the test is updated for
    // PSF.
    config.features_disabled.push_back(kProactiveSuggestionsFramework);
  }

  config.features_disabled.push_back(
      autofill::features::debug::kAutofillServerCommunication);

  if ([self isRunningTest:@selector(testStickySavePromptJourney)]) {
    config.features_enabled.push_back(kAutofillStickyInfobarIos);
  }

  if ([self isRunningTest:@selector
            (testUserData_AccountSave_AutofillAcrossIframe_XHR)]) {
    config.features_enabled.push_back(kAutofillFixXhrForXframe);
  }

  // TODO(crbug.com/428189566): Re-enable after the test is fixed for
  // ios-fieldtrial-rel.
  if ([self isRunningTest:@selector
            (DISABLED_testSubmissionCountReporting_ScheduledTask)] ||
      [self isRunningTest:@selector
            (DISABLED_testSubmissionCountReporting_UnloadPage)]) {
    config.features_enabled.push_back(kAutofillCountFormSubmissionInRenderer);
  }

  return config;
}

#pragma mark - Test helper methods

// Fills the president profile in the form by clicking on the button, submits
// the form to the save address profile infobar.
- (void)fillPresidentProfileAndShowSaveInfobar {
  GREYAssertTrue(self.testServer->Start(), @"Server did not start.");
  [ChromeEarlGrey loadURL:self.testServer->GetURL(kProfileForm)];

  // Ensure there are no saved profiles.
  GREYAssertEqual(0U, [AutofillAppInterface profilesCount],
                  @"There should be no saved profile.");

  [ChromeEarlGrey waitForWebStateContainingText:"Profile Autofill"];

  GREYAssertTrue([AutofillAppInterface waitForFormToBeCachedInMainFrame],
                 @"Forms were not parsed and cached.");

  [ChromeEarlGrey tapWebStateElementWithID:@"fill_profile_president"];
  [ChromeEarlGrey tapWebStateElementWithID:@"submit_profile"];
  [InfobarEarlGreyUI waitUntilInfobarBannerVisibleOrTimeout:YES];
}

// Triggers the save infobar via XHR submission.
- (void)triggerSaveInfobarViaXHRSubmission {
  // Load the xframe address form page.
  GREYAssertTrue(self.testServer->Start(), @"Server did not start.");
  [ChromeEarlGrey loadURL:self.testServer->GetURL(kXframeFormPage)];

  // Ensure there are no saved profiles at this point - make sure we start from
  // a clean slate.
  GREYAssertEqual(0U, [AutofillAppInterface profilesCount],
                  @"There should be no saved profile.");

  // Manually type the text in the fields so profile saving can be triggered
  // when autofill across iframes is enabled which require manually editing the
  // fields.
  TypeTextInXframeField(@"form_name", @"User");
  TypeTextInXframeField(@"form_address", @"1234 Pkw Ave");
  TypeTextInXframeField(@"form_city", @"MuteCity");
  TypeTextInXframeField(@"form_zip", @"12345");

  // Trigger XHR submission in the child frame using the dedicated button in the
  // main frame.
  [ChromeEarlGrey tapWebStateElementWithID:@"do-xhr-submit"];

  [InfobarEarlGreyUI waitUntilInfobarBannerVisibleOrTimeout:YES];
}

- (void)fillPresidentProfileAndShowSaveModal {
  [self fillPresidentProfileAndShowSaveInfobar];

  // Accept the banner.
  [[EarlGrey selectElementWithMatcher:BannerButtonMatcher()]
      performAction:grey_tap()];
  [InfobarEarlGreyUI waitUntilInfobarBannerVisibleOrTimeout:NO];
}

// Fills, submits the form and saves the address profile.
- (void)fillFormAndSaveProfile {
  [self fillPresidentProfileAndShowSaveModal];

  // Save the profile.
  [[EarlGrey selectElementWithMatcher:ModalButtonMatcher()]
      performAction:grey_tap()];

  // Ensure profile is saved locally.
  GREYAssertEqual(1U, [AutofillAppInterface profilesCount],
                  @"Profile should have been saved.");
}

// Loads, fills, and submits the full address form.
- (void)loadFullAddressFormWithParams:(FullAddressFormPageParams)params {
  // Start server.
  GREYAssertTrue(self.testServer->Start(), @"Server did not start.");

  auto makeQueryString = [](FullAddressFormPageParams params) -> std::string {
    std::vector<std::string_view> queryParameters;
    if (params.default_prevented) {
      queryParameters.push_back("preventDefault");
    }
    if (params.redirect) {
      queryParameters.push_back("redirectWhenSubmissionPrevented");
    }
    if (params.stop_immediate_propagation) {
      queryParameters.push_back("stopImmediatePropagation");
    }
    if (params.multiple_submissions) {
      queryParameters.push_back("triggerMultipleSubmissions");
    }
    if (params.multiple_submissions_skip_programmatic) {
      queryParameters.push_back("triggerMultipleSubmissionsNoProgrammatic");
    }
    return base::JoinString(queryParameters, "&");
  };

  // Get the URL for the served test page with the query parameters for setting
  // it up.
  const GURL baseURL = self.testServer->GetURL(kFullAddressFormPage);
  GURL::Replacements replacements;
  std::string query = makeQueryString(params).c_str();
  replacements.SetQueryStr(query);
  const GURL fullURL = baseURL.ReplaceComponents(replacements);

  // Load the URL and wait for its content to be loaded.
  [ChromeEarlGrey loadURL:fullURL];
  [ChromeEarlGrey waitForWebStateContainingText:"Address Form Test Page"];
  GREYAssertTrue([AutofillAppInterface waitForFormToBeCachedInMainFrame],
                 @"Forms were not parsed and cached.");

  // Call the helper function embedded in the page content to fill the form.
  [ChromeEarlGrey evaluateJavaScriptForSideEffect:@"FillForm();"];
}

- (void)loadAndSubmitFullAddressFormWithParams:
    (FullAddressFormPageParams)params {
  [self loadFullAddressFormWithParams:params];
  // Submit the form via the dedicated <button>.
  [ChromeEarlGrey tapWebStateElementWithID:@"submit-button"];
}

// Focuses on the name field and initiates autofill on the form with the saved
// profile.
- (void)focusOnNameAndAutofill {
  // Ensure there is a saved local profile.
  GREYAssertEqual(1U, [AutofillAppInterface profilesCount],
                  @"There should a saved local profile.");

  // Tap on a field to trigger form activity.
  [[EarlGrey selectElementWithMatcher:chrome_test_util::WebViewMatcher()]
      performAction:chrome_test_util::TapWebElementWithId(kFormElementName)];

  // Wait for the keyboard to appear.
  [ChromeEarlGrey waitForKeyboardToAppear];

  // Wait for suggestions as it may take some time because of form fetch
  // throttling or other delays.
  [ChromeEarlGrey
      waitForMatcher:chrome_test_util::AutofillSuggestionViewMatcher()];

  // Tap on the suggestion.
  [[EarlGrey selectElementWithMatcher:chrome_test_util::
                                          AutofillSuggestionViewMatcher()]
      performAction:grey_tap()];

  // Verify Web Content was filled.
  NSString* javaScriptCondition = [NSString
      stringWithFormat:@"document.getElementById('%s').value.length > 0",
                       kFormElementName];
  [ChromeEarlGrey waitForJavaScriptCondition:javaScriptCondition];
}

#pragma mark - Tests

// Ensures that the profile is updated after submitting the form.
- (void)testUserData_LocalUpdate {
  // Save a profile.
  [self fillFormAndSaveProfile];

  // Load the form.
  [ChromeEarlGrey loadURL:self.testServer->GetURL(kProfileForm)];

  // Autofill the form.
  [self focusOnNameAndAutofill];

  // Tap on email field.
  [[EarlGrey selectElementWithMatcher:chrome_test_util::WebViewMatcher()]
      performAction:chrome_test_util::TapWebElementWithId(kFormElementEmail)];

  // Wait for the keyboard to appear.
  [ChromeEarlGrey waitForKeyboardToAppear];

  // Populate the email field.
  // TODO(crbug.com/40916974): This should use grey_typeText when fixed.
  for (char letter : kEmail) {
    if (letter == '@') {
      [ChromeEarlGrey simulatePhysicalKeyboardEvent:@"@"
                                              flags:UIKeyModifierShift];
      continue;
    }

    [ChromeEarlGrey
        simulatePhysicalKeyboardEvent:[NSString stringWithFormat:@"%c", letter]
                                flags:0];
  }

  // Submit the form.
  [[EarlGrey selectElementWithMatcher:chrome_test_util::WebViewMatcher()]
      performAction:chrome_test_util::TapWebElementWithId(kFormElementSubmit)];

  [InfobarEarlGreyUI waitUntilInfobarBannerVisibleOrTimeout:YES];

  // Accept the banner.
  [[EarlGrey selectElementWithMatcher:UpdateBannerButtonMatcher()]
      performAction:grey_tap()];
  [InfobarEarlGreyUI waitUntilInfobarBannerVisibleOrTimeout:NO];

  // Update the profile.
  [[EarlGrey selectElementWithMatcher:UpdateModalButtonMatcher()]
      performAction:grey_tap()];

  // Ensure profile is updated locally.
  GREYAssertEqual(1U, [AutofillAppInterface profilesCount],
                  @"Profile should have been updated.");
}

// Ensures that the profile is saved to Account after submitting the form.
- (void)testUserData_AccountSave {
  [SigninEarlGrey
      signinWithFakeIdentity:[FakeSystemIdentity fakeIdentityWithMissingNames]];

  [self fillPresidentProfileAndShowSaveModal];

  // Verify that the address badge is displayed.
  [[EarlGrey selectElementWithMatcher:
                 grey_accessibilityID(
                     kBadgeButtonSaveAddressProfileAccessibilityIdentifier)]
      assertWithMatcher:grey_notNil()];

  id<GREYMatcher> footerMatcher = grey_text(
      l10n_util::GetNSStringF(IDS_IOS_AUTOFILL_SAVE_ADDRESS_IN_ACCOUNT_FOOTER,
                              base::UTF8ToUTF16(kEmail)));

  [[EarlGrey selectElementWithMatcher:footerMatcher]
      assertWithMatcher:grey_sufficientlyVisible()];

  // Save the profile.
  [[EarlGrey selectElementWithMatcher:ModalButtonMatcher()]
      performAction:grey_tap()];

  // Ensure profile is saved locally.
  GREYAssertEqual(1U, [AutofillAppInterface profilesCount],
                  @"Profile should have been saved.");

  [SigninEarlGrey signOut];
}

// Test that the profile can be saved for the edge case where the address form
// is hosted in a frame and is submitted there via XHR - when autofill across
// iframes is enabled.
- (void)testUserData_AccountSave_AutofillAcrossIframe_XHR {
  // Sign-in so the profile can be saved into the account.
  [SigninEarlGrey
      signinWithFakeIdentity:[FakeSystemIdentity fakeIdentityWithMissingNames]];

  // Trigger the save infobar via XHR submission in the child frame.
  [self triggerSaveInfobarViaXHRSubmission];

  // Accept the banner to save the profile.
  [[EarlGrey selectElementWithMatcher:BannerButtonMatcher()]
      performAction:grey_tap()];
  [InfobarEarlGreyUI waitUntilInfobarBannerVisibleOrTimeout:NO];

  // Save the profile.
  [[EarlGrey selectElementWithMatcher:ModalButtonMatcher()]
      performAction:grey_tap()];

  // Ensure profile is saved locally.
  GREYAssertEqual(1U, [AutofillAppInterface profilesCount],
                  @"Profile should have been saved.");

  [SigninEarlGrey signOut];
}

// Ensures that the profile is saved to Account after submitting and editing the
// form.
- (void)testUserData_AccountEdit {
  [SigninEarlGrey
      signinWithFakeIdentity:[FakeSystemIdentity fakeIdentityWithMissingNames]];

  [self fillPresidentProfileAndShowSaveModal];

  // Edit the profile.
  [[EarlGrey selectElementWithMatcher:ModalEditButtonMatcher()]
      performAction:grey_tap()];

  [[EarlGrey selectElementWithMatcher:TextFieldWithLabel(@"City")]
      performAction:grey_replaceText(@"New York")];

  id<GREYMatcher> footerMatcher = grey_text(
      l10n_util::GetNSStringF(IDS_IOS_AUTOFILL_SAVE_ADDRESS_IN_ACCOUNT_FOOTER,
                              base::UTF8ToUTF16(kEmail)));

  [[EarlGrey selectElementWithMatcher:footerMatcher]
      assertWithMatcher:grey_sufficientlyVisible()];

  // Save the profile.
  [[[EarlGrey selectElementWithMatcher:ModalButtonMatcher()]
         usingSearchAction:grey_scrollInDirection(kGREYDirectionDown, 200)
      onElementWithMatcher:EditProfileBottomSheet()] performAction:grey_tap()];

  // Ensure profile is saved locally.
  GREYAssertEqual(1U, [AutofillAppInterface profilesCount],
                  @"Profile should have been saved.");

  [SigninEarlGrey signOut];
}

// Ensures that if a local profile is filled in a form and submitted, the user
// is asked for a migration prompt and the profile is moved to the Account.
// TODO(crbug.com/520302619): Flaky on waterfall.
- (void)FLAKY_testUserData_MigrationToAccount {
  [AutofillAppInterface clearProfilesStore];

  // Store one local address.
  [AutofillAppInterface saveExampleProfile];

  [SigninEarlGrey
      signinWithFakeIdentity:[FakeSystemIdentity fakeIdentityWithMissingNames]];

  GREYAssertTrue(self.testServer->Start(), @"Server did not start.");
  [ChromeEarlGrey loadURL:self.testServer->GetURL(kProfileForm)];

  [self focusOnNameAndAutofill];

  // Submit the form.
  [[EarlGrey selectElementWithMatcher:chrome_test_util::WebViewMatcher()]
      performAction:chrome_test_util::TapWebElementWithId(kFormElementSubmit)];

  [InfobarEarlGreyUI waitUntilInfobarBannerVisibleOrTimeout:YES];

  // Accept the banner.
  [[EarlGrey selectElementWithMatcher:BannerButtonMatcher()]
      performAction:grey_tap()];
  [InfobarEarlGreyUI waitUntilInfobarBannerVisibleOrTimeout:NO];

  id<GREYMatcher> footerMatcher = grey_text(l10n_util::GetNSStringF(
      IDS_IOS_AUTOFILL_ADDRESS_MIGRATE_IN_ACCOUNT_FOOTER,
      base::UTF8ToUTF16(kEmail)));
  // Check if there is footer suggesting it's a migration prompt.
  [[EarlGrey selectElementWithMatcher:footerMatcher]
      assertWithMatcher:grey_sufficientlyVisible()];

  // Confirm to migrate the profile.
  [[EarlGrey selectElementWithMatcher:ModalMigrationButtonMatcher()]
      performAction:grey_tap()];

  // Ensure the count of profiles saved remains unchanged.
  GREYAssertEqual(1U, [AutofillAppInterface profilesCount],
                  @"Profile should have been saved.");

  [SigninEarlGrey signOut];
}

// Tests that the user can edit a field in the edit via in the save address
// flow.
- (void)testUserData_LocalEditViaBottomSheet {
  // Fill and submit the form.
  [self fillPresidentProfileAndShowSaveModal];

  // Edit the profile.
  [[EarlGrey selectElementWithMatcher:ModalEditButtonMatcher()]
      performAction:grey_tap()];

  [[EarlGrey selectElementWithMatcher:TextFieldWithLabel(@"City")]
      performAction:grey_replaceText(@"New York")];

  [[EarlGrey
      selectElementWithMatcher:grey_allOf(grey_accessibilityLabel(
                                              l10n_util::GetNSString(
                                                  IDS_IOS_AUTOFILL_COUNTRY)),
                                          grey_userInteractionEnabled(), nil)]
      performAction:grey_tap()];

  // Focus the search bar.
  [[EarlGrey selectElementWithMatcher:SearchBar()] performAction:grey_tap()];

  // Verify the scrim is visible when search bar is focused but not typed in.
  [[EarlGrey selectElementWithMatcher:SearchBarScrim()]
      assertWithMatcher:grey_notNil()];

  // Verify the cancel button is visible and unfocuses search bar when tapped.
  [ChromeEarlGreyUI clearAndDismissSearchBar];

  // Verify countries are searchable using their name in the current
  // locale.
  [[EarlGrey selectElementWithMatcher:SearchBar()] performAction:grey_tap()];

  [[EarlGrey selectElementWithMatcher:SearchBar()]
      performAction:grey_replaceText(@"Germany")];

  // Verify that scrim is not visible anymore.
  [[EarlGrey selectElementWithMatcher:SearchBarScrim()]
      assertWithMatcher:grey_nil()];

  [[EarlGrey selectElementWithMatcher:CountryEntry(@"Germany")]
      performAction:grey_tap()];

  // Save the profile.
  [[EarlGrey selectElementWithMatcher:ModalButtonMatcher()]
      performAction:grey_tap()];

  // Ensure profile is saved locally.
  GREYAssertEqual(1U, [AutofillAppInterface profilesCount],
                  @"Profile should have been saved.");
}

// Tests that the bottom sheet to edit address is just hidden on Cancel.
- (void)testUserData_LocalHideBottomSheetOnCancel {
  // Fill and submit the form.
  [self fillPresidentProfileAndShowSaveModal];

  // Edit the profile.
  [[EarlGrey selectElementWithMatcher:ModalEditButtonMatcher()]
      performAction:grey_tap()];

  // Tap "Cancel"
  [[EarlGrey
      selectElementWithMatcher:grey_allOf(
                                   grey_accessibilityID(
                                       kEditProfileBottomSheetCancelButton),
                                   grey_accessibilityTrait(
                                       UIAccessibilityTraitButton),
                                   nil)] performAction:grey_tap()];

  // Open modal by selecting the badge that shouldn't be accepted.
  [[EarlGrey selectElementWithMatcher:
                 grey_accessibilityID(
                     kBadgeButtonSaveAddressProfileAccessibilityIdentifier)]
      performAction:grey_tap()];

  [[EarlGrey selectElementWithMatcher:ModalButtonMatcher()]
      assertWithMatcher:grey_sufficientlyVisible()];

  // Save the profile.
  [[EarlGrey selectElementWithMatcher:ModalButtonMatcher()]
      performAction:grey_tap()];
}

// Tests that the save address flow is still working correctly when the address
// badge is removed.
- (void)FLAKY_testSaveWithoutBadge {
  [SigninEarlGrey
      signinWithFakeIdentity:[FakeSystemIdentity fakeIdentityWithMissingNames]];

  [self fillPresidentProfileAndShowSaveModal];

  // Verify that the address badge is not displayed.
  [[EarlGrey selectElementWithMatcher:
                 grey_accessibilityID(
                     kBadgeButtonSaveAddressProfileAccessibilityIdentifier)]
      assertWithMatcher:grey_nil()];

  id<GREYMatcher> footerMatcher = grey_text(
      l10n_util::GetNSStringF(IDS_IOS_AUTOFILL_SAVE_ADDRESS_IN_ACCOUNT_FOOTER,
                              base::UTF8ToUTF16(kEmail)));

  [[EarlGrey selectElementWithMatcher:footerMatcher]
      assertWithMatcher:grey_sufficientlyVisible()];

  // Save the profile.
  [[EarlGrey selectElementWithMatcher:ModalButtonMatcher()]
      performAction:grey_tap()];

  // Ensure profile is saved locally.
  GREYAssertEqual(1U, [AutofillAppInterface profilesCount],
                  @"Profile should have been saved.");

  [SigninEarlGrey signOut];
}

// Tests the sticky address prompt journey where the prompt remains there when
// navigating without an explicit user gesture, and then the prompt is dismissed
// when navigating with a user gesture. Test with the address save prompt but
// the type of address prompt doesn't matter in this test case.
- (void)testStickySavePromptJourney {
  [self fillPresidentProfileAndShowSaveInfobar];

  {
    // Reloading page from script shouldn't dismiss the infobar.
    NSString* script = @"location.reload();";
    [ChromeEarlGrey evaluateJavaScriptForSideEffect:script];
  }
  {
    // Assigning url from script to the page aka open an url shouldn't dismiss
    // the infobar.
    NSString* script = @"window.location.assign(window.location.href);";
    [ChromeEarlGrey evaluateJavaScriptForSideEffect:script];
  }
  {
    // Pushing new history entry without reloading content shouldn't dismiss the
    // infobar.
    NSString* script = @"history.pushState({}, '', 'destination2.html');";
    [ChromeEarlGrey evaluateJavaScriptForSideEffect:script];
  }
  {
    // Replacing history entry without reloading content shouldn't dismiss the
    // infobar.
    NSString* script = @"history.replaceState({}, '', 'destination3.html');";
    [ChromeEarlGrey evaluateJavaScriptForSideEffect:script];
  }

  // Wait some time for things to settle.
  base::test::ios::SpinRunLoopWithMinDelay(base::Milliseconds(200));

  // Verify that the prompt is still there after the non-user initiated
  // navigations.
  [[EarlGrey selectElementWithMatcher:grey_accessibilityID(
                                          kInfobarBannerViewIdentifier)]
      assertWithMatcher:grey_sufficientlyVisible()];

  // Navigate with an emulated user gesture and verify that dismisses the
  // prompt.
  [ChromeEarlGrey loadURL:self.testServer->GetURL(kProfileForm)];
  [InfobarEarlGreyUI waitUntilInfobarBannerVisibleOrTimeout:NO];
}

// Tests that there is an alert shown if the user tries to dismiss an alert
// after they edited a field in the edit prompt without saving.
// Note that this test is defined above.
- (void)MAYBE_testEditBottomSheetAlertBySwipingDown {
  // TODO(crbug.com/377270834): Fix implementation on iPad.
  if ([ChromeEarlGrey isIPadIdiom]) {
    EARL_GREY_TEST_SKIPPED(@"Test fails on iPad currently.");
  }

  // TODO(crbug.com/443713676): Re-enable the test.
#if !TARGET_OS_SIMULATOR
  if (base::ios::IsRunningOnIOS26OrLater()) {
    if (![ChromeEarlGrey isIPadIdiom]) {
      EARL_GREY_TEST_DISABLED(@"Test disabled on iOS 26.");
    }
  }
#endif

  // Fill and submit the form.
  [self fillPresidentProfileAndShowSaveModal];

  // Edit the profile.
  [[EarlGrey selectElementWithMatcher:ModalEditButtonMatcher()]
      performAction:grey_tap()];

  // Replace city field value.
  [[EarlGrey selectElementWithMatcher:TextFieldWithLabel(@"City")]
      performAction:grey_replaceText(@"New York")];

  // Swipe down the sheet.
  [[EarlGrey selectElementWithMatcher:EditProfileBottomSheet()]
      performAction:grey_swipeFastInDirection(kGREYDirectionDown)];

  id<GREYMatcher> keepEditingAlert = grey_text(
      l10n_util::GetNSString(IDS_IOS_VIEW_CONTROLLER_DISMISS_CANCEL_CHANGES));
  // Ensure the error alert is shown.
  [ChromeEarlGrey waitForUIElementToAppearWithMatcher:keepEditingAlert];

  // Keep editing.
  [[EarlGrey selectElementWithMatcher:keepEditingAlert]
      performAction:grey_tap()];

  // Swipe down the sheet again.
  [[EarlGrey selectElementWithMatcher:EditProfileBottomSheet()]
      performAction:grey_swipeFastInDirection(kGREYDirectionDown)];

  // Check that the save changes button exists.
  id<GREYMatcher> saveChangesAlert = grey_text(
      l10n_util::GetNSString(IDS_IOS_VIEW_CONTROLLER_DISMISS_SAVE_CHANGES));
  [ChromeEarlGrey waitForUIElementToAppearWithMatcher:saveChangesAlert];

  [[EarlGrey selectElementWithMatcher:saveChangesAlert]
      performAction:grey_tap()];
}

// Tests that the 'Save' button is only enabled when all the required fields are
// filled.
// TODO(crbug.com/407573862): Re-enable after the test is fixed for
// ios-fieldtrial-rel.
- (void)DISABLED_testSaveButtonEnabledStateDependingOnRequiredFields {
  [SigninEarlGreyUI
      signinWithFakeIdentity:[FakeSystemIdentity fakeIdentityWithMissingNames]];
  [ChromeEarlGrey waitForSyncTransportStateActiveWithTimeout:base::Seconds(10)];

  // Fill and submit the form.
  [self fillPresidentProfileAndShowSaveModal];

  // Edit the profile.
  [[EarlGrey selectElementWithMatcher:ModalEditButtonMatcher()]
      performAction:grey_tap()];

  // Ensure the 'Save' button is initially enabled.
  [[EarlGrey selectElementWithMatcher:ModalButtonMatcher()]
      assertWithMatcher:grey_enabled()];

  NSString* streetAddressLabel = base::SysUTF8ToNSString(
      autofill::FieldTypeToDeveloperRepresentationString(
          autofill::ADDRESS_HOME_STREET_ADDRESS));

  // Empty the street address field, which is required.
  [[EarlGrey selectElementWithMatcher:TextFieldWithLabel(streetAddressLabel)]
      performAction:grey_replaceText(@"")];

  // Scroll down to show the 'Save' button.
  [[EarlGrey selectElementWithMatcher:EditProfileBottomSheet()]
      performAction:grey_scrollToContentEdge(kGREYContentEdgeBottom)];

  // Ensure the 'Save' button is disabled.
  [[EarlGrey selectElementWithMatcher:ModalButtonMatcher()]
      assertWithMatcher:grey_not(grey_enabled())];

  // Re-fill the street address field.
  [[EarlGrey selectElementWithMatcher:TextFieldWithLabel(streetAddressLabel)]
      performAction:grey_replaceText(@"Street")];

  // Ensure the 'Save' button is enabled.
  [[EarlGrey selectElementWithMatcher:ModalButtonMatcher()]
      assertWithMatcher:grey_enabled()];

  // Sign out.
  [SigninEarlGrey signOut];
}

// Tests that submission is detected hence the infobar is displayed when the
// "form" event behind the submission is `defaultPrevented` while the
// corresponding feature allows it.
- (void)testSubmissionDetection_defaultPrevented_whenAllowed {
  // Sign-in so the profile can be saved into the account.
  [SigninEarlGrey
      signinWithFakeIdentity:[FakeSystemIdentity fakeIdentityWithMissingNames]];

  // Submit the form with `defaultPrevented` not considered.
  FullAddressFormPageParams params{.default_prevented = true, .redirect = true};
  [self loadAndSubmitFullAddressFormWithParams:params];

  // Wait on the infobar to be displayed after submission.
  [InfobarEarlGreyUI waitUntilInfobarBannerVisibleOrTimeout:YES];

  // Accept the banner to save the profile.
  [[EarlGrey selectElementWithMatcher:BannerButtonMatcher()]
      performAction:grey_tap()];

  // Wait for the save profile dialog to appear.
  [ChromeEarlGrey waitForMatcher:ModalButtonMatcher()];

  // Save the profile.
  [[EarlGrey selectElementWithMatcher:ModalButtonMatcher()]
      performAction:grey_tap()];

  // Ensure profile is saved.
  GREYAssertEqual(1U, [AutofillAppInterface profilesCount],
                  @"Profile should have been saved.");

  [SigninEarlGrey signOut];
}

// Tests that multiple submissions on the same form are deduped when deduping is
// enabled where only one submission per form element is allowed when.
- (void)testSubmissionDetectionWithDeduping {
  // Submit the form with `defaultPrevented` not considered and without
  // redirecting so the same form can be submitted multiple time.
  FullAddressFormPageParams params{.default_prevented = true,
                                   .redirect = false};
  [self loadAndSubmitFullAddressFormWithParams:params];

  // Wait on the infobar to be displayed after submission, meaning that
  // submission was detected.
  [InfobarEarlGreyUI waitUntilInfobarBannerVisibleOrTimeout:YES];

  // Spam submissions.
  for (int i = 0; i < 5; ++i) {
    [ChromeEarlGrey tapWebStateElementWithID:@"submit-button"];
  }

  // Wait some time so the hypothetical form submission messages would have been
  // sent over to the browser by then.
  base::test::ios::SpinRunLoopWithMinDelay(base::Milliseconds(200));

  // Verify that only one submission was actually recorded despite triggering
  // multiple submissions on the same form.
  chrome_test_util::GREYAssertErrorNil([MetricsAppInterface
      expectTotalCount:1
          forHistogram:@"Autofill.iOS.FormSubmission.OutcomeV2"]);
}

// Tests submission count reporting with the scheduled task for the 2 types of
// form submission, regular and programmatic.
// TODO(crbug.com/428189566): Re-enable after the test is fixed for
// ios-fieldtrial-rel.
- (void)DISABLED_testSubmissionCountReporting_ScheduledTask {
  // Load the page without submitting the form.
  [self loadFullAddressFormWithParams:{.default_prevented = true,
                                       .multiple_submissions = true}];

  // Submit the form which will trigger 4 extra submissions consisting of 3
  // button click submissions and 1 `form.submit()` programmatic submission,
  // for a total of 5 submissions.
  [ChromeEarlGrey tapWebStateElementWithID:@"submit-button"];

  // Navigate back so the scheduled reporting task can be completed which
  // requires the frame to be "active".
  [[EarlGrey selectElementWithMatcher:chrome_test_util::BackButton()]
      performAction:grey_tap()];
  [ChromeEarlGrey waitForWebStateContainingText:"Address Form Test Page"];

  // Verify that that all the form click submissions were recorded. Wait enough
  // time for the report to be sent from the renderer. The reporting period is 2
  // seconds.
  GREYAssertTrue(
      base::test::ios::WaitUntilConditionOrTimeout(
          base::Seconds(3),
          ^{
            // Expect 5 submissions in total, 4 regulars and 1 programmatic.
            NSError* error = [MetricsAppInterface
                 expectCount:4
                   forBucket:static_cast<int>(CountedSubmissionType::kHtmlEvent)
                forHistogram:
                    @"Autofill.iOS.FormActivity."
                    @"SubmissionDetectedBeforeProcessing.PerType.FromAll"];
            error = [MetricsAppInterface
                 expectCount:1
                   forBucket:static_cast<int>(
                                 CountedSubmissionType::kProgrammatic)
                forHistogram:
                    @"Autofill.iOS.FormActivity."
                    @"SubmissionDetectedBeforeProcessing.PerType.FromAll"];
            // Expect 2  regular submissions from the instant reports. Which
            // is the maximal number allowed by the quotas. Regular submissions
            // are handled in the isolated world.
            error = [MetricsAppInterface
                 expectCount:2
                   forBucket:static_cast<int>(CountedSubmissionType::kHtmlEvent)
                forHistogram:
                    @"Autofill.iOS.FormActivity."
                    @"SubmissionDetectedBeforeProcessing.PerType.FromInstant"];
            // Expect 1  programmatic submission from the instant reports. This
            // can bust the limit of 2 because the content world quotas are used
            // for the programmatic submission while the rest of Autofill is in
            // the isolated world (which includes the detection of regular click
            // submissions). Each world has its own quotas basically.
            error = [MetricsAppInterface
                 expectCount:1
                   forBucket:static_cast<int>(
                                 CountedSubmissionType::kProgrammatic)
                forHistogram:
                    @"Autofill.iOS.FormActivity."
                    @"SubmissionDetectedBeforeProcessing.PerType.FromInstant"];
            // Expect 2 regular submissions from the scheduled reports as all
            // the quotas were used for the 2 first reports.
            error = [MetricsAppInterface
                 expectCount:2
                   forBucket:static_cast<int>(CountedSubmissionType::kHtmlEvent)
                forHistogram:@"Autofill.iOS.FormActivity."
                             @"SubmissionDetectedBeforeProcessing.PerType."
                             @"FromScheduledTask"];
            // Expect 4 batches of reports, 3 from the instant reports (1
            // report per batch) and 1 batch with the 2 reports that were
            // reported by the scheduled task.
            error = [MetricsAppInterface
                 expectCount:3
                   forBucket:1
                forHistogram:@"Autofill.iOS.FormActivity."
                             @"SubmissionDetectedBeforeProcessing.BatchSize"];
            error = [MetricsAppInterface
                 expectCount:1
                   forBucket:2
                forHistogram:@"Autofill.iOS.FormActivity."
                             @"SubmissionDetectedBeforeProcessing.BatchSize"];
            return error == nil;
          }),
      @"Timed out waiting for the form submission metrics.");
}

// Tests submission count reporting when unloading a page.
// TODO(crbug.com/428189566): Re-enable after the test is fixed for
// ios-fieldtrial-rel.
- (void)DISABLED_testSubmissionCountReporting_UnloadPage {
  // Load page without submitting the form.
  [self loadFullAddressFormWithParams:{.default_prevented = true,
                                       .multiple_submissions_skip_programmatic =
                                           true}];

  // Submit the form which will trigger 4 extra submissions consisting of 3
  // button click submissions and 1 `form.submit()` programmatic submission,
  // for a total of 5 submissions.
  [ChromeEarlGrey tapWebStateElementWithID:@"submit-button"];

  // Reload the page content which triggers reporting.
  [ChromeEarlGrey reload];
  [ChromeEarlGrey waitForWebStateContainingText:"Address Form Test Page"];

  // Verify that that all the submissions were reported. No need for a long
  // timeout as the report was sent when reloading the page so all the metrics
  // should have been recorded at this point.
  GREYAssertTrue(
      base::test::ios::WaitUntilConditionOrTimeout(
          base::Milliseconds(200),
          ^{
            // Expect 4 regular submission in total.
            NSError* error = [MetricsAppInterface
                 expectCount:4
                   forBucket:static_cast<int>(CountedSubmissionType::kHtmlEvent)
                forHistogram:
                    @"Autofill.iOS.FormActivity."
                    @"SubmissionDetectedBeforeProcessing.PerType.FromAll"];
            // Expect 2  regular submissions from the instant reports. Which
            // is the maximal number allowed by the quotas. Regular submissions
            // are handled in the isolated world.
            error = [MetricsAppInterface
                 expectCount:2
                   forBucket:static_cast<int>(CountedSubmissionType::kHtmlEvent)
                forHistogram:
                    @"Autofill.iOS.FormActivity."
                    @"SubmissionDetectedBeforeProcessing.PerType.FromInstant"];
            // Expect 2 regular submissions from the report triggered by
            // unloading the page as all the quotas were used for the 2 first
            // reports.
            error = [MetricsAppInterface
                 expectCount:2
                   forBucket:static_cast<int>(CountedSubmissionType::kHtmlEvent)
                forHistogram:@"Autofill.iOS.FormActivity."
                             @"SubmissionDetectedBeforeProcessing.PerType."
                             @"FromUnloadPage"];
            // Expect 4 batches of reports, 2 from the instant reports (1
            // report per batch) and 1 batch with the 2 reports that were
            // reported when unloading the page.
            error = [MetricsAppInterface
                 expectCount:2
                   forBucket:1
                forHistogram:@"Autofill.iOS.FormActivity."
                             @"SubmissionDetectedBeforeProcessing.BatchSize"];
            error = [MetricsAppInterface
                 expectCount:1
                   forBucket:2
                forHistogram:@"Autofill.iOS.FormActivity."
                             @"SubmissionDetectedBeforeProcessing.BatchSize"];
            return error == nil;
          }),
      @"Timed out waiting for the form submission metrics.");
}

@end
