chromium/ios/chrome/browser/ui/settings/password/password_details/password_details_table_view_controller.mm

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

#import "ios/chrome/browser/ui/settings/password/password_details/password_details_table_view_controller.h"

#import "base/apple/foundation_util.h"
#import "base/i18n/time_formatting.h"
#import "base/ios/ios_util.h"
#import "base/metrics/histogram_functions.h"
#import "base/metrics/histogram_macros.h"
#import "base/metrics/user_metrics.h"
#import "base/strings/sys_string_conversions.h"
#import "components/crash/core/common/crash_key.h"
#import "components/password_manager/core/browser/password_manager_metrics_util.h"
#import "components/password_manager/core/common/password_manager_constants.h"
#import "ios/chrome/browser/passwords/model/password_checkup_metrics.h"
#import "ios/chrome/browser/shared/public/commands/application_commands.h"
#import "ios/chrome/browser/shared/public/commands/open_new_tab_command.h"
#import "ios/chrome/browser/shared/public/commands/snackbar_commands.h"
#import "ios/chrome/browser/shared/ui/symbols/symbols.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_multi_line_text_edit_item.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_multi_line_text_edit_item_delegate.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_text_button_item.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_text_edit_item.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_text_edit_item_delegate.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_text_header_footer_item.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_text_item.h"
#import "ios/chrome/browser/shared/ui/table_view/table_view_utils.h"
#import "ios/chrome/browser/shared/ui/util/pasteboard_util.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/ui/settings/cells/settings_image_detail_text_item.h"
#import "ios/chrome/browser/ui/settings/elements/enterprise_info_popover_view_controller.h"
#import "ios/chrome/browser/ui/settings/password/password_details/cells/table_view_stacked_details_item.h"
#import "ios/chrome/browser/ui/settings/password/password_details/credential_details.h"
#import "ios/chrome/browser/ui/settings/password/password_details/password_details_consumer.h"
#import "ios/chrome/browser/ui/settings/password/password_details/password_details_handler.h"
#import "ios/chrome/browser/ui/settings/password/password_details/password_details_menu_item.h"
#import "ios/chrome/browser/ui/settings/password/password_details/password_details_metrics_utils.h"
#import "ios/chrome/browser/ui/settings/password/password_details/password_details_table_view_constants.h"
#import "ios/chrome/browser/ui/settings/password/password_details/password_details_table_view_controller+Testing.h"
#import "ios/chrome/browser/ui/settings/password/password_details/password_details_table_view_controller_delegate.h"
#import "ios/chrome/browser/ui/settings/password/passwords_table_view_constants.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/elements/popover_label_view_controller.h"
#import "ios/chrome/common/ui/reauthentication/reauthentication_module.h"
#import "ios/chrome/common/ui/table_view/table_view_cells_constants.h"
#import "ios/chrome/grit/ios_branded_strings.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ui/base/l10n/l10n_util_mac.h"

using base::UmaHistogramEnumeration;
using password_manager::GetWarningTypeForDetailsContext;
using password_manager::constants::kMaxPasswordNoteLength;
using password_manager::constants::kPasswordManagerAuthValidity;
using password_manager::metrics_util::LogPasswordNoteActionInSettings;
using password_manager::metrics_util::PasswordNoteAction;

namespace {

// Crash key to investigate the content of the context menu configuration
// identifier when it can't successfully be casted to an NSNumber.
crash_reporter::CrashKeyString<64> configuration_identifier_crash_key(
    "iOS password details configuration identifier");

typedef NS_ENUM(NSInteger, SectionIdentifier) {
  SectionIdentifierPassword = kSectionIdentifierEnumZero,
  SectionIdentifierSite,
  SectionIdentifierCompromisedInfo,
  SectionIdentifierMoveToAccount,
};

typedef NS_ENUM(NSInteger, PasswordAccessReason) {
  PasswordAccessReasonShow = 0,
  PasswordAccessReasonCopy,
  PasswordAccessReasonEdit,
};

// Size of the symbols.
const CGFloat kSymbolSize = 15;
const CGFloat kRecommendationSymbolSize = 22;
// Minimal amount of characters in password note to display the warning.
const int kMinNoteCharAmountForWarning = 901;

// Returns true if the "Dismiss Warning" button should be shown.
bool ShouldAllowToDismissWarning(DetailsContext context, bool is_compromised) {
  switch (context) {
    case DetailsContext::kPasswordSettings:
    case DetailsContext::kOutsideSettings:
    case DetailsContext::kCompromisedIssues:
    case DetailsContext::kDismissedWarnings:
      return is_compromised;
    case DetailsContext::kReusedIssues:
    case DetailsContext::kWeakIssues:
      return false;
  }
}

// Returns true if the "Restore Warning" button should be shown.
bool ShouldAllowToRestoreWarning(DetailsContext context, bool is_muted) {
  switch (context) {
    case DetailsContext::kPasswordSettings:
    case DetailsContext::kOutsideSettings:
    case DetailsContext::kCompromisedIssues:
    case DetailsContext::kReusedIssues:
    case DetailsContext::kWeakIssues:
      return false;
    case DetailsContext::kDismissedWarnings:
      return is_muted;
  }
}

}  // namespace

#pragma mark - PasswordDetailsInfoItem

// Contains the website, username and password text items.
@interface PasswordDetailsInfoItem : NSObject

// Displays one or more websites on which this credential is used.
@property(nonatomic, strong) TableViewStackedDetailsItem* websiteItem;

// The text item related to the user display name value.
@property(nonatomic, strong) TableViewTextEditItem* userDisplayNameTextItem;

// The text item related to the username value.
@property(nonatomic, strong) TableViewTextEditItem* usernameTextItem;

// The text item related to the password value.
@property(nonatomic, strong) TableViewTextEditItem* passwordTextItem;

// The text item related to the password note.
@property(nonatomic, strong) TableViewMultiLineTextEditItem* passwordNoteItem;

// The text item related to the creation date value.
@property(nonatomic, strong) TableViewTextEditItem* creationDateTextItem;

// If yes, the footer informing about the max note length is shown.
@property(nonatomic, assign) BOOL isNoteFooterShown;

@end
@implementation PasswordDetailsInfoItem
@end

#pragma mark - PasswordDetailsTableViewController

@interface PasswordDetailsTableViewController () <
    TableViewTextEditItemDelegate,
    TableViewMultiLineTextEditItemDelegate,
    UIEditMenuInteractionDelegate> {
  // Index of the password the user wants to reveal.
  NSInteger _passwordIndexToReveal;

  // Title label displayed in the navigation bar.
  UILabel* _titleLabel;

  // Whether Settings have been dismissed.
  BOOL _settingsAreDismissed;

  // The button for password sharing.
  UIBarButtonItem* _shareButton;
}

// Array of credentials that are shown on the screen.
@property(nonatomic, strong) NSArray<CredentialDetails*>* credentials;

@property(nonatomic, strong) NSString* pageTitle;

// Whether the password is shown in plain text form or in masked form.
@property(nonatomic, assign, getter=isPasswordShown) BOOL passwordShown;

// Array of the password details info items used by the table view model.
@property(nonatomic, strong)
    NSMutableArray<PasswordDetailsInfoItem*>* passwordDetailsInfoItems;

// The view used to anchor error alert which is shown for the username. This is
// image icon in the `usernameTextItem` cell.
@property(nonatomic, weak) UIView* usernameErrorAnchorView;

// Denotes that the Done button in editing mode can be enabled after
// basic validation of data on all the fields. Does not account for whether the
// duplicate credential exists or not.
@property(nonatomic, assign) BOOL shouldEnableEditDoneButton;

// If YES, the password details are shown without requiring any authentication.
@property(nonatomic, assign) BOOL showPasswordWithoutAuth;

// YES if this is the details view for a blocked site (never saved password).
@property(nonatomic, assign) BOOL isBlockedSite;

// Stores the signed in user email, or the empty string if the user is not
// signed-in.
@property(nonatomic, readonly) NSString* userEmail;

// Used to avoid recording the "move to account offered" histogram twice for
// the same credential.
@property(nonatomic, strong)
    NSMutableSet<NSString*>* usernamesWithMoveToAccountOfferRecorded;

// Used to create and show the actions users can execute when they tap on a row
// in the tableView. These actions are displayed a pop-up.
// TODO(crbug.com/40284033): Remove available guard when min deployment target
// is bumped to iOS 16.0.
@property(nonatomic, strong)
    UIEditMenuInteraction* interactionMenu API_AVAILABLE(ios(16));

@end

@implementation PasswordDetailsTableViewController

#pragma mark - ViewController Life Cycle.

- (instancetype)init {
  self = [super initWithStyle:ChromeTableViewStyle()];
  if (self) {
    _shouldEnableEditDoneButton = NO;
    _showPasswordWithoutAuth = NO;
    _passwordIndexToReveal = 0;

    _titleLabel = [[UILabel alloc] init];
    _titleLabel.lineBreakMode = NSLineBreakByTruncatingHead;
    _titleLabel.font =
        [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline];
    _titleLabel.adjustsFontForContentSizeCategory = YES;
    self.usernamesWithMoveToAccountOfferRecorded = [[NSMutableSet alloc] init];
  }
  return self;
}

