<template>
  <div>
    <ElFormItem
      ref="passwordField"
      class="password-field"
      :label="label"
      :rules="{ validator: passwordValidator, trigger: 'blur', passwordValue }"
      data-test="new password form item"
    >
      <ElInput
        v-model="passwordValue"
        data-test="edit new password"
        :type="showPassword ? 'text' : 'password'"
        placeholder="Enter password"
        @input="validateConfirmPassword"
        @blur="validatePassword"
      >
        <TfButton
          slot="append"
          icon="eye"
          type="primary"
          size="icon-no-fill"
          @click="showPassword = !showPassword"
        />
      </ElInput>
    </ElFormItem>
    <ul data-test="password rules">
      <li
        v-for="(req, index) in requirements"
        :key="index"
        :class="{ 'valid': req.isValid(passwordValue), 'invalid': isInvalidRequirement(req) }"
        :data-test="`password rule ${index}`"
      >
        <slot name="icon">
          <icon name="check" />
        </slot>
        {{ req.label }}
        <ul
          v-if="req.rules"
          class="rules"
          data-test="password subrules"
        >
          <li
            v-for="(rule, ruleIndex) in req.rules"
            :key="ruleIndex"
            :class="{ 'valid': rule.isValid(passwordValue) }"
            :data-test="`password subrule ${ruleIndex}`"
          >
            <slot name="icon">
              <icon name="check" />
            </slot>
            {{ rule.label }}
          </li>
        </ul>
      </li>
    </ul>
    <ElFormItem
      ref="confirmPasswordField"
      label="Confirm password"
      prop="confirmValue"
      :rules="{ validator: confirmPasswordValidator, trigger: 'blur', confirmValue }"
      data-test="confirm password form item"
    >
      <ElInput
        v-model="confirmValue"
        data-test="edit confirm password"
        placeholder="Retype password"
        :type="showConfirmPassword ? 'text' : 'password'"
        @input="clearValidate"
      >
        <TfButton
          slot="append"
          icon="eye"
          type="primary"
          size="icon-no-fill"
          @click="showConfirmPassword = !showConfirmPassword"
        />
      </ElInput>
    </ElFormItem>
  </div>
</template>

