"use strict";

// Delay loading until createAppInfo is called and setup.
ChromeUtils.defineESModuleGetters(this, {
  AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
});

const { ExtensionAPI } = ExtensionCommon;

// The code in this class does not actually run in this test scope, it is
// serialized into a string which is later loaded by the WebExtensions
// framework in the same context as other extension APIs.  By writing it
// this way rather than as a big string constant we get lint coverage.
// But eslint doesn't understand that this code runs in a different context
// where the EventManager class is available so just tell it here:
/* global EventManager */
const API = class extends ExtensionAPI {
  static namespace = undefined;
  primeListener(event, fire, params, isInStartup) {
    if (isInStartup && event == "nonBlockingEvent") {
      return;
    }
    // eslint-disable-next-line no-undef
    let { eventName, throwError, ignoreListener } =
      this.constructor.testOptions || {};
    let { namespace } = this.constructor;

    if (eventName == event) {
      if (throwError) {
        throw new Error(throwError);
      }
      if (ignoreListener) {
        return;
      }
    }

    Services.obs.notifyObservers(
      { namespace, event, fire, params },
      "prime-event-listener"
    );

    const FIRE_TOPIC = `fire-${namespace}.${event}`;

    async function listener(subject) {
      try {
        if (subject.wrappedJSObject.waitForBackground) {
          await fire.wakeup();
        }
        await fire.async(subject.wrappedJSObject.listenerArgs);
      } catch (err) {
        let errSubject = { namespace, event, errorMessage: err.toString() };
        Services.obs.notifyObservers(errSubject, "listener-callback-exception");
      }
    }
    Services.obs.addObserver(listener, FIRE_TOPIC);

    return {
      unregister() {
        Services.obs.notifyObservers(
          { namespace, event, params },
          "unregister-primed-listener"
        );
        Services.obs.removeObserver(listener, FIRE_TOPIC);
      },
      convert(_fire) {
        Services.obs.notifyObservers(
          { namespace, event, params },
          "convert-event-listener"
        );
        fire = _fire;
      },
    };
  }

  getAPI(context) {
    let self = this;
    let { namespace } = this.constructor;

    // TODO: split into their own test tasks the expected value to be set on
    // EventManager resetIdleOnEvent in the following cases:
    // - an EventManager instance in the parent process
    //   - for an event page
    //   - for a persistent background page
    //   - for an extension context that isn't a background context
    // - an EventManager instance in the child process
    //   (for the same 3 kinds of contexts)
    const EventManagerWithAssertions = class extends EventManager {
      constructor(...args) {
        super(...args);
        this.assertResetOnIdleOnEvent();
      }

      assertResetOnIdleOnEvent() {
        const expectResetIdleOnEventFalse =
          this.context.extension.persistentBackground;
        if (expectResetIdleOnEventFalse && this.resetIdleOnEvent) {
          const details = {
            eventManagerName: this.name,
            resetIdleOnEvent: this.resetIdleOnEvent,
            envType: this.context.envType,
            viewType: this.context.viewType,
            isBackgroundContext: this.context.isBackgroundContext,
            persistentBackground: this.context.extension.persistentBackground,
          };
          throw new Error(
            `EventManagerWithAssertions: resetIdleOnEvent should be forcefully set to false - ${JSON.stringify(
              details
            )}`
          );
        }
      }
    };
    return {
      [namespace]: {
        testOptions(options) {
          // We want to be able to test errors on startup.
          // We use a global here because we test restarting AOM,
          // which causes the instance of this class to be destroyed.
          // eslint-disable-next-line no-undef
          self.constructor.testOptions = options;
        },
        onEvent1: new EventManagerWithAssertions({
          context,
          module: namespace,
          event: "onEvent1",
          register: (fire, ...params) => {
            let data = { namespace, event: "onEvent1", params };
            Services.obs.notifyObservers(data, "register-event-listener");
            return () => {
              Services.obs.notifyObservers(data, "unregister-event-listener");
            };
          },
        }).api(),

        onEvent2: new EventManagerWithAssertions({
          context,
          module: namespace,
          event: "onEvent2",
          register: (fire, ...params) => {
            let data = { namespace, event: "onEvent2", params };
            Services.obs.notifyObservers(data, "register-event-listener");
            return () => {
              Services.obs.notifyObservers(data, "unregister-event-listener");
            };
          },
        }).api(),

        onEvent3: new EventManagerWithAssertions({
          context,
          module: namespace,
          event: "onEvent3",
          register: (fire, ...params) => {
            let data = { namespace, event: "onEvent3", params };
            Services.obs.notifyObservers(data, "register-event-listener");
            return () => {
              Services.obs.notifyObservers(data, "unregister-event-listener");
            };
          },
        }).api(),

        nonBlockingEvent: new EventManagerWithAssertions({
          context,
          module: namespace,
          event: "nonBlockingEvent",
          register: (fire, ...params) => {
            let data = { namespace, event: "nonBlockingEvent", params };
            Services.obs.notifyObservers(data, "register-event-listener");
            return () => {
              Services.obs.notifyObservers(data, "unregister-event-listener");
            };
          },
        }).api(),
      },
    };
  }
};

function makeModule(namespace, options = {}) {
  const SCHEMA = [
    {
      namespace,
      functions: [
        {
          name: "testOptions",
          type: "function",
          async: true,
          parameters: [
            {
              name: "options",
              type: "object",
              additionalProperties: {
                type: "any",
              },
            },
          ],
        },
      ],
      events: [
        {
          name: "onEvent1",
          type: "function",
          extraParameters: [{ type: "any", optional: true }],
        },
        {
          name: "onEvent2",
          type: "function",
          extraParameters: [{ type: "any", optional: true }],
        },
        {
          name: "onEvent3",
          type: "function",
          extraParameters: [
            { type: "object", optional: true, additionalProperties: true },
            { type: "any", optional: true },
          ],
        },
        {
          name: "nonBlockingEvent",
          type: "function",
          extraParameters: [{ type: "any", optional: true }],
        },
      ],
    },
  ];

  const API_SCRIPT = `
    this.${namespace} = ${API.toString()};
    this.${namespace}.namespace = "${namespace}";
  `;

  // MODULE_INFO for registerModules
  let { startupBlocking } = options;
  return {
    schema: `data:,${JSON.stringify(SCHEMA)}`,
    scopes: ["addon_parent"],
    paths: [[namespace]],
    startupBlocking,
    url: URL.createObjectURL(new Blob([API_SCRIPT])),
  };
}

// Two modules, primary test module is startupBlocking
const MODULE_INFO = {
  startupBlocking: makeModule("startupBlocking", { startupBlocking: true }),
  nonStartupBlocking: makeModule("nonStartupBlocking"),
};

const global = this;

// Wait for the given event (topic) to occur a specific number of times
// (count).  If fn is not supplied, the Promise returned from this function
// resolves as soon as that many instances of the event have been observed.
// If fn is supplied, this function also waits for the Promise that fn()
// returns to complete and ensures that the given event does not occur more
// than `count` times before then.  On success, resolves with an array
// of the subjects from each of the observed events.
async function promiseObservable(topic, count, fn = null) {
  let _countResolve;
  let results = [];
  function listener(subject, _topic) {
    const eventDetails = subject.wrappedJSObject;
    results.push(eventDetails);
    if (results.length > count) {
      ok(
        false,
        `Got unexpected ${topic} event with ${JSON.stringify(eventDetails)}`
      );
    } else if (results.length == count) {
      _countResolve();
    }
  }
  Services.obs.addObserver(listener, topic);

  try {
    await Promise.all([
      new Promise(resolve => {
        _countResolve = resolve;
      }),
      fn && fn(),
    ]);
  } finally {
    Services.obs.removeObserver(listener, topic);
  }

  return results;
}

