/* import-globals-from ../../../../../testing/mochitest/tests/SimpleTest/SimpleTest.js */
/* import-globals-from ../../../../../testing/mochitest/tests/SimpleTest/EventUtils.js */
/* import-globals-from ../../../../../toolkit/components/satchel/test/satchel_common.js */
/* eslint-disable no-unused-vars */

"use strict";

let formFillChromeScript;
let defaultTextColor;
let defaultDisabledTextColor;
let expectingPopup = null;

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

async function sleep(ms = 500, reason = "Intentionally wait for UI ready") {
  SimpleTest.requestFlakyTimeout(reason);
  await new Promise(resolve => setTimeout(resolve, ms));
}

async function focusAndWaitForFieldsIdentified(
  input,
  mustBeIdentified = false
) {
  info("expecting the target input being focused and indentified");
  if (typeof input === "string") {
    input = document.querySelector(input);
  }
  const rootElement = input.form || input.ownerDocument.documentElement;
  const previouslyFocused = input != document.activeElement;

  input.focus();

  if (mustBeIdentified) {
    rootElement.removeAttribute("test-formautofill-identified");
  }
  if (rootElement.hasAttribute("test-formautofill-identified")) {
    return;
  }
  if (!previouslyFocused) {
    await new Promise(resolve => {
      formFillChromeScript.addMessageListener(
        "FormAutofillTest:FieldsIdentified",
        function onIdentified() {
          formFillChromeScript.removeMessageListener(
            "FormAutofillTest:FieldsIdentified",
            onIdentified
          );
          resolve();
        }
      );
    });
  }
  // In order to ensure that "markAsAutofillField" is fully executed, a short period
  // of timeout is still required.
  await sleep(300, "Guarantee asynchronous identifyAutofillFields is invoked");
  rootElement.setAttribute("test-formautofill-identified", "true");
}

async function setInput(selector, value, userInput = false) {
  const input = document.querySelector("input" + selector);
  if (userInput) {
    SpecialPowers.wrap(input).setUserInput(value);
  } else {
    input.value = value;
  }
  await focusAndWaitForFieldsIdentified(input);

  return input;
}

function clickOnElement(selector) {
  let element = document.querySelector(selector);

  if (!element) {
    throw new Error("Can not find the element");
  }

  SimpleTest.executeSoon(() => element.click());
}

// The equivalent helper function to getAdaptedProfiles in FormAutofillHandler.jsm that
// transforms the given profile to expected filled profile.
function _getAdaptedProfile(profile) {
  const adaptedProfile = Object.assign({}, profile);

  if (profile["street-address"]) {
    adaptedProfile["street-address"] = FormAutofillUtils.toOneLineAddress(
      profile["street-address"]
    );
  }

  return adaptedProfile;
}

async function checkFieldHighlighted(elem, expectedValue) {
  let isHighlightApplied;
  await SimpleTest.promiseWaitForCondition(function checkHighlight() {
    isHighlightApplied = elem.matches(":autofill");
    return isHighlightApplied === expectedValue;
  }, `Checking #${elem.id} highlight style`);

  is(isHighlightApplied, expectedValue, `Checking #${elem.id} highlight style`);
}

async function checkFieldPreview(elem, expectedValue) {
  is(
    SpecialPowers.wrap(elem).previewValue,
    expectedValue,
    `Checking #${elem.id} previewValue`
  );
  let isTextColorApplied;
  await SimpleTest.promiseWaitForCondition(function checkPreview() {
    const computedStyle = window.getComputedStyle(elem);
    const actualColor = computedStyle.getPropertyValue("color");
    if (elem.disabled) {
      isTextColorApplied = actualColor !== defaultDisabledTextColor;
    } else {
      isTextColorApplied = actualColor !== defaultTextColor;
    }
    return isTextColorApplied === !!expectedValue;
  }, `Checking #${elem.id} preview style`);

  is(isTextColorApplied, !!expectedValue, `Checking #${elem.id} preview style`);
}

async function checkFormFieldsStyle(profile, isPreviewing = true) {
  const elems = document.querySelectorAll("input, select");

  for (const elem of elems) {
    let fillableValue;
    let previewValue;
    let isElementEligible =
      FormAutofillUtils.isCreditCardOrAddressFieldType(elem) &&
      FormAutofillUtils.isFieldAutofillable(elem);
    if (!isElementEligible) {
      fillableValue = "";
      previewValue = "";
    } else {
      fillableValue = profile && profile[elem.id];
      previewValue = (isPreviewing && fillableValue) || "";
    }
    await checkFieldHighlighted(elem, !!fillableValue);
    await checkFieldPreview(elem, previewValue);
  }
}

function checkFieldValue(elem, expectedValue) {
  if (typeof elem === "string") {
    elem = document.querySelector(elem);
  }
  is(elem.value, String(expectedValue), "Checking " + elem.id + " field");
}

