import AutofillField from "../models/autofill-field";
import AutofillPageDetails from "../models/autofill-page-details";
import { sendExtensionMessage } from "../utils";
import {
  fieldContainsKeyword,
  getSubmitButtonKeywordsSet,
  isNonLoginUsernameField,
  isAmbiguousFieldNonLogin,
  KeywordMatchMode,
} from "../utils/qualification";

import {
  InlineMenuFieldQualificationService as InlineMenuFieldQualificationServiceInterface,
  SubmitButtonKeywordsMap,
} from "./abstractions/inline-menu-field-qualifications.service";
import {
  AutoFillConstants,
  CreditCardAutoFillConstants,
  IdentityAutoFillConstants,
  SubmitChangePasswordButtonNames,
  SubmitLoginButtonNames,
} from "./autofill-constants";
import AutofillService from "./autofill.service";

export class InlineMenuFieldQualificationService implements InlineMenuFieldQualificationServiceInterface {
  private searchFieldNamesSet = new Set(AutoFillConstants.SearchFieldNames);
  private excludedAutofillFieldTypesSet = new Set(AutoFillConstants.ExcludedAutofillLoginTypes);
  private usernameFieldTypes = new Set(["text", "email", "number", "tel"]);
  private usernameAutocompleteValue = "username";
  private emailAutocompleteValue = "email";
  private webAuthnAutocompleteValue = "webauthn";
  private loginUsernameAutocompleteValues = new Set([
    this.usernameAutocompleteValue,
    this.emailAutocompleteValue,
    this.webAuthnAutocompleteValue,
  ]);
  private fieldIgnoreListString = AutoFillConstants.FieldIgnoreList.join(",");
  private currentPasswordAutocompleteValue = "current-password";
  private newPasswordAutoCompleteValue = "new-password";
  private submitButtonKeywordsMap: SubmitButtonKeywordsMap = new WeakMap();
  private newEmailFieldKeywords = new Set(AutoFillConstants.NewEmailFieldKeywords);
  private creditCardFieldKeywords = [
    ...new Set([
      ...CreditCardAutoFillConstants.CardHolderFieldNames,
      ...CreditCardAutoFillConstants.CardNumberFieldNames,
      ...CreditCardAutoFillConstants.CardExpiryFieldNames,
      ...CreditCardAutoFillConstants.ExpiryMonthFieldNames,
      ...CreditCardAutoFillConstants.ExpiryYearFieldNames,
      ...CreditCardAutoFillConstants.CVVFieldNames,
      ...CreditCardAutoFillConstants.CardBrandFieldNames,
    ]),
  ];
  private creditCardNameAutocompleteValues = new Set([
    "cc-name",
    "cc-given-name,",
    "cc-additional-name",
    "cc-family-name",
  ]);
  private creditCardExpirationDateAutocompleteValue = "cc-exp";
  private creditCardExpirationMonthAutocompleteValue = "cc-exp-month";
  private creditCardExpirationYearAutocompleteValue = "cc-exp-year";
  private creditCardCvvAutocompleteValue = "cc-csc";
  private creditCardNumberAutocompleteValue = "cc-number";
  private creditCardTypeAutocompleteValue = "cc-type";
  private creditCardAutocompleteValues = new Set([
    ...this.creditCardNameAutocompleteValues,
    this.creditCardExpirationDateAutocompleteValue,
    this.creditCardExpirationMonthAutocompleteValue,
    this.creditCardExpirationYearAutocompleteValue,
    this.creditCardNumberAutocompleteValue,
    this.creditCardCvvAutocompleteValue,
    this.creditCardTypeAutocompleteValue,
  ]);
  private identityHonorificPrefixAutocompleteValue = "honorific-prefix";
  private identityFullNameAutocompleteValue = "name";
  private identityFirstNameAutocompleteValue = "given-name";
  private identityMiddleNameAutocompleteValue = "additional-name";
  private identityLastNameAutocompleteValue = "family-name";
  private identityNameAutocompleteValues = new Set([
    this.identityFullNameAutocompleteValue,
    this.identityHonorificPrefixAutocompleteValue,
    this.identityFirstNameAutocompleteValue,
    this.identityMiddleNameAutocompleteValue,
    this.identityLastNameAutocompleteValue,
    "honorific-suffix",
    "nickname",
  ]);
  private identityCompanyAutocompleteValue = "organization";
  private identityStreetAddressAutocompleteValue = "street-address";
  private identityAddressLine1AutocompleteValue = "address-line1";
  private identityAddressLine2AutocompleteValue = "address-line2";
  private identityAddressLine3AutocompleteValue = "address-line3";
  private identityAddressCityAutocompleteValue = "address-level2";
  private identityAddressStateAutocompleteValue = "address-level1";
  private identityAddressAutoCompleteValues = new Set([
    this.identityStreetAddressAutocompleteValue,
    this.identityAddressLine1AutocompleteValue,
    this.identityAddressLine2AutocompleteValue,
    this.identityAddressLine3AutocompleteValue,
    this.identityAddressCityAutocompleteValue,
    this.identityAddressStateAutocompleteValue,
    "shipping",
    "billing",
    "address-level4",
    "address-level3",
  ]);
  private identityCountryAutocompleteValues = new Set(["country", "country-name"]);
  private identityPostalCodeAutocompleteValue = "postal-code";
  private identityPhoneAutocompleteValue = "tel";
  private identityPhoneNumberAutocompleteValues = new Set([
    this.identityPhoneAutocompleteValue,
    "tel-country-code",
    "tel-area-code",
    "tel-local",
    "tel-extension",
  ]);
  private identityAutocompleteValues = new Set([
    ...this.identityNameAutocompleteValues,
    ...this.loginUsernameAutocompleteValues,
    ...this.identityCompanyAutocompleteValue,
    ...this.identityAddressAutoCompleteValues,
    ...this.identityCountryAutocompleteValues,
    ...this.identityPhoneNumberAutocompleteValues,
    this.identityCompanyAutocompleteValue,
    this.identityPostalCodeAutocompleteValue,
  ]);
  private totpFieldAutocompleteValue = "one-time-code";
  private premiumEnabled = false;

