/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/* import-globals-from ../../../../base/content/utilityOverlay.js */

async function openNewPrefsTab(paneID, scrollPaneTo, otherArgs) {
  const tabmail = document.getElementById("tabmail");
  const prefsTabMode = tabmail.tabModes.preferencesTab;

  is(prefsTabMode.tabs.length, 0, "Prefs tab is not open");

  let prefsDocument = await new Promise(resolve => {
    Services.obs.addObserver(function documentLoaded(subject) {
      if (subject.URL.startsWith("about:preferences")) {
        Services.obs.removeObserver(documentLoaded, "chrome-document-loaded");
        subject.ownerGlobal.setTimeout(() => resolve(subject));
      }
    }, "chrome-document-loaded");
    openPreferencesTab(paneID, scrollPaneTo, otherArgs);
  });
  ok(prefsDocument.URL.startsWith("about:preferences"), "Prefs tab is open");

  prefsDocument = prefsTabMode.tabs[0].browser.contentDocument;
  const prefsWindow = prefsDocument.ownerGlobal;
  window.resizeTo(screen.availWidth, screen.availHeight);

  if (paneID) {
    await new Promise(resolve => prefsWindow.setTimeout(resolve));
    is(
      prefsWindow.gLastCategory.category,
      paneID,
      `Selected pane is ${paneID}`
    );
  } else {
    // If we don't wait here for other scripts to run, they
    // could be in a bad state if our test closes the tab.
    await new Promise(resolve => prefsWindow.setTimeout(resolve));
  }

  registerCleanupOnce();

  await new Promise(resolve => prefsWindow.setTimeout(resolve));
  const container = prefsDocument.getElementById("preferencesContainer");
  if (scrollPaneTo && container.scrollHeight > container.clientHeight) {
    Assert.greater(
      container.scrollTop,
      0,
      "Prefs page did scroll when it was supposed to"
    );
  }
  return { prefsDocument, prefsWindow };
}

async function openExistingPrefsTab(paneID, scrollPaneTo, otherArgs) {
  const tabmail = document.getElementById("tabmail");
  const prefsTabMode = tabmail.tabModes.preferencesTab;

  is(prefsTabMode.tabs.length, 1, "Prefs tab is open");

  const prefsDocument = prefsTabMode.tabs[0].browser.contentDocument;
  const prefsWindow = prefsDocument.ownerGlobal;
  window.resizeTo(screen.availWidth, screen.availHeight);

  if (paneID && prefsWindow.gLastCategory.category != paneID) {
    openPreferencesTab(paneID, scrollPaneTo, otherArgs);
  }

  await new Promise(resolve => prefsWindow.setTimeout(resolve));
  is(prefsWindow.gLastCategory.category, paneID, `Selected pane is ${paneID}`);

  if (scrollPaneTo) {
    Assert.greater(
      prefsDocument.getElementById("preferencesContainer").scrollTop,
      0,
      "Prefs page did scroll when it was supposed to"
    );
  }

  registerCleanupOnce();
  return { prefsDocument, prefsWindow };
}

function registerCleanupOnce() {
  if (registerCleanupOnce.alreadyRegistered) {
    return;
  }
  registerCleanupFunction(closePrefsTab);
  registerCleanupOnce.alreadyRegistered = true;
}

async function closePrefsTab() {
  info("Closing prefs tab");
  const tabmail = document.getElementById("tabmail");
  const prefsTab = tabmail.tabModes.preferencesTab.tabs[0];
  if (prefsTab) {
    tabmail.closeTab(prefsTab);
  }
}

/**
 * Tests a checkbox sets the preference is set in the right state when the preferences tab opens,
 * that the preference it relates to is set properly, and any UI elements that should be disabled
 * by it are disabled.
 *
 * Each of the tests arguments is an object describing a test, containing:
 *   checkboxID - the ID of the checkbox to test
 *   pref - the name of a preference,
 *   prefValues - an array of two values: pref value when not checked, pref value when checked
 *                (optional, defaults to [false, true])
 *   enabledElements - an array of CSS selectors (optional)
 *   enabledInverted - if the elements should be disabled when the checkbox is checked (optional)
 *   unaffectedElements - array of CSS selectors that should not be affected by
 *                        the toggling of the checkbox.
 */