function trackEvents(wrapper) {
  let events = new Map();
  for (let event of ["background-script-event", "start-background-script"]) {
    events.set(event, false);
    wrapper.extension.once(event, () => events.set(event, true));
  }
  return events;
}

const server = createHttpServer({ hosts: ["example.com"] });
server.registerDirectory("/data/", do_get_file("data"));

add_setup(async function setup() {
  // The blob:-URL registered above in MODULE_INFO gets loaded at
  // https://searchfox.org/mozilla-central/rev/0fec57c05d3996cc00c55a66f20dd5793a9bfb5d/toolkit/components/extensions/ExtensionCommon.jsm#1649
  Services.prefs.setBoolPref(
    "security.allow_parent_unrestricted_js_loads",
    true
  );
  registerCleanupFunction(() => {
    Services.prefs.clearUserPref("security.allow_parent_unrestricted_js_loads");
  });

  AddonTestUtils.init(global);
  AddonTestUtils.overrideCertDB();
  AddonTestUtils.createAppInfo(
    "xpcshell@tests.mozilla.org",
    "XPCShell",
    "42",
    "42"
  );

  ExtensionParent.apiManager.registerModules(MODULE_INFO);
});

add_task(async function test_persistent_events() {
  await AddonTestUtils.promiseStartupManager();

  let extension = ExtensionTestUtils.loadExtension({
    useAddonManager: "permanent",
    background() {
      let register1 = true,
        register2 = true;
      if (localStorage.getItem("skip1")) {
        register1 = false;
      }
      if (localStorage.getItem("skip2")) {
        register2 = false;
      }

      let listener1 = arg => browser.test.sendMessage("listener1", arg);
      let listener2 = arg => browser.test.sendMessage("listener2", arg);
      let listener3 = arg => browser.test.sendMessage("listener3", arg);

      if (register1) {
        browser.startupBlocking.onEvent1.addListener(listener1, "listener1");
      }
      if (register2) {
        browser.startupBlocking.onEvent1.addListener(listener2, "listener2");
        browser.startupBlocking.onEvent2.addListener(listener3, "listener3");
      }

      browser.test.onMessage.addListener(msg => {
        if (msg == "unregister2") {
          browser.startupBlocking.onEvent2.removeListener(listener3);
          localStorage.setItem("skip2", true);
        } else if (msg == "unregister1") {
          localStorage.setItem("skip1", true);
          browser.test.sendMessage("unregistered");
        }
      });

      browser.test.sendMessage("ready");
    },
  });

  function check(
    info,
    what,
    { listener1 = true, listener2 = true, listener3 = true } = {}
  ) {
    let count = (listener1 ? 1 : 0) + (listener2 ? 1 : 0) + (listener3 ? 1 : 0);
    equal(info.length, count, `Got ${count} ${what} events`);

    let i = 0;
    if (listener1) {
      equal(info[i].event, "onEvent1", `Got ${what} on event1 for listener 1`);
      deepEqual(
        info[i].params,
        ["listener1"],
        `Got event1 ${what} args for listener 1`
      );
      ++i;
    }

    if (listener2) {
      equal(info[i].event, "onEvent1", `Got ${what} on event1 for listener 2`);
      deepEqual(
        info[i].params,
        ["listener2"],
        `Got event1 ${what} args for listener 2`
      );
      ++i;
    }

    if (listener3) {
      equal(info[i].event, "onEvent2", `Got ${what} on event2 for listener 3`);
      deepEqual(
        info[i].params,
        ["listener3"],
        `Got event2 ${what} args for listener 3`
      );
      ++i;
    }
  }

  // Check that the regular event registration process occurs when
  // the extension is installed.
  let [observed] = await Promise.all([
    promiseObservable("register-event-listener", 3),
    extension.startup(),
  ]);
  check(observed, "register");

  await extension.awaitMessage("ready");

  // Check that the regular unregister process occurs when
  // the browser shuts down.
  [observed] = await Promise.all([
    promiseObservable("unregister-event-listener", 3),
    new Promise(resolve => extension.extension.once("shutdown", resolve)),
    AddonTestUtils.promiseShutdownManager(),
  ]);
  check(observed, "unregister");

  // Check that listeners are primed at the next browser startup.
  [observed] = await Promise.all([
    promiseObservable("prime-event-listener", 3),
    AddonTestUtils.promiseStartupManager(),
  ]);
  check(observed, "prime");

  assertPersistentListeners(extension, "startupBlocking", "onEvent1", {
    primed: true,
    primedListenersCount: 2,
  });

  assertPersistentListeners(extension, "startupBlocking", "onEvent2", {
    primed: true,
    primedListenersCount: 1,
  });

  // Check that primed listeners are converted to regular listeners
  // when the background page is started after browser startup.
  let p = promiseObservable("convert-event-listener", 3);
  AddonTestUtils.notifyLateStartup();
  observed = await p;

  check(observed, "convert");

  await extension.awaitMessage("ready");

  // Check that when the event is triggered, all the plumbing worked
  // correctly for the primed-then-converted listener.
  let listenerArgs = { test: "kaboom" };
  Services.obs.notifyObservers(
    { listenerArgs },
    "fire-startupBlocking.onEvent1"
  );

  let details = await extension.awaitMessage("listener1");
  deepEqual(details, listenerArgs, "Listener 1 fired");
  details = await extension.awaitMessage("listener2");
  deepEqual(details, listenerArgs, "Listener 2 fired");

  // Check that the converted listener is properly unregistered at
  // browser shutdown.
  [observed] = await Promise.all([
    promiseObservable("unregister-primed-listener", 3),
    AddonTestUtils.promiseShutdownManager(),
  ]);
  check(observed, "unregister");

  // Start up again, listener should be primed
  [observed] = await Promise.all([
    promiseObservable("prime-event-listener", 3),
    AddonTestUtils.promiseStartupManager(),
  ]);
  check(observed, "prime");

  // Check that triggering the event before the listener has been converted
  // causes the background page to be loaded and the listener to be converted,
  // and the listener is invoked.
  p = promiseObservable("convert-event-listener", 3);
  listenerArgs.test = "startup event";
  Services.obs.notifyObservers(
    { listenerArgs },
    "fire-startupBlocking.onEvent2"
  );
  observed = await p;

  check(observed, "convert");

  details = await extension.awaitMessage("listener3");
  deepEqual(details, listenerArgs, "Listener 3 fired for event during startup");

  await extension.awaitMessage("ready");

  // Check that triggering onEvent1 emits calls to both listener1 and listener2
  // (See Bug 1795801).
  [observed] = await Promise.all([
    promiseObservable("unregister-primed-listener", 3),
    AddonTestUtils.promiseShutdownManager(),
  ]);
  check(observed, "unregister");
  [observed] = await Promise.all([
    promiseObservable("prime-event-listener", 3),
    AddonTestUtils.promiseStartupManager(),
  ]);
  check(observed, "prime");
  assertPersistentListeners(extension, "startupBlocking", "onEvent1", {
    primed: true,
    primedListenersCount: 2,
  });
  assertPersistentListeners(extension, "startupBlocking", "onEvent2", {
    primed: true,
    primedListenersCount: 1,
  });

  p = promiseObservable("convert-event-listener", 3);
  listenerArgs.test = "startup event";
  Services.obs.notifyObservers(
    { listenerArgs },
    "fire-startupBlocking.onEvent1"
  );
  observed = await p;

  check(observed, "convert");

  const [detailsListener1Call, detailsListener2Call] = await Promise.all([
    extension.awaitMessage("listener1"),
    extension.awaitMessage("listener2"),
  ]);
  deepEqual(
    detailsListener1Call,
    listenerArgs,
    "Listener 1 fired for event during startup"
  );
  deepEqual(
    detailsListener2Call,
    listenerArgs,
    "Listener 2 fired for event during startup"
  );

  await extension.awaitMessage("ready");

  // Check that the unregister process works when we manually remove
  // a listener.
  p = promiseObservable("unregister-primed-listener", 1);
  extension.sendMessage("unregister2");
  observed = await p;
  check(observed, "unregister", { listener1: false, listener2: false });

  // Check that we only get unregisters for the remaining events after
  // one listener has been removed.
  observed = await promiseObservable("unregister-primed-listener", 2, () =>
    AddonTestUtils.promiseShutdownManager()
  );
  check(observed, "unregister", { listener3: false });

  // Check that after restart, only listeners that were present at
  // the end of the last session are primed.
  observed = await promiseObservable("prime-event-listener", 2, () =>
    AddonTestUtils.promiseStartupManager()
  );
  check(observed, "prime", { listener3: false });

  // Check that if the background script does not re-register listeners,
  // the primed listeners are unregistered after the background page
  // starts up.
  p = promiseObservable("unregister-primed-listener", 1, () =>
    extension.awaitMessage("ready")
  );

  AddonTestUtils.notifyLateStartup();
  observed = await p;
  check(observed, "unregister", { listener1: false, listener3: false });

  // Just listener1 should be registered now, fire event1 to confirm.
  listenerArgs.test = "third time";
  Services.obs.notifyObservers(
    { listenerArgs },
    "fire-startupBlocking.onEvent1"
  );
  details = await extension.awaitMessage("listener1");
  deepEqual(details, listenerArgs, "Listener 1 fired");

  // Tell the extension not to re-register listener1 on the next startup
  extension.sendMessage("unregister1");
  await extension.awaitMessage("unregistered");

  // Shut down, start up
  observed = await promiseObservable("unregister-primed-listener", 1, () =>
    AddonTestUtils.promiseShutdownManager()
  );
  check(observed, "unregister", { listener2: false, listener3: false });

  observed = await promiseObservable("prime-event-listener", 1, () =>
    AddonTestUtils.promiseStartupManager()
  );
  check(observed, "register", { listener2: false, listener3: false });

  // Check that firing event1 causes the listener fire callback to
  // reject.
  p = promiseObservable("listener-callback-exception", 1);
  Services.obs.notifyObservers(
    { listenerArgs, waitForBackground: true },
    "fire-startupBlocking.onEvent1"
  );
  equal(
    (await p)[0].errorMessage,
    "Error: primed listener startupBlocking.onEvent1 not re-registered",
    "Primed listener that was not re-registered received an error when event was triggered during startup"
  );

  await extension.awaitMessage("ready");

  await extension.unload();

  await AddonTestUtils.promiseShutdownManager();
});