  /**
   * Validates the provided field to indicate if the field is a new email field used for account creation/registration.
   *
   * @param field - The field to validate
   */
  private isExplicitIdentityEmailField(field: AutofillField): boolean {
    const matchFieldAttributeValues = [field.type, field.htmlName, field.htmlID, field.placeholder];
    for (let attrIndex = 0; attrIndex < matchFieldAttributeValues.length; attrIndex++) {
      const attributeValueToMatch = matchFieldAttributeValues[attrIndex];

      if (!attributeValueToMatch) {
        continue;
      }

      for (let keywordIndex = 0; keywordIndex < matchFieldAttributeValues.length; keywordIndex++) {
        if (this.newEmailFieldKeywords.has(attributeValueToMatch)) {
          return true;
        }
      }
    }

    return false;
  }

  constructor() {
    void sendExtensionMessage("getUserPremiumStatus").then((premiumStatus) => {
      this.premiumEnabled = !!premiumStatus?.result;
    });
  }

  /**
   * Validates the provided field as a field for a login form.
   *
   * @param field - The field to validate, should be a username or password field.
   * @param pageDetails - The details of the page that the field is on.
   */
  isFieldForLoginForm(field: AutofillField, pageDetails: AutofillPageDetails): boolean {
    /**
     * Totp inline menu is available only for premium users.
     */
    if (this.premiumEnabled) {
      const isTotpField = this.isTotpField(field);
      // Autofill does not fill totp inputs with a "password" `type` attribute value
      const passwordType = field.type === "password";
      if (isTotpField && !passwordType) {
        return true;
      }
    }

    const isCurrentPasswordField = this.isCurrentPasswordField(field);
    if (isCurrentPasswordField) {
      return this.isPasswordFieldForLoginForm(field, pageDetails);
    }

    const isUsernameField = this.isUsernameField(field);
    if (!isUsernameField) {
      return false;
    }

    return this.isUsernameFieldForLoginForm(field, pageDetails);
  }

  /**
   * Validates the provided field as a field for a credit card form.
   *
   * @param field - The field to validate
   * @param pageDetails - The details of the page that the field is on.
   */
  isFieldForCreditCardForm(field: AutofillField, pageDetails: AutofillPageDetails): boolean {
    // If the field contains any of the standardized autocomplete attribute values
    // for credit card fields, we should assume that the field is part of a credit card form.
    if (this.fieldContainsAutocompleteValues(field, this.creditCardAutocompleteValues)) {
      return true;
    }

    let parentForm;

    const fieldForm = field.form;

    if (fieldForm) {
      parentForm = pageDetails.forms[fieldForm];
    }

    // If the field does not have a parent form
    if (!parentForm) {
      // If a credit card number field is not present on the page or there are multiple credit
      // card number fields, this field is not part of a credit card form.
      const numberFieldsInPageDetails = pageDetails.fields.filter(this.isFieldForCardNumber);
      if (numberFieldsInPageDetails.length !== 1) {
        return false;
      }

      // If a credit card CVV field is not present on the page or there are multiple credit card
      // CVV fields, this field is not part of a credit card form.
      const cvvFieldsInPageDetails = pageDetails.fields.filter(this.isFieldForCardCvv);
      if (cvvFieldsInPageDetails.length !== 1) {
        return false;
      }

      return fieldContainsKeyword(field, this.creditCardFieldKeywords);
    }

    // If the field has a parent form, check the fields from that form exclusively
    const fieldsFromSameForm = pageDetails.fields.filter((f) => f.form === field.form);

    // If a credit card number field is not present on the page or there are multiple credit
    // card number fields, this field is not part of a credit card form.
    const numberFieldsInPageDetails = fieldsFromSameForm.filter(this.isFieldForCardNumber);
    if (numberFieldsInPageDetails.length !== 1) {
      return false;
    }

    // If a credit card CVV field is not present on the page or there are multiple credit card
    // CVV fields, this field is not part of a credit card form.
    const cvvFieldsInPageDetails = fieldsFromSameForm.filter(this.isFieldForCardCvv);
    if (cvvFieldsInPageDetails.length !== 1) {
      return false;
    }

    return fieldContainsKeyword(field, [...this.creditCardFieldKeywords]);
  }