#pragma mark - UIViewController

- (void)viewDidLoad {
  [super viewDidLoad];

  self.tableView.accessibilityIdentifier = kPasswordDetailsViewControllerID;
  self.tableView.allowsSelectionDuringEditing = YES;

  if (@available(iOS 16.0, *)) {
    _interactionMenu = [[UIEditMenuInteraction alloc] initWithDelegate:self];
    [self.tableView addInteraction:self.interactionMenu];
  }
}

- (void)viewWillAppear:(BOOL)animated {
  [super viewWillAppear:animated];
  // Title may change between the call to -init and -viewWillAppear, so we want
  // to wait until the last moment possible before setting the titleView.
  self.navigationItem.titleView = _titleLabel;
}

- (void)didMoveToParentViewController:(UIViewController*)parent {
  [super didMoveToParentViewController:parent];

  if (!parent) {
    [self.handler passwordDetailsTableViewControllerWasDismissed];
  }
}

#pragma mark - LegacyChromeTableViewController

- (void)editButtonPressed {
  // Share button should be hidden during editing.
  _shareButton.hidden = YES;

  // If there are no passwords or passkeys, proceed with editing without
  // reauthentication.
  if (![self hasAtLeastOnePasswordOrPasskey]) {
    [super editButtonPressed];

    // Reload view to show the delete button.
    [self reloadData];
    return;
  }

  // Enter editing mode.
  if (!self.tableView.editing && !self.isPasswordShown) {
    [self showPasswordFor:PasswordAccessReasonEdit];
    return;
  }

  if (self.tableView.editing) {
    // If password value was changed show confirmation dialog before saving.
    // Editing mode will be exited only if user confirms saving.
    if ([self passwordsDidChange]) {
      DCHECK(self.handler);
      // TODO(crbug.com/40884045): Show Password Edit Dialog when Password
      // Grouping is enabled.
      [self.handler showPasswordEditDialogWithOrigin:self.pageTitle];
    } else {
      [self passwordEditingConfirmed];
    }
    return;
  }

  [super editButtonPressed];
  [self reloadData];
}

- (void)loadModel {
  [super loadModel];

  self.passwordDetailsInfoItems = [[NSMutableArray alloc] init];

  for (CredentialDetails* credentialsDetails in _credentials) {
    [self addPasswordDetailsToModel:credentialsDetails];
  }
}

- (BOOL)showCancelDuringEditing {
  return YES;
}

#pragma mark - Items

- (TableViewStackedDetailsItem*)websiteItemForPasswordDetails:
    (CredentialDetails*)passwordDetails {
  TableViewStackedDetailsItem* item = [[TableViewStackedDetailsItem alloc]
      initWithType:PasswordDetailsItemTypeWebsite];
  item.titleText = l10n_util::GetNSString(IDS_IOS_SHOW_PASSWORD_VIEW_SITES);
  item.detailTexts = passwordDetails.websites;
  item.detailTextColor = [UIColor colorNamed:kTextSecondaryColor];
  item.accessibilityTraits = UIAccessibilityTraitNotEnabled;
  return item;
}

- (TableViewTextEditItem*)usernameItemForPasswordDetails:
    (CredentialDetails*)passwordDetails {
  TableViewTextEditItem* item = [[TableViewTextEditItem alloc]
      initWithType:PasswordDetailsItemTypeUsername];
  item.textFieldBackgroundColor = [UIColor clearColor];
  item.fieldNameLabelText =
      l10n_util::GetNSString(IDS_IOS_SHOW_PASSWORD_VIEW_USERNAME);
  item.textFieldValue = passwordDetails.username;  // Empty for a new form.
  // If password is missing (federated credential) don't allow to edit username.
  if (passwordDetails.credentialType != CredentialTypeFederation) {
    item.textFieldEnabled = self.tableView.editing;
    item.autoCapitalizationType = UITextAutocapitalizationTypeNone;
    item.delegate = self;
  } else {
    item.textFieldEnabled = NO;
  }
  item.hideIcon = YES;
  item.textFieldPlaceholder = l10n_util::GetNSString(
      IDS_IOS_PASSWORD_SETTINGS_USERNAME_PLACEHOLDER_TEXT);
  if (!self.tableView.editing) {
    item.textFieldTextColor = [UIColor colorNamed:kTextSecondaryColor];
  }

  // For testing: only use this custom accessibility identifier if there are
  // more than one password shown on the Password Details.
  if (_credentials.count > 1) {
    item.customTextfieldAccessibilityIdentifier = [NSString
        stringWithFormat:@"%@%@%@", kUsernameTextfieldForPasswordDetailsID,
                         passwordDetails.username, passwordDetails.websites[0]];
  }
  return item;
}

- (TableViewTextEditItem*)userDisplayNameItemForPasswordDetails:
    (CredentialDetails*)passwordDetails {
  TableViewTextEditItem* item = [[TableViewTextEditItem alloc]
      initWithType:PasswordDetailsItemTypeUsername];
  item.textFieldBackgroundColor = [UIColor clearColor];
  item.fieldNameLabelText =
      l10n_util::GetNSString(IDS_IOS_SHOW_PASSKEY_DISPLAY_NAME);
  item.textFieldValue = passwordDetails.userDisplayName;
  item.textFieldEnabled = self.tableView.editing;
  item.autoCapitalizationType = UITextAutocapitalizationTypeNone;
  item.delegate = self;
  item.hideIcon = YES;
  if (!self.tableView.editing) {
    item.textFieldTextColor = [UIColor colorNamed:kTextSecondaryColor];
  }

  // For testing: only use this custom accessibility identifier if there are
  // more than one password shown on the Password Details.
  if (_credentials.count > 1) {
    item.customTextfieldAccessibilityIdentifier = [NSString
        stringWithFormat:@"%@%@%@",
                         kUserDisplayNameTextfieldForPasswordDetailsID,
                         passwordDetails.userDisplayName,
                         passwordDetails.websites[0]];
  }
  return item;
}

- (TableViewTextEditItem*)creationDateItemForPasswordDetails:
    (CredentialDetails*)passwordDetails {
  TableViewTextEditItem* item = [[TableViewTextEditItem alloc]
      initWithType:PasswordDetailsItemTypeUsername];
  item.textFieldBackgroundColor = [UIColor clearColor];
  item.fieldNameLabelText =
      l10n_util::GetNSString(IDS_IOS_SHOW_PASSKEY_CREATION_DATE);
  item.textFieldValue =
      passwordDetails.creationTime.has_value()
          ? l10n_util::GetNSStringF(IDS_IOS_PASSKEY_CREATION_DATE,
                                    base::TimeFormatShortDateNumeric(
                                        *(passwordDetails.creationTime)))
          : @"";
  item.textFieldEnabled = NO;
  item.autoCapitalizationType = UITextAutocapitalizationTypeNone;
  item.delegate = self;
  item.hideIcon = YES;
  if (!self.tableView.editing) {
    item.textFieldTextColor = [UIColor colorNamed:kTextSecondaryColor];
  }

  // For testing: only use this custom accessibility identifier if there are
  // more than one password shown on the Password Details.
  if (_credentials.count > 1) {
    item.customTextfieldAccessibilityIdentifier = [NSString
        stringWithFormat:@"%@%@%@", kCreationDateTextfieldForPasswordDetailsID,
                         item.textFieldValue, passwordDetails.websites[0]];
  }
  return item;
}

- (TableViewTextEditItem*)passwordItemForPasswordDetails:
    (CredentialDetails*)passwordDetails {
  TableViewTextEditItem* item = [[TableViewTextEditItem alloc]
      initWithType:PasswordDetailsItemTypePassword];
  item.textFieldBackgroundColor = [UIColor clearColor];
  item.fieldNameLabelText =
      l10n_util::GetNSString(IDS_IOS_SHOW_PASSWORD_VIEW_PASSWORD);
  item.textFieldValue = [self isPasswordShown] || self.tableView.editing
                            ? passwordDetails.password
                            : kMaskedPassword;
  item.textFieldEnabled = self.tableView.editing;
  item.hideIcon = YES;
  item.autoCapitalizationType = UITextAutocapitalizationTypeNone;
  item.keyboardType = UIKeyboardTypeURL;
  item.returnKeyType = UIReturnKeyDone;
  item.delegate = self;
  item.textFieldPlaceholder = l10n_util::GetNSString(
      IDS_IOS_PASSWORD_SETTINGS_PASSWORD_PLACEHOLDER_TEXT);

  // During editing password is exposed so eye icon shouldn't be shown.
  if (!self.tableView.editing) {
    UIImage* image =
        [self isPasswordShown]
            ? DefaultSymbolWithPointSize(kHideActionSymbol, kSymbolSize)
            : DefaultSymbolWithPointSize(kShowActionSymbol, kSymbolSize);
    item.identifyingIcon = image;
    item.identifyingIconEnabled = YES;
    item.identifyingIconAccessibilityLabel = l10n_util::GetNSString(
        [self isPasswordShown] ? IDS_IOS_SETTINGS_PASSWORD_HIDE_BUTTON
                               : IDS_IOS_SETTINGS_PASSWORD_SHOW_BUTTON);
  }
  if (!self.tableView.editing) {
    item.textFieldTextColor = [UIColor colorNamed:kTextSecondaryColor];
  }

  // For testing: only use this custom accessibility identifier if there are
  // more than one password shown on the Password Details.
  if (_credentials.count > 1) {
    item.customTextfieldAccessibilityIdentifier = [NSString
        stringWithFormat:@"%@%@%@", kPasswordTextfieldForPasswordDetailsID,
                         passwordDetails.username, passwordDetails.websites[0]];
  }
  return item;
}