// This test checks whether primed listeners are correctly unregistered when
// a background page load is interrupted. In particular, it verifies that the
// fire.wakeup() and fire.async() promises settle eventually.
add_task(async function test_shutdown_before_background_loaded() {
  await AddonTestUtils.promiseStartupManager();

  let extension = ExtensionTestUtils.loadExtension({
    useAddonManager: "permanent",
    background() {
      let listener = arg => browser.test.sendMessage("triggered", arg);
      browser.startupBlocking.onEvent1.addListener(listener, "triggered");
      browser.test.sendMessage("bg_started");
    },
  });
  await Promise.all([
    promiseObservable("register-event-listener", 1),
    extension.startup(),
  ]);
  await extension.awaitMessage("bg_started");

  await Promise.all([
    promiseObservable("unregister-event-listener", 1),
    new Promise(resolve => extension.extension.once("shutdown", resolve)),
    AddonTestUtils.promiseShutdownManager(),
  ]);

  let primeListenerPromise = promiseObservable("prime-event-listener", 1);
  let fire;
  let fireWakeupBeforeBgFail;
  let fireAsyncBeforeBgFail;

  let bgAbortedPromise = new Promise(resolve => {
    let Management = ExtensionParent.apiManager;
    Management.once("extension-browser-inserted", (eventName, browser) => {
      browser.fixupAndLoadURIString = async () => {
        // The fire.wakeup/fire.async promises created while loading the
        // background page should settle when the page fails to load.
        fire = (await primeListenerPromise)[0].fire;
        fireWakeupBeforeBgFail = fire.wakeup();
        fireAsyncBeforeBgFail = fire.async();

        extension.extension.once("background-script-aborted", resolve);
        info("Forcing the background load to fail");
        browser.remove();
      };
    });
  });

  let unregisterPromise = promiseObservable("unregister-primed-listener", 1);

  await Promise.all([
    primeListenerPromise,
    AddonTestUtils.promiseStartupManager(),
  ]);
  await bgAbortedPromise;
  info("Loaded extension and aborted load of background page");

  await unregisterPromise;
  info("Primed listener has been unregistered");

  await fireWakeupBeforeBgFail;
  info("fire.wakeup() before background load failure should settle");

  await Assert.rejects(
    fireAsyncBeforeBgFail,
    /Error: listener not re-registered/,
    "fire.async before background load failure should be rejected"
  );

  await fire.wakeup();
  info("fire.wakeup() after background load failure should settle");

  await Assert.rejects(
    fire.async(),
    /Error: primed listener startupBlocking.onEvent1 not re-registered/,
    "fire.async after background load failure should be rejected"
  );

  info(
    "Expect fire.wakeup call after load failure to restart the background page"
  );
  await extension.awaitMessage("bg_started");

  await AddonTestUtils.promiseShutdownManager();

  // End of the abnormal shutdown test. Now restart the extension to verify
  // that the persistent listeners have not been unregistered.

  // Suppress background page start until an explicit notification.
  await Promise.all([
    promiseObservable("prime-event-listener", 1),
    AddonTestUtils.promiseStartupManager({ earlyStartup: false }),
  ]);
  info("Triggering persistent event to force the background page to start");
  Services.obs.notifyObservers(
    { listenerArgs: 123 },
    "fire-startupBlocking.onEvent1"
  );
  AddonTestUtils.notifyEarlyStartup();
  await extension.awaitMessage("bg_started");
  equal(await extension.awaitMessage("triggered"), 123, "triggered event");

  await Promise.all([
    promiseObservable("unregister-primed-listener", 1),
    AddonTestUtils.promiseShutdownManager(),
  ]);

  // And lastly, verify that a primed listener is correctly removed when the
  // extension unloads normally before the delayed background page can load.
  await Promise.all([
    promiseObservable("prime-event-listener", 1),
    AddonTestUtils.promiseStartupManager({ earlyStartup: false }),
  ]);

  info("Unloading extension before background page has loaded");
  await Promise.all([
    promiseObservable("unregister-primed-listener", 1),
    extension.unload(),
  ]);

  await AddonTestUtils.promiseShutdownManager();
});

