var gListener = null;
const kURL =
  "data:text/html;charset=utf-8,Caret browsing is fun.<input id='in'>";

const kPrefShortcutEnabled = "accessibility.browsewithcaret_shortcut.enabled";
const kPrefWarnOnEnable = "accessibility.warn_on_browsewithcaret";
const kPrefCaretBrowsingOn = "accessibility.browsewithcaret";

var oldPrefs = {};
for (let pref of [
  kPrefShortcutEnabled,
  kPrefWarnOnEnable,
  kPrefCaretBrowsingOn,
]) {
  oldPrefs[pref] = Services.prefs.getBoolPref(pref);
}

Services.prefs.setBoolPref(kPrefShortcutEnabled, true);
Services.prefs.setBoolPref(kPrefWarnOnEnable, true);
Services.prefs.setBoolPref(kPrefCaretBrowsingOn, false);

registerCleanupFunction(function () {
  for (let pref of [
    kPrefShortcutEnabled,
    kPrefWarnOnEnable,
    kPrefCaretBrowsingOn,
  ]) {
    Services.prefs.setBoolPref(pref, oldPrefs[pref]);
  }
});

// NB: not using BrowserTestUtils.promiseAlertDialog here because there's no way to
// undo waiting for a dialog. If we don't want the window to be opened, and
// wait for it to verify that it indeed does not open, we need to be able to
// then "stop" waiting so that when we next *do* want it to open, our "old"
// listener doesn't fire and do things we don't want (like close the window...).
let gCaretPromptOpeningObserver;
function promiseCaretPromptOpened() {
  return new Promise(resolve => {
    function observer(subject) {
      info("Dialog opened.");
      resolve(subject);
      gCaretPromptOpeningObserver();
    }
    Services.obs.addObserver(observer, "common-dialog-loaded");
    gCaretPromptOpeningObserver = () => {
      Services.obs.removeObserver(observer, "common-dialog-loaded");
      gCaretPromptOpeningObserver = () => {};
    };
  });
}

function hitF7() {
  SimpleTest.executeSoon(() => EventUtils.synthesizeKey("KEY_F7"));
}

async function toggleCaretNoDialog(expected) {
  let openedDialog = false;
  promiseCaretPromptOpened().then(function (win) {
    openedDialog = true;
    win.close(); // This will eventually return focus here and allow the test to continue...
  });
  // Cause the dialog to appear synchronously when focused element is in chrome,
  // otherwise, i.e., when focused element is in remote content, it appears
  // asynchronously.
  const focusedElementInChrome = Services.focus.focusedElement;
  const isAsync = focusedElementInChrome?.isRemoteBrowser;
  const waitForF7KeyHandled = new Promise(resolve => {
    let eventCount = 0;
    const expectedEventCount = isAsync ? 2 : 1;
    let listener = async event => {
      if (event.key == "F7") {
        info("F7 keypress is fired");
        if (++eventCount == expectedEventCount) {
          window.removeEventListener("keypress", listener, {
            capture: true,
            mozSystemGroup: true,
          });
          // Wait for the event handled in chrome.
          await TestUtils.waitForTick();
          resolve();
          return;
        }
        info(
          "Waiting for next F7 keypress which is a reply event from the remote content"
        );
      }
    };
    info(
      `Synthesizing "F7" key press and wait ${expectedEventCount} keypress events...`
    );
    window.addEventListener("keypress", listener, {
      capture: true,
      mozSystemGroup: true,
    });
  });
  hitF7();
  await waitForF7KeyHandled;

  let expectedStr = expected ? "on." : "off.";
  ok(
    !openedDialog,
    "Shouldn't open a dialog to turn caret browsing " + expectedStr
  );
  // Need to clean up if the dialog wasn't opened, so the observer doesn't get
  // re-triggered later on causing "issues".
  if (!openedDialog) {
    gCaretPromptOpeningObserver();
  }
  let prefVal = Services.prefs.getBoolPref(kPrefCaretBrowsingOn);
  is(prefVal, expected, "Caret browsing should now be " + expectedStr);
}

function waitForFocusOnInput(browser) {
  return SpecialPowers.spawn(browser, [], async function () {
    let textEl = content.document.getElementById("in");
    return ContentTaskUtils.waitForCondition(() => {
      return content.document.activeElement == textEl;
    }, "Input should get focused.");
  });
}

