"use strict";

const { PromiseTestUtils } = ChromeUtils.importESModule(
  "resource://testing-common/PromiseTestUtils.sys.mjs"
);
PromiseTestUtils.allowMatchingRejectionsGlobally(
  /Message manager disconnected/
);

function extensionShortcutsReady(id) {
  let extension = WebExtensionPolicy.getByID(id).extension;
  return BrowserTestUtils.waitForCondition(() => {
    return extension.shortcuts.keysetsMap.has(window);
  }, "Wait for add-on keyset to be registered");
}

async function loadShortcutsView() {
  // Load the theme view initially so we can verify that the category is switched
  // to "extension" when the shortcuts view is loaded.
  let win = await loadInitialView("theme");
  let categoryUtils = new CategoryUtilities(win);

  is(
    categoryUtils.getSelectedViewId(),
    "addons://list/theme",
    "The theme category is selected"
  );

  let shortcutsLink = win.document.querySelector(
    '#page-options [action="manage-shortcuts"]'
  );
  ok(!shortcutsLink.hidden, "The shortcuts link is visible");

  let loaded = waitForViewLoad(win);
  shortcutsLink.click();
  await loaded;

  is(
    categoryUtils.getSelectedViewId(),
    "addons://list/extension",
    "The extension category is now selected"
  );

  return win;
}