// This test checks whether primed listeners are correctly primed to
// restart the background once the background has been shutdown or
// put to sleep.
add_task(async function test_background_restarted() {
  await AddonTestUtils.promiseStartupManager();

  let extension = ExtensionTestUtils.loadExtension({
    useAddonManager: "permanent",
    background() {
      let listener = arg => browser.test.sendMessage("triggered", arg);
      browser.startupBlocking.onEvent1.addListener(listener, "triggered");
      browser.test.sendMessage("bg_started");
    },
  });
  await Promise.all([
    promiseObservable("register-event-listener", 1),
    extension.startup(),
  ]);
  await extension.awaitMessage("bg_started");
  assertPersistentListeners(extension, "startupBlocking", "onEvent1", {
    primed: false,
  });

  // Shutdown the background page
  await Promise.all([
    promiseObservable("unregister-event-listener", 1),
    extension.terminateBackground(),
  ]);
  // When sleeping the background, its events should become persisted
  assertPersistentListeners(extension, "startupBlocking", "onEvent1", {
    primed: true,
  });

  info("Triggering persistent event to force the background page to start");
  Services.obs.notifyObservers(
    { listenerArgs: 123 },
    "fire-startupBlocking.onEvent1"
  );
  await extension.awaitMessage("bg_started");
  equal(await extension.awaitMessage("triggered"), 123, "triggered event");

  await extension.unload();
  await AddonTestUtils.promiseShutdownManager();
});

// This test checks whether primed listeners are correctly primed to
// restart the background once the background has been shutdown or
// put to sleep.
add_task(
  { pref_set: [["extensions.eventPages.enabled", true]] },
  async function test_eventpage_startup() {
    await AddonTestUtils.promiseStartupManager();

    let extension = ExtensionTestUtils.loadExtension({
      useAddonManager: "permanent",
      manifest: {
        browser_specific_settings: { gecko: { id: "eventpage@test" } },
        background: { persistent: false },
      },
      background() {
        let listener = arg => browser.test.sendMessage("triggered", arg);
        browser.startupBlocking.onEvent1.addListener(listener, "triggered");
        let listenerNs = arg => browser.test.sendMessage("triggered-et2", arg);
        browser.nonStartupBlocking.onEvent1.addListener(
          listenerNs,
          "triggered-et2"
        );
        browser.test.onMessage.addListener(() => {
          let listener = arg => browser.test.sendMessage("triggered2", arg);
          browser.startupBlocking.onEvent2.addListener(listener, "triggered2");
          browser.test.sendMessage("async-registered-listener");
        });
        browser.test.sendMessage("bg_started");
      },
    });
    await Promise.all([
      promiseObservable("register-event-listener", 2),
      extension.startup(),
    ]);
    await extension.awaitMessage("bg_started");
    extension.sendMessage("async-register-listener");
    await extension.awaitMessage("async-registered-listener");

    async function testAfterRestart() {
      assertPersistentListeners(extension, "startupBlocking", "onEvent1", {
        primed: true,
      });
      // async registration should not be primed or persisted
      assertPersistentListeners(extension, "startupBlocking", "onEvent2", {
        primed: false,
        persisted: false,
      });

      let events = trackEvents(extension);
      ok(
        !events.get("background-script-event"),
        "Should not have received a background script event"
      );
      ok(
        !events.get("start-background-script"),
        "Background script should not be started"
      );

      info("Triggering persistent event to force the background page to start");
      let converted = promiseObservable("convert-event-listener", 1);
      Services.obs.notifyObservers(
        { listenerArgs: 123 },
        "fire-startupBlocking.onEvent1"
      );
      await extension.awaitMessage("bg_started");
      await converted;
      equal(await extension.awaitMessage("triggered"), 123, "triggered event");
      ok(
        events.get("background-script-event"),
        "Should have received a background script event"
      );
      ok(
        events.get("start-background-script"),
        "Background script should be started"
      );
    }

    // Shutdown the background page
    await Promise.all([
      promiseObservable("unregister-event-listener", 3),
      new Promise(resolve => extension.extension.once("shutdown", resolve)),
      AddonTestUtils.promiseShutdownManager(),
    ]);
    await AddonTestUtils.promiseStartupManager({ lateStartup: false });
    await extension.awaitStartup();
    assertPersistentListeners(extension, "nonStartupBlocking", "onEvent1", {
      primed: false,
      persisted: true,
    });
    await testAfterRestart();

    extension.sendMessage("async-register-listener");
    await extension.awaitMessage("async-registered-listener");

    // We sleep twice to ensure startup and shutdown work correctly
    info("test event listener registration during termination");
    let registrationEvents = Promise.all([
      promiseObservable("unregister-event-listener", 2),
      promiseObservable("unregister-primed-listener", 1),
      promiseObservable("prime-event-listener", 2),
    ]);
    await extension.terminateBackground();
    await registrationEvents;

    assertPersistentListeners(extension, "nonStartupBlocking", "onEvent1", {
      primed: true,
      persisted: true,
    });

    // Ensure onEvent2 does not fire, testAfterRestart will fail otherwise.
    Services.obs.notifyObservers(
      { listenerArgs: 123 },
      "fire-startupBlocking.onEvent2"
    );
    await testAfterRestart();

    registrationEvents = Promise.all([
      promiseObservable("unregister-primed-listener", 2),
      promiseObservable("prime-event-listener", 2),
    ]);
    await extension.terminateBackground({
      // Calls to `extension.terminateBackground` may not always terminate the background context
      // (e.g. when `pendingRunListenerPromisesCount` is non-zero). This could happen when `testAfterRestart`
      // receives the messages from the background script before the child confirmed the completion
      // of the event dispatched to the parent.
      //
      // When `terminateBackground` is ignored, the event page would terminate at the next scheduled
      // idle check, which defaults to 30 seconds and can cause test timeouts.
      disableResetIdleForTest: true,
    });
    await registrationEvents;
    await testAfterRestart();

    await extension.unload();
    await AddonTestUtils.promiseShutdownManager();
  }
);

