/**
 * Test for LoginFormState._getPasswordFields using LoginFormFactory.
 */

/* globals todo_check_eq */
"use strict";

const { LoginFormFactory } = ChromeUtils.importESModule(
  "resource://gre/modules/shared/LoginFormFactory.sys.mjs"
);
const { LoginFormState } = ChromeUtils.importESModule(
  "resource://gre/modules/LoginManagerChild.sys.mjs"
);
const TESTCASES = [
  {
    description: "Empty document",
    document: ``,
    returnedFieldIDsByFormLike: [],
    minPasswordLength: undefined,
  },
  {
    description: "Non-password input with no <form> present",
    document: `<input>`,
    // Only the IDs of password fields should be in this array
    returnedFieldIDsByFormLike: [[]],
    minPasswordLength: undefined,
  },
  {
    description: "1 password field outside of a <form>",
    document: `<input id="pw1" type=password>`,
    returnedFieldIDsByFormLike: [["pw1"]],
    minPasswordLength: undefined,
  },
  {
    description: "5 empty password fields outside of a <form>",
    document: `<input id="pw1" type=password>
      <input id="pw2" type=password>
      <input id="pw3" type=password>
      <input id="pw4" type=password>
      <input id="pw5" type=password>`,
    returnedFieldIDsByFormLike: [["pw1", "pw2", "pw3", "pw4", "pw5"]],
    minPasswordLength: undefined,
  },
  {
    description: "6 empty password fields outside of a <form>",
    document: `<input id="pw1" type=password>
      <input id="pw2" type=password>
      <input id="pw3" type=password>
      <input id="pw4" type=password>
      <input id="pw5" type=password>
      <input id="pw6" type=password>`,
    returnedFieldIDsByFormLike: [[]],
    minPasswordLength: undefined,
  },
  {
    description:
      "4 password fields outside of a <form> (1 empty, 3 full) with minPasswordLength=2",
    document: `<input id="pw1" type=password>
      <input id="pw2" type=password value="pass2">
      <input id="pw3" type=password value="pass3">
      <input id="pw4" type=password value="pass4">`,
    returnedFieldIDsByFormLike: [["pw2", "pw3", "pw4"]],
    minPasswordLength: 2,
  },
  {
    description: "Form with 1 password field",
    document: `<form><input id="pw1" type=password></form>`,
    returnedFieldIDsByFormLike: [["pw1"]],
    minPasswordLength: undefined,
  },
  {
    description: "Form with 2 password fields",
    document: `<form><input id="pw1" type=password><input id='pw2' type=password></form>`,
    returnedFieldIDsByFormLike: [["pw1", "pw2"]],
    minPasswordLength: undefined,
  },
  {
    description: "1 password field in a form, 1 outside",
    document: `<form><input id="pw1" type=password></form><input id="pw2" type=password>`,
    returnedFieldIDsByFormLike: [["pw1"], ["pw2"]],
    minPasswordLength: undefined,
  },
  {
    description:
      "2 password fields outside of a <form> with 1 linked via @form",
    document: `<input id="pw1" type=password><input id="pw2" type=password form='form1'>
      <form id="form1"></form>`,
    returnedFieldIDsByFormLike: [["pw1"], ["pw2"]],
    minPasswordLength: undefined,
  },
  {
    description:
      "2 password fields outside of a <form> with 1 linked via @form + minPasswordLength",
    document: `<input id="pw1" type=password><input id="pw2" type=password form="form1">
      <form id="form1"></form>`,
    returnedFieldIDsByFormLike: [[], []],
    minPasswordLength: 2,
  },
  {
    description: "minPasswordLength should also skip white-space only fields",
    /* eslint-disable no-tabs */
    document: `<input id="pw-space" type=password value=" ">
               <input id="pw-tab" type=password value="	">
               <input id="pw-newline" type=password form="form1" value="
                                                                        ">
      <form id="form1"></form>`,
    /* eslint-enable no-tabs */
    returnedFieldIDsByFormLike: [[], []],
    minPasswordLength: 2,
  },
  {
    description: "minPasswordLength should skip too-short field values",
    document: `<form>
               <input id="pw-empty" type=password>
               <input id="pw-tooshort" type=password value="p">
               <input id="pw" type=password value="pz">
               </form>`,
    returnedFieldIDsByFormLike: [["pw"]],
    minPasswordLength: 2,
  },
  {
    description: "minPasswordLength should allow matching-length field values",
    document: `<form>
               <input id="pw-empty" type=password>
               <input id="pw-matchlen" type=password value="pz">
               <input id="pw" type=password value="pazz">
               </form>`,
    returnedFieldIDsByFormLike: [["pw-matchlen", "pw"]],
    minPasswordLength: 2,
  },
  {
    description:
      "2 password fields outside of a <form> with 1 linked via @form + minPasswordLength with 1 empty",
    document: `<input id="pw1" type=password value=" pass1 "><input id="pw2" type=password form="form1">
      <form id="form1"></form>`,
    returnedFieldIDsByFormLike: [["pw1"], []],
    minPasswordLength: 2,
    fieldOverrideRecipe: {
      // Ensure a recipe without `notPasswordSelector` doesn't cause a problem.
      hosts: ["localhost:8080"],
    },
  },
  {
    description:
      "3 password fields outside of a <form> with 1 linked via @form + minPasswordLength",
    document: `<input id="pw1" type=password value="pass1"><input id="pw2" type=password form="form1" value="pass2"><input id="pw3" type=password value="pass3">
      <form id="form1"><input id="pw4" type=password></form>`,
    returnedFieldIDsByFormLike: [["pw3"], ["pw2"]],
    minPasswordLength: 2,
    fieldOverrideRecipe: {
      hosts: ["localhost:8080"],
      notPasswordSelector: "#pw1",
    },
  },
  {
    beforeGetFunction(doc) {
      doc.getElementById("pw1").remove();
    },
    description:
      "1 password field outside of a <form> which gets removed/disconnected",
    document: `<input id="pw1" type=password>`,
    returnedFieldIDsByFormLike: [[]],
    minPasswordLength: undefined,
  },
];