  /** Validates the provided field as a field for an account creation form.
   *
   * @param field - The field to validate
   * @param pageDetails - The details of the page that the field is on.
   */
  isFieldForAccountCreationForm(field: AutofillField, pageDetails: AutofillPageDetails): boolean {
    if (
      this.isExcludedFieldType(field, this.excludedAutofillFieldTypesSet) ||
      this.isTotpField(field)
    ) {
      return false;
    }

    if (!this.isUsernameField(field) && !this.isPasswordField(field)) {
      return false;
    }

    let parentForm;

    const fieldForm = field.form;

    if (fieldForm) {
      parentForm = pageDetails.forms[fieldForm];
    }

    if (!parentForm) {
      // If the field does not have a parent form, but we can identify that the page contains at least
      // one new password field, we should assume that the field is part of an account creation form.
      const newPasswordFields = pageDetails.fields.filter(this.isNewPasswordField);
      if (newPasswordFields.length >= 1) {
        return true;
      }

      // If no password fields are found on the page, check for keywords that indicate the field is
      // part of an account creation form.
      return fieldContainsKeyword(field, AutoFillConstants.AccountCreationFieldKeywords);
    }

    // If the field has a parent form, check the fields from that form exclusively
    const fieldsFromSameForm = pageDetails.fields.filter((f) => f.form === field.form);
    const newPasswordFields = fieldsFromSameForm.filter(this.isNewPasswordField);
    if (newPasswordFields.length >= 1) {
      return true;
    }

    return fieldContainsKeyword(field, AutoFillConstants.AccountCreationFieldKeywords);
  }

  /**
   * Validates the provided field as a field for an identity form.
   *
   * @param field - The field to validate
   * @param _pageDetails - Currently unused, will likely be required in the future
   */
  isFieldForIdentityForm(field: AutofillField, _pageDetails: AutofillPageDetails): boolean {
    if (this.isExcludedFieldType(field, this.excludedAutofillFieldTypesSet)) {
      return false;
    }

    return (
      // Recognize explicit identity email fields (like id="new-email")
      this.isFieldForIdentityEmail(field) ||
      this.fieldContainsAutocompleteValues(field, this.identityAutocompleteValues)
    );
  }

  /**
   * Validates the provided field as a password field for a login form.
   *
   * @param field - The field to validate
   * @param pageDetails - The details of the page that the field is on.
   */
  private isPasswordFieldForLoginForm(
    field: AutofillField,
    pageDetails: AutofillPageDetails,
  ): boolean {
    let parentForm;

    const fieldForm = field.form;

    if (fieldForm) {
      parentForm = pageDetails.forms[fieldForm];
    }

    // If the provided field is set with an autocomplete value of "current-password", we should assume that
    // the page developer intends for this field to be interpreted as a password field for a login form.
    if (this.fieldContainsAutocompleteValues(field, this.currentPasswordAutocompleteValue)) {
      if (!parentForm) {
        return (
          pageDetails.fields.filter(this.isNewPasswordField).filter((f) => f.viewable).length === 0
        );
      }

      return (
        pageDetails.fields
          .filter(this.isNewPasswordField)
          .filter((f) => f.viewable && f.form === field.form).length === 0
      );
    }

    const usernameFieldsInPageDetails = pageDetails.fields.filter(this.isUsernameField);
    const passwordFieldsInPageDetails = pageDetails.fields.filter(this.isCurrentPasswordField);

    // If a single username and a single password field exists on the page, we
    // should assume that this field is part of a login form.
    if (usernameFieldsInPageDetails.length === 1 && passwordFieldsInPageDetails.length === 1) {
      return true;
    }

    // If the field is not structured within a form, we need to identify if the field is present on
    // a page with multiple password fields. If that isn't the case, we can assume this is a login form field.
    if (!parentForm) {
      // If no parent form is found, and multiple password fields are present, we should assume that
      // the passed field belongs to a user account creation form.
      if (passwordFieldsInPageDetails.length > 1) {
        return false;
      }

      // If multiple username fields exist on the page, we should assume that
      // the provided field is part of an account creation form.
      const visibleUsernameFields = usernameFieldsInPageDetails.filter((f) => f.viewable);
      if (visibleUsernameFields.length > 1) {
        return false;
      }

      // If a single username field or less is present on the page, then we can assume that the
      // provided field is for a login form.
      return true;
    }

    // If the field has a form parent and there are multiple visible password fields
    // in the form, this is not a login form field
    const visiblePasswordFieldsInPageDetails = passwordFieldsInPageDetails.filter(
      (f) => f.form === field.form && f.viewable,
    );
    if (visiblePasswordFieldsInPageDetails.length > 1) {
      return false;
    }

    // If the form has any visible username fields, we should treat the field as part of a login form
    const visibleUsernameFields = usernameFieldsInPageDetails.filter(
      (f) => f.form === field.form && f.viewable,
    );
    if (visibleUsernameFields.length > 0) {
      return true;
    }

    // If the field has a form parent and a username field exists this is a password field
    return true;
  }