add_task(async function test_eventPage_asyncUnregistered_persistentListeners() {
  let extension = ExtensionTestUtils.loadExtension({
    useAddonManager: "permanent",
    manifest: {
      browser_specific_settings: {
        gecko: { id: "eventpage-sync-removed-listeners@test" },
      },
      background: { persistent: false },
    },
    background() {
      let listeners = {
        startupBlocking: arg =>
          browser.test.sendMessage("triggered-startupBlocking", arg),
        nonStartupBlocking: arg =>
          browser.test.sendMessage("triggered-nonStartupBlocking", arg),
      };

      browser.startupBlocking.onEvent1.addListener(listeners.startupBlocking);
      browser.nonStartupBlocking.onEvent1.addListener(
        listeners.nonStartupBlocking
      );

      if (localStorage.getItem("sync-unregister-listeners")) {
        browser.test.log("Removing listeners synchronously");
        browser.startupBlocking.onEvent1.removeListener(
          listeners.startupBlocking
        );
        browser.nonStartupBlocking.onEvent1.removeListener(
          listeners.nonStartupBlocking
        );
      }

      browser.test.onMessage.addListener((msg, ...args) => {
        switch (msg) {
          case "async-unregister-listener": {
            const apiNs = args[0];
            browser[apiNs].onEvent1.removeListener(listeners[apiNs]);
            break;
          }
          case "sync-unregister-listeners-next": {
            localStorage.setItem("sync-unregister-listeners", true);
            break;
          }
          default:
            browser.test.fail(`Got unexpected test message: ${msg}`);
        }
        browser.test.sendMessage(`${msg}:done`);
      });
      browser.test.sendMessage("bg_started");
    },
  });

  async function unregisterListeners() {
    info("Unregister startupBlocking listener");
    extension.sendMessage("async-unregister-listener", "startupBlocking");
    await extension.awaitMessage("async-unregister-listener:done");
    info("Unregister nonStartupBlocking listener");
    extension.sendMessage("async-unregister-listener", "nonStartupBlocking");
    await extension.awaitMessage("async-unregister-listener:done");
    assertPersistentListeners(extension, "nonStartupBlocking", "onEvent1", {
      primed: false,
      persisted: true,
    });
    assertPersistentListeners(extension, "startupBlocking", "onEvent1", {
      primed: false,
      persisted: true,
    });
  }

  await AddonTestUtils.promiseStartupManager();

  await Promise.all([
    promiseObservable("register-event-listener", 2),
    extension.startup(),
  ]);
  await extension.awaitMessage("bg_started");

  info(
    "Verify new persistent listeners are still persisted if removed asynchronously"
  );
  await unregisterListeners();

  info("Verify that unregistered persistent listeners are primed as expected");
  await Promise.all([
    promiseObservable("prime-event-listener", 2),
    extension.terminateBackground(),
  ]);
  assertPersistentListeners(extension, "nonStartupBlocking", "onEvent1", {
    primed: true,
  });
  assertPersistentListeners(extension, "startupBlocking", "onEvent1", {
    primed: true,
  });

  info("Verify listeners are primed after AOM restart");
  await Promise.all([
    new Promise(resolve => extension.extension.once("shutdown", resolve)),
    AddonTestUtils.promiseShutdownManager(),
  ]);
  await AddonTestUtils.promiseStartupManager({ lateStartup: false });
  await extension.awaitStartup();

  assertPersistentListeners(extension, "nonStartupBlocking", "onEvent1", {
    primed: false,
    persisted: true,
  });
  assertPersistentListeners(extension, "startupBlocking", "onEvent1", {
    primed: true,
  });

  let converted = promiseObservable(
    "convert-event-listener",
    // Only startupBlocking is primed and expected to be converted.
    1
  );
  Services.obs.notifyObservers(
    { listenerArgs: "eventReceivedData" },
    "fire-startupBlocking.onEvent1"
  );
  await extension.awaitMessage("bg_started");
  await converted;
  equal(
    await extension.awaitMessage("triggered-startupBlocking"),
    "eventReceivedData",
    "triggered startupBlocking listener"
  );

  info(
    "Verify existing persisted listeners are still persisted when removed asynchronously"
  );
  // Sanity check
  assertPersistentListeners(extension, "nonStartupBlocking", "onEvent1", {
    primed: false,
    persisted: true,
  });
  assertPersistentListeners(extension, "startupBlocking", "onEvent1", {
    primed: false,
    persisted: true,
  });

  await unregisterListeners();

  info(
    "Prepare test extension to synchronously unregister the primed listeners on next event page startup"
  );
  extension.sendMessage("sync-unregister-listeners-next");
  await extension.awaitMessage("sync-unregister-listeners-next:done");
  await extension.terminateBackground();
  Services.obs.notifyObservers(
    { listenerArgs: "eventReceivedData" },
    "fire-nonStartupBlocking.onEvent1"
  );
  await extension.awaitMessage("bg_started");

  info(
    "Verify listeners are still persisted after being removed synchronously"
  );
  assertPersistentListeners(extension, "nonStartupBlocking", "onEvent1", {
    persisted: true,
  });
  assertPersistentListeners(extension, "startupBlocking", "onEvent1", {
    persisted: true,
  });

  await extension.unload();
  await AddonTestUtils.promiseShutdownManager();
});

// This test verifies primeListener behavior for errors or ignored listeners.
add_task(async function test_background_primeListener_errors() {
  await AddonTestUtils.promiseStartupManager();

  // The internal APIs to shutdown the background work with any
  // background, and in the shutdown case, events will be persisted
  // and primed for a restart.
  let extension = ExtensionTestUtils.loadExtension({
    useAddonManager: "permanent",
    background() {
      // Listen for options being set so a restart will have them.
      browser.test.onMessage.addListener(async (message, options) => {
        if (message == "set-options") {
          await browser.startupBlocking.testOptions(options);
          browser.test.sendMessage("set-options:done");
        }
      });
      let listener = arg => browser.test.sendMessage("triggered", arg);
      browser.startupBlocking.onEvent1.addListener(listener, "triggered");
      let listener2 = arg => browser.test.sendMessage("triggered", arg);
      browser.startupBlocking.onEvent2.addListener(listener2, "triggered");
      browser.test.sendMessage("bg_started");
    },
  });
  await Promise.all([
    promiseObservable("register-event-listener", 1),
    extension.startup(),
  ]);
  await extension.awaitMessage("bg_started");
  assertPersistentListeners(extension, "startupBlocking", "onEvent1", {
    primed: false,
  });

  // If an event is removed from an api, a permission is removed,
  // or some other option prevents priming, ensure that
  // primelistener works correctly.
  // In this scenario we are testing that an event is not renewed
  // on startup because the API does not re-prime it.  The result
  // is that the event is also not persisted.  However the other
  // events that are renewed should still be primed and persisted.
  extension.sendMessage("set-options", {
    eventName: "onEvent1",
    ignoreListener: true,
  });
  await extension.awaitMessage("set-options:done");

  // Shutdown the background page
  await Promise.all([
    promiseObservable("unregister-event-listener", 2),
    extension.terminateBackground(),
  ]);
  // startupBlocking.onEvent1 was not re-primed and should not be persisted, but
  // onEvent2 should still be primed and persisted.
  assertPersistentListeners(extension, "startupBlocking", "onEvent1", {
    primed: false,
    persisted: false,
  });
  assertPersistentListeners(extension, "startupBlocking", "onEvent2", {
    primed: true,
  });

  info("Triggering persistent event to force the background page to start");
  Services.obs.notifyObservers(
    { listenerArgs: 123 },
    "fire-startupBlocking.onEvent2"
  );
  await extension.awaitMessage("bg_started");
  equal(await extension.awaitMessage("triggered"), 123, "triggered event");

  // On restart, test an exception, it should not be re-primed.
  extension.sendMessage("set-options", {
    eventName: "onEvent1",
    throwError: "error",
  });
  await extension.awaitMessage("set-options:done");

  // Shutdown the background page
  await Promise.all([
    promiseObservable("unregister-event-listener", 1),
    extension.terminateBackground(),
  ]);
  // startupBlocking.onEvent1 failed and should not be persisted
  assertPersistentListeners(extension, "startupBlocking", "onEvent1", {
    primed: false,
    persisted: false,
  });

  info("Triggering event to verify background starts after prior error");
  Services.obs.notifyObservers(
    { listenerArgs: 123 },
    "fire-startupBlocking.onEvent2"
  );
  await extension.awaitMessage("bg_started");
  equal(await extension.awaitMessage("triggered"), 123, "triggered event");

  info("reset options for next test");
  extension.sendMessage("set-options", {});
  await extension.awaitMessage("set-options:done");

  // Test errors on app restart
  info("Test errors during app startup");
  await AddonTestUtils.promiseRestartManager();
  await extension.awaitStartup();
  await extension.awaitMessage("bg_started");

  info("restart AOM and verify primed listener");
  await AddonTestUtils.promiseRestartManager({ earlyStartup: false });
  await extension.awaitStartup();
  assertPersistentListeners(extension, "startupBlocking", "onEvent1", {
    primed: true,
    persisted: true,
  });
  AddonTestUtils.notifyEarlyStartup();

  Services.obs.notifyObservers(
    { listenerArgs: 123 },
    "fire-startupBlocking.onEvent1"
  );
  await extension.awaitMessage("bg_started");
  equal(await extension.awaitMessage("triggered"), 123, "triggered event");

  // Test that an exception happening during priming clears the
  // event from being persisted when restarting the browser, and that
  // the background correctly starts.
  info("test exception during primeListener on startup");
  extension.sendMessage("set-options", {
    eventName: "onEvent1",
    throwError: "error",
  });
  await extension.awaitMessage("set-options:done");

  await AddonTestUtils.promiseRestartManager({ earlyStartup: false });
  await extension.awaitStartup();
  AddonTestUtils.notifyEarlyStartup();

  // At this point, the exception results in the persisted entry
  // being cleared.
  assertPersistentListeners(extension, "startupBlocking", "onEvent1", {
    primed: false,
    persisted: false,
  });

  AddonTestUtils.notifyLateStartup();

  await extension.awaitMessage("bg_started");

  // The background added the listener back during top level execution,
  // verify it is in the persisted list.
  assertPersistentListeners(extension, "startupBlocking", "onEvent1", {
    primed: false,
    persisted: true,
  });

  // reset options
  extension.sendMessage("set-options", {});
  await extension.awaitMessage("set-options:done");
  await extension.unload();
  await AddonTestUtils.promiseShutdownManager();
});