for (let tc of TESTCASES) {
  info("Sanity checking the testcase: " + tc.description);

  (function () {
    let testcase = tc;
    add_task(async function () {
      info("Starting testcase: " + testcase.description);
      let document = MockDocument.createTestDocument(
        "http://localhost:8080/test/",
        testcase.document
      );

      let mapRootElementToFormLike = new Map();
      for (let input of document.querySelectorAll("input")) {
        let formLike = LoginFormFactory.createFromField(input);
        let existingFormLike = mapRootElementToFormLike.get(
          formLike.rootElement
        );
        if (!existingFormLike) {
          mapRootElementToFormLike.set(formLike.rootElement, formLike);
          continue;
        }

        // If the formLike is already present, ensure that the properties are the same.
        info(
          "Checking if the new FormLike for the same root has the same properties"
        );
        formLikeEqual(formLike, existingFormLike);
      }

      if (testcase.beforeGetFunction) {
        await testcase.beforeGetFunction(document);
      }

      Assert.strictEqual(
        mapRootElementToFormLike.size,
        testcase.returnedFieldIDsByFormLike.length,
        "Check the correct number of different formLikes were returned"
      );

      let formLikeIndex = -1;
      for (let formLikeFromInput of mapRootElementToFormLike.values()) {
        formLikeIndex++;
        let pwFields = LoginFormState._getPasswordFields(formLikeFromInput, {
          fieldOverrideRecipe: testcase.fieldOverrideRecipe,
          minPasswordLength: testcase.minPasswordLength,
        });

        if (
          ChromeUtils.getClassName(formLikeFromInput.rootElement) ===
          "HTMLFormElement"
        ) {
          let formLikeFromForm = LoginFormFactory.createFromForm(
            formLikeFromInput.rootElement
          );
          info(
            "Checking that the FormLike created for the <form> matches" +
              " the one from a password field"
          );
          formLikeEqual(formLikeFromInput, formLikeFromForm);
        }

        if (testcase.returnedFieldIDsByFormLike[formLikeIndex].length === 0) {
          Assert.strictEqual(
            pwFields,
            null,
            "If no password fields were found null should be returned"
          );
        } else {
          Assert.strictEqual(
            pwFields.length,
            testcase.returnedFieldIDsByFormLike[formLikeIndex].length,
            "Check the # of password fields for formLike #" + formLikeIndex
          );
        }

        for (
          let i = 0;
          i < testcase.returnedFieldIDsByFormLike[formLikeIndex].length;
          i++
        ) {
          let expectedID =
            testcase.returnedFieldIDsByFormLike[formLikeIndex][i];
          Assert.strictEqual(
            pwFields[i].element.id,
            expectedID,
            "Check password field " + i + " ID"
          );
        }
      }
    });
  })();
}

const EMOJI_TESTCASES = [
  {
    description:
      "Single characters composed of 2 code units should ideally fail minPasswordLength of 2",
    document: `<form>
               <input id="pw" type=password value="💩">
               </form>`,
    returnedFieldIDsByFormLike: [["pw"]],
    minPasswordLength: 2,
  },
  {
    description:
      "Single characters composed of multiple code units should ideally fail minPasswordLength of 2",
    document: `<form>
               <input id="pw" type=password value="👪">
               </form>`,
    minPasswordLength: 2,
  },
];

// Note: Bug 780449 tracks our handling of emoji and multi-code-point characters in password fields
// and the .length we should expect when a password value includes them
for (let tc of EMOJI_TESTCASES) {
  info("Sanity checking the testcase: " + tc.description);

  (function () {
    let testcase = tc;
    add_task(async function () {
      info("Starting testcase: " + testcase.description);
      let document = MockDocument.createTestDocument(
        "http://localhost:8080/test/",
        testcase.document
      );
      let input = document.querySelector("input[type='password']");
      Assert.ok(input, "Found the password field");
      let formLike = LoginFormFactory.createFromField(input);
      let pwFields = LoginFormState._getPasswordFields(formLike, {
        minPasswordLength: testcase.minPasswordLength,
      });
      info("Got password fields: " + pwFields.length);
      todo_check_eq(
        pwFields.length,
        0,
        "Check a single-character (emoji) password is excluded from the password fields collection"
      );
    });
  })();
}