async function testCheckboxes(paneID, scrollPaneTo, ...tests) {
  for (const initiallyChecked of [true, false]) {
    info(`Opening ${paneID} with prefs set to ${initiallyChecked}`);

    for (const test of tests) {
      let wantedValue = initiallyChecked;
      if (test.prefValues) {
        wantedValue = wantedValue ? test.prefValues[1] : test.prefValues[0];
      }
      if (typeof wantedValue == "number") {
        Services.prefs.setIntPref(test.pref, wantedValue);
      } else {
        Services.prefs.setBoolPref(test.pref, wantedValue);
      }
    }

    const { prefsDocument, prefsWindow } = await openNewPrefsTab(
      paneID,
      scrollPaneTo
    );

    const testUIState = function (test, checked) {
      let wantedValue = checked;
      if (test.prefValues) {
        wantedValue = wantedValue ? test.prefValues[1] : test.prefValues[0];
      }
      const checkbox = prefsDocument.getElementById(test.checkboxID);
      is(
        checkbox.checked,
        checked,
        wantedValue,
        `Checkbox #${test.checkboxID} should ` +
          (checked ? "be" : "") +
          " checked"
      );
      if (typeof wantedValue == "number") {
        is(
          Services.prefs.getIntPref(test.pref, -999),
          wantedValue,
          `Pref ${test.pref} should be ${wantedValue}`
        );
      } else {
        is(
          Services.prefs.getBoolPref(test.pref),
          wantedValue,
          `Pref ${test.pref} should be ${wantedValue}`
        );
      }

      if (test.enabledElements) {
        let disabled = checked;
        if (test.enabledInverted) {
          disabled = !disabled;
        }
        for (const selector of test.enabledElements) {
          const elements = prefsDocument.querySelectorAll(selector);
          Assert.greaterOrEqual(
            elements.length,
            1,
            `At least one element matched '${selector}'`
          );
          for (const element of elements) {
            is(
              element.disabled,
              !disabled,
              "Element " + (disabled ? "isn't" : "is") + " disabled"
            );
          }
        }
      }
    };

    const testUnaffected = function (ids, states) {
      ids.forEach((sel, index) => {
        const isOk =
          prefsDocument.querySelector(sel).disabled === states[index];
        is(isOk, true, `Element "${sel}" is unaffected`);
      });
    };

    for (const test of tests) {
      info(`Checking ${test.checkboxID}`);

      const unaffectedSelectors = test.unaffectedElements || [];
      const unaffectedStates = unaffectedSelectors.map(
        sel => prefsDocument.querySelector(sel).disabled
      );

      const checkbox = prefsDocument.getElementById(test.checkboxID);
      checkbox.scrollIntoView({ block: "end", behavior: "instant" });
      testUIState(test, initiallyChecked);

      EventUtils.synthesizeMouseAtCenter(checkbox, {}, prefsWindow);
      testUIState(test, !initiallyChecked);
      testUnaffected(unaffectedSelectors, unaffectedStates);

      EventUtils.synthesizeMouseAtCenter(checkbox, {}, prefsWindow);
      testUIState(test, initiallyChecked);
      testUnaffected(unaffectedSelectors, unaffectedStates);
    }

    await closePrefsTab();
  }
}

/**
 * Tests a set of radio buttons is in the right state when the preferences tab opens, and when
 * the selected button changes that the preference it relates to is set properly, and any related
 * UI elements that should be disabled are disabled.
 *
 * Each of the tests arguments is an object describing a test, containing:
 *   pref - the name of an integer preference,
 *   states - an array with each element describing a radio button:
 *     id - the ID of the button to test,
 *     prefValue - the value the pref should be set to
 *     enabledElements - an array of CSS selectors to elements that should be enabled when this
 *                       radio button is selected (optional)
 */