add_task(async function test_non_background_context_listener_not_persisted() {
  await AddonTestUtils.promiseStartupManager();

  let extension = ExtensionTestUtils.loadExtension({
    useAddonManager: "permanent",
    background() {
      let listener = arg => browser.test.sendMessage("triggered", arg);
      browser.startupBlocking.onEvent1.addListener(listener, "triggered");
      browser.test.sendMessage(
        "bg_started",
        browser.runtime.getURL("extpage.html")
      );
    },
    files: {
      "extpage.html": `<script src="extpage.js"></script>`,
      "extpage.js": function () {
        let listener = arg =>
          browser.test.sendMessage("extpage-triggered", arg);
        browser.startupBlocking.onEvent2.addListener(
          listener,
          "extpage-triggered"
        );
        // Send a message to signal the extpage has registered the listener,
        // after calling an async method and wait it to be resolved to make sure
        // the addListener call to have been handled in the parent process by
        // the time we will assert the persisted listeners.
        browser.runtime.getPlatformInfo().then(() => {
          browser.test.sendMessage("extpage_started");
        });
      },
    },
  });

  await extension.startup();
  const extpage_url = await extension.awaitMessage("bg_started");

  assertPersistentListeners(extension, "startupBlocking", "onEvent1", {
    persisted: true,
    primed: false,
  });

  assertPersistentListeners(extension, "startupBlocking", "onEvent2", {
    persisted: false,
  });

  const page = await ExtensionTestUtils.loadContentPage(extpage_url);
  await extension.awaitMessage("extpage_started");

  // Expect the onEvent2 listener subscribed by the extpage to not be persisted.
  assertPersistentListeners(extension, "startupBlocking", "onEvent2", {
    persisted: false,
  });

  await page.close();
  await extension.unload();
  await AddonTestUtils.promiseShutdownManager();
});

// Test support for event page tests
const background = async function () {
  let listener2 = () =>
    browser.test.sendMessage("triggered:non-startupblocking");
  browser.startupBlocking.onEvent1.addListener(() => {});
  browser.startupBlocking.nonBlockingEvent.addListener(() => {});
  browser.nonStartupBlocking.onEvent2.addListener(listener2);
  browser.test.sendMessage("bg_started");
};

const background_update = async function () {
  browser.startupBlocking.onEvent1.addListener(() => {});
  browser.nonStartupBlocking.onEvent2.addListener(() => {});
  browser.test.sendMessage("updated_bg_started");
};

function testPersistentListeners(extension, expect) {
  for (let [ns, event, persisted, primed] of expect) {
    assertPersistentListeners(extension, ns, event, {
      persisted,
      primed,
    });
  }
}

add_task(
  { pref_set: [["extensions.eventPages.enabled", true]] },
  async function test_startupblocking_behavior() {
    await AddonTestUtils.promiseStartupManager();

    let extension = ExtensionTestUtils.loadExtension({
      useAddonManager: "permanent",
      manifest: {
        background: { persistent: false },
      },
      background,
    });

    await extension.startup();
    await extension.awaitMessage("bg_started");

    // All are persisted on startup
    testPersistentListeners(extension, [
      ["startupBlocking", "onEvent1", true, false],
      ["startupBlocking", "nonBlockingEvent", true, false],
      ["nonStartupBlocking", "onEvent2", true, false],
    ]);

    info("Test after mocked browser restart");
    await Promise.all([
      new Promise(resolve => extension.extension.once("shutdown", resolve)),
      AddonTestUtils.promiseShutdownManager(),
    ]);
    await AddonTestUtils.promiseStartupManager({ lateStartup: false });
    await extension.awaitStartup();

    testPersistentListeners(extension, [
      // Startup blocking event is expected to be persisted and primed.
      ["startupBlocking", "onEvent1", true, true],
      // A non-startup-blocking event shouldn't be primed yet.
      ["startupBlocking", "nonBlockingEvent", true, false],
      // Non "Startup blocking" event is expected to be persisted but not primed yet.
      ["nonStartupBlocking", "onEvent2", true, false],
    ]);

    // Complete the browser startup and fire the startup blocking event
    // to let the backgrund script to run.
    AddonTestUtils.notifyLateStartup();
    Services.obs.notifyObservers({}, "fire-startupBlocking.onEvent1");
    await extension.awaitMessage("bg_started");

    info("Test after terminate background script");
    await extension.terminateBackground();

    // After the background is terminated, all are persisted and primed.
    testPersistentListeners(extension, [
      ["startupBlocking", "onEvent1", true, true],
      ["startupBlocking", "nonBlockingEvent", true, true],
      ["nonStartupBlocking", "onEvent2", true, true],
    ]);

    info("Notify event for the non-startupBlocking API event");
    Services.obs.notifyObservers({}, "fire-nonStartupBlocking.onEvent2");
    await extension.awaitMessage("bg_started");
    await extension.awaitMessage("triggered:non-startupblocking");

    await extension.unload();
    await AddonTestUtils.promiseShutdownManager();
  }
);

add_task(
  { pref_set: [["extensions.eventPages.enabled", true]] },
  async function test_startupblocking_behavior_upgrade() {
    let id = "persistent-upgrade@test";
    await AddonTestUtils.promiseStartupManager();

    let extensionData = {
      useAddonManager: "permanent",
      manifest: {
        version: "1.0",
        browser_specific_settings: {
          gecko: { id },
        },
        background: { persistent: false },
      },
      background,
    };
    let extension = ExtensionTestUtils.loadExtension(extensionData);
    await extension.startup();
    await extension.awaitMessage("bg_started");

    // All are persisted on startup
    testPersistentListeners(extension, [
      ["startupBlocking", "onEvent1", true, false],
      ["startupBlocking", "nonBlockingEvent", true, false],
      ["nonStartupBlocking", "onEvent2", true, false],
    ]);

    // Prepare the extension that will be updated.
    extensionData.manifest.version = "2.0";
    extensionData.background = background_update;

    info("Test after a upgrade");
    await extension.upgrade(extensionData);
    // upgrade should start the background
    await extension.awaitMessage("updated_bg_started");

    // Nothing should be primed at this point after the background
    // has started.  We look specifically for nonBlockingEvent to
    // no longer be a part of the persisted listeners.
    testPersistentListeners(extension, [
      ["startupBlocking", "onEvent1", true, false],
      ["startupBlocking", "nonBlockingEvent", false, false],
      ["nonStartupBlocking", "onEvent2", true, false],
    ]);

    await extension.unload();
    await AddonTestUtils.promiseShutdownManager();
  }
);

