/* Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/publicdomain/zero/1.0/ */

"use strict";

add_task(async function () {
  // Prevent the URL Bar to steal the focus.
  const preventUrlBarFocus = e => {
    e.preventDefault();
  };
  window.gURLBar.addEventListener("beforefocus", preventUrlBarFocus);
  registerCleanupFunction(() => {
    window.gURLBar.removeEventListener("beforefocus", preventUrlBarFocus);
  });

  const AutocompletePopup = require("resource://devtools/client/shared/autocomplete-popup.js");

  info("Create an autocompletion popup and an input that will be bound to it");
  const { doc } = await createHost();

  const input = doc.createElement("input");
  doc.body.append(input, doc.createElement("input"));

  const onSelectCalled = [];
  const onClickCalled = [];
  const popup = new AutocompletePopup(doc, {
    input,
    position: "top",
    autoSelect: true,
    onSelect: item => onSelectCalled.push(item),
    onClick: (e, item) => onClickCalled.push(item),
  });

  input.focus();
  ok(hasFocus(input), "input has focus");

  info(
    "Check that Tab moves the focus out of the input when the popup isn't opened"
  );
  EventUtils.synthesizeKey("KEY_Tab");
  is(onClickCalled.length, 0, "onClick wasn't called");
  is(hasFocus(input), false, "input does not have the focus anymore");
  info("Set the focus back to the input and open the popup");
  input.focus();
  await new Promise(res => setTimeout(res, 0));
  ok(hasFocus(input), "input is focused");

  await populateAndOpenPopup(popup);

  const checkSelectedItem = (expected, info) =>
    checkPopupSelectedItem(popup, input, expected, info);

  checkSelectedItem(popupItems[0], "First item from top is selected");
  is(
    onSelectCalled[0].label,
    popupItems[0].label,
    "onSelect was called with expected param"
  );

  info("Check that arrow down/up navigates into the list");
  EventUtils.synthesizeKey("KEY_ArrowDown");
  checkSelectedItem(popupItems[1], "item-1 is selected");
  is(
    onSelectCalled[1].label,
    popupItems[1].label,
    "onSelect was called with expected param"
  );

  EventUtils.synthesizeKey("KEY_ArrowDown");
  checkSelectedItem(popupItems[2], "item-2 is selected");
  is(
    onSelectCalled[2].label,
    popupItems[2].label,
    "onSelect was called with expected param"
  );

  EventUtils.synthesizeKey("KEY_ArrowDown");
  checkSelectedItem(popupItems[0], "item-0 is selected");
  is(
    onSelectCalled[3].label,
    popupItems[0].label,
    "onSelect was called with expected param"
  );

  EventUtils.synthesizeKey("KEY_ArrowUp");
  checkSelectedItem(popupItems[2], "item-2 is selected");
  is(
    onSelectCalled[4].label,
    popupItems[2].label,
    "onSelect was called with expected param"
  );

  EventUtils.synthesizeKey("KEY_ArrowUp");
  checkSelectedItem(popupItems[1], "item-2 is selected");
  is(
    onSelectCalled[5].label,
    popupItems[1].label,
    "onSelect was called with expected param"
  );

  info("Check that Escape closes the popup");
  let onPopupClosed = popup.once("popup-closed");
  EventUtils.synthesizeKey("KEY_Escape");
  await onPopupClosed;
  ok(true, "popup was closed with Escape key");
  ok(hasFocus(input), "input still has the focus");
  is(onClickCalled.length, 0, "onClick wasn't called");

  info("Fill the input");
  const value = "item";
  EventUtils.sendString(value);
  is(input.value, value, "input has the expected value");
  is(
    input.selectionStart,
    value.length,
    "input cursor is at expected position"
  );
  info("Open the popup again");
  await populateAndOpenPopup(popup);

  info("Check that Arrow Left + Shift does not close the popup");
  const timeoutRes = "TIMED_OUT";
  const onRaceEnded = Promise.race([
    // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
    await new Promise(res => setTimeout(() => res(timeoutRes), 500)),
    popup.once("popup-closed"),
  ]);
  EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true });
  const raceResult = await onRaceEnded;
  is(raceResult, timeoutRes, "popup wasn't closed");
  ok(popup.isOpen, "popup is still open");
  is(input.selectionEnd - input.selectionStart, 1, "text was selected");
  ok(hasFocus(input), "input still has the focus");

  info("Check that Arrow Left closes the popup");
  onPopupClosed = popup.once("popup-closed");
  EventUtils.synthesizeKey("KEY_ArrowLeft");
  await onPopupClosed;
  is(
    input.selectionStart,
    value.length - 1,
    "input cursor was moved one char back"
  );
  is(input.selectionEnd, input.selectionStart, "selection was removed");
  is(onClickCalled.length, 0, "onClick wasn't called");
  ok(hasFocus(input), "input still has the focus");

  info("Open the popup again");
  await populateAndOpenPopup(popup);

  info("Check that Arrow Right + Shift does not trigger onClick");
  EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true });
  is(onClickCalled.length, 0, "onClick wasn't called");
  is(input.selectionEnd - input.selectionStart, 1, "input text was selected");
  ok(hasFocus(input), "input still has the focus");

  info("Check that Arrow Right triggers onClick");
  EventUtils.synthesizeKey("KEY_ArrowRight");
  is(onClickCalled.length, 1, "onClick was called");
  is(
    onClickCalled[0],
    popupItems[0],
    "onClick was called with the selected item"
  );
  ok(hasFocus(input), "input still has the focus");

  info("Check that Enter triggers onClick");
  EventUtils.synthesizeKey("KEY_Enter");
  is(onClickCalled.length, 2, "onClick was called");
  is(
    onClickCalled[1],
    popupItems[0],
    "onClick was called with the selected item"
  );
  ok(hasFocus(input), "input still has the focus");

  info("Check that Tab triggers onClick");
  EventUtils.synthesizeKey("KEY_Tab");
  is(onClickCalled.length, 3, "onClick was called");
  is(
    onClickCalled[2],
    popupItems[0],
    "onClick was called with the selected item"
  );
  ok(hasFocus(input), "input still has the focus");

  info(
    "Check that Shift+Tab does not trigger onClick and move the focus out of the input"
  );
  EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
  is(onClickCalled.length, 3, "onClick wasn't called");
  is(hasFocus(input), false, "input does not have the focus anymore");

  const onPopupClose = popup.once("popup-closed");
  popup.hidePopup();
  await onPopupClose;
});