- (TableViewMultiLineTextEditItem*)noteItemForPasswordDetails:
    (CredentialDetails*)passwordDetails {
  TableViewMultiLineTextEditItem* item = [[TableViewMultiLineTextEditItem alloc]
      initWithType:PasswordDetailsItemTypeNote];
  item.label = l10n_util::GetNSString(IDS_IOS_SHOW_PASSWORD_VIEW_NOTE);
  item.text = passwordDetails.note;
  item.editingEnabled = self.tableView.editing;
  item.delegate = self;
  return item;
}

- (TableViewTextEditItem*)federationItemForPasswordDetails:
    (CredentialDetails*)passwordDetails {
  TableViewTextEditItem* item = [[TableViewTextEditItem alloc]
      initWithType:PasswordDetailsItemTypeFederation];
  item.textFieldBackgroundColor = [UIColor clearColor];
  item.fieldNameLabelText =
      l10n_util::GetNSString(IDS_IOS_SHOW_PASSWORD_VIEW_FEDERATION);
  item.textFieldValue = passwordDetails.federation;
  item.textFieldEnabled = NO;
  item.hideIcon = YES;
  return item;
}

- (TableViewTextItem*)changePasswordItem {
  TableViewTextItem* item = [[TableViewTextItem alloc]
      initWithType:PasswordDetailsItemTypeChangePasswordButton];
  item.text = l10n_util::GetNSString(IDS_IOS_CHANGE_COMPROMISED_PASSWORD);
  item.textColor = self.tableView.editing
                       ? [UIColor colorNamed:kTextSecondaryColor]
                       : [UIColor colorNamed:kBlueColor];
  item.accessibilityTraits = UIAccessibilityTraitButton;
  return item;
}

- (SettingsImageDetailTextItem*)changePasswordRecommendationItem {
  SettingsImageDetailTextItem* item = [[SettingsImageDetailTextItem alloc]
      initWithType:PasswordDetailsItemTypeChangePasswordRecommendation];
  item.detailText = l10n_util::GetNSString(
      IDS_IOS_CHANGE_COMPROMISED_PASSWORD_DESCRIPTION_BRANDED);
  item.image = [self compromisedIcon];
  item.imageViewTintColor = [UIColor colorNamed:kRed500Color];
  item.accessibilityIdentifier = kCompromisedWarningID;
  return item;
}

- (TableViewTextItem*)dismissWarningItem {
  TableViewTextItem* item = [[TableViewTextItem alloc]
      initWithType:PasswordDetailsItemTypeDismissWarningButton];
  item.text = l10n_util::GetNSString(IDS_IOS_DISMISS_WARNING);
  item.textColor = self.tableView.editing
                       ? [UIColor colorNamed:kTextSecondaryColor]
                       : [UIColor colorNamed:kBlueColor];
  item.accessibilityTraits = UIAccessibilityTraitButton;
  return item;
}

- (TableViewTextItem*)restoreWarningItem {
  TableViewTextItem* item = [[TableViewTextItem alloc]
      initWithType:PasswordDetailsItemTypeRestoreWarningButton];
  item.text = l10n_util::GetNSString(IDS_IOS_RESTORE_WARNING);
  item.textColor = self.tableView.editing
                       ? [UIColor colorNamed:kTextSecondaryColor]
                       : [UIColor colorNamed:kBlueColor];
  item.accessibilityTraits = UIAccessibilityTraitButton;
  return item;
}

- (TableViewTextItem*)deleteButtonItemForPasswordDetails:
    (CredentialDetails*)passwordDetails {
  TableViewTextItem* item = [[TableViewTextItem alloc]
      initWithType:PasswordDetailsItemTypeDeleteButton];
  int itemText = IDS_IOS_CONFIRM_PASSWORD_DELETION;
  if (self.isBlockedSite) {
    itemText = IDS_IOS_DELETE_ACTION_TITLE;
  } else if (passwordDetails.credentialType == CredentialTypePasskey) {
    itemText = IDS_IOS_CONFIRM_PASSKEY_DELETION;
  }
  item.text = l10n_util::GetNSString(itemText);
  item.textColor = [UIColor colorNamed:kRedColor];
  item.accessibilityTraits = UIAccessibilityTraitButton;
  item.accessibilityIdentifier = [NSString
      stringWithFormat:@"%@%@%@", kDeleteButtonForPasswordDetailsID,
                       passwordDetails.username, passwordDetails.websites[0]];
  return item;
}

- (TableViewTextItem*)moveToAccountButtonItem {
  TableViewTextItem* item = [[TableViewTextItem alloc]
      initWithType:PasswordDetailsItemTypeMoveToAccountButton];
  item.text = l10n_util::GetNSString(IDS_IOS_SAVE_PASSWORD_TO_ACCOUNT_STORE);
  item.textColor = self.tableView.editing
                       ? [UIColor colorNamed:kTextSecondaryColor]
                       : [UIColor colorNamed:kBlueColor];
  item.enabled = !self.tableView.editing;
  item.accessibilityIdentifier = kMovePasswordToAccountButtonID;
  return item;
}

- (SettingsImageDetailTextItem*)moveToAccountRecommendationItem {
  DCHECK(_userEmail.length)
      << "User must be signed-in to move a password to the "
         "account;";
  SettingsImageDetailTextItem* item = [[SettingsImageDetailTextItem alloc]
      initWithType:PasswordDetailsItemTypeMoveToAccountRecommendation];
  item.detailText = l10n_util::GetNSStringF(
      IDS_IOS_SAVE_PASSWORD_TO_ACCOUNT_STORE_DESCRIPTION,
      base::SysNSStringToUTF16(self.userEmail));
  item.image = CustomSymbolWithPointSize(kCloudAndArrowUpSymbol,
                                         kRecommendationSymbolSize);
  item.imageViewTintColor = [UIColor colorNamed:kBlueColor];
  return item;
}

#pragma mark - UITableViewDelegate

// Makes sure that the note footer is displayed correctly when it is scrolled to
// during password editing.
- (void)tableView:(UITableView*)tableView
    willDisplayFooterView:(UIView*)view
               forSection:(NSInteger)section {
  if ([view isKindOfClass:[TableViewTextHeaderFooterView class]]) {
    TableViewTextHeaderFooterView* footer =
        base::apple::ObjCCastStrict<TableViewTextHeaderFooterView>(view);
    NSString* footerText =
        self.passwordDetailsInfoItems[section].isNoteFooterShown
            ? l10n_util::GetNSStringF(
                  IDS_IOS_SETTINGS_PASSWORDS_TOO_LONG_NOTE_DESCRIPTION,
                  base::NumberToString16(kMaxPasswordNoteLength))
            : @"";
    [footer setSubtitle:footerText];
  }
}