function focusInput(browser) {
  return SpecialPowers.spawn(browser, [], async function () {
    let textEl = content.document.getElementById("in");
    textEl.focus();
  });
}

add_task(async function checkTogglingCaretBrowsing() {
  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, kURL);
  await focusInput(tab.linkedBrowser);

  let promiseGotKey = promiseCaretPromptOpened();
  hitF7();
  let prompt = await promiseGotKey;
  let doc = prompt.document;
  let dialog = doc.getElementById("commonDialog");
  is(dialog.defaultButton, "cancel", "No button should be the default");
  ok(
    !doc.getElementById("checkbox").checked,
    "Checkbox shouldn't be checked by default."
  );
  let promiseDialogUnloaded = BrowserTestUtils.waitForEvent(prompt, "unload");

  dialog.cancelDialog();
  await promiseDialogUnloaded;
  info("Dialog unloaded");
  await waitForFocusOnInput(tab.linkedBrowser);
  ok(
    !Services.prefs.getBoolPref(kPrefCaretBrowsingOn),
    "Caret browsing should still be off after cancelling the dialog."
  );

  promiseGotKey = promiseCaretPromptOpened();
  hitF7();
  prompt = await promiseGotKey;

  doc = prompt.document;
  dialog = doc.getElementById("commonDialog");
  is(dialog.defaultButton, "cancel", "No button should be the default");
  ok(
    !doc.getElementById("checkbox").checked,
    "Checkbox shouldn't be checked by default."
  );
  promiseDialogUnloaded = BrowserTestUtils.waitForEvent(prompt, "unload");

  dialog.acceptDialog();
  await promiseDialogUnloaded;
  info("Dialog unloaded");
  await waitForFocusOnInput(tab.linkedBrowser);
  ok(
    Services.prefs.getBoolPref(kPrefCaretBrowsingOn),
    "Caret browsing should be on after accepting the dialog."
  );

  await toggleCaretNoDialog(false);

  promiseGotKey = promiseCaretPromptOpened();
  hitF7();
  prompt = await promiseGotKey;
  doc = prompt.document;
  dialog = doc.getElementById("commonDialog");

  is(dialog.defaultButton, "cancel", "No button should be the default");
  ok(
    !doc.getElementById("checkbox").checked,
    "Checkbox shouldn't be checked by default."
  );

  promiseDialogUnloaded = BrowserTestUtils.waitForEvent(prompt, "unload");
  dialog.cancelDialog();
  await promiseDialogUnloaded;
  info("Dialog unloaded");
  await waitForFocusOnInput(tab.linkedBrowser);

  ok(
    !Services.prefs.getBoolPref(kPrefCaretBrowsingOn),
    "Caret browsing should still be off after cancelling the dialog."
  );

  Services.prefs.setBoolPref(kPrefShortcutEnabled, true);
  Services.prefs.setBoolPref(kPrefWarnOnEnable, true);
  Services.prefs.setBoolPref(kPrefCaretBrowsingOn, false);

  BrowserTestUtils.removeTab(tab);
});

add_task(async function toggleCheckboxNoCaretBrowsing() {
  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, kURL);
  await focusInput(tab.linkedBrowser);

  let promiseGotKey = promiseCaretPromptOpened();
  hitF7();
  let prompt = await promiseGotKey;
  let doc = prompt.document;
  let dialog = doc.getElementById("commonDialog");
  is(dialog.defaultButton, "cancel", "No button should be the default");
  let checkbox = doc.getElementById("checkbox");
  ok(!checkbox.checked, "Checkbox shouldn't be checked by default.");

  // Check the box:
  checkbox.click();

  let promiseDialogUnloaded = BrowserTestUtils.waitForEvent(prompt, "unload");

  // Say no:
  dialog.getButton("cancel").click();

  await promiseDialogUnloaded;
  info("Dialog unloaded");
  await waitForFocusOnInput(tab.linkedBrowser);
  ok(
    !Services.prefs.getBoolPref(kPrefCaretBrowsingOn),
    "Caret browsing should still be off."
  );
  ok(
    !Services.prefs.getBoolPref(kPrefShortcutEnabled),
    "Shortcut should now be disabled."
  );

  await toggleCaretNoDialog(false);
  ok(
    !Services.prefs.getBoolPref(kPrefShortcutEnabled),
    "Shortcut should still be disabled."
  );

  Services.prefs.setBoolPref(kPrefShortcutEnabled, true);
  Services.prefs.setBoolPref(kPrefWarnOnEnable, true);
  Services.prefs.setBoolPref(kPrefCaretBrowsingOn, false);

  BrowserTestUtils.removeTab(tab);
});