add_task(
  { pref_set: [["extensions.eventPages.enabled", true]] },
  async function test_startupblocking_behavior_staged_upgrade() {
    AddonManager.checkUpdateSecurity = false;
    let id = "persistent-staged-upgrade@test";

    // register an update file.
    AddonTestUtils.registerJSON(server, "/test_update.json", {
      addons: {
        [id]: {
          updates: [
            {
              version: "2.0",
              update_link:
                "http://example.com/addons/test_settings_staged_restart.xpi",
            },
          ],
        },
      },
    });

    let extensionData = {
      useAddonManager: "permanent",
      manifest: {
        version: "2.0",
        browser_specific_settings: {
          gecko: { id, update_url: `http://example.com/test_update.json` },
        },
        background: { persistent: false },
      },
      background: background_update,
    };

    // Prepare the update first.
    server.registerFile(
      `/addons/test_settings_staged_restart.xpi`,
      AddonTestUtils.createTempWebExtensionFile(extensionData)
    );

    // Prepare the extension that will be updated.
    extensionData.manifest.version = "1.0";
    extensionData.background = async function () {
      // we're testing persistence, not operation, so no action in listeners.
      browser.startupBlocking.onEvent1.addListener(() => {});
      // nonBlockingEvent will be removed on upgrade
      browser.startupBlocking.nonBlockingEvent.addListener(() => {});
      browser.nonStartupBlocking.onEvent2.addListener(() => {});

      // Force a staged updated.
      browser.runtime.onUpdateAvailable.addListener(async details => {
        // This should be the version of the pending update.
        browser.test.assertEq("2.0", details.version, "correct version");
        browser.test.sendMessage("delay");
      });

      browser.test.sendMessage("bg_started");
    };

    await AddonTestUtils.promiseStartupManager();
    let extension = ExtensionTestUtils.loadExtension(extensionData);

    await extension.startup();
    await extension.awaitMessage("bg_started");

    // All are persisted but not primed on startup
    testPersistentListeners(extension, [
      ["startupBlocking", "onEvent1", true, false],
      ["startupBlocking", "nonBlockingEvent", true, false],
      ["nonStartupBlocking", "onEvent2", true, false],
    ]);

    info("Test after a staged update");
    // first, deal with getting and staging an upgrade
    let addon = await AddonManager.getAddonByID(id);
    Assert.equal(addon.version, "1.0", "1.0 is loaded");

    let update = await AddonTestUtils.promiseFindAddonUpdates(addon);
    let install = update.updateAvailable;
    Assert.ok(install, `install is available ${update.error}`);

    await AddonTestUtils.promiseCompleteAllInstalls([install]);

    Assert.equal(
      install.state,
      AddonManager.STATE_POSTPONED,
      "update is staged for install"
    );
    await extension.awaitMessage("delay");

    await AddonTestUtils.promiseShutdownManager();

    // restarting allows upgrade to proceed
    await AddonTestUtils.promiseStartupManager();
    // upgrade should always start the background
    await extension.awaitMessage("updated_bg_started");

    // Since this is an upgraded addon, the background will have started
    // and we no longer have primed listeners.  Check only the persisted
    // values, and that nonBlockingEvent is not persisted.
    testPersistentListeners(extension, [
      ["startupBlocking", "onEvent1", true, false],
      ["startupBlocking", "nonBlockingEvent", false, false],
      ["nonStartupBlocking", "onEvent2", true, false],
    ]);

    await extension.unload();
    await AddonTestUtils.promiseShutdownManager();
  }
);