async function triggerAutofillAndCheckProfile(profile) {
  let adaptedProfile = _getAdaptedProfile(profile);
  const promises = [];
  for (const [fieldName, value] of Object.entries(adaptedProfile)) {
    info(`triggerAutofillAndCheckProfile: ${fieldName}`);
    const element = document.getElementById(fieldName);
    const expectingEvent =
      document.activeElement == element ? "input" : "change";
    const checkFieldAutofilled = Promise.all([
      new Promise(resolve => {
        let beforeInputFired = false;
        let hadEditor = SpecialPowers.wrap(element).hasEditor;
        element.addEventListener(
          "beforeinput",
          event => {
            beforeInputFired = true;
            is(
              event.inputType,
              "insertReplacementText",
              'inputType value should be "insertReplacementText"'
            );
            is(
              event.data,
              String(value),
              `data value of "beforeinput" should be "${value}"`
            );
            is(
              event.dataTransfer,
              null,
              'dataTransfer of "beforeinput" should be null'
            );
            is(
              event.getTargetRanges().length,
              0,
              'getTargetRanges() of "beforeinput" should return empty array'
            );
            is(
              event.cancelable,
              SpecialPowers.getBoolPref(
                "dom.input_event.allow_to_cancel_set_user_input"
              ),
              `"beforeinput" event should be cancelable on ${element.tagName} unless it's suppressed by the pref`
            );
            is(
              event.bubbles,
              true,
              `"beforeinput" event should always bubble on ${element.tagName}`
            );
            resolve();
          },
          { once: true }
        );
        element.addEventListener(
          "input",
          event => {
            if (element.tagName == "INPUT" && element.type == "text") {
              if (hadEditor) {
                ok(
                  beforeInputFired,
                  `"beforeinput" event should've been fired before "input" event on ${element.tagName}`
                );
              } else {
                ok(
                  beforeInputFired,
                  `"beforeinput" event should've been fired before "input" event on ${element.tagName}`
                );
              }
              ok(
                event instanceof InputEvent,
                `"input" event should be dispatched with InputEvent interface on ${element.tagName}`
              );
              is(
                event.inputType,
                "insertReplacementText",
                'inputType value should be "insertReplacementText"'
              );
              is(event.data, String(value), `data value should be "${value}"`);
              is(event.dataTransfer, null, "dataTransfer should be null");
              is(
                event.getTargetRanges().length,
                0,
                "getTargetRanges() should return empty array"
              );
            } else {
              ok(
                !beforeInputFired,
                `"beforeinput" event shouldn't be fired on ${element.tagName}`
              );
              ok(
                event instanceof Event && !(event instanceof UIEvent),
                `"input" event should be dispatched with Event interface on ${element.tagName}`
              );
            }
            is(
              event.cancelable,
              false,
              `"input" event should be never cancelable on ${element.tagName}`
            );
            is(
              event.bubbles,
              true,
              `"input" event should always bubble on ${element.tagName}`
            );
            resolve();
          },
          { once: true }
        );
      }),
      new Promise(resolve =>
        element.addEventListener(expectingEvent, resolve, { once: true })
      ),
    ]).then(() => checkFieldValue(element, value));

    promises.push(checkFieldAutofilled);
  }
  // Press Enter key and trigger form autofill.
  synthesizeKey("KEY_Enter");

  return Promise.all(promises);
}

async function onStorageChanged(type) {
  info(`expecting the storage changed: ${type}`);
  return new Promise(resolve => {
    formFillChromeScript.addMessageListener(
      "formautofill-storage-changed",
      function onChanged(data) {
        formFillChromeScript.removeMessageListener(
          "formautofill-storage-changed",
          onChanged
        );
        is(data.data, type, `Receive ${type} storage changed event`);
        resolve();
      }
    );
  });
}

function checkMenuEntries(expectedValues, isFormAutofillResult = true) {
  let actualValues = getMenuEntries();
  // Expect one more item would appear at the bottom as the footer if the result is from form autofill.
  let expectedLength = isFormAutofillResult
    ? expectedValues.length + 1
    : expectedValues.length;

  is(actualValues.length, expectedLength, " Checking length of expected menu");
  for (let i = 0; i < expectedValues.length; i++) {
    is(actualValues[i], expectedValues[i], " Checking menu entry #" + i);
  }
}

function invokeAsyncChromeTask(message, payload = {}) {
  info(`expecting the chrome task finished: ${message}`);
  return formFillChromeScript.sendQuery(message, payload);
}

async function addAddress(address) {
  await invokeAsyncChromeTask("FormAutofillTest:AddAddress", { address });
  await sleep();
}

async function removeAddress(guid) {
  return invokeAsyncChromeTask("FormAutofillTest:RemoveAddress", { guid });
}