  /**
   * Validates the provided field as a username field for a login form.
   *
   * @param field - The field to validate
   * @param pageDetails - The details of the page that the field is on.
   */
  private isUsernameFieldForLoginForm(
    field: AutofillField,
    pageDetails: AutofillPageDetails,
  ): boolean {
    // If the provided field is set with an autocomplete of "username", we should assume that
    // the page developer intends for this field to be interpreted as a username field.

    // Exclude non-login email field from being treated as a login username field
    if (this.isExplicitIdentityEmailField(field)) {
      return false;
    }

    if (this.fieldContainsAutocompleteValues(field, this.loginUsernameAutocompleteValues)) {
      const newPasswordFieldsInPageDetails = pageDetails.fields.filter(
        (field) => field.viewable && this.isNewPasswordField(field),
      );
      return newPasswordFieldsInPageDetails.length === 0;
    }

    // If any keywords in the field's data indicates that this is a field for a "new" or "changed"
    // username, we should assume that this field is not for a login form.
    if (fieldContainsKeyword(field, AutoFillConstants.AccountCreationFieldKeywords)) {
      return false;
    }

    // If the field is not explicitly set as a username field, we need to qualify
    // the field based on the other fields that are present on the page.
    let parentForm;

    const fieldForm = field.form;

    if (fieldForm) {
      parentForm = pageDetails.forms[fieldForm];
    }
    const passwordFieldsInPageDetails = pageDetails.fields.filter(this.isCurrentPasswordField);

    if (isNonLoginUsernameField(field, pageDetails)) {
      return false;
    }

    // If the field is not structured within a form, we need to identify if the field is used in conjunction
    // with a password field. If that's the case, then we should assume that it is a form field element.
    if (!parentForm) {
      // If a formless field is present in a webpage with a single password field, we
      // should assume that it is part of a login workflow.
      const visiblePasswordFieldsInPageDetails = passwordFieldsInPageDetails.filter(
        (passwordField) => passwordField.viewable,
      );
      if (visiblePasswordFieldsInPageDetails.length === 1) {
        return true;
      }

      // If more than a single password field exists on the page, we should assume that the field
      // is part of an account creation form.
      if (visiblePasswordFieldsInPageDetails.length > 1) {
        return false;
      }

      // If no visible fields are found on the page, but we have a single password
      // field we should assume that the field is part of a login form.
      if (passwordFieldsInPageDetails.length === 1) {
        return true;
      }
      // If the page does not contain any password fields, it might be part of a multistep login form.
      // Before defaulting to login, consult the tie-breaker.
      if (isAmbiguousFieldNonLogin(field, pageDetails)) {
        return false;
      }
      return true;
    }

    // If the field is structured within a form, but no password fields are present in the form,
    // we need to consider whether the field is part of a multistep login form.
    if (passwordFieldsInPageDetails.length === 0) {
      // If the form contains a single login-eligible field (excluding non-login types like
      // checkboxes and textareas), we should assume it is part of a multistep login form.
      const loginEligibleFieldsWithinForm = pageDetails.fields.filter(
        (pageDetailsField) =>
          pageDetailsField.form === field.form &&
          pageDetailsField.type != null &&
          this.usernameFieldTypes.has(pageDetailsField.type),
      );
      if (loginEligibleFieldsWithinForm.length === 1) {
        // Before defaulting to login, consult the tie-breaker.
        if (isAmbiguousFieldNonLogin(field, pageDetails)) {
          return false;
        }
        return true;
      }

      // If multiple login-eligible fields exist within the form, we should check if a
      // single visible field exists. If so, we should assume that the field is part of a login form.
      return loginEligibleFieldsWithinForm.filter((field) => field.viewable).length === 1;
    }

    // If a single password field exists within the page details, and that password field is part of
    // the same form as the provided field, we should assume that the field is part of a login form.
    const visiblePasswordFieldsInPageDetails = passwordFieldsInPageDetails.filter(
      (passwordField) => passwordField.form === field.form && passwordField.viewable,
    );
    if (visiblePasswordFieldsInPageDetails.length === 1) {
      return true;
    }

    // If multiple visible password fields exist within the page details, we need to assume that the
    // provided field is part of an account creation form.
    if (visiblePasswordFieldsInPageDetails.length > 1) {
      return false;
    }

    // If we have a single password field we should assume that the field is part of a login form.
    if (passwordFieldsInPageDetails.length === 1) {
      return true;
    }

    // If no visible password fields are found, this field might be part of a multipart form.
    return true;
  }