- (void)tableView:(UITableView*)tableView
    didSelectRowAtIndexPath:(NSIndexPath*)indexPath {
  TableViewModel* model = self.tableViewModel;
  NSInteger itemType = [model itemTypeForIndexPath:indexPath];
  switch (itemType) {
    case PasswordDetailsItemTypeWebsite:
    case PasswordDetailsItemTypeFederation:
      [self ensureContextMenuShownForItemType:itemType
                                    tableView:tableView
                                  atIndexPath:indexPath];
      break;
    case PasswordDetailsItemTypeUsername: {
      if (self.tableView.editing) {
        UITableViewCell* cell =
            [self.tableView cellForRowAtIndexPath:indexPath];
        TableViewTextEditCell* textFieldCell =
            base::apple::ObjCCastStrict<TableViewTextEditCell>(cell);
        [textFieldCell.textField becomeFirstResponder];
      } else {
        [self ensureContextMenuShownForItemType:itemType
                                      tableView:tableView
                                    atIndexPath:indexPath];
      }
      break;
    }
    case PasswordDetailsItemTypePassword: {
      if (self.tableView.editing) {
        UITableViewCell* cell =
            [self.tableView cellForRowAtIndexPath:indexPath];
        TableViewTextEditCell* textFieldCell =
            base::apple::ObjCCastStrict<TableViewTextEditCell>(cell);
        [textFieldCell.textField becomeFirstResponder];
      } else {
        [self ensureContextMenuShownForItemType:itemType
                                      tableView:tableView
                                    atIndexPath:indexPath];
      }
      break;
    }
    case PasswordDetailsItemTypeChangePasswordButton:
      if (!self.tableView.editing) {
        DCHECK(self.applicationCommandsHandler);
        CredentialDetails* passwordDetails =
            self.credentials[indexPath.section];
        DCHECK(passwordDetails.changePasswordURL.has_value());

        CHECK(password_manager::ShouldRecordPasswordCheckUserAction(
            passwordDetails.context, passwordDetails.compromised));

        password_manager::LogChangePasswordOnWebsite(
            GetWarningTypeForDetailsContext(passwordDetails.context));

        OpenNewTabCommand* command = [OpenNewTabCommand
            commandWithURLFromChrome:passwordDetails.changePasswordURL.value()];
        [self.applicationCommandsHandler closeSettingsUIAndOpenURL:command];
      }
      break;
    case PasswordDetailsItemTypeNote: {
      UITableViewCell* cell = [self.tableView cellForRowAtIndexPath:indexPath];
      TableViewMultiLineTextEditCell* textFieldCell =
          base::apple::ObjCCastStrict<TableViewMultiLineTextEditCell>(cell);
      [textFieldCell.textView becomeFirstResponder];
      break;
    }
    case PasswordDetailsItemTypeDismissWarningButton:
      if (!self.tableView.editing) {
        [self didTapDismissWarningButtonAtPasswordIndex:indexPath.section];
        [self.tableView deselectRowAtIndexPath:indexPath animated:YES];
      }
      break;
    case PasswordDetailsItemTypeRestoreWarningButton:
      if (!self.tableView.editing) {
        [self didTapRestoreWarningButtonAtPasswordIndex:indexPath.section];
        [self.tableView deselectRowAtIndexPath:indexPath animated:YES];
      }
      break;
    case PasswordDetailsItemTypeDeleteButton:
      if (self.tableView.editing) {
        UITableViewCell* cell =
            [self.tableView cellForRowAtIndexPath:indexPath];
        [self didTapDeleteButton:cell atPasswordIndex:indexPath.section];
        [self.tableView deselectRowAtIndexPath:indexPath animated:YES];
      }
      break;
    case PasswordDetailsItemTypeMoveToAccountButton:
      if (!self.tableView.editing) {
        UITableViewCell* cell =
            [self.tableView cellForRowAtIndexPath:indexPath];
        [self moveCredentialToAccountStore:indexPath.section anchorView:cell];
        [self.tableView deselectRowAtIndexPath:indexPath animated:YES];
      }
      break;
    case PasswordDetailsItemTypeChangePasswordRecommendation:
    case PasswordDetailsItemTypeMoveToAccountRecommendation:
      break;
  }
}

- (UITableViewCellEditingStyle)tableView:(UITableView*)tableView
           editingStyleForRowAtIndexPath:(NSIndexPath*)indexPath {
  return UITableViewCellEditingStyleNone;
}

- (BOOL)tableView:(UITableView*)tableview
    shouldIndentWhileEditingRowAtIndexPath:(NSIndexPath*)indexPath {
  return NO;
}

// If the context menu is not shown for a given item type, constructs that
// menu and shows it. This method should only be called for item types
// representing the cells with the site, username and password.
- (void)ensureContextMenuShownForItemType:(NSInteger)itemType
                                tableView:(UITableView*)tableView
                              atIndexPath:(NSIndexPath*)indexPath {
  if (@available(iOS 16.0, *)) {
    CGRect row = [tableView rectForRowAtIndexPath:indexPath];
    CGPoint editMenuLocation =
        CGPointMake(row.origin.x + row.size.width / 2, row.origin.y);
    UIEditMenuConfiguration* configuration = [UIEditMenuConfiguration
        configurationWithIdentifier:[NSNumber numberWithInt:itemType]
                        sourcePoint:editMenuLocation];
    [self.interactionMenu presentEditMenuWithConfiguration:configuration];
    base::RecordAction(
        base::UserMetricsAction("MobilePasswordDetailsShowCopyContextMenu"));
  }
#if !defined(__IPHONE_16_0) || __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_16_0
  else {
    // TODO(crbug.com/40930648): Replace UIMenuController with
    // UIEditMenuInteraction in iOS 16+.
    UIMenuController* menu = [UIMenuController sharedMenuController];
    if (![menu isMenuVisible]) {
      menu.menuItems = [self menuItemsForItemType:itemType];

      [menu showMenuFromView:tableView
                        rect:[tableView rectForRowAtIndexPath:indexPath]];
    }
  }
#endif
}

- (BOOL)tableView:(UITableView*)tableView
    shouldHighlightRowAtIndexPath:(NSIndexPath*)indexPath {
  NSInteger itemType = [self.tableViewModel itemTypeForIndexPath:indexPath];
  switch (itemType) {
    case PasswordDetailsItemTypeWebsite:
    case PasswordDetailsItemTypeFederation:
    case PasswordDetailsItemTypeUsername:
    case PasswordDetailsItemTypePassword:
    case PasswordDetailsItemTypeChangePasswordButton:
    case PasswordDetailsItemTypeMoveToAccountButton:
      return !self.editing;
    case PasswordDetailsItemTypeDeleteButton:
    case PasswordDetailsItemTypeNote:
      return self.editing;
    case PasswordDetailsItemTypeChangePasswordRecommendation:
    case PasswordDetailsItemTypeMoveToAccountRecommendation:
      return NO;
  }
  return YES;
}

- (CGFloat)tableView:(UITableView*)tableView
    heightForFooterInSection:(NSInteger)section {
  if (!self.passwordDetailsInfoItems[section].isNoteFooterShown) {
    return 0;
  }

  return [super tableView:tableView heightForFooterInSection:section];
}

#pragma mark - UITableViewDataSource

- (UITableViewCell*)tableView:(UITableView*)tableView
        cellForRowAtIndexPath:(NSIndexPath*)indexPath {
  UITableViewCell* cell = [super tableView:tableView
                     cellForRowAtIndexPath:indexPath];

  NSInteger itemType = [self.tableViewModel itemTypeForIndexPath:indexPath];
  cell.tag = itemType;
  cell.selectionStyle = UITableViewCellSelectionStyleDefault;

  switch (itemType) {
    case PasswordDetailsItemTypeUsername: {
      TableViewTextEditCell* textFieldCell =
          base::apple::ObjCCastStrict<TableViewTextEditCell>(cell);
      textFieldCell.textField.delegate = self;
      [textFieldCell.identifyingIconButton
                 addTarget:self
                    action:@selector(didTapUsernameErrorInfo:)
          forControlEvents:UIControlEventTouchUpInside];
      self.usernameErrorAnchorView = textFieldCell.iconView;
      break;
    }
    case PasswordDetailsItemTypePassword: {
      TableViewTextEditCell* textFieldCell =
          base::apple::ObjCCastStrict<TableViewTextEditCell>(cell);
      textFieldCell.textField.delegate = self;
      [textFieldCell.identifyingIconButton
                 addTarget:self
                    action:@selector(didTapShowHideButton:)
          forControlEvents:UIControlEventTouchUpInside];
      textFieldCell.identifyingIconButton.tag = indexPath.section;
      break;
    }
    case PasswordDetailsItemTypeChangePasswordRecommendation:
    case PasswordDetailsItemTypeMoveToAccountRecommendation:
    case PasswordDetailsItemTypeNote: {
      cell.selectionStyle = UITableViewCellSelectionStyleNone;
      break;
    }
    case PasswordDetailsItemTypeMoveToAccountButton: {
      // Record the "move to account offered" metric.
      // 1) The metric mustn't be recorded for credentials that are not visible
      // in the scroll view yet, so do it when the button cell is configured for
      // display (the button, not the text that comes before).
      // 2) The metric mustn't be recorded for the same credential again upon
      // model changes, e.g. credential removed or moved to account. Such events
      // reconfigure cells, so check if this username was already seen before
      // recording. The username is the closest thing to a stable identifier of
      // the credential. It can be edited, leading to a second recording, but
      // that shouldn't happen often. This approach is good enough.
      if (![self.usernamesWithMoveToAccountOfferRecorded
              containsObject:self.credentials[indexPath.section].username]) {
        [self.usernamesWithMoveToAccountOfferRecorded
            addObject:self.credentials[indexPath.section].username];
        // TODO(crbug.com/40880533): Use a common function for recording sites.
        base::UmaHistogramEnumeration(
            "PasswordManager.AccountStorage.MoveToAccountStoreFlowOffered",
            password_manager::metrics_util::MoveToAccountStoreTrigger::
                kExplicitlyTriggeredInSettings);
      }
      break;
    }
    case PasswordDetailsItemTypeNoteFooter:
    case PasswordDetailsItemTypeWebsite:
    case PasswordDetailsItemTypeFederation:
    case PasswordDetailsItemTypeChangePasswordButton:
    case PasswordDetailsItemTypeDismissWarningButton:
    case PasswordDetailsItemTypeRestoreWarningButton:
    case PasswordDetailsItemTypeDeleteButton:
      break;
  }
  return cell;
}