async function updateAddress(guid, address) {
  return invokeAsyncChromeTask("FormAutofillTest:UpdateAddress", {
    address,
    guid,
  });
}

async function checkAddresses(expectedAddresses) {
  return invokeAsyncChromeTask("FormAutofillTest:CheckAddresses", {
    expectedAddresses,
  });
}

async function cleanUpAddresses() {
  return invokeAsyncChromeTask("FormAutofillTest:CleanUpAddresses");
}

async function addCreditCard(creditcard) {
  await invokeAsyncChromeTask("FormAutofillTest:AddCreditCard", { creditcard });
  await sleep();
}

async function removeCreditCard(guid) {
  return invokeAsyncChromeTask("FormAutofillTest:RemoveCreditCard", { guid });
}

async function checkCreditCards(expectedCreditCards) {
  return invokeAsyncChromeTask("FormAutofillTest:CheckCreditCards", {
    expectedCreditCards,
  });
}

async function cleanUpCreditCards() {
  return invokeAsyncChromeTask("FormAutofillTest:CleanUpCreditCards");
}

async function cleanUpStorage() {
  await cleanUpAddresses();
  await cleanUpCreditCards();
}

async function canTestOSKeyStoreLogin() {
  let { canTest } = await invokeAsyncChromeTask(
    "FormAutofillTest:CanTestOSKeyStoreLogin"
  );
  return canTest;
}

async function waitForOSKeyStoreLogin(login = false) {
  await invokeAsyncChromeTask("FormAutofillTest:OSKeyStoreLogin", { login });
}

function patchRecordCCNumber(record) {
  const number = record["cc-number"];
  const ccNumberFmt = {
    affix: "****",
    label: number.substr(-4),
  };

  return Object.assign({}, record, { ccNumberFmt });
}

// Utils for registerPopupShownListener(in satchel_common.js) that handles dropdown popup
// Please call "initPopupListener()" in your test and "await expectPopup()"
// if you want to wait for dropdown menu displayed.
function expectPopup() {
  info("expecting a popup");
  return new Promise(resolve => {
    expectingPopup = resolve;
  });
}

function notExpectPopup(ms = 500) {
  info("not expecting a popup");
  return new Promise((resolve, reject) => {
    expectingPopup = reject.bind(this, "Unexpected Popup");
    // TODO: We don't have an event to notify no popup showing, so wait for 500
    // ms (in default) to predict any unexpected popup showing.
    setTimeout(resolve, ms);
  });
}

function popupShownListener() {
  info("popup shown for test ");
  if (expectingPopup) {
    expectingPopup();
    expectingPopup = null;
  }
}

function initPopupListener() {
  registerPopupShownListener(popupShownListener);
}

async function triggerPopupAndHoverItem(fieldSelector, selectIndex) {
  await focusAndWaitForFieldsIdentified(fieldSelector);
  synthesizeKey("KEY_ArrowDown");
  await expectPopup();
  for (let i = 0; i <= selectIndex; i++) {
    synthesizeKey("KEY_ArrowDown");
  }
  await notifySelectedIndex(selectIndex);
}

function formAutoFillCommonSetup() {
  // Remove the /creditCard path segement when referenced from the 'creditCard' subdirectory.
  let chromeURL = SimpleTest.getTestFileURL(
    "formautofill_parent_utils.js"
  ).replace(/\/creditCard/, "");
  formFillChromeScript = SpecialPowers.loadChromeScript(chromeURL);
  formFillChromeScript.addMessageListener("onpopupshown", ({ results }) => {
    gLastAutoCompleteResults = results;
    if (gPopupShownListener) {
      gPopupShownListener({ results });
    }
  });

  add_setup(async () => {
    info(`expecting the storage setup`);
    await formFillChromeScript.sendQuery("setup");
  });

  SimpleTest.registerCleanupFunction(async () => {
    info(`expecting the storage cleanup`);
    await formFillChromeScript.sendQuery("cleanup");

    formFillChromeScript.destroy();
    expectingPopup = null;
  });

  document.addEventListener(
    "DOMContentLoaded",
    function () {
      defaultTextColor = window
        .getComputedStyle(document.querySelector("input"))
        .getPropertyValue("color");

      // This is needed for test_formautofill_preview_highlight.html to work properly
      let disabledInput = document.querySelector(`input[disabled]`);
      if (disabledInput) {
        defaultDisabledTextColor = window
          .getComputedStyle(disabledInput)
          .getPropertyValue("color");
      }
    },
    { once: true }
  );
}

/*
 * Extremely over-simplified detection of card type from card number just for
 * our tests. This is needed to test the aria-label of credit card menu entries.
 */
function getCCTypeName(creditCard) {
  return creditCard["cc-number"][0] == "4" ? "Visa" : "MasterCard";
}

formAutoFillCommonSetup();