  /**
   * Validates the provided field as a credit card name field.
   *
   * @param field - The field to validate
   */
  isFieldForCardholderName = (field: AutofillField): boolean => {
    if (this.fieldContainsAutocompleteValues(field, this.creditCardNameAutocompleteValues)) {
      return true;
    }

    return fieldContainsKeyword(
      field,
      CreditCardAutoFillConstants.CardHolderFieldNames,
      KeywordMatchMode.MatchesToken,
    );
  };

  /**
   * Validates the provided field as a credit card number field.
   *
   * @param field - The field to validate
   */
  isFieldForCardNumber = (field: AutofillField): boolean => {
    if (this.fieldContainsAutocompleteValues(field, this.creditCardNumberAutocompleteValue)) {
      return true;
    }

    return fieldContainsKeyword(
      field,
      CreditCardAutoFillConstants.CardNumberFieldNames,
      KeywordMatchMode.MatchesToken,
    );
  };

  /**
   * Validates the provided field as a credit card expiration date field.
   *
   * @param field - The field to validate
   */
  isFieldForCardExpirationDate = (field: AutofillField): boolean => {
    if (
      this.fieldContainsAutocompleteValues(field, this.creditCardExpirationDateAutocompleteValue)
    ) {
      return true;
    }

    return fieldContainsKeyword(
      field,
      CreditCardAutoFillConstants.CardExpiryFieldNames,
      KeywordMatchMode.MatchesToken,
    );
  };

  /**
   * Validates the provided field as a credit card expiration month field.
   *
   * @param field - The field to validate
   */
  isFieldForCardExpirationMonth = (field: AutofillField): boolean => {
    if (
      this.fieldContainsAutocompleteValues(field, this.creditCardExpirationMonthAutocompleteValue)
    ) {
      return true;
    }

    return fieldContainsKeyword(
      field,
      CreditCardAutoFillConstants.ExpiryMonthFieldNames,
      KeywordMatchMode.MatchesToken,
    );
  };

  /**
   * Validates the provided field as a credit card expiration year field.
   *
   * @param field - The field to validate
   */
  isFieldForCardExpirationYear = (field: AutofillField): boolean => {
    if (
      this.fieldContainsAutocompleteValues(field, this.creditCardExpirationYearAutocompleteValue)
    ) {
      return true;
    }

    return fieldContainsKeyword(
      field,
      CreditCardAutoFillConstants.ExpiryYearFieldNames,
      KeywordMatchMode.MatchesToken,
    );
  };

  /**
   * Validates the provided field as a credit card CVV field.
   *
   * @param field - The field to validate
   */
  isFieldForCardCvv = (field: AutofillField): boolean => {
    if (this.fieldContainsAutocompleteValues(field, this.creditCardCvvAutocompleteValue)) {
      return true;
    }

    return fieldContainsKeyword(
      field,
      CreditCardAutoFillConstants.CVVFieldNames,
      KeywordMatchMode.MatchesToken,
    );
  };

  /**
   * Validates the provided field as an identity title type field.
   *
   * @param field - The field to validate
   */
  isFieldForIdentityTitle = (field: AutofillField): boolean => {
    if (
      this.fieldContainsAutocompleteValues(field, this.identityHonorificPrefixAutocompleteValue)
    ) {
      return true;
    }

    return fieldContainsKeyword(
      field,
      IdentityAutoFillConstants.TitleFieldNames,
      KeywordMatchMode.MatchesToken,
    );
  };

  /**
   * Validates the provided field as an identity full name field.
   *
   * @param field
   */
  isFieldForIdentityFirstName = (field: AutofillField): boolean => {
    if (this.fieldContainsAutocompleteValues(field, this.identityFirstNameAutocompleteValue)) {
      return true;
    }

    return fieldContainsKeyword(
      field,
      IdentityAutoFillConstants.FirstnameFieldNames,
      KeywordMatchMode.MatchesToken,
    );
  };

  /**
   * Validates the provided field as an identity middle name field.
   *
   * @param field - The field to validate
   */
  isFieldForIdentityMiddleName = (field: AutofillField): boolean => {
    if (this.fieldContainsAutocompleteValues(field, this.identityMiddleNameAutocompleteValue)) {
      return true;
    }

    return fieldContainsKeyword(
      field,
      IdentityAutoFillConstants.MiddlenameFieldNames,
      KeywordMatchMode.MatchesToken,
    );
  };

  /**
   *  Validates the provided field as an identity last name field.
   *
   * @param field - The field to validate
   */
  isFieldForIdentityLastName = (field: AutofillField): boolean => {
    if (this.fieldContainsAutocompleteValues(field, this.identityLastNameAutocompleteValue)) {
      return true;
    }

    return fieldContainsKeyword(
      field,
      IdentityAutoFillConstants.LastnameFieldNames,
      KeywordMatchMode.MatchesToken,
    );
  };

  /**
   * Validates the provided field as an identity full name field.
   *
   * @param field - The field to validate
   */
  isFieldForIdentityFullName = (field: AutofillField): boolean => {
    if (this.fieldContainsAutocompleteValues(field, this.identityFullNameAutocompleteValue)) {
      return true;
    }

    return fieldContainsKeyword(
      field,
      IdentityAutoFillConstants.FullNameFieldNames,
      KeywordMatchMode.MatchesToken,
    );
  };