const popupItems = [
  { label: "item-0", value: "value-0" },
  { label: "item-1", value: "value-1" },
  { label: "item-2", value: "value-2" },
];

async function populateAndOpenPopup(popup) {
  popup.setItems(popupItems);
  await popup.openPopup();
}

/**
 * Returns true if the give node is currently focused.
 */
function hasFocus(node) {
  return (
    node.ownerDocument.activeElement == node && node.ownerDocument.hasFocus()
  );
}

/**
 * Check that the selected item in the popup is the expected one. Also check that the
 * active descendant is properly set and that the popup has the focus.
 *
 * @param {AutocompletePopup} popup
 * @param {HTMLInput} input
 * @param {Object} expectedSelectedItem
 * @param {String} info
 */
function checkPopupSelectedItem(popup, input, expectedSelectedItem, info) {
  is(popup.selectedItem.label, expectedSelectedItem.label, info);
  checkActiveDescendant(popup, input);
  ok(hasFocus(input), "input still has the focus");
}

function checkActiveDescendant(popup, input) {
  const activeElement = input.ownerDocument.activeElement;
  const descendantId = activeElement.getAttribute("aria-activedescendant");
  const popupItem = popup._tooltip.panel.querySelector(`#${descendantId}`);
  const cloneItem = input.ownerDocument.querySelector(`#${descendantId}`);

  ok(popupItem, "Active descendant is found in the popup list");
  ok(cloneItem, "Active descendant is found in the list clone");
  is(
    stripNS(popupItem.outerHTML),
    cloneItem.outerHTML,
    "Cloned item has the same HTML as the original element"
  );
}

function stripNS(text) {
  return text.replace(RegExp(' xmlns="http://www.w3.org/1999/xhtml"', "g"), "");
}