// Regression test for Bug 1795801:
// - verifies that multiple listeners sharing the same event and set of extra
//   params are being stored in the startupData and then all primed on the next
//   startup
// - verifies behaviors expected when startupData stored from an older
//   Firefox version (one that didn't include Bug 1795801 changes) is
//   loaded from a new Firefox version
// - a small smoke test to also verify the behaviors when startupData stored
//   by a newer version is being loaded by an older one (where Bug 1795801
//   changes have not been introduced yet).
add_task(async function test_migrate_startupData_to_new_format() {
  await AddonTestUtils.promiseStartupManager();

  const extension = ExtensionTestUtils.loadExtension({
    useAddonManager: "permanent",
    manifest: {
      background: { persistent: false },
    },
    background() {
      const eventParams = [
        { fromCustomParam1: "value1" },
        ["fromCustomParam2"],
      ];
      const otherEventParams = [
        { fromCustomParam1: "value2" },
        ["fromCustomParam2Other"],
      ];
      browser.nonStartupBlocking.onEvent3.addListener(
        function listener1(arg) {
          browser.test.log("listener1 called on nonStartupBlocking.onEvent3");
          browser.test.sendMessage("listener1", arg);
        },
        ...eventParams
      );
      browser.nonStartupBlocking.onEvent3.addListener(
        function listener2(arg) {
          browser.test.log("listener2 called on nonStartupBlocking.onEvent3");
          browser.test.sendMessage("listener2", arg);
        },
        ...eventParams
      );
      browser.nonStartupBlocking.onEvent3.addListener(
        function listener3(arg) {
          browser.test.log("listener3 called on nonStartupBlocking.onEvent3");
          browser.test.sendMessage("listener3", arg);
        },
        ...otherEventParams
      );
      browser.test.sendMessage("ready");
    },
  });

  // Data expected to be stored in the extension startupData with the new
  // format and old format.
  const STARTUP_DATA = {
    newPersistentListenersFormat: {
      nonStartupBlocking: {
        onEvent3: [
          // 2 listeners registered with the same set of extra params
          [{ fromCustomParam1: "value1" }, ["fromCustomParam2"]],
          [{ fromCustomParam1: "value1" }, ["fromCustomParam2"]],
          // 1 listener registered with different set of extra params
          [{ fromCustomParam1: "value2" }, ["fromCustomParam2Other"]],
        ],
      },
    },
    oldPersistentListenersFormat: {
      nonStartupBlocking: {
        onEvent3: [
          [{ fromCustomParam1: "value1" }, ["fromCustomParam2"]],
          [{ fromCustomParam1: "value2" }, ["fromCustomParam2Other"]],
        ],
      },
    },
  };

  function getXPIStatesFilePath() {
    let { path } = ChromeUtils.importESModule(
      "resource://gre/modules/addons/XPIExports.sys.mjs"
    ).XPIExports.XPIInternal.XPIStates._jsonFile;
    ok(
      typeof path === "string" && !!path.length,
      `Found XPIStates file path: ${path}`
    );
    return path;
  }

  async function tamperStartupData(testExtensionWrapper) {
    const { startupData } = testExtensionWrapper.extension;
    Assert.deepEqual(
      startupData.persistentListeners,
      STARTUP_DATA.newPersistentListenersFormat,
      "Got data stored from extension.startupData.persistentListeners"
    );

    startupData.persistentListeners = STARTUP_DATA.oldPersistentListenersFormat;

    // Force the data to be stored on disk (by requesting AddonTestUtils to flush
    // the XPIStates after having tampered them to make sure they are in the
    // format we expect from older Firefox versions).
    testExtensionWrapper.extension.saveStartupData();
    await AddonTestUtils.loadAddonsList(/* flush */ true);
    const { XPIExports } = ChromeUtils.importESModule(
      "resource://gre/modules/addons/XPIExports.sys.mjs"
    );
    XPIExports.XPIInternal.XPIStates.save();
    await XPIExports.XPIInternal.XPIStates._jsonFile._save();
    return getXPIStatesFilePath();
  }

  async function assertDiskStoredPersistentListeners(
    extensionId,
    xpiStatesPath,
    expectedData
  ) {
    const xpiStatesData = await IOUtils.readJSON(xpiStatesPath, {
      decompress: true,
    });
    const startupData =
      xpiStatesData["app-profile"]?.addons[extensionId]?.startupData;
    ok(startupData, `Found startupData for test extension ${extensionId}`);
    Assert.deepEqual(
      startupData.persistentListeners,
      expectedData,
      "Got the expected tampered addon startupData stored on disk"
    );
  }

  await extension.startup();

  await extension.awaitMessage("ready");
  assertPersistentListeners(extension, "nonStartupBlocking", "onEvent3", {
    persisted: true,
  });

  info(
    "Manually tampering startupData.persistentListeners to match the format older Firefox format"
  );
  const xpiStatesFilePath = await tamperStartupData(extension);
  await AddonTestUtils.promiseShutdownManager();
  await assertDiskStoredPersistentListeners(
    extension.id,
    xpiStatesFilePath,
    STARTUP_DATA.oldPersistentListenersFormat
  );

  info(
    "Confirm that the expected listeners have been primed and the startupData migrated to the new format"
  );

  {
    await AddonTestUtils.promiseStartupManager();
    await extension.awaitStartup;

    assertPersistentListeners(extension, "nonStartupBlocking", "onEvent3", {
      primed: true,
      // Old format of startupData.persistentListeners did not have a listenersCount
      // property and so only two primed listeners are expected on the first startup
      // after the addon startupData have been tampered to match the format expected
      // by an older Firefox version.
      primedListenersCount: 2,
    });

    const promiseListenersConverted = promiseObservable(
      "convert-event-listener",
      2
    );
    Services.obs.notifyObservers(
      { listenerArgs: "test-startup" },
      "fire-nonStartupBlocking.onEvent3"
    );
    await promiseListenersConverted;

    deepEqual(
      await extension.awaitMessage("listener1"),
      "test-startup",
      "Listener1 fired for event during startup"
    );

    deepEqual(
      await extension.awaitMessage("listener3"),
      "test-startup",
      "Listener3 fired for event during startup"
    );

    await extension.awaitMessage("ready");

    Assert.deepEqual(
      extension.extension.startupData.persistentListeners,
      STARTUP_DATA.newPersistentListenersFormat,
      "Got startupData.persistentListeners migrated to the new format"
    );
  }

  info(
    "Confirm that the startupData written on disk have been migrated to the new format"
  );

  await AddonTestUtils.promiseShutdownManager();
  await assertDiskStoredPersistentListeners(
    extension.id,
    xpiStatesFilePath,
    STARTUP_DATA.newPersistentListenersFormat
  );

  info(
    "Verify that both listeners are called after migrating to the new format"
  );
  {
    await AddonTestUtils.promiseStartupManager();
    await extension.awaitStartup;

    assertPersistentListeners(extension, "nonStartupBlocking", "onEvent3", {
      primed: true,
      primedListenersCount: 3,
    });

    const promiseListenersConverted = promiseObservable(
      "convert-event-listener",
      2
    );
    Services.obs.notifyObservers(
      { listenerArgs: "test-startup" },
      "fire-nonStartupBlocking.onEvent3"
    );
    await promiseListenersConverted;

    // Now we expect both the listeners to have been called.
    deepEqual(
      await extension.awaitMessage("listener1"),
      "test-startup",
      "Listener1 fired for event during startup"
    );

    deepEqual(
      await extension.awaitMessage("listener2"),
      "test-startup",
      "Listener2 fired for event during startup"
    );

    deepEqual(
      await extension.awaitMessage("listener3"),
      "test-startup",
      "Listener3 fired for event during startup"
    );

    await extension.awaitMessage("ready");
  }

  await extension.unload();
  await AddonTestUtils.promiseShutdownManager();

  // The additional assertions below are meant to provide a smoke test covering
  // the behavior we would expect if an older Firefox versions (one that would
  // expect the old format) is loading persistentListeners from startupData
  // using the new format.
  info("Verify backward compatibility with old format");

  const { ExtensionUtils } = ChromeUtils.importESModule(
    "resource://gre/modules/ExtensionUtils.sys.mjs"
  );
  const { DefaultMap } = ExtensionUtils;
  const loadedListeners = new DefaultMap(() => new DefaultMap(() => new Map()));

  // Logic from older Firefox versions expecting the old format
  // (https://searchfox.org/mozilla-central/rev/cd2121e7d8/toolkit/components/extensions/ExtensionCommon.jsm#2360-2371)
  let found = false;
  for (let [module, entry] of Object.entries(
    STARTUP_DATA.newPersistentListenersFormat
  )) {
    for (let [event, paramlists] of Object.entries(entry)) {
      for (let paramlist of paramlists) {
        let key = uneval(paramlist);
        loadedListeners.get(module).get(event).set(key, { params: paramlist });
        found = true;
      }
    }
  }

  Assert.ok(
    found,
    "Expect persistentListeners to have been found from the old loading logic"
  );

  // We expect the older Firefox version to don't choke on loading
  // the new format, a primed listener is still expected to be
  // found because the old Firefox version will be overriding a single
  // entry in the inmemory Map with the multiple entries from the
  // ondisk format listing the same extra params for multiple listeners,
  // Bug 1795801 would still be hit, but no other change in behavior is
  // expected to be hit with the old logic.
  Assert.ok(
    loadedListeners
      .get("nonStartupBlocking")
      .get("onEvent3")
      .has(uneval([{ fromCustomParam1: "value1" }, ["fromCustomParam2"]])),
    "Expect the listener params key to be found in older Firefox versions"
  );
});

add_task(
  { pref_set: [["extensions.eventPages.enabled", true]] },
  async function test_resetOnIdleOnEvent_false_on_other_extpages() {
    await AddonTestUtils.promiseStartupManager();

    const extension = ExtensionTestUtils.loadExtension({
      useAddonManager: "temporary",
      files: {
        "extpage.html": `<!DOCTYPE html><script src="extpage.js"></script>`,
        "extpage.js": function () {
          // We expect this to throw if the EventManagerWithAssertions constructor
          // throws when asserting that resetIdleOnEvent was forcefully set to
          // false for a non-event page context.
          browser.nonStartupBlocking.onEvent2.addListener(() => {});
          browser.test.sendMessage("extpage:loaded");
        },
      },
    });

    await extension.startup();

    const awaitRegisteredEventListener = promiseObservable(
      "register-event-listener",
      1
    );
    const page = await ExtensionTestUtils.loadContentPage(
      `moz-extension://${extension.uuid}/extpage.html`
    );

    info("Wait for the extension page script to complete");

    await Promise.all([
      extension.awaitMessage("extpage:loaded"),
      awaitRegisteredEventListener,
    ]);
    await page.close();

    await extension.unload();
    await AddonTestUtils.promiseShutdownManager();
  }
);