  /**
   * Validates the provided field as an identity address field.
   *
   * @param field - The field to validate
   */
  isFieldForIdentityAddress1 = (field: AutofillField): boolean => {
    if (this.fieldContainsAutocompleteValues(field, this.identityAddressLine1AutocompleteValue)) {
      return true;
    }

    return fieldContainsKeyword(
      field,
      [
        ...IdentityAutoFillConstants.AddressFieldNames,
        ...IdentityAutoFillConstants.Address1FieldNames,
      ],
      KeywordMatchMode.MatchesToken,
    );
  };

  /**
   * Validates the provided field as an identity address field.
   *
   * @param field - The field to validate
   */
  isFieldForIdentityAddress2 = (field: AutofillField): boolean => {
    if (this.fieldContainsAutocompleteValues(field, this.identityAddressLine2AutocompleteValue)) {
      return true;
    }

    return fieldContainsKeyword(
      field,
      IdentityAutoFillConstants.Address2FieldNames,
      KeywordMatchMode.MatchesToken,
    );
  };

  /**
   * Validates the provided field as an identity address field.
   *
   * @param field - The field to validate
   */
  isFieldForIdentityAddress3 = (field: AutofillField): boolean => {
    if (this.fieldContainsAutocompleteValues(field, this.identityAddressLine3AutocompleteValue)) {
      return true;
    }

    return fieldContainsKeyword(
      field,
      IdentityAutoFillConstants.Address3FieldNames,
      KeywordMatchMode.MatchesToken,
    );
  };

  /**
   * Validates the provided field as an identity city field.
   *
   * @param field - The field to validate
   */
  isFieldForIdentityCity = (field: AutofillField): boolean => {
    if (this.fieldContainsAutocompleteValues(field, this.identityAddressCityAutocompleteValue)) {
      return true;
    }

    return fieldContainsKeyword(
      field,
      IdentityAutoFillConstants.CityFieldNames,
      KeywordMatchMode.MatchesToken,
    );
  };

  /**
   * Validates the provided field as an identity state field.
   *
   * @param field - The field to validate
   */
  isFieldForIdentityState = (field: AutofillField): boolean => {
    if (this.fieldContainsAutocompleteValues(field, this.identityAddressStateAutocompleteValue)) {
      return true;
    }

    return fieldContainsKeyword(
      field,
      IdentityAutoFillConstants.StateFieldNames,
      KeywordMatchMode.MatchesToken,
    );
  };

  /**
   * Validates the provided field as an identity postal code field.
   *
   * @param field - The field to validate
   */
  isFieldForIdentityPostalCode = (field: AutofillField): boolean => {
    if (this.fieldContainsAutocompleteValues(field, this.identityPostalCodeAutocompleteValue)) {
      return true;
    }

    return fieldContainsKeyword(
      field,
      IdentityAutoFillConstants.PostalCodeFieldNames,
      KeywordMatchMode.MatchesToken,
    );
  };

  /**
   * Validates the provided field as an identity country field.
   *
   * @param field - The field to validate
   */
  isFieldForIdentityCountry = (field: AutofillField): boolean => {
    if (this.fieldContainsAutocompleteValues(field, this.identityCountryAutocompleteValues)) {
      return true;
    }

    return fieldContainsKeyword(
      field,
      IdentityAutoFillConstants.CountryFieldNames,
      KeywordMatchMode.MatchesToken,
    );
  };

  /**
   * Validates the provided field as an identity company field.
   *
   * @param field - The field to validate
   */
  isFieldForIdentityCompany = (field: AutofillField): boolean => {
    if (this.fieldContainsAutocompleteValues(field, this.identityCompanyAutocompleteValue)) {
      return true;
    }

    return fieldContainsKeyword(
      field,
      IdentityAutoFillConstants.CompanyFieldNames,
      KeywordMatchMode.MatchesToken,
    );
  };

  /**
   * Validates the provided field as an identity phone field.
   *
   * @param field - The field to validate
   */
  isFieldForIdentityPhone = (field: AutofillField): boolean => {
    if (this.fieldContainsAutocompleteValues(field, this.identityPhoneAutocompleteValue)) {
      return true;
    }

    return fieldContainsKeyword(
      field,
      IdentityAutoFillConstants.PhoneFieldNames,
      KeywordMatchMode.MatchesToken,
    );
  };

  /**
   * Validates the provided field as an identity email field.
   *
   * @param field - The field to validate
   */
  isFieldForIdentityEmail = (field: AutofillField): boolean => {
    if (this.isExplicitIdentityEmailField(field)) {
      return true;
    }

    if (
      this.fieldContainsAutocompleteValues(field, this.emailAutocompleteValue) ||
      field.type === "email" ||
      field.htmlName === "email"
    ) {
      return true;
    }

    return fieldContainsKeyword(
      field,
      IdentityAutoFillConstants.EmailFieldNames,
      KeywordMatchMode.MatchesToken,
    );
  };