- (BOOL)tableView:(UITableView*)tableView
    canEditRowAtIndexPath:(NSIndexPath*)indexPath {
  NSInteger itemType = [self.tableViewModel itemTypeForIndexPath:indexPath];
  switch (itemType) {
    case PasswordDetailsItemTypeWebsite:
    case PasswordDetailsItemTypeFederation:
    case PasswordDetailsItemTypeUsername:
    case PasswordDetailsItemTypePassword:
    case PasswordDetailsItemTypeNote:
    case PasswordDetailsItemTypeDeleteButton:
      return YES;
  }
  return NO;
}

#pragma mark - SettingsControllerProtocol

- (void)reportDismissalUserAction {
  base::RecordAction(
      base::UserMetricsAction("MobilePasswordDetailsSettingsClose"));
}

- (void)reportBackUserAction {
  base::RecordAction(
      base::UserMetricsAction("MobilePasswordDetailsSettingsBack"));
}

- (void)settingsWillBeDismissed {
  DCHECK(!_settingsAreDismissed);

  _settingsAreDismissed = YES;
}

#pragma mark - PasswordDetailsConsumer

- (void)setCredentials:(NSArray<CredentialDetails*>*)credentials
              andTitle:(NSString*)title {
  BOOL hadCredentials = [_credentials count];
  _credentials = credentials;
  _pageTitle = title;

  [self updateNavigationTitle];
  // Update the model even if all credentials are deleted and the view
  // controller will be dismissed. UIKit could still trigger events that execute
  // CHECK in this file that would fail if `_credentials` and the model are not
  // in sync.
  [self reloadData];

  if (![credentials count]) {
    // onAllPasswordsDeleted() mustn't be called twice.
    if (hadCredentials) {
      [self.handler onAllPasswordsDeleted];
    }
  }
}

- (void)setIsBlockedSite:(BOOL)isBlockedSite {
  _isBlockedSite = isBlockedSite;
}

- (void)setUserEmail:(NSString*)userEmail {
  _userEmail = userEmail;
}

- (void)setupRightShareButton:(BOOL)policyEnabled {
  SEL selector = policyEnabled ? @selector(onShareButtonPressed)
                               : @selector(onPolicyDisabledShareButtonPressed:);
  UIBarButtonItem* shareButton = [[UIBarButtonItem alloc]
      initWithImage:DefaultSymbolWithPointSize(kShareSymbol,
                                               kSymbolActionPointSize)
              style:UIBarButtonItemStylePlain
             target:self
             action:selector];
  shareButton.accessibilityIdentifier = kPasswordShareButtonID;
  shareButton.enabled = [self hasAtLeastOnePassword];
  _shareButton = shareButton;
  self.navigationItem.rightBarButtonItems =
      @[ self.navigationItem.rightBarButtonItem, shareButton ];
}

#pragma mark - TableViewTextEditItemDelegate

- (void)tableViewItemDidBeginEditing:(TableViewTextEditItem*)tableViewItem {
  [self reconfigureCellsForItems:@[ tableViewItem ]];
}

- (void)tableViewItemDidChange:(TableViewTextEditItem*)tableViewItem {
  BOOL usernameValid = [self checkIfValidUsernames];
  BOOL passwordValid = [self checkIfValidPasswords];
  BOOL noteValid = [self checkIfValidNotes];

  self.shouldEnableEditDoneButton = usernameValid && passwordValid && noteValid;
  [self toggleNavigationBarRightButtonItem];
}

- (void)tableViewItemDidEndEditing:(TableViewTextEditItem*)tableViewItem {
  if ([tableViewItem.fieldNameLabelText
          isEqualToString:l10n_util::GetNSString(
                              IDS_IOS_SHOW_PASSWORD_VIEW_PASSWORD)]) {
    [self checkIfValidPasswords];
  }
  [self reconfigureCellsForItems:@[ tableViewItem ]];
}

#pragma mark - TableViewMultiLineTextEditItemDelegate

- (void)textViewItemDidChange:(TableViewMultiLineTextEditItem*)tableViewItem {
  // Update save button state based on the note's length and validity of other
  // input fields.
  BOOL noteValid = tableViewItem.text.length <= kMaxPasswordNoteLength;
  tableViewItem.validText = noteValid;
  self.shouldEnableEditDoneButton =
      noteValid && [self checkIfValidUsernames] && [self checkIfValidPasswords];
  [self toggleNavigationBarRightButtonItem];
  [self reconfigureCellsForItems:@[ tableViewItem ]];

  // Notify that the note character limit has been reached via VoiceOver.
  if (!noteValid) {
    NSString* tooLongNoteMessage = l10n_util::GetNSStringF(
        IDS_IOS_SETTINGS_PASSWORDS_TOO_LONG_NOTE_DESCRIPTION,
        base::NumberToString16(kMaxPasswordNoteLength));
    UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification,
                                    tooLongNoteMessage);
  }

  BOOL shouldDisplayNoteFooter =
      tableViewItem.text.length >= kMinNoteCharAmountForWarning;
  NSIndexPath* indexPath = [self.tableViewModel
      indexPathForItem:static_cast<TableViewItem*>(tableViewItem)];

  // Refresh the cells' height and update note footer based on note's length.
  [self.tableView beginUpdates];
  if (shouldDisplayNoteFooter !=
      self.passwordDetailsInfoItems[indexPath.section].isNoteFooterShown) {
    self.passwordDetailsInfoItems[indexPath.section].isNoteFooterShown =
        shouldDisplayNoteFooter;

    UITableViewHeaderFooterView* footer =
        [self.tableView footerViewForSection:indexPath.section];
    TableViewTextHeaderFooterView* textFooter =
        base::apple::ObjCCastStrict<TableViewTextHeaderFooterView>(footer);
    NSString* footerText =
        shouldDisplayNoteFooter
            ? l10n_util::GetNSStringF(
                  IDS_IOS_SETTINGS_PASSWORDS_TOO_LONG_NOTE_DESCRIPTION,
                  base::NumberToString16(kMaxPasswordNoteLength))
            : @"";
    [textFooter setSubtitle:footerText];
  }
  [self.tableView endUpdates];
}

#pragma mark - SettingsRootTableViewController

- (BOOL)shouldHideToolbar {
  return YES;
}

#pragma mark - Private

// Applies tint colour and resizes image.
- (UIImage*)compromisedIcon {
  return DefaultSymbolTemplateWithPointSize(kErrorCircleFillSymbol,
                                            kRecommendationSymbolSize);
}

// Reveals password to the user.
- (void)showPasswordFor:(PasswordAccessReason)reason {
  switch (reason) {
    case PasswordAccessReasonShow: {
      self.passwordShown = YES;
      self.passwordDetailsInfoItems[_passwordIndexToReveal]
          .passwordTextItem.textFieldValue =
          self.credentials[_passwordIndexToReveal].password;
      self.passwordDetailsInfoItems[_passwordIndexToReveal]
          .passwordTextItem.identifyingIcon =
          DefaultSymbolWithPointSize(kHideActionSymbol, kSymbolSize);
      self.passwordDetailsInfoItems[_passwordIndexToReveal]
          .passwordTextItem.identifyingIconAccessibilityLabel =
          l10n_util::GetNSString(IDS_IOS_SETTINGS_PASSWORD_HIDE_BUTTON);
      [self reconfigureCellsForItems:@[
        self.passwordDetailsInfoItems[_passwordIndexToReveal].passwordTextItem
      ]];

      CredentialDetails* passwordDetails =
          self.credentials[_passwordIndexToReveal];
      DetailsContext detailsContext = passwordDetails.context;
      // When details was opened from the Password Manager, only log password
      // check actions if the password is compromised.
      if (password_manager::ShouldRecordPasswordCheckUserAction(
              detailsContext, passwordDetails.compromised)) {
        password_manager::LogRevealPassword(
            GetWarningTypeForDetailsContext(detailsContext));
      }
      break;
    }
    case PasswordAccessReasonCopy: {
      NSString* copiedString =
          self.credentials[self.tableView.indexPathForSelectedRow.section]
              .password;
      StoreTextInPasteboard(copiedString);

      [self showToast:l10n_util::GetNSString(
                          IDS_IOS_SETTINGS_PASSWORD_WAS_COPIED_MESSAGE)
           forSuccess:YES];
      break;
    }
    case PasswordAccessReasonEdit:
      // Called super because we want to update only `tableView.editing`.
      [super editButtonPressed];
      [self reloadData];
      break;
  }
  [self logPasswordAccessWith:reason];
}

