/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";

const SITE_SPECIFIC_PREF = "browser.zoom.siteSpecific";
const FULL_ZOOM_PREF = "browser.content.full-zoom";

let gContentPrefs = Cc["@mozilla.org/content-pref/service;1"].getService(
  Ci.nsIContentPrefService2
);

// A single monitor for the tests.  If it receives any
// incognito data in event listeners it will fail.
let monitor;
add_task(async function startup() {
  monitor = await startIncognitoMonitorExtension();
});
registerCleanupFunction(async function finish() {
  await monitor.unload();
});

add_task(async function test_zoom_api() {
  async function background() {
    function promiseUpdated(tabId, attr) {
      return new Promise(resolve => {
        let onUpdated = (tabId_, changeInfo, tab) => {
          if (tabId == tabId_ && attr in changeInfo) {
            browser.tabs.onUpdated.removeListener(onUpdated);

            resolve({ changeInfo, tab });
          }
        };
        browser.tabs.onUpdated.addListener(onUpdated);
      });
    }

    let deferred = {};
    browser.test.onMessage.addListener((message, msg, result) => {
      if (message == "msg-done" && deferred[msg]) {
        deferred[msg].resolve(result);
      }
    });

    let _id = 0;
    function msg(...args) {
      return new Promise((resolve, reject) => {
        let id = ++_id;
        deferred[id] = { resolve, reject };
        browser.test.sendMessage("msg", id, ...args);
      });
    }

    let lastZoomEvent = {};
    let promiseZoomEvents = {};
    browser.tabs.onZoomChange.addListener(info => {
      lastZoomEvent[info.tabId] = info;
      if (promiseZoomEvents[info.tabId]) {
        promiseZoomEvents[info.tabId]();
        promiseZoomEvents[info.tabId] = null;
      }
    });

    let awaitZoom = async (tabId, newValue) => {
      let listener;

      // eslint-disable-next-line no-async-promise-executor
      await new Promise(async resolve => {
        listener = info => {
          if (info.tabId == tabId && info.newZoomFactor == newValue) {
            resolve();
          }
        };
        browser.tabs.onZoomChange.addListener(listener);

        let zoomFactor = await browser.tabs.getZoom(tabId);
        if (zoomFactor == newValue) {
          resolve();
        }
      });

      browser.tabs.onZoomChange.removeListener(listener);
    };

    let checkZoom = async (tabId, newValue, oldValue = null) => {
      let awaitEvent;
      if (oldValue != null && !lastZoomEvent[tabId]) {
        awaitEvent = new Promise(resolve => {
          promiseZoomEvents[tabId] = resolve;
        });
      }

      let [apiZoom, realZoom] = await Promise.all([
        browser.tabs.getZoom(tabId),
        msg("get-zoom", tabId),
        awaitEvent,
      ]);

      browser.test.assertEq(
        newValue,
        apiZoom,
        `Got expected zoom value from API`
      );
      browser.test.assertEq(
        newValue,
        realZoom,
        `Got expected zoom value from parent`
      );

      if (oldValue != null) {
        let event = lastZoomEvent[tabId];
        lastZoomEvent[tabId] = null;
        browser.test.assertEq(
          tabId,
          event.tabId,
          `Got expected zoom event tab ID`
        );
        browser.test.assertEq(
          newValue,
          event.newZoomFactor,
          `Got expected zoom event zoom factor`
        );
        browser.test.assertEq(
          oldValue,
          event.oldZoomFactor,
          `Got expected zoom event old zoom factor`
        );

        browser.test.assertEq(
          3,
          Object.keys(event.zoomSettings).length,
          `Zoom settings should have 3 keys`
        );
        browser.test.assertEq(
          "automatic",
          event.zoomSettings.mode,
          `Mode should be "automatic"`
        );
        browser.test.assertEq(
          "per-origin",
          event.zoomSettings.scope,
          `Scope should be "per-origin"`
        );
        browser.test.assertEq(
          1,
          event.zoomSettings.defaultZoomFactor,
          `Default zoom should be 1`
        );
      }
    };

    try {
      let tabs = await browser.tabs.query({});
      browser.test.assertEq(tabs.length, 4, "We have 4 tabs");

      let tabIds = tabs.splice(1).map(tab => tab.id);
      await checkZoom(tabIds[0], 1);

      await browser.tabs.setZoom(tabIds[0], 2);
      await checkZoom(tabIds[0], 2, 1);

      let zoomSettings = await browser.tabs.getZoomSettings(tabIds[0]);
      browser.test.assertEq(
        3,
        Object.keys(zoomSettings).length,
        `Zoom settings should have 3 keys`
      );
      browser.test.assertEq(
        "automatic",
        zoomSettings.mode,
        `Mode should be "automatic"`
      );
      browser.test.assertEq(
        "per-origin",
        zoomSettings.scope,
        `Scope should be "per-origin"`
      );
      browser.test.assertEq(
        1,
        zoomSettings.defaultZoomFactor,
        `Default zoom should be 1`
      );

      browser.test.log(`Switch to tab 2`);
      await browser.tabs.update(tabIds[1], { active: true });
      await checkZoom(tabIds[1], 1);

      browser.test.log(`Navigate tab 2 to origin of tab 1`);
      browser.tabs.update(tabIds[1], { url: "http://example.com" });
      await promiseUpdated(tabIds[1], "url");
      await checkZoom(tabIds[1], 2, 1);

      browser.test.log(`Update zoom in tab 2, expect changes in both tabs`);
      await browser.tabs.setZoom(tabIds[1], 1.5);
      await checkZoom(tabIds[1], 1.5, 2);

      browser.test.log(`Switch to tab 3, expect zoom to affect private window`);
      await browser.tabs.setZoom(tabIds[2], 3);
      await checkZoom(tabIds[2], 3, 1);

      browser.test.log(
        `Switch to tab 1, expect asynchronous zoom change just after the switch`
      );
      await Promise.all([
        awaitZoom(tabIds[0], 1.5),
        browser.tabs.update(tabIds[0], { active: true }),
      ]);
      await checkZoom(tabIds[0], 1.5, 2);

      browser.test.log("Set zoom to 0, expect it set to 1");
      await browser.tabs.setZoom(tabIds[0], 0);
      await checkZoom(tabIds[0], 1, 1.5);

      browser.test.log("Change zoom externally, expect changes reflected");
      await msg("enlarge");
      await checkZoom(tabIds[0], 1.1, 1);

      await Promise.all([
        browser.tabs.setZoom(tabIds[0], 0),
        browser.tabs.setZoom(tabIds[1], 0),
        browser.tabs.setZoom(tabIds[2], 0),
      ]);
      await Promise.all([
        checkZoom(tabIds[0], 1, 1.1),
        checkZoom(tabIds[1], 1, 1.5),
        checkZoom(tabIds[2], 1, 3),
      ]);

      browser.test.log("Check that invalid zoom values throw an error");
      await browser.test.assertRejects(
        browser.tabs.setZoom(tabIds[0], 42),
        /Zoom value 42 out of range/,
        "Expected an out of range error"
      );

      browser.test.log("Disable site-specific zoom, expect correct scope");
      await msg("site-specific", false);
      zoomSettings = await browser.tabs.getZoomSettings(tabIds[0]);

      browser.test.assertEq(
        "per-tab",
        zoomSettings.scope,
        `Scope should be "per-tab"`
      );

      await msg("site-specific", null);

      browser.test.onMessage.addListener(async msg => {
        if (msg === "set-global-zoom-done") {
          zoomSettings = await browser.tabs.getZoomSettings(tabIds[0]);

          browser.test.assertEq(
            5,
            zoomSettings.defaultZoomFactor,
            `Default zoom should be 5 after being changed`
          );

          browser.test.notifyPass("tab-zoom");
        }
      });
      await msg("set-global-zoom");
    } catch (e) {
      browser.test.fail(`Error: ${e} :: ${e.stack}`);
      browser.test.notifyFail("tab-zoom");
    }
  }

  let extension = ExtensionTestUtils.loadExtension({
    manifest: {
      permissions: ["tabs"],
    },
    incognitoOverride: "spanning",
    background,
  });

  extension.onMessage("msg", (id, msg, ...args) => {
    const {
      Management: {
        global: { tabTracker },
      },
    } = ChromeUtils.importESModule("resource://gre/modules/Extension.sys.mjs");

    let resp;
    if (msg == "get-zoom") {
      let tab = tabTracker.getTab(args[0]);
      resp = ZoomManager.getZoomForBrowser(tab.linkedBrowser);
    } else if (msg == "set-zoom") {
      let tab = tabTracker.getTab(args[0]);
      ZoomManager.setZoomForBrowser(tab.linkedBrowser);
    } else if (msg == "set-global-zoom") {
      resp = gContentPrefs.setGlobal(
        FULL_ZOOM_PREF,
        5,
        Cu.createLoadContext(),
        {
          handleCompletion() {
            extension.sendMessage("set-global-zoom-done", id, resp);
          },
        }
      );
    } else if (msg == "enlarge") {
      FullZoom.enlarge();
    } else if (msg == "site-specific") {
      if (args[0] == null) {
        SpecialPowers.clearUserPref(SITE_SPECIFIC_PREF);
      } else {
        SpecialPowers.setBoolPref(SITE_SPECIFIC_PREF, args[0]);
      }
    }

    extension.sendMessage("msg-done", id, resp);
  });

  let url = "https://example.com/";
  let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
  let tab2 = await BrowserTestUtils.openNewForegroundTab(
    gBrowser,
    "http://example.org/"
  );

  let privateWindow = await BrowserTestUtils.openNewBrowserWindow({
    private: true,
  });
  let selectedBrowser = privateWindow.gBrowser.selectedBrowser;
  BrowserTestUtils.loadURIString(selectedBrowser, url);
  await BrowserTestUtils.browserLoaded(selectedBrowser, false, url);

  gBrowser.selectedTab = tab1;

  await extension.startup();

  await extension.awaitFinish("tab-zoom");

  await extension.unload();

  await new Promise(resolve => {
    gContentPrefs.setGlobal(FULL_ZOOM_PREF, null, Cu.createLoadContext(), {
      handleCompletion() {
        resolve();
      },
    });
  });

  privateWindow.close();
  BrowserTestUtils.removeTab(tab1);
  BrowserTestUtils.removeTab(tab2);
});
