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

"use strict";

/* import-globals-from ../../mochitest/states.js */
/* import-globals-from ../../mochitest/role.js */
loadScripts(
  { name: "states.js", dir: MOCHITESTS_DIR },
  { name: "role.js", dir: MOCHITESTS_DIR }
);

ChromeUtils.defineESModuleGetters(this, {
  PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
  PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
  UrlbarProvider: "resource:///modules/UrlbarUtils.sys.mjs",
  UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.sys.mjs",
  UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs",
  UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs",
  UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs",
});

function isEventForAutocompleteItem(event) {
  return event.accessible.role == ROLE_COMBOBOX_OPTION;
}

function isEventForButton(event) {
  return event.accessible.role == ROLE_PUSHBUTTON;
}

function isEventForOneOffEngine(event) {
  let parent = event.accessible.parent;
  return (
    event.accessible.role == ROLE_PUSHBUTTON &&
    parent &&
    parent.role == ROLE_GROUPING &&
    parent.name
  );
}

function isEventForMenuPopup(event) {
  return event.accessible.role == ROLE_MENUPOPUP;
}

function isEventForMenuItem(event) {
  return event.accessible.role == ROLE_MENUITEM;
}

function isEventForResultButton(event) {
  let parent = event.accessible.parent;
  return (
    event.accessible.role == ROLE_PUSHBUTTON &&
    parent?.role == ROLE_COMBOBOX_LIST
  );
}

/**
 * A test provider.
 */
class TipTestProvider extends UrlbarProvider {
  constructor(matches) {
    super();
    this._matches = matches;
  }
  get name() {
    return "TipTestProvider";
  }
  get type() {
    return UrlbarUtils.PROVIDER_TYPE.PROFILE;
  }
  async isActive() {
    return true;
  }
  isRestricting() {
    return true;
  }
  async startQuery(context, addCallback) {
    this._context = context;
    for (const match of this._matches) {
      addCallback(this, match);
    }
  }
}