// Shows a snack bar with `message` and provides haptic feedback. The haptic
// feedback is either for success or for error, depending on `success`. Deselect
// cell if there was one selected.
- (void)showToast:(NSString*)message forSuccess:(BOOL)success {
  TriggerHapticFeedbackForNotification(success
                                           ? UINotificationFeedbackTypeSuccess
                                           : UINotificationFeedbackTypeError);
  [self.snackbarCommandsHandler showSnackbarWithMessage:message
                                             buttonText:nil
                                          messageAction:nil
                                       completionAction:nil];

  if ([self.tableView indexPathForSelectedRow]) {
    [self.tableView
        deselectRowAtIndexPath:[self.tableView indexPathForSelectedRow]
                      animated:YES];
  }
}

// Checks if the usernames are valid and updates items accordingly.
- (BOOL)checkIfValidUsernames {
  DCHECK(self.credentials.count == self.passwordDetailsInfoItems.count);

  for (NSUInteger i = 0; i < self.passwordDetailsInfoItems.count; i++) {
    NSString* newUsernameValue =
        self.passwordDetailsInfoItems[i].usernameTextItem.textFieldValue;
    BOOL usernameChanged =
        ![newUsernameValue isEqualToString:self.credentials[i].username];
    BOOL showUsernameAlreadyUsed =
        usernameChanged &&
        [self.delegate isUsernameReused:newUsernameValue
                              forDomain:self.credentials[i].signonRealm];
    self.passwordDetailsInfoItems[i].usernameTextItem.hasValidText =
        !showUsernameAlreadyUsed;
    self.passwordDetailsInfoItems[i].usernameTextItem.identifyingIconEnabled =
        showUsernameAlreadyUsed;
    [self reconfigureCellsForItems:@[
      self.passwordDetailsInfoItems[i].usernameTextItem
    ]];

    if (showUsernameAlreadyUsed) {
      return NO;
    }
  }
  return YES;
}

// Checks if the passwords are valid and updates items accordingly.
- (BOOL)checkIfValidPasswords {
  DCHECK(self.credentials.count == self.passwordDetailsInfoItems.count);

  for (NSUInteger i = 0; i < self.passwordDetailsInfoItems.count; i++) {
    if (self.credentials[i].credentialType == CredentialTypePasskey) {
      continue;
    }

    BOOL passwordEmpty = [self.passwordDetailsInfoItems[i]
                                 .passwordTextItem.textFieldValue length] == 0;
    self.passwordDetailsInfoItems[i].passwordTextItem.hasValidText =
        !passwordEmpty;
    [self reconfigureCellsForItems:@[
      self.passwordDetailsInfoItems[i].passwordTextItem
    ]];

    if (passwordEmpty) {
      return NO;
    }
  }
  return YES;
}

// Checks if notes are valid.
- (BOOL)checkIfValidNotes {
  DCHECK(self.credentials.count == self.passwordDetailsInfoItems.count);

  for (NSUInteger i = 0; i < self.passwordDetailsInfoItems.count; i++) {
    if (self.passwordDetailsInfoItems[i].passwordNoteItem.text.length >
        kMaxPasswordNoteLength) {
      return NO;
    }
  }
  return YES;
}

// Removes the given section if it exists.
- (void)removeSectionWithIdentifier:(NSInteger)sectionIdentifier
                   withRowAnimation:(UITableViewRowAnimation)animation {
  TableViewModel* model = self.tableViewModel;
  if ([model hasSectionForSectionIdentifier:sectionIdentifier]) {
    NSInteger section = [model sectionForSectionIdentifier:sectionIdentifier];
    [model removeSectionWithIdentifier:sectionIdentifier];
    [[self tableView] deleteSections:[NSIndexSet indexSetWithIndex:section]
                    withRowAnimation:animation];
  }
}

// Enables/Disables the right bar button item in the navigation bar.
- (void)toggleNavigationBarRightButtonItem {
  self.navigationItem.rightBarButtonItem.enabled =
      self.shouldEnableEditDoneButton;
}

- (BOOL)hasAtLeastOnePassword {
  for (CredentialDetails* credentialDetails in self.credentials) {
    if (credentialDetails.password.length > 0) {
      return YES;
    }
  }
  return NO;
}

- (BOOL)hasAtLeastOnePasswordOrPasskey {
  for (CredentialDetails* credentialDetails in self.credentials) {
    if (credentialDetails.credentialType == CredentialTypePasskey ||
        credentialDetails.password.length > 0) {
      return YES;
    }
  }
  return NO;
}

- (BOOL)passwordsDidChange {
  DCHECK(self.credentials.count == self.passwordDetailsInfoItems.count);

  for (NSUInteger i = 0; i < self.passwordDetailsInfoItems.count; i++) {
    if (self.credentials[i].credentialType != CredentialTypePasskey &&
        ![self.credentials[i].password
            isEqualToString:self.passwordDetailsInfoItems[i]
                                .passwordTextItem.textFieldValue]) {
      return YES;
    }
  }
  return NO;
}

// Updates the title displayed in the navigation bar.
- (void)updateNavigationTitle {
  if (self.pageTitle.length == 0) {
    // When no pageTitle is supplied, use origin of first password.
    CredentialDetails* firstPassword = self.credentials.firstObject;
    self.pageTitle = firstPassword.origins.firstObject;
  }
  _titleLabel.text = self.pageTitle;
}

// Creates the model items corresponding to a `PasswordDetails` and adds them to
// the `model`.
- (void)addPasswordDetailsToModel:(CredentialDetails*)credentialDetails {
  TableViewModel* model = self.tableViewModel;
  PasswordDetailsInfoItem* passwordItem =
      [[PasswordDetailsInfoItem alloc] init];

  NSInteger sectionForWebsite;
  NSInteger sectionForPassword;
  NSInteger sectionForCompromisedInfo;
  NSInteger sectionForMoveCredential;

  // Password details are displayed in its own section when Grouping is enabled.
  NSInteger nextSection = kSectionIdentifierEnumZero + [model numberOfSections];
  [model addSectionWithIdentifier:nextSection];

  sectionForWebsite = nextSection;
  sectionForPassword = nextSection;
  sectionForCompromisedInfo = nextSection;
  sectionForMoveCredential = nextSection;

  // Add sites to section.
  passwordItem.websiteItem =
      [self websiteItemForPasswordDetails:credentialDetails];
  [model addItem:passwordItem.websiteItem
      toSectionWithIdentifier:sectionForWebsite];

  // Add username and password to section according to credential type.
  switch (credentialDetails.credentialType) {
    case CredentialTypeRegularPassword: {
      passwordItem.usernameTextItem =
          [self usernameItemForPasswordDetails:credentialDetails];
      [model addItem:passwordItem.usernameTextItem
          toSectionWithIdentifier:sectionForPassword];

      passwordItem.passwordTextItem =
          [self passwordItemForPasswordDetails:credentialDetails];
      [model addItem:passwordItem.passwordTextItem
          toSectionWithIdentifier:sectionForPassword];

      passwordItem.passwordNoteItem =
          [self noteItemForPasswordDetails:credentialDetails];
      [model addItem:passwordItem.passwordNoteItem
          toSectionWithIdentifier:sectionForPassword];

      passwordItem.isNoteFooterShown =
          self.tableView.editing && passwordItem.passwordNoteItem.text.length >=
                                        kMinNoteCharAmountForWarning;
      TableViewTextHeaderFooterItem* footer =
          [[TableViewTextHeaderFooterItem alloc]
              initWithType:PasswordDetailsItemTypeNoteFooter];
      footer.subtitle =
          passwordItem.isNoteFooterShown
              ? l10n_util::GetNSStringF(
                    IDS_IOS_SETTINGS_PASSWORDS_TOO_LONG_NOTE_DESCRIPTION,
                    base::NumberToString16(kMaxPasswordNoteLength))
              : @"";
      [model setFooter:footer forSectionWithIdentifier:sectionForPassword];

      if (credentialDetails.isCompromised || credentialDetails.isMuted) {
        [model addItem:[self changePasswordRecommendationItem]
            toSectionWithIdentifier:sectionForCompromisedInfo];

        if (credentialDetails.changePasswordURL.has_value()) {
          [model addItem:[self changePasswordItem]
              toSectionWithIdentifier:sectionForCompromisedInfo];
        }

        if (ShouldAllowToDismissWarning(credentialDetails.context,
                                        credentialDetails.compromised)) {
          [model addItem:[self dismissWarningItem]
              toSectionWithIdentifier:sectionForCompromisedInfo];
        } else if (ShouldAllowToRestoreWarning(credentialDetails.context,
                                               credentialDetails.muted)) {
          [model addItem:[self restoreWarningItem]
              toSectionWithIdentifier:sectionForCompromisedInfo];
        }
      }
      break;
    }
    case CredentialTypeFederation: {
      passwordItem.usernameTextItem =
          [self usernameItemForPasswordDetails:credentialDetails];
      [model addItem:passwordItem.usernameTextItem
          toSectionWithIdentifier:sectionForPassword];

      // Federated password forms don't have password value.
      [model addItem:[self federationItemForPasswordDetails:credentialDetails]
          toSectionWithIdentifier:sectionForPassword];
      break;
    }

    case CredentialTypeBlocked: {
      break;
    }

    case CredentialTypePasskey: {
      passwordItem.userDisplayNameTextItem =
          [self userDisplayNameItemForPasswordDetails:credentialDetails];
      [model addItem:passwordItem.userDisplayNameTextItem
          toSectionWithIdentifier:sectionForPassword];

      passwordItem.usernameTextItem =
          [self usernameItemForPasswordDetails:credentialDetails];
      [model addItem:passwordItem.usernameTextItem
          toSectionWithIdentifier:sectionForPassword];

      passwordItem.creationDateTextItem =
          [self creationDateItemForPasswordDetails:credentialDetails];
      [model addItem:passwordItem.creationDateTextItem
          toSectionWithIdentifier:sectionForPassword];
      break;
    }
  }

  if (credentialDetails.shouldOfferToMoveToAccount) {
    [model addItem:[self moveToAccountRecommendationItem]
        toSectionWithIdentifier:sectionForMoveCredential];
    [model addItem:[self moveToAccountButtonItem]
        toSectionWithIdentifier:sectionForMoveCredential];
  }

  if (self.tableView.editing) {
    [model addItem:[self deleteButtonItemForPasswordDetails:credentialDetails]
        toSectionWithIdentifier:sectionForPassword];
  }
  [self.passwordDetailsInfoItems addObject:passwordItem];
}