  /**
   * Validates the provided field as an identity username field.
   *
   * @param field - The field to validate
   */
  isFieldForIdentityUsername = (field: AutofillField): boolean => {
    if (this.fieldContainsAutocompleteValues(field, this.usernameAutocompleteValue)) {
      return true;
    }

    return fieldContainsKeyword(
      field,
      IdentityAutoFillConstants.UserNameFieldNames,
      KeywordMatchMode.MatchesToken,
    );
  };

  /**
   * Validates the provided field as a username field.
   *
   * @param field - The field to validate
   */
  isUsernameField = (field: AutofillField): boolean => {
    const fieldType = field.type;
    if (
      !fieldType ||
      !this.usernameFieldTypes.has(fieldType) ||
      this.isExcludedFieldType(field, this.excludedAutofillFieldTypesSet) ||
      this.fieldHasDisqualifyingAttributeValue(field) ||
      this.isTotpField(field)
    ) {
      return false;
    }

    return fieldContainsKeyword(field, AutoFillConstants.UsernameFieldNames);
  };

  /**
   * Validates the provided field as an email field.
   *
   * @param field - The field to validate
   */
  isEmailField = (field: AutofillField): boolean => {
    if (this.isTotpField(field)) {
      return false;
    }

    if (field.type === "email") {
      return true;
    }

    return (
      !this.isExcludedFieldType(field, this.excludedAutofillFieldTypesSet) &&
      fieldContainsKeyword(field, AutoFillConstants.EmailFieldNames)
    );
  };

  /**
   * Validates the provided field as a current password field.
   *
   * @param field - The field to validate
   */
  isCurrentPasswordField = (field: AutofillField): boolean => {
    if (
      this.fieldContainsAutocompleteValues(field, this.newPasswordAutoCompleteValue) ||
      fieldContainsKeyword(field, AutoFillConstants.AccountCreationFieldKeywords)
    ) {
      return false;
    }

    return this.isPasswordField(field);
  };

  /**
   * Validates the provided field as a current password field for an update password form.
   *
   * @param field - The field to validate
   */
  isUpdateCurrentPasswordField = (field: AutofillField): boolean => {
    if (this.fieldContainsAutocompleteValues(field, this.newPasswordAutoCompleteValue)) {
      return false;
    }

    return (
      this.isPasswordField(field) &&
      fieldContainsKeyword(field, AutoFillConstants.UpdatePasswordFieldKeywords)
    );
  };

  /**
   * Validates the provided field as a new password field.
   *
   * @param field - The field to validate
   */
  isNewPasswordField = (field: AutofillField): boolean => {
    if (this.fieldContainsAutocompleteValues(field, this.currentPasswordAutocompleteValue)) {
      return false;
    }

    return (
      this.isPasswordField(field) &&
      fieldContainsKeyword(field, AutoFillConstants.AccountCreationFieldKeywords)
    );
  };

  /**
   * Validates the provided field as a password field.
   *
   * @param field - The field to validate
   */
  private isPasswordField = (field: AutofillField): boolean => {
    const isInputPasswordType = field.type === "password";
    if (
      (!isInputPasswordType &&
        this.isExcludedFieldType(field, this.excludedAutofillFieldTypesSet)) ||
      this.fieldHasDisqualifyingAttributeValue(field) ||
      this.isTotpField(field)
    ) {
      return false;
    }

    return isInputPasswordType || this.isLikePasswordField(field);
  };

  /**
   * Validates the provided field as a field to indicate if the
   * field potentially acts as a password field.
   *
   * @param field - The field to validate
   */
  private isLikePasswordField(field: AutofillField): boolean {
    if (field.type !== "text") {
      return false;
    }

    const testedValues = [field.htmlID, field.htmlName, field.placeholder];
    for (let i = 0; i < testedValues.length; i++) {
      const attributeValueToMatch = testedValues[i];

      if (!attributeValueToMatch) {
        continue;
      }

      if (this.valueIsLikePassword(attributeValueToMatch)) {
        return true;
      }
    }

    return false;
  }

  /**
   * Validates the provided value to indicate if the value is like a password.
   *
   * @param value - The value to validate
   */
  private valueIsLikePassword(value: string): boolean {
    if (value == null) {
      return false;
    }
    // Removes all whitespace, _ and - characters
    const cleanedValue = value.toLowerCase().replace(/[\s_-]/g, "");

    if (cleanedValue.indexOf("password") < 0) {
      return false;
    }

    return !AutoFillConstants.PasswordFieldExcludeList.some((i) => cleanedValue.indexOf(i) > -1);
  }

  /**
   * Validates whether the provided field is a TOTP field.
   *
   * @param field - The field to validate
   */
  isTotpField = (field: AutofillField): boolean => {
    if (fieldContainsKeyword(field, [...AutoFillConstants.RecoveryCodeFieldNames])) {
      return false;
    }

    if (this.fieldContainsAutocompleteValues(field, this.totpFieldAutocompleteValue)) {
      return true;
    }

    return (
      !this.isExcludedFieldType(field, this.excludedAutofillFieldTypesSet) &&
      fieldContainsKeyword(field, AutoFillConstants.TotpFieldNames)
    );
  };