// Check that the URL bar manages accessibility focus appropriately.
async function runTests() {
  // TODO: Remove in https://bugzilla.mozilla.org/show_bug.cgi?id=1923383
  await SpecialPowers.pushPrefEnv({
    set: [["browser.urlbar.scotchBonnet.enableOverride", false]],
  });
  registerCleanupFunction(async function () {
    await UrlbarTestUtils.promisePopupClose(window);
    await PlacesUtils.history.clear();
  });

  await PlacesTestUtils.addVisits([
    // eslint-disable-next-line @microsoft/sdl/no-insecure-url
    "http://example1.com/blah",
    // eslint-disable-next-line @microsoft/sdl/no-insecure-url
    "http://example2.com/blah",
    // eslint-disable-next-line @microsoft/sdl/no-insecure-url
    "http://example1.com/",
    // eslint-disable-next-line @microsoft/sdl/no-insecure-url
    "http://example2.com/",
  ]);

  // Ensure initial state.
  await UrlbarTestUtils.promisePopupClose(window);

  let focused = waitForEvent(
    EVENT_FOCUS,
    event => event.accessible.role == ROLE_ENTRY
  );
  gURLBar.focus();
  let event = await focused;
  let textBox = event.accessible;
  // Ensure the URL bar is ready for a new URL to be typed.
  // Sometimes, when this test runs, the existing text isn't selected when the
  // URL bar is focused. Pressing escape twice ensures that the popup is
  // closed and that the existing text is selected.
  EventUtils.synthesizeKey("KEY_Escape");
  EventUtils.synthesizeKey("KEY_Escape");

  info("Ensuring no focus change when first text is typed");
  await UrlbarTestUtils.promiseAutocompleteResultPopup({
    window,
    waitForFocus,
    value: "example",
    fireInputEvent: true,
  });
  // Wait a tick for a11y events to fire.
  await TestUtils.waitForTick();
  testStates(textBox, STATE_FOCUSED);

  info("Ensuring no focus change on backspace");
  EventUtils.synthesizeKey("KEY_Backspace");
  await UrlbarTestUtils.promiseSearchComplete(window);
  // Wait a tick for a11y events to fire.
  await TestUtils.waitForTick();
  testStates(textBox, STATE_FOCUSED);

  info("Ensuring no focus change on text selection and delete");
  EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true });
  EventUtils.synthesizeKey("KEY_Delete");
  await UrlbarTestUtils.promiseSearchComplete(window);
  // Wait a tick for a11y events to fire.
  await TestUtils.waitForTick();
  testStates(textBox, STATE_FOCUSED);

  info("Ensuring autocomplete focus on down arrow (1)");
  focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem);
  EventUtils.synthesizeKey("KEY_ArrowDown");
  event = await focused;
  testStates(event.accessible, STATE_FOCUSED);

  info("Ensuring focus of another autocomplete item on down arrow");
  focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem);
  EventUtils.synthesizeKey("KEY_ArrowDown");
  event = await focused;
  testStates(event.accessible, STATE_FOCUSED);

  info("Ensuring previous arrow selection state doesn't get stale on input");
  focused = waitForEvent(EVENT_FOCUS, textBox);
  EventUtils.sendString("z");
  await focused;
  EventUtils.synthesizeKey("KEY_Backspace");
  await UrlbarTestUtils.promiseSearchComplete(window);
  testStates(textBox, STATE_FOCUSED);

  info("Ensuring focus of another autocomplete item on down arrow");
  focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem);
  EventUtils.synthesizeKey("KEY_ArrowDown");
  event = await focused;
  testStates(event.accessible, STATE_FOCUSED);

  if (AppConstants.platform == "macosx") {
    info("Ensuring focus of another autocomplete item on ctrl-n");
    focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem);
    EventUtils.synthesizeKey("n", { ctrlKey: true });
    event = await focused;
    testStates(event.accessible, STATE_FOCUSED);

    info("Ensuring focus of another autocomplete item on ctrl-p");
    focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem);
    EventUtils.synthesizeKey("p", { ctrlKey: true });
    event = await focused;
    testStates(event.accessible, STATE_FOCUSED);
  }

  info("Ensuring focus of another autocomplete item on up arrow");
  focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem);
  EventUtils.synthesizeKey("KEY_ArrowUp");
  event = await focused;
  testStates(event.accessible, STATE_FOCUSED);

  info("Ensuring text box focus on left arrow");
  focused = waitForEvent(EVENT_FOCUS, textBox);
  EventUtils.synthesizeKey("KEY_ArrowLeft");
  await focused;
  testStates(textBox, STATE_FOCUSED);

  gURLBar.view.close();
  // On Mac, down arrow when not at the end of the field moves to the end.
  // Move back to the end so the next press of down arrow opens the popup.
  EventUtils.synthesizeKey("KEY_ArrowRight");

  info("Ensuring autocomplete focus on down arrow (2)");
  focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem);
  EventUtils.synthesizeKey("KEY_ArrowDown");
  event = await focused;
  testStates(event.accessible, STATE_FOCUSED);

  info("Ensuring autocomplete focus on arrow up for search settings button");
  focused = waitForEvent(EVENT_FOCUS, isEventForButton);
  EventUtils.synthesizeKey("KEY_ArrowUp");
  event = await focused;
  testStates(event.accessible, STATE_FOCUSED);

  info("Ensuring text box focus when text is typed");
  focused = waitForEvent(EVENT_FOCUS, textBox);
  EventUtils.sendString("z");
  await focused;
  testStates(textBox, STATE_FOCUSED);
  EventUtils.synthesizeKey("KEY_Backspace");
  await UrlbarTestUtils.promiseSearchComplete(window);

  info("Ensuring autocomplete focus on down arrow (3)");
  focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem);
  EventUtils.synthesizeKey("KEY_ArrowDown");
  event = await focused;
  testStates(event.accessible, STATE_FOCUSED);

  info("Ensuring text box focus on backspace");
  focused = waitForEvent(EVENT_FOCUS, textBox);
  EventUtils.synthesizeKey("KEY_Backspace");
  await focused;
  testStates(textBox, STATE_FOCUSED);
  await UrlbarTestUtils.promiseSearchComplete(window);

  info("Ensuring autocomplete focus on arrow down (4)");
  focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem);
  EventUtils.synthesizeKey("KEY_ArrowDown");
  event = await focused;
  testStates(event.accessible, STATE_FOCUSED);

  // Arrow down to the last result.
  const resultCount = UrlbarTestUtils.getResultCount(window);
  while (UrlbarTestUtils.getSelectedRowIndex(window) != resultCount - 1) {
    EventUtils.synthesizeKey("KEY_ArrowDown");
  }

  info("Ensuring one-off search button focus on arrow down");
  focused = waitForEvent(EVENT_FOCUS, isEventForOneOffEngine);
  EventUtils.synthesizeKey("KEY_ArrowDown");
  event = await focused;
  testStates(event.accessible, STATE_FOCUSED);

  info("Ensuring autocomplete focus on arrow up");
  focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem);
  EventUtils.synthesizeKey("KEY_ArrowUp");
  event = await focused;
  testStates(event.accessible, STATE_FOCUSED);

  info("Ensuring text box focus on text selection");
  focused = waitForEvent(EVENT_FOCUS, textBox);
  EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true });
  await focused;
  testStates(textBox, STATE_FOCUSED);

  if (AppConstants.platform == "macosx") {
    // On Mac, ctrl-n after arrow left/right does not re-open the popup.
    // Type some text so the next press of ctrl-n opens the popup.
    EventUtils.sendString("ple");

    info("Ensuring autocomplete focus on ctrl-n");
    focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem);
    EventUtils.synthesizeKey("n", { ctrlKey: true });
    event = await focused;
    testStates(event.accessible, STATE_FOCUSED);
  }

  if (
    AppConstants.platform == "macosx" &&
    Services.prefs.getBoolPref("widget.macos.native-context-menus", false)
  ) {
    // With native context menus, we do not observe accessibility events and we
    // cannot send synthetic key events to the menu.
    info("Opening and closing context native context menu");
    let contextMenu = gURLBar.querySelector("menupopup");
    let popupshown = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
    EventUtils.synthesizeMouseAtCenter(gURLBar.querySelector("moz-input-box"), {
      type: "contextmenu",
    });
    await popupshown;
    let popuphidden = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden");
    contextMenu.hidePopup();
    await popuphidden;
  } else {
    info(
      "Ensuring context menu gets menu event on launch, and item focus on down"
    );
    let menuEvent = waitForEvent(
      nsIAccessibleEvent.EVENT_MENUPOPUP_START,
      isEventForMenuPopup
    );
    EventUtils.synthesizeMouseAtCenter(gURLBar.querySelector("moz-input-box"), {
      type: "contextmenu",
    });
    await menuEvent;

    focused = waitForEvent(EVENT_FOCUS, isEventForMenuItem);
    EventUtils.synthesizeKey("KEY_ArrowDown");
    event = await focused;
    testStates(event.accessible, STATE_FOCUSED);

    focused = waitForEvent(EVENT_FOCUS, textBox);
    let closed = waitForEvent(
      nsIAccessibleEvent.EVENT_MENUPOPUP_END,
      isEventForMenuPopup
    );
    EventUtils.synthesizeKey("KEY_Escape");
    await closed;
    await focused;
  }
  info("Ensuring address bar is focused after context menu is dismissed.");
  testStates(textBox, STATE_FOCUSED);
}