// Moves password at specified index from profile store to account store.
- (void)moveCredentialToAccountStore:(int)passwordIndex
                          anchorView:(UIView*)anchorView {
  DCHECK_GE(passwordIndex, 0);
  DCHECK(self.handler);

  __weak __typeof(self) weakSelf = self;
  NSString* toastMessage = l10n_util::GetNSStringF(
      IDS_IOS_PASSWORD_SAVED_TO_ACCOUNT_SNACKBAR_MESSAGE,
      base::SysNSStringToUTF16(self.userEmail));
  [self.handler moveCredentialToAccountStore:self.credentials[passwordIndex]
                                  anchorView:anchorView
                             movedCompletion:^{
                               [weakSelf showToast:toastMessage forSuccess:YES];
                             }];
}

// Notifies the handler that the share button was pressed by the user.
- (void)onShareButtonPressed {
  CHECK(self.handler);

  // Replace `_shareButton` with a spinner.
  UIActivityIndicatorView* spinner = GetMediumUIActivityIndicatorView();
  [spinner startAnimating];
  UIBarButtonItem* spinnerBarButtonItem =
      [[UIBarButtonItem alloc] initWithCustomView:spinner];
  self.navigationItem.rightBarButtonItems =
      @[ self.navigationItem.rightBarButtonItem, spinnerBarButtonItem ];

  [self.handler onShareButtonPressed];
}

// Displays the popup informing that password sharing is disabled by the
// administrator.
- (void)onPolicyDisabledShareButtonPressed:(UIBarButtonItem*)button {
  EnterpriseInfoPopoverViewController* popoverViewController =
      [[EnterpriseInfoPopoverViewController alloc]
                 initWithMessage:
                     l10n_util::GetNSString(
                         IDS_IOS_PASSWORD_SHARING_ENTERPRISE_POLICY_DISABLED_MESSAGE)
                  enterpriseName:nil
          isPresentingFromButton:YES
                addLearnMoreLink:NO];
  popoverViewController.popoverPresentationController.barButtonItem = button;
  popoverViewController.popoverPresentationController.permittedArrowDirections =
      UIPopoverArrowDirectionAny;

  [self presentViewController:popoverViewController
                     animated:YES
                   completion:nil];
}

#pragma mark - AutofillEditTableViewController

- (BOOL)isItemAtIndexPathTextEditCell:(NSIndexPath*)cellPath {
  NSInteger itemType = [self.tableViewModel itemTypeForIndexPath:cellPath];
  switch (static_cast<PasswordDetailsItemType>(itemType)) {
    case PasswordDetailsItemTypeUsername:
    case PasswordDetailsItemTypePassword:
      return YES;
    case PasswordDetailsItemTypeWebsite:
    case PasswordDetailsItemTypeFederation:
    case PasswordDetailsItemTypeChangePasswordButton:
    case PasswordDetailsItemTypeChangePasswordRecommendation:
    case PasswordDetailsItemTypeDismissWarningButton:
    case PasswordDetailsItemTypeRestoreWarningButton:
    case PasswordDetailsItemTypeDeleteButton:
    case PasswordDetailsItemTypeMoveToAccountButton:
    case PasswordDetailsItemTypeMoveToAccountRecommendation:
    case PasswordDetailsItemTypeNoteFooter:
    case PasswordDetailsItemTypeNote:
      return NO;
  }
}

#pragma mark - UIEditMenuInteractionDelegate

// TODO(crbug.com/40284033): Remove available guard when min deployment target
// is bumped to iOS 16.0.
- (UIMenu*)editMenuInteraction:(UIEditMenuInteraction*)interaction
          menuForConfiguration:(UIEditMenuConfiguration*)configuration
              suggestedActions:(NSArray<UIMenuElement*>*)suggestedActions
    API_AVAILABLE(ios(16)) {
  NSUInteger itemType =
      [base::apple::ObjCCast<NSNumber>(configuration.identifier) intValue];
  // TODO(crbug.com/343291599): Clean up crash key and
  // DumpWithoutCrashing when finished with the investigation.
  if (!itemType) {
    std::string configurationIdentifierString = base::SysNSStringToUTF8(
        [NSString stringWithFormat:@"%@", configuration.identifier]);
    configuration_identifier_crash_key.Set(configurationIdentifierString);
    base::debug::DumpWithoutCrashing();

    return nil;
  }

  UIAction* copy = [UIAction
      actionWithTitle:l10n_util::GetNSString(
                          IDS_IOS_SETTINGS_SITE_COPY_MENU_ITEM)
                image:nil
           identifier:nil
              handler:^(__kindof UIAction* _Nonnull action) {
                base::RecordAction(
                    base::UserMetricsAction("MobilePasswordDetailsCopy"));
                [self copyPasswordDetailsHelper:itemType];
              }];
  return [UIMenu menuWithChildren:@[ copy ]];
}

#pragma mark - Actions

// Called when the user tapped on the show/hide button near password.
- (void)didTapShowHideButton:(UIButton*)buttonView {
  [self.tableView deselectRowAtIndexPath:self.tableView.indexPathForSelectedRow
                                animated:NO];
  _passwordIndexToReveal = [buttonView tag];

  if (self.isPasswordShown) {
    self.passwordShown = NO;
    self.passwordDetailsInfoItems[_passwordIndexToReveal]
        .passwordTextItem.textFieldValue = kMaskedPassword;

    self.passwordDetailsInfoItems[_passwordIndexToReveal]
        .passwordTextItem.identifyingIcon =
        DefaultSymbolWithPointSize(kShowActionSymbol, kSymbolSize);
    self.passwordDetailsInfoItems[_passwordIndexToReveal]
        .passwordTextItem.identifyingIconAccessibilityLabel =
        l10n_util::GetNSString(IDS_IOS_SETTINGS_PASSWORD_SHOW_BUTTON);
    [self reconfigureCellsForItems:@[
      self.passwordDetailsInfoItems[_passwordIndexToReveal].passwordTextItem
    ]];
  } else {
    [self showPasswordFor:PasswordAccessReasonShow];
    base::RecordAction(
        base::UserMetricsAction("MobilePasswordDetailsViewPassword"));
  }
}

// Called when the user tap error info icon in the username input.
- (void)didTapUsernameErrorInfo:(UIButton*)buttonView {
  NSString* text = l10n_util::GetNSString(IDS_IOS_USERNAME_ALREADY_USED);

  NSAttributedString* attributedText = [[NSAttributedString alloc]
      initWithString:text
          attributes:@{
            NSForegroundColorAttributeName :
                [UIColor colorNamed:kTextSecondaryColor],
            NSFontAttributeName :
                [UIFont preferredFontForTextStyle:UIFontTextStyleSubheadline]
          }];

  PopoverLabelViewController* errorInfoPopover =
      [[PopoverLabelViewController alloc]
          initWithPrimaryAttributedString:attributedText
                secondaryAttributedString:nil];

  errorInfoPopover.popoverPresentationController.sourceView =
      self.usernameErrorAnchorView;
  errorInfoPopover.popoverPresentationController.sourceRect =
      self.usernameErrorAnchorView.bounds;
  errorInfoPopover.popoverPresentationController.permittedArrowDirections =
      UIPopoverArrowDirectionAny;
  [self presentViewController:errorInfoPopover animated:YES completion:nil];
}