  /**
   * Validates the provided field to indicate if the field has a
   * disqualifying attribute that would impede autofill entirely.
   *
   * @param field - The field to validate
   */
  private fieldHasDisqualifyingAttributeValue(field: AutofillField): boolean {
    const checkedAttributeValues = [field.htmlID, field.htmlName, field.placeholder];

    for (let i = 0; i < checkedAttributeValues.length; i++) {
      const checkedAttributeValue = checkedAttributeValues[i];
      const cleanedValue = checkedAttributeValue?.toLowerCase().replace(/[\s_-]/g, "");

      if (cleanedValue && this.fieldIgnoreListString.indexOf(cleanedValue) > -1) {
        return true;
      }
    }

    return false;
  }

  /**
   * Validates the provided field to indicate if the field is excluded from autofill.
   *
   * @param field - The field to validate
   * @param excludedTypes - The set of excluded types
   */
  private isExcludedFieldType(field: AutofillField, excludedTypes: Set<string>): boolean {
    const fieldType = field.type;

    if (fieldType && excludedTypes.has(fieldType)) {
      return true;
    }

    return this.isSearchField(field);
  }

  /**
   * Validates the provided field to indicate if the field is a search field.
   *
   * @param field - The field to validate
   */
  private isSearchField(field: AutofillField): boolean {
    const matchFieldAttributeValues = [field.type, field.htmlName, field.htmlID, field.placeholder];
    for (let attrIndex = 0; attrIndex < matchFieldAttributeValues.length; attrIndex++) {
      const attributeValueToMatch = matchFieldAttributeValues[attrIndex];

      if (!attributeValueToMatch) {
        continue;
      }

      // Separate camel case words and case them to lower case values
      const camelCaseSeparatedFieldAttribute = attributeValueToMatch
        .replace(/([a-z])([A-Z])/g, "$1 $2")
        .toLowerCase();
      // Split the attribute by non-alphabetical characters to get the keywords
      const attributeKeywords = camelCaseSeparatedFieldAttribute.split(/[^a-z]/gi);

      for (let keywordIndex = 0; keywordIndex < attributeKeywords.length; keywordIndex++) {
        if (this.searchFieldNamesSet.has(attributeKeywords[keywordIndex])) {
          return true;
        }
      }
    }

    return false;
  }

  /**
   * Validates the provided field to indicate if the field is a submit button for a login form.
   *
   * @param element - The element to validate
   */
  isElementLoginSubmitButton = (element: HTMLElement): boolean => {
    const keywordValues = this.getSubmitButtonKeywords(element);
    return SubmitLoginButtonNames.some((keyword) => keywordValues.indexOf(keyword) > -1);
  };

  /**
   * Validates the provided field to indicate if the field is a submit button for a change password form.
   *
   * @param element - The element to validate
   */
  isElementChangePasswordSubmitButton = (element: HTMLElement): boolean => {
    const keywordValues = this.getSubmitButtonKeywords(element);
    return SubmitChangePasswordButtonNames.some((keyword) => keywordValues.indexOf(keyword) > -1);
  };

  /**
   * Gather the keywords from the provided element to validate the submit button.
   *
   * @param element - The element to validate
   */
  private getSubmitButtonKeywords(element: HTMLElement): string {
    if (!this.submitButtonKeywordsMap.has(element)) {
      const keywordsSet = getSubmitButtonKeywordsSet(element);
      this.submitButtonKeywordsMap.set(element, Array.from(keywordsSet).join(","));
    }

    return this.submitButtonKeywordsMap.get(element) || "";
  }

  /**
   * Separates the provided field data into space-separated values and checks if any
   * of the values are present in the provided set of autocomplete values.
   *
   * @param autofillFieldData - The field autocomplete value to validate
   * @param compareValues - The set of autocomplete values to check against
   */
  private fieldContainsAutocompleteValues(
    autofillFieldData: AutofillField,
    compareValues: string | Set<string>,
  ) {
    const fieldAutocompleteValue = autofillFieldData.autoCompleteType;
    if (!fieldAutocompleteValue || typeof fieldAutocompleteValue !== "string") {
      return false;
    }

    if (typeof compareValues === "string") {
      return AutofillService.autoCompleteTypeIncludesToken(fieldAutocompleteValue, compareValues);
    }

    for (const token of compareValues) {
      if (AutofillService.autoCompleteTypeIncludesToken(fieldAutocompleteValue, token)) {
        return true;
      }
    }

    return false;
  }

  /**
   * Validates the provided field has `current-password` attribute
   *
   * * @param field - The field to validate
   */
  hasCurrentPasswordAutocomplete = (field: AutofillField): boolean => {
    return this.fieldContainsAutocompleteValues(field, this.currentPasswordAutocompleteValue);
  };
}