add_task(async function toggleCheckboxWantCaretBrowsing() {
  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, kURL);
  await focusInput(tab.linkedBrowser);

  let promiseGotKey = promiseCaretPromptOpened();
  hitF7();
  let prompt = await promiseGotKey;
  let doc = prompt.document;
  let dialog = doc.getElementById("commonDialog");
  is(dialog.defaultButton, "cancel", "No button should be the default");
  let checkbox = doc.getElementById("checkbox");
  ok(!checkbox.checked, "Checkbox shouldn't be checked by default.");

  // Check the box:
  checkbox.click();

  let promiseDialogUnloaded = BrowserTestUtils.waitForEvent(prompt, "unload");

  // Say yes:
  dialog.acceptDialog();
  await promiseDialogUnloaded;
  info("Dialog unloaded");
  await waitForFocusOnInput(tab.linkedBrowser);
  ok(
    Services.prefs.getBoolPref(kPrefCaretBrowsingOn),
    "Caret browsing should now be on."
  );
  ok(
    Services.prefs.getBoolPref(kPrefShortcutEnabled),
    "Shortcut should still be enabled."
  );
  ok(
    !Services.prefs.getBoolPref(kPrefWarnOnEnable),
    "Should no longer warn when enabling."
  );

  await toggleCaretNoDialog(false);
  await toggleCaretNoDialog(true);
  await toggleCaretNoDialog(false);

  Services.prefs.setBoolPref(kPrefShortcutEnabled, true);
  Services.prefs.setBoolPref(kPrefWarnOnEnable, true);
  Services.prefs.setBoolPref(kPrefCaretBrowsingOn, false);

  BrowserTestUtils.removeTab(tab);
});

// Test for bug 1743878: Many repeated modal caret-browsing dialogs, if you
// accidentally hold down F7 for a few seconds
add_task(async function testF7SpamDoesNotOpenDialogs() {
  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
  registerCleanupFunction(() => BrowserTestUtils.removeTab(tab));

  let promiseGotKey = promiseCaretPromptOpened();
  hitF7();
  let prompt = await promiseGotKey;
  let doc = prompt.document;
  let dialog = doc.getElementById("commonDialog");

  let promiseDialogUnloaded = BrowserTestUtils.waitForEvent(prompt, "unload");

  // Listen for an additional prompt to open, which should not happen.
  let promiseDialogOrTimeout = () =>
    Promise.race([
      promiseCaretPromptOpened(),
      // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
      new Promise(resolve => setTimeout(resolve, 100)),
    ]);

  let openedPromise = promiseDialogOrTimeout();

  // Hit F7 two more times: once to test that _awaitingToggleCaretBrowsingPrompt
  // is applied, and again to test that its value isn't somehow reset by
  // pressing F7 while the dialog is open.
  for (let i = 0; i < 2; i++) {
    await new Promise(resolve =>
      SimpleTest.executeSoon(() => {
        hitF7();
        resolve();
      })
    );
  }

  // Say no:
  dialog.cancelDialog();
  await promiseDialogUnloaded;
  info("Dialog unloaded");

  let openedDialog = await openedPromise;
  ok(!openedDialog, "No additional dialog should have opened.");

  // If the test fails, clean up any dialogs we erroneously opened so they don't
  // interfere with other tests.
  let extraDialogs = 0;
  while (openedDialog) {
    extraDialogs += 1;
    let doc = openedDialog.document;
    let dialog = doc.getElementById("commonDialog");
    openedPromise = promiseDialogOrTimeout();
    dialog.cancelDialog();
    openedDialog = await openedPromise;
  }
  if (extraDialogs) {
    info(`Closed ${extraDialogs} extra dialogs.`);
  }

  // Either way, we now have an extra observer, so clean it up.
  gCaretPromptOpeningObserver();

  Services.prefs.setBoolPref(kPrefShortcutEnabled, true);
  Services.prefs.setBoolPref(kPrefWarnOnEnable, true);
  Services.prefs.setBoolPref(kPrefCaretBrowsingOn, false);
});