#if !defined(__IPHONE_16_0) || __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_16_0

// Returns an array of UIMenuItems to display in a context menu on the site
// cell.
- (NSArray*)menuItemsForItemType:(NSInteger)itemType {
  PasswordDetailsMenuItem* copyOption = [[PasswordDetailsMenuItem alloc]
      initWithTitle:l10n_util::GetNSString(IDS_IOS_SETTINGS_SITE_COPY_MENU_ITEM)
             action:@selector(copyPasswordDetails:)];
  copyOption.itemType = itemType;
  return @[ copyOption ];
}

// Copies the password information to system pasteboard and shows a toast of
// success/failure.
- (void)copyPasswordDetails:(id)sender {
  base::RecordAction(base::UserMetricsAction("MobilePasswordDetailsCopy"));

  UIMenuController* menu =
      base::apple::ObjCCastStrict<UIMenuController>(sender);
  PasswordDetailsMenuItem* menuItem =
      base::apple::ObjCCastStrict<PasswordDetailsMenuItem>(
          menu.menuItems.firstObject);

  [self copyPasswordDetailsHelper:menuItem.itemType];
}

#endif

// A helper function that copies the password information to system pasteboard
// and shows a toast of success/failure.
- (void)copyPasswordDetailsHelper:(NSInteger)itemType {
  NSString* message = nil;

  switch (itemType) {
    case PasswordDetailsItemTypeWebsite: {
      CredentialDetails* detailsToCopy;
      detailsToCopy =
          self.credentials[self.tableView.indexPathForSelectedRow.section];
      message =
          l10n_util::GetNSString(IDS_IOS_SETTINGS_SITES_WERE_COPIED_MESSAGE);
      // Copy websites to pasteboard separated by a whitespace.
      NSArray<NSString*>* websites = detailsToCopy.websites;
      NSMutableString* websitesForPasteboard =
          [websites.firstObject mutableCopy];

      for (NSUInteger index = 1U; index < websites.count; index++) {
        [websitesForPasteboard appendFormat:@" %@", websites[index]];
      }
      StoreTextInPasteboard(websitesForPasteboard);
      break;
    }
    case PasswordDetailsItemTypeUsername: {
      NSString* copiedString =
          self.credentials[self.tableView.indexPathForSelectedRow.section]
              .username;

      StoreTextInPasteboard(copiedString);
      message =
          l10n_util::GetNSString(IDS_IOS_SETTINGS_USERNAME_WAS_COPIED_MESSAGE);
      break;
    }
    case PasswordDetailsItemTypeFederation: {
      NSString* copiedString =
          self.credentials[self.tableView.indexPathForSelectedRow.section]
              .federation;
      StoreTextInPasteboard(copiedString);
      return;
    }
    case PasswordDetailsItemTypePassword: {
      [self showPasswordFor:PasswordAccessReasonCopy];
      return;
    }
  }

  if (message.length) {
    [self showToast:message forSuccess:YES];
  }
}

- (void)didTapDismissWarningButtonAtPasswordIndex:(NSUInteger)passwordIndex {
  CHECK(passwordIndex >= 0 && passwordIndex < self.credentials.count);
  CHECK(self.delegate);

  password_manager::LogMuteCompromisedWarning();

  [self.delegate dismissWarningForPassword:self.credentials[passwordIndex]];
}

- (void)didTapRestoreWarningButtonAtPasswordIndex:(NSUInteger)passwordIndex {
  CHECK(passwordIndex >= 0 && passwordIndex < self.credentials.count);
  CHECK(self.delegate);

  password_manager::LogUnmuteCompromisedWarning();

  [self.delegate restoreWarningForCurrentPassword];
}

- (void)didTapDeleteButton:(UITableViewCell*)cell
           atPasswordIndex:(NSUInteger)passwordIndex {
  CHECK(passwordIndex >= 0 && passwordIndex < self.credentials.count);
  CHECK(self.handler);
  [self.handler showCredentialDeleteDialogWithCredentialDetails:
                    self.credentials[passwordIndex]
                                                     anchorView:cell];
}

- (void)dismissView {
  [self.view endEditing:YES];
  [self.handler dismissPasswordDetailsTableViewController];
}

#if !defined(__IPHONE_16_0) || __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_16_0
#pragma mark - UIResponder

- (BOOL)canBecomeFirstResponder {
  return YES;
}

- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
  if (action == @selector(copyPasswordDetails:)) {
    return YES;
  }
  return NO;
}
#endif

#pragma mark - Metrics

- (void)logPasswordAccessWith:(PasswordAccessReason)reason {
  switch (reason) {
    case PasswordAccessReasonShow:
      UMA_HISTOGRAM_ENUMERATION(
          "PasswordManager.AccessPasswordInSettings",
          password_manager::metrics_util::ACCESS_PASSWORD_VIEWED,
          password_manager::metrics_util::ACCESS_PASSWORD_COUNT);
      break;
    case PasswordAccessReasonCopy:
      UMA_HISTOGRAM_ENUMERATION(
          "PasswordManager.AccessPasswordInSettings",
          password_manager::metrics_util::ACCESS_PASSWORD_COPIED,
          password_manager::metrics_util::ACCESS_PASSWORD_COUNT);
      break;
    case PasswordAccessReasonEdit:
      UMA_HISTOGRAM_ENUMERATION(
          "PasswordManager.AccessPasswordInSettings",
          password_manager::metrics_util::ACCESS_PASSWORD_EDITED,
          password_manager::metrics_util::ACCESS_PASSWORD_COUNT);
      break;
  }
}

- (void)logChangeBetweenOldNote:(NSString*)oldNote
                    currentNote:(NSString*)currentNote {
  PasswordNoteAction action;
  if (oldNote == currentNote) {
    action = PasswordNoteAction::kNoteNotChanged;
  } else if (oldNote.length != 0 && currentNote.length != 0) {
    action = PasswordNoteAction::kNoteEditedInEditDialog;
  } else if (oldNote.length == 0) {
    action = PasswordNoteAction::kNoteAddedInEditDialog;
  } else {
    action = PasswordNoteAction::kNoteRemovedInEditDialog;
  }
  LogPasswordNoteActionInSettings(action);
}

#pragma mark - Public

- (void)passwordEditingConfirmed {
  DCHECK(self.credentials.count == self.passwordDetailsInfoItems.count);
  for (NSUInteger i = 0; i < self.passwordDetailsInfoItems.count; i++) {
    CredentialDetails* credential = self.credentials[i];
    NSString* oldUsername = credential.username;
    NSString* oldUserDisplayName = credential.userDisplayName;
    NSString* oldPassword = credential.password;
    NSString* oldNote = credential.note;

    PasswordDetailsInfoItem* passwordDetailsInfoItem =
        self.passwordDetailsInfoItems[i];

    credential.username =
        passwordDetailsInfoItem.usernameTextItem.textFieldValue;
    credential.userDisplayName =
        passwordDetailsInfoItem.userDisplayNameTextItem.textFieldValue;
    credential.password =
        passwordDetailsInfoItem.passwordTextItem.textFieldValue;
    credential.note = passwordDetailsInfoItem.passwordNoteItem.text;

    [self logChangeBetweenOldNote:oldNote currentNote:credential.note];
    [self.delegate passwordDetailsViewController:self
                        didEditCredentialDetails:credential
                                 withOldUsername:oldUsername
                              oldUserDisplayName:oldUserDisplayName
                                     oldPassword:oldPassword
                                         oldNote:oldNote];

    if (credential.credentialType != CredentialTypePasskey &&
        (oldUsername != credential.username ||
         oldPassword != credential.password)) {
      DetailsContext detailsContext = credential.context;
      // When details was opened from the Password Manager, only log password
      // check actions if the password is compromised.
      if (password_manager::ShouldRecordPasswordCheckUserAction(
              detailsContext, credential.compromised)) {
        password_manager::LogEditPassword(
            GetWarningTypeForDetailsContext(detailsContext));
      }
    }
  }
  [self.delegate didFinishEditingPasswordDetails];

  // Share button is hidden during editing, make it visible again.
  _shareButton.hidden = NO;

  [super editButtonPressed];
  [self reloadData];
}

- (void)showEditViewWithoutAuthentication {
  self.showPasswordWithoutAuth = YES;
  [self editButtonPressed];
}

- (void)setupLeftCancelButton {
  UIBarButtonItem* cancelButton = [[UIBarButtonItem alloc]
      initWithBarButtonSystemItem:UIBarButtonSystemItemCancel
                           target:self
                           action:@selector(dismissView)];
  self.backButtonItem = cancelButton;
  self.navigationItem.leftBarButtonItem = self.backButtonItem;
}

// TODO(crbug.com/322967526): Add timer to display the spinner for min amount of
// time and possibly a completion to only proceed with sharing views after share
// button is back.
- (void)showShareButton {
  self.navigationItem.rightBarButtonItems =
      @[ self.navigationItem.rightBarButtonItem, _shareButton ];
}

@end