async function testRadioButtons(paneID, scrollPaneTo, ...tests) {
  for (const { pref, states } of tests) {
    for (const initialState of states) {
      info(`Opening ${paneID} with ${pref} set to ${initialState.prefValue}`);

      if (typeof initialState.prefValue == "number") {
        Services.prefs.setIntPref(pref, initialState.prefValue);
      } else if (typeof initialState.prefValue == "boolean") {
        Services.prefs.setBoolPref(pref, initialState.prefValue);
      } else {
        Services.prefs.setCharPref(pref, initialState.prefValue);
      }

      const { prefsDocument, prefsWindow } = await openNewPrefsTab(
        paneID,
        scrollPaneTo
      );

      const testUIState = function (currentState) {
        info(`Testing with ${pref} set to ${currentState.prefValue}`);
        for (const state of states) {
          const isCurrentState = state == currentState;
          const radio = prefsDocument.getElementById(state.id);
          is(radio.selected, isCurrentState, `${state.id}.selected`);

          if (state.enabledElements) {
            for (const selector of state.enabledElements) {
              const elements = prefsDocument.querySelectorAll(selector);
              Assert.greaterOrEqual(
                elements.length,
                1,
                `At least one element matched '${selector}'`
              );
              for (const element of elements) {
                is(
                  element.disabled,
                  !isCurrentState,
                  "Element " + (isCurrentState ? "isn't" : "is") + " disabled"
                );
              }
            }
          }
        }
        if (typeof initialState.prefValue == "number") {
          is(
            Services.prefs.getIntPref(pref, -999),
            currentState.prefValue,
            `Pref ${pref} should be ${currentState.prefValue}`
          );
        } else if (typeof initialState.prefValue == "boolean") {
          is(
            Services.prefs.getBoolPref(pref),
            currentState.prefValue,
            `Pref ${pref} should be ${currentState.prefValue}`
          );
        } else {
          is(
            Services.prefs.getCharPref(pref, "FAKE VALUE"),
            currentState.prefValue,
            `Pref ${pref} should be ${currentState.prefValue}`
          );
        }
      };

      // Check the initial setup is correct.
      testUIState(initialState);
      // Cycle through possible values, checking each one.
      for (const state of states) {
        if (state == initialState) {
          continue;
        }
        const radio = prefsDocument.getElementById(state.id);
        radio.scrollIntoView({ block: "end", behavior: "instant" });
        EventUtils.synthesizeMouseAtCenter(radio, {}, prefsWindow);
        testUIState(state);
      }
      // Go back to the initial value.
      const initialRadio = prefsDocument.getElementById(initialState.id);
      initialRadio.scrollIntoView({ block: "end", behavior: "instant" });
      EventUtils.synthesizeMouseAtCenter(initialRadio, {}, prefsWindow);
      testUIState(initialState);

      await closePrefsTab();
    }
  }
}

/**
 * Clicks on a button to open a subdialog, waits for that subdialog to load,
 * runs a callback, then clicks a button to close the subdialog.
 *
 * @param {Element} buttonToClick - The button which opens the subdialog.
 * @param {string} dialogURL - URL of the subdialog we are expecting to open.
 * @param {Function} callback - A (possibly async) callback function to run
 *   once the subdialog is open and has focus. The callback takes one
 *   argument, the `window` of the subdialog.
 * @param {string} buttonNameOrID - The name or ID of the button to click,
 *   which causes the subdialog to close.
 */
async function promiseSubDialog(
  buttonToClick,
  dialogURL,
  callback,
  buttonNameOrID = "accept"
) {
  const openPromise = BrowserTestUtils.promiseAlertDialogOpen(
    undefined,
    dialogURL,
    { isSubDialog: true }
  );
  EventUtils.synthesizeMouseAtCenter(
    buttonToClick,
    {},
    buttonToClick.ownerGlobal
  );
  const dialogWindow = await openPromise;
  const dialogDocument = dialogWindow.document;
  const basename = dialogWindow.location.href.split("/").at(-1);
  info(`${basename} opened`);

  await TestUtils.waitForCondition(
    () => Services.focus.focusedWindow == dialogWindow,
    "waiting for subdialog to be focused"
  );

  await callback(dialogWindow);

  const closePromise = BrowserTestUtils.waitForEvent(
    dialogWindow.browsingContext.topChromeWindow,
    "dialogclose",
    true
  );
  EventUtils.synthesizeMouseAtCenter(
    dialogDocument.querySelector("dialog")?.getButton(buttonNameOrID) ||
      dialogDocument.getElementById(buttonNameOrID),
    {},
    dialogWindow
  );
  await closePromise;
  info(`${basename} closed`);
}