add_task(async function testUpdatingCommands() {
  let commands = {
    commandZero: {},
    commandOne: {
      suggested_key: { default: "Shift+Alt+7" },
    },
    commandTwo: {
      description: "Command Two!",
      suggested_key: { default: "Alt+4" },
    },
    commandThree: {
      description: "Command Three!",
      suggested_key: { default: "Alt+F12" },
    },
    _execute_browser_action: {
      suggested_key: { default: "Shift+Alt+9" },
    },
  };
  let extension = ExtensionTestUtils.loadExtension({
    manifest: {
      commands,
      browser_action: { default_popup: "popup.html" },
    },
    background() {
      browser.commands.onCommand.addListener(commandName => {
        browser.test.sendMessage("oncommand", commandName);
      });
      browser.test.sendMessage("ready");
    },
    useAddonManager: "temporary",
  });

  await extension.startup();
  await extension.awaitMessage("ready");
  await extensionShortcutsReady(extension.id);

  async function checkShortcut(name, key, modifiers) {
    info(`Synthesize keyboard shortcut ${JSON.stringify({ key, modifiers })}`);
    EventUtils.synthesizeKey(key, modifiers);
    let message = await extension.awaitMessage("oncommand");
    is(
      message,
      name,
      `Expected onCommand listener to fire with the correct name: ${name}`
    );
  }

  // Load the about:addons shortcut view before verify that emitting
  // the key events does trigger the expected extension commands.
  // There is apparently a race (more likely to be triggered on an
  // optimized build) between:
  // - the new opened browser window to be ready to listen for the
  //   keyboard events that are expected to triggered one of the key
  //   in the extension keyset
  // - and the test calling EventUtils.syntesizeKey to test that
  //   the expected extension command listener is notified.
  //
  // Loading the shortcut view before calling checkShortcut seems to be
  // enough to consistently avoid that race condition.
  let win = await loadShortcutsView();

  // Check that the original shortcuts work.
  await checkShortcut("commandOne", "7", { shiftKey: true, altKey: true });
  await checkShortcut("commandTwo", "4", { altKey: true });
  await checkShortcut("commandThree", "VK_F12", { altKey: true });

  let doc = win.document;

  let card = doc.querySelector(`.card[addon-id="${extension.id}"]`);
  ok(card, `There is a card for the extension`);

  let inputs = card.querySelectorAll(".shortcut-input");
  is(
    inputs.length,
    Object.keys(commands).length,
    "There is an input for each command"
  );

  let nameOrder = Array.from(inputs).map(input => input.getAttribute("name"));
  Assert.deepEqual(
    nameOrder,
    [
      "commandOne",
      "commandTwo",
      "commandThree",
      "_execute_browser_action",
      "commandZero",
    ],
    "commandZero should be last since it is unset"
  );

  let count = 1;
  for (let input of inputs) {
    // Change the shortcut.
    input.focus();
    EventUtils.synthesizeKey("8", { shiftKey: true, altKey: true });
    count++;

    // Wait for the shortcut attribute to change.
    await BrowserTestUtils.waitForCondition(
      () => input.getAttribute("shortcut") == "Alt+Shift+8",
      "Wait for shortcut to update to Alt+Shift+8"
    );

    // Check that the change worked (but skip if browserAction).
    if (input.getAttribute("name") != "_execute_browser_action") {
      await checkShortcut(input.getAttribute("name"), "8", {
        shiftKey: true,
        altKey: true,
      });
    }

    // Change it again so it doesn't conflict with the next command.
    input.focus();
    EventUtils.synthesizeKey(count.toString(), {
      shiftKey: true,
      altKey: true,
    });
    await BrowserTestUtils.waitForCondition(
      () => input.getAttribute("shortcut") == `Alt+Shift+${count}`,
      `Wait for shortcut to update to Alt+Shift+${count}`
    );
  }

  // Test F1-F19 keys are all considered valid and trigger the command
  // as expected.
  const shortcutInput = Array.from(inputs)
    .filter(input => input.getAttribute("name") != "_execute_browser_action")
    .pop();
  const shortcutInputCmdName = shortcutInput.getAttribute("name");
  info(`Verify F1-F19 keys shortcuts on ${shortcutInputCmdName}`);
  for (let i = 1; i <= 19; i++) {
    const key = `F${i}`;
    const synthesizeKey = `VK_${key}`;
    shortcutInput.focus();
    EventUtils.synthesizeKey(synthesizeKey, { shiftKey: true, altKey: true });
    // Wait for the shortcut attribute to change.
    await BrowserTestUtils.waitForCondition(
      () => shortcutInput.getAttribute("shortcut") == `Alt+Shift+${key}`,
      `Wait for shortcut to update to Alt+Shift+${key}`
    );
    await checkShortcut(shortcutInputCmdName, synthesizeKey, {
      shiftKey: true,
      altKey: true,
    });
  }

  // Check that errors can be shown.
  let input = inputs[0];
  let error = doc.querySelector(".error-message");
  let label = error.querySelector(".error-message-label");
  is(error.style.visibility, "hidden", "The error is initially hidden");

  // Try a shortcut with only shift for a modifier.
  input.focus();
  EventUtils.synthesizeKey("J", { shiftKey: true });
  let possibleErrors = ["shortcuts-modifier-mac", "shortcuts-modifier-other"];
  ok(possibleErrors.includes(label.dataset.l10nId), `The message is set`);
  is(error.style.visibility, "visible", "The error is shown");

  // Escape should clear the focus and hide the error.
  is(doc.activeElement, input, "The input is focused");
  EventUtils.synthesizeKey("Escape", {});
  Assert.notEqual(doc.activeElement, input, "The input is no longer focused");
  is(error.style.visibility, "hidden", "The error is hidden");

  // Check if assigning already assigned shortcut is prevented.
  input.focus();
  EventUtils.synthesizeKey("2", { shiftKey: true, altKey: true });
  is(label.dataset.l10nId, "shortcuts-exists", `The message is set`);
  is(error.style.visibility, "visible", "The error is shown");

  // Check the label uses the description first, and has a default for the special cases.
  function checkLabel(name, value) {
    let input = doc.querySelector(`.shortcut-input[name="${name}"]`);
    let label = input.previousElementSibling;
    if (label.dataset.l10nId) {
      is(label.dataset.l10nId, value, "The l10n-id is set");
    } else {
      is(label.textContent, value, "The textContent is set");
    }
  }
  checkLabel("commandOne", "commandOne");
  checkLabel("commandTwo", "Command Two!");
  checkLabel("_execute_browser_action", "shortcuts-browserAction2");

  await closeView(win);
  await extension.unload();
});

