/**
 * Test for LoginFormState._getFormFields.
 */

"use strict";

const { LoginFormFactory } = ChromeUtils.importESModule(
  "resource://gre/modules/LoginFormFactory.sys.mjs"
);

const { LoginManagerChild } = ChromeUtils.importESModule(
  "resource://gre/modules/LoginManagerChild.sys.mjs"
);

const TESTENVIRONMENTS = {
  filledPW1WithGeneratedPassword: {
    generatedPWFieldSelectors: ["#pw1"],
  },
};

const TESTCASES = [
  {
    description: "1 password field outside of a <form>",
    document: `<input id="pw1" type=password>`,
    returnedFieldIDs: {
      usernameField: null,
      newPasswordField: "pw1",
      oldPasswordField: null,
    },
    skipEmptyFields: undefined,
    extraTestEnvironments: [TESTENVIRONMENTS.filledPW1WithGeneratedPassword],
    extraTestPreferences: [],
  },
  {
    description: "1 text field outside of a <form> without a password field",
    document: `<input id="un1">`,
    returnedFieldIDs: {
      usernameField: null,
      newPasswordField: null,
      oldPasswordField: null,
    },
    skipEmptyFields: undefined,
    // there is no password field to fill, so no sense testing with gen. passwords
    extraTestEnvironments: [],
    extraTestPreferences: [],
  },
  {
    description: "1 username & password field outside of a <form>",
    document: `<input id="un1">
      <input id="pw1" type=password>`,
    returnedFieldIDs: {
      usernameField: "un1",
      newPasswordField: "pw1",
      oldPasswordField: null,
    },
    skipEmptyFields: undefined,
    extraTestEnvironments: [TESTENVIRONMENTS.filledPW1WithGeneratedPassword],
    extraTestPreferences: [],
  },
  {
    beforeGetFunction(doc, formLike) {
      // Access the formLike.elements lazy getter to have it cached.
      Assert.equal(
        formLike.elements.length,
        2,
        "Check initial elements length"
      );
      doc.getElementById("un1").remove();
    },
    description: "1 username & password field outside of a <form>, un1 removed",
    document: `<input id="un1">
      <input id="pw1" type=password>`,
    returnedFieldIDs: {
      usernameField: null,
      newPasswordField: "pw1",
      oldPasswordField: null,
    },
    skipEmptyFields: undefined,
    extraTestEnvironments: [TESTENVIRONMENTS.filledPW1WithGeneratedPassword],
    extraTestPreferences: [],
  },
  {
    description: "1 username & password field in a <form>",
    document: `<form>
      <input id="un1">
      <input id="pw1" type=password>
      </form>`,
    returnedFieldIDs: {
      usernameField: "un1",
      newPasswordField: "pw1",
      oldPasswordField: null,
    },
    skipEmptyFields: undefined,
    extraTestEnvironments: [TESTENVIRONMENTS.filledPW1WithGeneratedPassword],
    extraTestPreferences: [],
  },
  {
    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>`,
    returnedFieldIDs: {
      usernameField: null,
      newPasswordField: "pw1",
      oldPasswordField: null,
    },
    skipEmptyFields: undefined,
    extraTestEnvironments: [TESTENVIRONMENTS.filledPW1WithGeneratedPassword],
    extraTestPreferences: [],
  },
  {
    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>`,
    returnedFieldIDs: {
      usernameField: null,
      newPasswordField: null,
      oldPasswordField: null,
    },
    skipEmptyFields: undefined,
    extraTestEnvironments: [TESTENVIRONMENTS.filledPW1WithGeneratedPassword],
    extraTestPreferences: [],
  },
  {
    description:
      "4 password fields outside of a <form> (1 empty, 3 full) with skipEmpty",
    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">`,
    returnedFieldIDs: {
      usernameField: null,
      newPasswordField: null,
      oldPasswordField: null,
    },
    skipEmptyFields: true,
    // This test assumes that pw1 has not been filled, so don't test prefilling it
    extraTestEnvironments: [],
    extraTestPreferences: [],
  },
  {
    description: "Form with 1 password field",
    document: `<form><input id="pw1" type=password></form>`,
    returnedFieldIDs: {
      usernameField: null,
      newPasswordField: "pw1",
      oldPasswordField: null,
    },
    skipEmptyFields: undefined,
    extraTestEnvironments: [TESTENVIRONMENTS.filledPW1WithGeneratedPassword],
    extraTestPreferences: [],
  },
  {
    description: "Form with 2 password fields",
    document: `<form><input id="pw1" type=password><input id='pw2' type=password></form>`,
    returnedFieldIDs: {
      usernameField: null,
      newPasswordField: "pw1",
      oldPasswordField: null,
    },
    skipEmptyFields: undefined,
    extraTestEnvironments: [TESTENVIRONMENTS.filledPW1WithGeneratedPassword],
    extraTestPreferences: [],
  },
  {
    description: "1 password field in a form, 1 outside (not processed)",
    document: `<form><input id="pw1" type=password></form><input id="pw2" type=password>`,
    returnedFieldIDs: {
      usernameField: null,
      newPasswordField: "pw1",
      oldPasswordField: null,
    },
    skipEmptyFields: undefined,
    extraTestEnvironments: [TESTENVIRONMENTS.filledPW1WithGeneratedPassword],
    extraTestPreferences: [],
  },
  {
    description:
      "1 password field in a form, 1 text field outside (not processed)",
    document: `<form><input id="pw1" type=password></form><input>`,
    returnedFieldIDs: {
      usernameField: null,
      newPasswordField: "pw1",
      oldPasswordField: null,
    },
    skipEmptyFields: undefined,
    extraTestEnvironments: [TESTENVIRONMENTS.filledPW1WithGeneratedPassword],
    extraTestPreferences: [],
  },
  {
    description:
      "1 text field in a form, 1 password field outside (not processed)",
    document: `<form><input></form><input id="pw1" type=password>`,
    returnedFieldIDs: {
      usernameField: null,
      newPasswordField: null,
      oldPasswordField: null,
    },
    skipEmptyFields: undefined,
    extraTestEnvironments: [TESTENVIRONMENTS.filledPW1WithGeneratedPassword],
    extraTestPreferences: [],
  },
  {
    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>`,
    returnedFieldIDs: {
      usernameField: null,
      newPasswordField: "pw1",
      oldPasswordField: null,
    },
    skipEmptyFields: undefined,
    extraTestEnvironments: [TESTENVIRONMENTS.filledPW1WithGeneratedPassword],
    extraTestPreferences: [],
  },
  {
    description:
      "2 password fields outside of a <form> with 1 linked via @form + skipEmpty",
    document: `<input id="pw1" type=password><input id="pw2" type=password form="form1">
      <form id="form1"></form>`,
    returnedFieldIDs: {
      usernameField: null,
      newPasswordField: null,
      oldPasswordField: null,
    },
    skipEmptyFields: true,
    extraTestEnvironments: [TESTENVIRONMENTS.filledPW1WithGeneratedPassword],
    extraTestPreferences: [],
  },
  {
    description:
      "2 password fields outside of a <form> with 1 linked via @form + skipEmpty with 1 empty",
    document: `<input id="pw1" type=password value="pass1"><input id="pw2" type=password form="form1">
      <form id="form1"></form>`,
    returnedFieldIDs: {
      usernameField: null,
      newPasswordField: "pw1",
      oldPasswordField: null,
    },
    skipEmptyFields: true,
    extraTestEnvironments: [TESTENVIRONMENTS.filledPW1WithGeneratedPassword],
    extraTestPreferences: [],
  },
  {
    description:
      "3 password fields, 2nd and 3rd are filled with generated passwords",
    document: `<input id="pw1" type=password>
      <input id="pw2" type=password value="pass2">
      <input id="pw3" type=password value="pass3">`,
    returnedFieldIDs: {
      usernameField: null,
      newPasswordField: "pw2",
      confirmPasswordField: "pw3",
      oldPasswordField: "pw1",
    },
    skipEmptyFields: undefined,
    generatedPWFieldSelectors: ["#pw2", "#pw3"],
    // this test doesn't make sense to run with different filled generated password values
    extraTestEnvironments: [],
    extraTestPreferences: [],
  },
  // begin of getusername heuristic tests
  {
    description: "multiple non-username like input fields in a <form>",
    document: `<form>
      <input id="un1">
      <input id="un2">
      <input id="un3">
      <input id="pw1" type=password>
      </form>`,
    returnedFieldIDs: {
      usernameField: "un3",
      newPasswordField: "pw1",
      oldPasswordField: null,
    },
    skipEmptyFields: undefined,
    extraTestEnvironments: [TESTENVIRONMENTS.filledPW1WithGeneratedPassword],
    extraTestPreferences: [],
  },
  {
    description:
      "1 username input and multiple non-username like input in a <form>",
    document: `<form>
      <input id="un1">
      <input id="un2" autocomplete="username">
      <input id="un3">
      <input id="pw1" type=password>
      </form>`,
    returnedFieldIDs: {
      usernameField: "un2",
      newPasswordField: "pw1",
      oldPasswordField: null,
    },
    skipEmptyFields: undefined,
    extraTestEnvironments: [TESTENVIRONMENTS.filledPW1WithGeneratedPassword],
    extraTestPreferences: [],
  },
  {
    description:
      "1 email input and multiple non-username like input in a <form>",
    document: `<form>
      <input id="un1">
      <input id="un2" autocomplete="email">
      <input id="un3">
      <input id="pw1" type=password>
      </form>`,
    returnedFieldIDs: {
      usernameField: "un2",
      newPasswordField: "pw1",
      oldPasswordField: null,
    },
    skipEmptyFields: undefined,
    extraTestEnvironments: [TESTENVIRONMENTS.filledPW1WithGeneratedPassword],
    extraTestPreferences: [],
  },
  {
    description:
      "1 username & 1 email field, the email field is more close to the password",
    document: `<form>
      <input id="un1" autocomplete="username">
      <input id="un2" autocomplete="email">
      <input id="pw1" type=password>
      </form>`,
    returnedFieldIDs: {
      usernameField: "un1",
      newPasswordField: "pw1",
      oldPasswordField: null,
    },
    skipEmptyFields: undefined,
    extraTestEnvironments: [TESTENVIRONMENTS.filledPW1WithGeneratedPassword],
    extraTestPreferences: [],
  },
  {
    description:
      "1 username and 1 email field, the username field is more close to the password",
    document: `<form>
      <input id="un1" autocomplete="email">
      <input id="un2" autocomplete="username">
      <input id="pw1" type=password>
      </form>`,
    returnedFieldIDs: {
      usernameField: "un2",
      newPasswordField: "pw1",
      oldPasswordField: null,
    },
    skipEmptyFields: undefined,
    extraTestEnvironments: [TESTENVIRONMENTS.filledPW1WithGeneratedPassword],
    extraTestPreferences: [],
  },
  {
    description: "2 username fields in a <form>",
    document: `<form>
      <input id="un1" autocomplete="username">
      <input id="un2" autocomplete="username">
      <input id="un3">
      <input id="pw1" type=password>
      </form>`,
    returnedFieldIDs: {
      usernameField: "un2",
      newPasswordField: "pw1",
      oldPasswordField: null,
    },
    skipEmptyFields: undefined,
    extraTestEnvironments: [TESTENVIRONMENTS.filledPW1WithGeneratedPassword],
    extraTestPreferences: [],
  },
  {
    description: "2 email fields in a <form>",
    document: `<form>
      <input id="un1" autocomplete="email">
      <input id="un2" autocomplete="email">
      <input id="un3">
      <input id="pw1" type=password>
      </form>`,
    returnedFieldIDs: {
      usernameField: "un1",
      newPasswordField: "pw1",
      oldPasswordField: null,
    },
    skipEmptyFields: undefined,
    extraTestEnvironments: [TESTENVIRONMENTS.filledPW1WithGeneratedPassword],
    extraTestPreferences: [],
  },
  {
    description: "the password field precedes the username field",
    document: `<form>
      <input id="un1">
      <input id="pw1" type=password>
      <input id="un2" autocomplete="username">
      </form>`,
    returnedFieldIDs: {
      usernameField: "un1",
      newPasswordField: "pw1",
      oldPasswordField: null,
    },
    skipEmptyFields: undefined,
    extraTestEnvironments: [TESTENVIRONMENTS.filledPW1WithGeneratedPassword],
    extraTestPreferences: [],
  },
  // end of getusername heuristic tests
  {
    description: "1 username field in a <form>",
    document: `<form>
      <input id="un1" autocomplete="username">
      </form>`,
    returnedFieldIDs: {
      usernameField: "un1",
      newPasswordField: null,
      oldPasswordField: null,
    },
    skipEmptyFields: undefined,
    extraTestEnvironments: [],
    extraTestPreferences: [],
  },
  {
    description: "1 input field in a <form>",
    document: `<form>
      <input id="un1"">
      </form>`,
    returnedFieldIDs: {
      usernameField: null,
      newPasswordField: null,
      oldPasswordField: null,
    },
    skipEmptyFields: undefined,
    extraTestEnvironments: [],
    extraTestPreferences: [],
  },
  {
    description: "1 username field in a <form> with usernameOnlyForm pref off",
    document: `<form>
      <input id="un1" autocomplete="username">
      </form>`,
    returnedFieldIDs: {
      usernameField: null,
      newPasswordField: null,
      oldPasswordField: null,
    },
    skipEmptyFields: undefined,
    extraTestEnvironments: [],
    extraTestPreferences: [["signon.usernameOnlyForm.enabled", false]],
  },
];

const TEST_ENVIRONMENT_CASES = TESTCASES.flatMap(tc => {
  let arr = [tc];
  // also run this test case with this different state
  for (let env of tc.extraTestEnvironments) {
    arr.push({
      ...tc,
      ...env,
    });
  }
  return arr;
});

function _setPrefs() {
  Services.prefs.setBoolPref("signon.usernameOnlyForm.enabled", true);
  registerCleanupFunction(() => {
    Services.prefs.clearUserPref("signon.usernameOnlyForm.enabled");
  });
}

_setPrefs();

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

  (function () {
    let testcase = tc;
    add_task(async function () {
      info("Starting testcase: " + testcase.description);

      for (let pref of testcase.extraTestPreferences) {
        Services.prefs.setBoolPref(pref[0], pref[1]);
      }

      info("Document string: " + testcase.document);
      let document = MockDocument.createTestDocument(
        "http://localhost:8080/test/",
        testcase.document
      );

      let input = document.querySelector("input");
      MockDocument.mockOwnerDocumentProperty(
        input,
        document,
        "http://localhost:8080/test/"
      );

      let formLike = LoginFormFactory.createFromField(input);

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

      let lmc = new LoginManagerChild();
      let loginFormState = lmc.stateForDocument(formLike.ownerDocument);
      loginFormState.generatedPasswordFields = _generateDocStateFromTestCase(
        testcase,
        document
      );

      let actual = loginFormState._getFormFields(
        formLike,
        testcase.skipEmptyFields,
        new Set()
      );

      [
        "usernameField",
        "newPasswordField",
        "oldPasswordField",
        "confirmPasswordField",
      ].forEach(fieldName => {
        Assert.ok(
          fieldName in actual,
          "_getFormFields return value includes " + fieldName
        );
      });
      for (let key of Object.keys(testcase.returnedFieldIDs)) {
        let expectedID = testcase.returnedFieldIDs[key];
        if (expectedID === null) {
          Assert.strictEqual(
            actual[key],
            expectedID,
            "Check returned field " + key + " is null"
          );
        } else {
          Assert.strictEqual(
            actual[key].id,
            expectedID,
            "Check returned field " + key + " ID"
          );
        }
      }

      for (let pref of tc.extraTestPreferences) {
        Services.prefs.clearUserPref(pref[0]);
      }
    });
  })();
}

function _generateDocStateFromTestCase(stateProperties, document) {
  // prepopulate the document form state LMC holds with
  // any generated password fields defined in this testcase
  let generatedPasswordFields = new Set();
  info(
    "stateProperties has generatedPWFieldSelectors: " +
      stateProperties.generatedPWFieldSelectors?.join(", ")
  );

  if (stateProperties.generatedPWFieldSelectors?.length) {
    stateProperties.generatedPWFieldSelectors.forEach(sel => {
      let field = document.querySelector(sel);
      if (field) {
        generatedPasswordFields.add(field);
      } else {
        info(`No password field: ${sel} found in this document`);
      }
    });
  }
  return generatedPasswordFields;
}