// We test TIP results in their own test so the spoofed results don't interfere
// with the main test.
async function runTipTests() {
  let matches = [
    new UrlbarResult(
      UrlbarUtils.RESULT_TYPE.URL,
      UrlbarUtils.RESULT_SOURCE.HISTORY,
      // eslint-disable-next-line @microsoft/sdl/no-insecure-url
      { url: "http://mozilla.org/a" }
    ),
    new UrlbarResult(
      UrlbarUtils.RESULT_TYPE.TIP,
      UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
      {
        // eslint-disable-next-line @microsoft/sdl/no-insecure-url
        helpUrl: "http://example.com/",
        type: "test",
        titleL10n: { id: "urlbar-search-tips-confirm" },
        buttons: [
          {
            // eslint-disable-next-line @microsoft/sdl/no-insecure-url
            url: "http://example.com/",
            l10n: { id: "urlbar-search-tips-confirm" },
          },
        ],
      }
    ),
    new UrlbarResult(
      UrlbarUtils.RESULT_TYPE.URL,
      UrlbarUtils.RESULT_SOURCE.HISTORY,
      // eslint-disable-next-line @microsoft/sdl/no-insecure-url
      { url: "http://mozilla.org/b" }
    ),
    new UrlbarResult(
      UrlbarUtils.RESULT_TYPE.URL,
      UrlbarUtils.RESULT_SOURCE.HISTORY,
      // eslint-disable-next-line @microsoft/sdl/no-insecure-url
      { url: "http://mozilla.org/c" }
    ),
  ];

  // Ensure the tip appears in the expected position.
  matches[1].suggestedIndex = 2;

  let provider = new TipTestProvider(matches);
  UrlbarProvidersManager.registerProvider(provider);

  registerCleanupFunction(async function () {
    UrlbarProvidersManager.unregisterProvider(provider);
  });

  let focused = waitForEvent(
    EVENT_FOCUS,
    event => event.accessible.role == ROLE_ENTRY
  );
  gURLBar.focus();
  let event = await focused;
  let textBox = event.accessible;

  EventUtils.synthesizeKey("KEY_Escape");
  EventUtils.synthesizeKey("KEY_Escape");

  info("Ensuring no focus change when first text is typed");
  await UrlbarTestUtils.promiseAutocompleteResultPopup({
    window,
    waitForFocus,
    value: "example",
    fireInputEvent: true,
  });
  // Wait a tick for a11y events to fire.
  await TestUtils.waitForTick();
  testStates(textBox, STATE_FOCUSED);

  info("Ensuring autocomplete focus on down arrow (1)");
  focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem);
  EventUtils.synthesizeKey("KEY_ArrowDown");
  event = await focused;
  testStates(event.accessible, STATE_FOCUSED);

  info("Ensuring the tip button is focused on down arrow");
  info("Also ensuring that the tip button is a part of a labelled group");
  focused = waitForEvent(EVENT_FOCUS, isEventForResultButton);
  EventUtils.synthesizeKey("KEY_ArrowDown");
  event = await focused;
  testStates(event.accessible, STATE_FOCUSED);

  info("Ensuring the help button is focused on tab");
  info("Also ensuring that the help button is a part of a labelled group");
  focused = waitForEvent(EVENT_FOCUS, isEventForResultButton);
  EventUtils.synthesizeKey("KEY_Tab");
  event = await focused;
  testStates(event.accessible, STATE_FOCUSED);

  info("Ensuring autocomplete focus on down arrow (2)");
  focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem);
  EventUtils.synthesizeKey("KEY_ArrowDown");
  event = await focused;
  testStates(event.accessible, STATE_FOCUSED);

  info("Ensuring the help button is focused on shift+tab");
  focused = waitForEvent(EVENT_FOCUS, isEventForResultButton);
  EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
  event = await focused;
  testStates(event.accessible, STATE_FOCUSED);

  info("Ensuring text box focus on left arrow, and not back to the tip button");
  focused = waitForEvent(EVENT_FOCUS, textBox);
  EventUtils.synthesizeKey("KEY_ArrowLeft");
  await focused;
  testStates(textBox, STATE_FOCUSED);
}

addAccessibleTask(``, runTests);
addAccessibleTask(``, runTipTests);