async function startExtensionWithCommands(numCommands) {
  let commands = {};

  for (let i = 0; i < numCommands; i++) {
    commands[`command-${i}`] = {};
  }

  let extension = ExtensionTestUtils.loadExtension({
    manifest: {
      commands,
    },
    background() {
      browser.test.sendMessage("ready");
    },
    useAddonManager: "temporary",
  });

  await extension.startup();
  await extension.awaitMessage("ready");
  await extensionShortcutsReady(extension.id);

  return extension;
}

add_task(async function testExpanding() {
  const numCommands = 7;
  const visibleCommands = 5;

  let extension = await startExtensionWithCommands(numCommands);

  let win = await loadShortcutsView();
  let doc = win.document;

  let card = doc.querySelector(`.card[addon-id="${extension.id}"]`);
  ok(!card.hasAttribute("expanded"), "The card is not expanded");

  let shortcutRows = card.querySelectorAll(".shortcut-row");
  is(shortcutRows.length, numCommands, `There are ${numCommands} shortcuts`);

  function assertCollapsedVisibility() {
    for (let i = 0; i < shortcutRows.length; i++) {
      let row = shortcutRows[i];
      if (i < visibleCommands) {
        Assert.notEqual(
          getComputedStyle(row).display,
          "none",
          `The first ${visibleCommands} rows are visible`
        );
      } else {
        is(getComputedStyle(row).display, "none", "The other rows are hidden");
      }
    }
  }

  // Check the visibility of the rows.
  assertCollapsedVisibility();

  let expandButton = card.querySelector(".expand-button");
  ok(expandButton, "There is an expand button");
  let l10nAttrs = doc.l10n.getAttributes(expandButton);
  is(l10nAttrs.id, "shortcuts-card-expand-button", "The expand text is shown");
  is(
    l10nAttrs.args.numberToShow,
    numCommands - visibleCommands,
    "The number to be shown is set on the expand button"
  );

  // Expand the card.
  expandButton.click();

  is(card.getAttribute("expanded"), "true", "The card is now expanded");

  for (let row of shortcutRows) {
    Assert.notEqual(
      getComputedStyle(row).display,
      "none",
      "All the rows are visible"
    );
  }

  // The collapse text is now shown.
  l10nAttrs = doc.l10n.getAttributes(expandButton);
  is(
    l10nAttrs.id,
    "shortcuts-card-collapse-button",
    "The colapse text is shown"
  );

  // Collapse the card.
  expandButton.click();

  ok(!card.hasAttribute("expanded"), "The card is now collapsed again");

  assertCollapsedVisibility({ collapsed: true });

  await closeView(win);
  await extension.unload();
});

add_task(async function testOneExtraCommandIsNotCollapsed() {
  const numCommands = 6;
  let extension = await startExtensionWithCommands(numCommands);

  let win = await loadShortcutsView();
  let doc = win.document;

  // The card is not expanded, since it doesn't collapse.
  let card = doc.querySelector(`.card[addon-id="${extension.id}"]`);
  ok(!card.hasAttribute("expanded"), "The card is not expanded");

  // Each shortcut has a row.
  let shortcutRows = card.querySelectorAll(".shortcut-row");
  is(shortcutRows.length, numCommands, `There are ${numCommands} shortcuts`);

  // There's no expand button, since it can't be expanded.
  let expandButton = card.querySelector(".expand-button");
  ok(!expandButton, "There is no expand button");

  // All of the rows are visible, to avoid a "Show 1 More" button.
  for (let row of shortcutRows) {
    Assert.notEqual(
      getComputedStyle(row).display,
      "none",
      "All the rows are visible"
    );
  }

  await closeView(win);
  await extension.unload();
});
