/* eslint max-len: ["error", 80] */

const { AddonTestUtils } = ChromeUtils.importESModule(
  "resource://testing-common/AddonTestUtils.sys.mjs"
);
const { ExtensionParent } = ChromeUtils.importESModule(
  "resource://gre/modules/ExtensionParent.sys.mjs"
);

AddonTestUtils.initMochitest(this);

// This test function helps to detect when an addon options browser have been
// inserted in the about:addons page.
function waitOptionsBrowserInserted() {
  return new Promise(resolve => {
    async function listener(eventName, browser) {
      // wait for a webextension XUL browser element that is owned by the
      // "about:addons" page.
      if (browser.ownerGlobal.top.location.href == "about:addons") {
        ExtensionParent.apiManager.off("extension-browser-inserted", listener);
        resolve(browser);
      }
    }
    ExtensionParent.apiManager.on("extension-browser-inserted", listener);
  });
}

add_setup(async () => {
  await SpecialPowers.pushPrefEnv({
    set: [
      ["extensions.htmlaboutaddons.inline-options.enabled", true],
      ["widget.macos.allow-native-select", false],
    ],
  });
});

add_task(async function testInlineOptions() {
  const HEIGHT_SHORT = 300;
  const HEIGHT_TALL = 600;

  let id = "inline@mochi.test";
  let extension = ExtensionTestUtils.loadExtension({
    manifest: {
      browser_specific_settings: { gecko: { id } },
      options_ui: {
        page: "options.html",
      },
    },
    files: {
      "options.html": `
        <html>
          <head>
            <style type="text/css">
              body > p { height: ${HEIGHT_SHORT}px; margin: 0; }
              body.bigger > p { height: ${HEIGHT_TALL}px; }
            </style>
            <script src="options.js"></script>
          </head>
          <body>
            <p>Some text</p>
          </body>
        </html>
      `,
      "options.js": () => {
        browser.test.onMessage.addListener(msg => {
          if (msg == "toggle-class") {
            document.body.classList.toggle("bigger");
          } else if (msg == "get-height") {
            browser.test.sendMessage("height", document.body.clientHeight);
          }
        });

        browser.test.sendMessage("options-loaded", window.location.href);
      },
    },
    useAddonManager: "temporary",
  });
  await extension.startup();

  let win = await loadInitialView("extension");
  let doc = win.document;

  // Make sure we found the right card.
  let card = getAddonCard(win, id);
  ok(card, "Found the card");

  // The preferences option should be visible.
  let preferences = card.querySelector('[action="preferences"]');
  ok(!preferences.hidden, "The preferences option is visible");

  // Open the preferences page.
  let loaded = waitForViewLoad(win);
  preferences.click();
  await loaded;

  // Verify we're on the preferences tab.
  card = doc.querySelector("addon-card");
  is(card.addon.id, id, "The right page was loaded");
  let { deck, tabGroup } = card.details;
  let { selectedViewName } = deck;
  is(selectedViewName, "preferences", "The preferences tab is shown");

  info("Check that there are two buttons and they're visible");
  let detailsBtn = tabGroup.querySelector('[name="details"]');
  ok(!detailsBtn.hidden, "The details button is visible");
  let prefsBtn = tabGroup.querySelector('[name="preferences"]');
  ok(!prefsBtn.hidden, "The preferences button is visible");

  // Wait for the browser to load.
  let url = await extension.awaitMessage("options-loaded");

  // Check the attributes of the options browser.
  let browser = card.querySelector("inline-options-browser browser");
  ok(browser, "The visible view has a browser");
  is(
    browser.currentURI.spec,
    card.addon.optionsURL,
    "The browser has the expected options URL"
  );
  is(url, card.addon.optionsURL, "Browser has the expected options URL loaded");
  Assert.greater(browser.clientWidth, 0, "The browser has a width");
  ok(
    card.querySelector('[action="preferences"]').hidden,
    "The preferences option is hidden now"
  );

  async function waitForHeightChange(expectedHeight) {
    await TestUtils.waitForCondition(
      () => browser.clientHeight === expectedHeight
    );
    // Wait for the resize event to be sent to the remote browser.
    await new Promise(r =>
      window.requestAnimationFrame(() => window.requestAnimationFrame(r))
    );
  }

  await waitForHeightChange(HEIGHT_SHORT);

  // Check resizing the browser through extension CSS.
  await extension.sendMessage("get-height");
  let height = await extension.awaitMessage("height");
  is(height, HEIGHT_SHORT, "The height is smaller to start");
  is(height, browser.clientHeight, "The browser is the same size");

  info("Resize the browser to be taller");
  await extension.sendMessage("toggle-class");
  await waitForHeightChange(HEIGHT_TALL);
  await extension.sendMessage("get-height");
  height = await extension.awaitMessage("height");
  is(height, HEIGHT_TALL, "The height is bigger now");
  is(height, browser.clientHeight, "The browser is the same size");

  info("Shrink the browser again");
  await extension.sendMessage("toggle-class");
  await waitForHeightChange(HEIGHT_SHORT);
  await extension.sendMessage("get-height");
  height = await extension.awaitMessage("height");
  is(height, HEIGHT_SHORT, "The browser shrunk back");
  is(height, browser.clientHeight, "The browser is the same size");

  info("Switching to details view");
  detailsBtn.click();

  info("Check the browser dimensions to make sure it's hidden");
  is(browser.clientWidth, 0, "The browser is hidden now");

  info("Switch back, check browser is shown");
  prefsBtn.click();

  Assert.greater(browser.clientWidth, 0, "The browser has a width");

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

// Regression test against bug 1409697
add_task(async function testCardRerender() {
  let id = "rerender@mochi.test";
  let extension = ExtensionTestUtils.loadExtension({
    manifest: {
      browser_specific_settings: { gecko: { id } },
      options_ui: {
        page: "options.html",
      },
    },
    files: {
      "options.html": `
        <html>
          <body>
            <p>Some text</p>
          </body>
        </html>
      `,
    },
    useAddonManager: "permanent",
  });
  await extension.startup();

  let win = await loadInitialView("extension");
  let doc = win.document;

  let card = getAddonCard(win, id);
  let loaded = waitForViewLoad(win);
  card.querySelector('[action="expand"]').click();
  await loaded;

  card = doc.querySelector("addon-card");

  let browserAdded = waitOptionsBrowserInserted();
  card.querySelector('.tab-button[name="preferences"]').click();
  await browserAdded;

  is(
    doc.querySelectorAll("inline-options-browser").length,
    1,
    "There is 1 inline-options-browser"
  );
  is(doc.querySelectorAll("browser").length, 1, "There is 1 browser");

  info("Reload the add-on and ensure there's still only one browser");
  let updated = BrowserTestUtils.waitForEvent(card, "update");
  card.addon.reload();
  await updated;

  // Since the add-on was disabled, we'll be on the details tab.
  is(card.details.deck.selectedViewName, "details", "View changed to details");
  is(
    doc.querySelectorAll("inline-options-browser").length,
    1,
    "There is 1 inline-options-browser"
  );
  is(doc.querySelectorAll("browser").length, 0, "The browser was destroyed");

  // Load the permissions tab again.
  browserAdded = waitOptionsBrowserInserted();
  card.querySelector('.tab-button[name="preferences"]').click();
  await browserAdded;

  // Switching to preferences will create a new browser element.
  is(
    card.details.deck.selectedViewName,
    "preferences",
    "View switched to preferences"
  );
  is(
    doc.querySelectorAll("inline-options-browser").length,
    1,
    "There is 1 inline-options-browser"
  );
  is(doc.querySelectorAll("browser").length, 1, "There is a new browser");

  info("Re-rendering card to ensure a second browser isn't added");
  updated = BrowserTestUtils.waitForEvent(card, "update");
  card.render();
  await updated;

  is(
    card.details.deck.selectedViewName,
    "details",
    "Rendering reverted to the details view"
  );
  is(
    doc.querySelectorAll("inline-options-browser").length,
    1,
    "There is still only 1 inline-options-browser after re-render"
  );
  is(doc.querySelectorAll("browser").length, 0, "There is no browser");

  let newBrowserAdded = waitOptionsBrowserInserted();
  card.showPrefs();
  await newBrowserAdded;

  is(
    doc.querySelectorAll("inline-options-browser").length,
    1,
    "There is still only 1 inline-options-browser after opening preferences"
  );
  is(doc.querySelectorAll("browser").length, 1, "There is 1 browser");

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

add_task(async function testRemovedOnDisable() {
  let id = "disable@mochi.test";
  const xpiFile = AddonTestUtils.createTempWebExtensionFile({
    manifest: {
      browser_specific_settings: { gecko: { id } },
      options_ui: {
        page: "options.html",
      },
    },
    files: {
      "options.html": "<h1>Options!</h1>",
    },
  });
  let addon = await AddonManager.installTemporaryAddon(xpiFile);

  let win = await loadInitialView("extension");
  let doc = win.document;

  // Opens the prefs page.
  let loaded = waitForViewLoad(win);
  getAddonCard(win, id).querySelector("[action=preferences]").click();
  await loaded;

  let inlineOptions = doc.querySelector("inline-options-browser");
  ok(inlineOptions, "There's an inline-options-browser element");
  ok(inlineOptions.querySelector("browser"), "The browser exists");

  let card = getAddonCard(win, id);
  let { deck } = card.details;
  is(deck.selectedViewName, "preferences", "Preferences are the active tab");

  info("Disabling the add-on");
  let updated = BrowserTestUtils.waitForEvent(card, "update");
  await addon.disable();
  await updated;

  is(deck.selectedViewName, "details", "Details are now the active tab");
  ok(inlineOptions, "There's an inline-options-browser element");
  ok(!inlineOptions.querySelector("browser"), "The browser has been removed");

  info("Enabling the add-on");
  updated = BrowserTestUtils.waitForEvent(card, "update");
  await addon.enable();
  await updated;

  is(deck.selectedViewName, "details", "Details are still the active tab");
  ok(inlineOptions, "There's an inline-options-browser element");
  ok(!inlineOptions.querySelector("browser"), "The browser is not created yet");

  info("Switching to preferences tab");
  let changed = BrowserTestUtils.waitForEvent(deck, "view-changed");
  let browserAdded = waitOptionsBrowserInserted();
  deck.selectedViewName = "preferences";
  await changed;
  await browserAdded;

  is(deck.selectedViewName, "preferences", "Preferences are selected");
  ok(inlineOptions, "There's an inline-options-browser element");
  ok(inlineOptions.querySelector("browser"), "The browser is re-created");

  await closeView(win);
  await addon.uninstall();
});

add_task(async function testUpgradeTemporary() {
  let id = "upgrade-temporary@mochi.test";
  async function loadExtension(version) {
    let extension = ExtensionTestUtils.loadExtension({
      manifest: {
        browser_specific_settings: { gecko: { id } },
        version,
        options_ui: {
          page: "options.html",
        },
      },
      files: {
        "options.html": `
          <html>
            <head>
              <script src="options.js"></script>
            </head>
            <body>
              <p>Version <pre>${version}</pre></p>
            </body>
          </html>
        `,
        "options.js": () => {
          browser.test.onMessage.addListener(msg => {
            if (msg === "get-version") {
              let version = document.querySelector("pre").textContent;
              browser.test.sendMessage("version", version);
            }
          });
          window.onload = () => browser.test.sendMessage("options-loaded");
        },
      },
      useAddonManager: "temporary",
    });
    await extension.startup();
    return extension;
  }

  let firstExtension = await loadExtension("1");
  let win = await loadInitialView("extension");
  let doc = win.document;

  let card = getAddonCard(win, id);
  let loaded = waitForViewLoad(win);
  card.querySelector('[action="expand"]').click();
  await loaded;

  card = doc.querySelector("addon-card");
  let browserAdded = waitOptionsBrowserInserted();
  card.querySelector('.tab-button[name="preferences"]').click();
  await browserAdded;

  await firstExtension.awaitMessage("options-loaded");
  await firstExtension.sendMessage("get-version");
  let version = await firstExtension.awaitMessage("version");
  is(version, "1", "Version 1 page is loaded");

  let updated = BrowserTestUtils.waitForEvent(card, "update");
  browserAdded = waitOptionsBrowserInserted();
  let secondExtension = await loadExtension("2");
  await updated;
  await browserAdded;
  await secondExtension.awaitMessage("options-loaded");

  await secondExtension.sendMessage("get-version");
  version = await secondExtension.awaitMessage("version");
  is(version, "2", "Version 2 page is loaded");
  let { deck } = card.details;
  is(deck.selectedViewName, "preferences", "Preferences are still shown");

  await closeView(win);
  await firstExtension.unload();
  await secondExtension.unload();
});

add_task(async function testReloadExtension() {
  let id = "reload@mochi.test";
  let xpiFile = AddonTestUtils.createTempWebExtensionFile({
    manifest: {
      browser_specific_settings: { gecko: { id } },
      options_ui: {
        page: "options.html",
      },
    },
    files: {
      "options.html": `
        <html>
          <head>
          </head>
          <body>
            <p>Options</p>
          </body>
        </html>
      `,
    },
  });
  let addon = await AddonManager.installTemporaryAddon(xpiFile);

  let win = await loadInitialView("extension");
  let doc = win.document;

  let card = getAddonCard(win, id);
  let loaded = waitForViewLoad(win);
  card.querySelector('[action="expand"]').click();
  await loaded;

  card = doc.querySelector("addon-card");
  let { deck } = card.details;
  is(deck.selectedViewName, "details", "Details load first");

  let browserAdded = waitOptionsBrowserInserted();
  card.querySelector('.tab-button[name="preferences"]').click();
  await browserAdded;

  is(deck.selectedViewName, "preferences", "Preferences are shown");

  let updated = BrowserTestUtils.waitForEvent(card, "update");
  browserAdded = waitOptionsBrowserInserted();
  let addonStarted = AddonTestUtils.promiseWebExtensionStartup(id);
  await addon.reload();
  await addonStarted;
  await updated;
  await browserAdded;
  is(deck.selectedViewName, "preferences", "Preferences are still shown");

  await closeView(win);
  await addon.uninstall();
});

async function testSelectPosition(optionsBrowser, zoom) {
  let popupShownPromise = BrowserTestUtils.waitForSelectPopupShown(window);
  await BrowserTestUtils.synthesizeMouseAtCenter("select", {}, optionsBrowser);
  let popup = await popupShownPromise;
  let popupLeft = popup.shadowRoot.querySelector(
    ".menupopup-arrowscrollbox"
  ).screenX;
  let browserLeft = optionsBrowser.screenX * zoom;
  Assert.lessOrEqual(
    Math.abs(popupLeft - browserLeft),
    1,
    `Popup should be correctly positioned: ${popupLeft} vs. ${browserLeft}`
  );
  popup.hidePopup();
}

async function testOptionsZoom(type = "full") {
  let id = `${type}-zoom@mochi.test`;
  let zoomProp = `${type}Zoom`;
  let extension = ExtensionTestUtils.loadExtension({
    manifest: {
      browser_specific_settings: { gecko: { id } },
      options_ui: {
        page: "options.html",
      },
    },
    files: {
      "options.html": `
        <!doctype html>
        <script src="options.js"></script>
        <body style="height: 500px">
          <p>Some text</p>
          <p>
            <select>
              <option>A</option>
              <option>B</option>
            </select>
          </p>
        </body>
      `,
      "options.js": () => {
        window.addEventListener("load", function () {
          browser.test.sendMessage("options-loaded");
        });
      },
    },
    useAddonManager: "permanent",
  });
  await extension.startup();

  let win = await loadInitialView("extension");
  let doc = win.document;

  gBrowser.selectedBrowser[zoomProp] = 2;

  let card = getAddonCard(win, id);
  let loaded = waitForViewLoad(win);
  card.querySelector('[action="expand"]').click();
  await loaded;

  card = doc.querySelector("addon-card");

  let browserAdded = waitOptionsBrowserInserted();
  card.querySelector('.tab-button[name="preferences"]').click();
  let optionsBrowser = await browserAdded;
  // Wait for the browser to load.
  await extension.awaitMessage("options-loaded");

  is(optionsBrowser[zoomProp], 2, `Options browser inherited ${zoomProp}`);

  await testSelectPosition(optionsBrowser, type == "full" ? 2 : 1);

  gBrowser.selectedBrowser[zoomProp] = 0.5;

  is(
    optionsBrowser[zoomProp],
    0.5,
    `Options browser reacts to ${zoomProp} change`
  );

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

add_task(function testOptionsFullZoom() {
  return testOptionsZoom("full");
});

add_task(function testOptionsTextZoom() {
  return testOptionsZoom("text");
});

add_task(async function testInputAndQuickFind() {
  let extension = ExtensionTestUtils.loadExtension({
    manifest: {
      options_ui: {
        page: "options.html",
      },
    },
    files: {
      "options.html": `
        <html>
          <body>
            <input name="some-input" type="text">
            <script src="options.js"></script>
          </body>
        </html>
      `,
      "options.js": () => {
        let input = document.querySelector("input");
        browser.test.assertEq(
          "some-input",
          input.getAttribute("name"),
          "Expected options page input"
        );
        input.addEventListener("input", event => {
          browser.test.sendMessage("input-changed", event.target.value);
        });

        browser.test.sendMessage("options-loaded", window.location.href);
      },
    },
    useAddonManager: "temporary",
  });
  await extension.startup();

  let win = await loadInitialView("extension");
  let doc = win.document;

  // Make sure we found the right card.
  let card = getAddonCard(win, extension.id);
  ok(card, "Found the card");

  // The preferences option should be visible.
  let preferences = card.querySelector('[action="preferences"]');
  ok(!preferences.hidden, "The preferences option is visible");

  // Open the preferences page.
  let loaded = waitForViewLoad(win);
  preferences.click();
  await loaded;

  // Verify we're on the preferences tab.
  card = doc.querySelector("addon-card");
  is(card.addon.id, extension.id, "The right page was loaded");

  // Wait for the browser to load.
  let url = await extension.awaitMessage("options-loaded");

  // Check the attributes of the options browser.
  let browser = card.querySelector("inline-options-browser browser");
  ok(browser, "The visible view has a browser");
  ok(card.addon.optionsURL.length, "Options URL is not empty");
  is(
    browser.currentURI.spec,
    card.addon.optionsURL,
    "The browser has the expected options URL"
  );
  is(url, card.addon.optionsURL, "Browser has the expected options URL loaded");

  // Focus the options browser.
  browser.focus();

  // Focus the input in the options page.
  await SpecialPowers.spawn(browser, [], () => {
    content.document.querySelector("input").focus();
  });

  info("input in options page should be focused, typing...");
  // Type '/'.
  EventUtils.synthesizeKey("/");

  let inputValue = await extension.awaitMessage("input-changed");
  is(inputValue, "/", "Expected input to contain a slash");

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