<script>
  /**
   * Password field with validations and confirm password field
   * Acts like a standard ElForm component so must be supplied a v-model
   *
   * example usage:
   * ```
   * <ElForm>
   *   <TfPasswordField v-model="passwordProp"/>
   * </ElForm>
   * ```
   *
   * @vuedoc
   * @exports Forms/PasswordField
   * @category Components
   */
  export default {
    name: 'TfPasswordField',
    props: {
      value: {
        type: String,
        required: true,
      },
      passwordRequirementsError: {
        type: String,
        default: 'Password does not meet requirements.',
      },
      confirmPasswordError: {
        type: String,
        default: 'Passwords must match.',
      },
      label: {
        type: String,
        default: 'Create your password',
      },
      requirements: {
        type: Array,
        validator: (value) => value.every((v) => {
          const validRules = v.rules
            ? v.rules.every((rule) => rule.label && rule.isValid && typeof rule.isValid === 'function')
            : true;
          return v.label
            && v.isValid && typeof v.isValid === 'function'
            && validRules;
        }),
        default: () => [
          {
            label: 'Password must be at least 10 characters',
            /**
             * Password length rule
             *
             * @param {string} value
             * @returns {boolean}
             */
            isValid(value) { return value ? value.length > 9 : false; },
          },
          {
            label: 'Password must contain 3 of the following:',
            /**
             * Password complexity rules
             *
             * @param {string} value
             * @returns {boolean}
             */
            isValid(value) {
              return this.rules.filter(({ isValid }) => isValid(value)).length >= 3;
            },
            rules: [
              {
                label: 'at least 1 uppercase character (A-Z)',
                /**
                 * Password complexity - uppercase letter
                 *
                 * @param {string} value
                 * @returns {boolean}
                 */
                isValid(value) { return /[A-Z]/.test(value); },
              },
              {
                label: 'at least 1 lowercase character (a-z)',
                /**
                 * Password complexity - lowercase letter
                 *
                 * @param {string} value
                 * @returns {boolean}
                 */
                isValid(value) { return /[a-z]/.test(value); },
              },
              {
                label: 'at least 1 digit (0-9)',
                /**
                 * Password complexity - digit
                 *
                 * @param {string} value
                 * @returns {boolean}
                 */
                isValid(value) { return /[0-9]/.test(value); },
              },
              {
                label: 'at least 1 special character ($)',
                /**
                 * Password complexity - special characters
                 *
                 * @param {string} value
                 * @returns {boolean}
                 */
                isValid(value) { return /[!@#$%^&*)(+=._-]/.test(value); },
              },
            ],
          },
        ],
      },
      /* Adding this prop for completeness' sake (we are passing it as
       a :sync prop on the component when it is used); the value is not
       consumed by the component, but this way the fact that it IS to
       be passed as a :sync prop is documented. */
      isValidPassword: {
        type: Boolean,
        default: false,
      },
    },
    data: () => ({
      showPassword: false,
      showConfirmPassword: false,
      isDirty: false,
      confirmValue: '',
    }),
    computed: {
      /**
       * Is the password valid:
       * - Do all the requirements pass
       * - Do the password and confirm passwords match
       *
       * @returns {boolean}
       */
      isValid() {
        return this.requirements.every(
          (req) => req.isValid(this.passwordValue),
        ) && this.passwordValue === this.confirmValue;
      },
      /**
       * Getter/setter for password value
       *
       * @returns {string}
       */
      passwordValue: {
        get() {
          return this.value;
        },
        set(value) {
          this.$emit('input', value);
        },
      },
    },
    watch: {
      /**
       * Emit password value to the parent when the validity changes
       *
       * @param {object} newValue
       */
      isValid(newValue) {
        this.$emit('update:isValidPassword', newValue);
      },
    },
    methods: {
      /**
       * Show error state on password field if it doesn't meet the requirements
       *
       * @param {object} rule
       * @param {string} value
       * @param {Function} callback
       */
      passwordValidator(rule, value, callback) {
        const isValid = this.requirements.every(
          (req) => req.isValid(this.passwordValue),
        );

        if (!isValid) {
          callback(new Error(this.passwordRequirementsError));
        } else {
          callback();
        }
      },
      /**
       * Validation rule for confirming the passwords match
       *
       * @param {object} rule
       * @param {string} value
       * @param {Function} callback
       */
      confirmPasswordValidator(rule, value, callback) {
        if (rule.confirmValue && rule.confirmValue !== this.passwordValue) {
          callback(new Error(this.confirmPasswordError));
        } else {
          callback();
        }
      },

      /**
       * Run the confirm password matches validator
       */
      validatePassword() {
        this.$refs.passwordField.clearValidate();
        this.$refs.passwordField.validate('blur');
      },

      /**
       * Run the confirm password matches validator
       * Use $nextTick to avoid a race condition where the passwordValue isn't updated before the validator runs.
       */
      validateConfirmPassword() {
        this.$refs.passwordField.clearValidate();
        this.$nextTick(() => this.$refs.confirmPasswordField.validate('blur'));
      },

      /**
       * Mark fields as invalid if:
       * 1. the password field has a value
       * 2. the user has tabbed/clicked away from the field
       * 3. the requirements have not been met
       *
       * @param {object} requirement
       * @returns {boolean}
       */
      isInvalidRequirement(requirement) {
        return this.isDirty
          && !requirement.isValid(this.passwordValue);
      },

      /**
       * Clear validation state
       */
      clearValidate() {
        this.$refs.confirmPasswordField.clearValidate();
      },
    },
  };
</script>

<style lang="scss" scoped>
  @import 'src/assets/styles/variables';

  .tf-button--icon-no-fill.icon-eye {
    padding: 2px 0;
  }

  .el-form-item {
    margin-left: 0;

    &.password-field {
      margin-bottom: 24px;
    }
  }

  .invalid {
    color: $tf-red;
  }

  li, .rules li {
    margin: 10px 0;
    color: $tf-light-gray;
    font-weight: 600;
  }

  .rules li {
    padding-left: 20px;
  }

  .fa-icon {
    height: 12px;
    width: auto;
  }

  .valid, .rules li.valid {
    color: $tf-green;
  }
</style>
