"use strict";

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

AddonTestUtils.init(this);
AddonTestUtils.overrideCertDB();

// The app and platform version here should be >= of the version set in the extensions.webExtensionsMinPlatformVersion preference,
// otherwise test_persistent_listener_after_staged_update will fail because no compatible updates will be found.
AddonTestUtils.createAppInfo(
  "xpcshell@tests.mozilla.org",
  "XPCShell",
  "42",
  "42"
);

let { promiseShutdownManager, promiseStartupManager, promiseRestartManager } =
  AddonTestUtils;

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

let scopes = AddonManager.SCOPE_PROFILE | AddonManager.SCOPE_APPLICATION;
Services.prefs.setIntPref("extensions.enabledScopes", scopes);

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;
}

/**
 * That that we get the expected events
 *
 * @param {Extension} extension
 * @param {Map} events
 * @param {object} expect
 * @param {boolean} expect.background   delayed startup event expected
 * @param {boolean} expect.started      background has already started
 * @param {boolean} expect.delayedStart startup is delayed, notify start and
 *                                      expect the starting event
 * @param {boolean} expect.request      wait for the request event
 */
async function testPersistentRequestStartup(extension, events, expect = {}) {
  equal(
    events.get("background-script-event"),
    !!expect.background,
    "Should have gotten a background script event"
  );
  equal(
    events.get("start-background-script"),
    !!expect.started,
    "Background script should be started"
  );

  if (!expect.started) {
    AddonTestUtils.notifyEarlyStartup();
    await ExtensionParent.browserPaintedPromise;

    equal(
      events.get("start-background-script"),
      !!expect.delayedStart,
      "Should have gotten start-background-script event"
    );
  }

  if (expect.request) {
    await extension.awaitMessage("got-request");
    ok(true, "Background page loaded and received webRequest event");
  }
}

// Test that a non-blocking listener does not start the background on
// startup, but that it does work after startup.
add_task(async function test_nonblocking() {
  await promiseStartupManager();

  let extension = ExtensionTestUtils.loadExtension({
    useAddonManager: "permanent",
    manifest: {
      permissions: ["webRequest", "http://example.com/"],
    },

    background() {
      browser.webRequest.onBeforeRequest.addListener(
        () => {
          browser.test.sendMessage("got-request");
        },
        { urls: ["http://example.com/data/file_sample.html"] }
      );
      browser.test.sendMessage("ready");
    },
  });

  // First install runs background immediately, this sets persistent listeners
  await extension.startup();
  await extension.awaitMessage("ready");

  // Restart to get APP_STARTUP, the background should not start
  await promiseRestartManager({ lateStartup: false });
  await extension.awaitStartup();
  assertPersistentListeners(extension, "webRequest", "onBeforeRequest", {
    primed: false,
  });

  // Test an early startup event
  let events = trackEvents(extension);

  await ExtensionTestUtils.fetch(
    "http://example.com/",
    "http://example.com/data/file_sample.html"
  );

  await testPersistentRequestStartup(extension, events, {
    background: false,
    delayedStart: false,
    request: false,
  });

  AddonTestUtils.notifyLateStartup();
  await extension.awaitMessage("ready");
  assertPersistentListeners(extension, "webRequest", "onBeforeRequest", {
    primed: false,
  });

  // Test an event after startup
  await ExtensionTestUtils.fetch(
    "http://example.com/",
    "http://example.com/data/file_sample.html"
  );

  await testPersistentRequestStartup(extension, events, {
    background: false,
    started: true,
    request: true,
  });

  await extension.unload();

  await promiseShutdownManager();
});

// Test that a non-blocking listener does not start the background on
// startup, but that it does work after startup.
add_task(async function test_eventpage_nonblocking() {
  Services.prefs.setBoolPref("extensions.eventPages.enabled", true);
  await promiseStartupManager();

  let id = "event-nonblocking@test";
  let extension = ExtensionTestUtils.loadExtension({
    useAddonManager: "permanent",
    manifest: {
      browser_specific_settings: { gecko: { id } },
      permissions: ["webRequest", "http://example.com/"],
      background: { persistent: false },
    },

    background() {
      browser.webRequest.onBeforeRequest.addListener(
        () => {
          browser.test.sendMessage("got-request");
        },
        { urls: ["http://example.com/data/file_sample.html"] }
      );
    },
  });

  // First install runs background immediately, this sets persistent listeners
  await extension.startup();

  // Restart to get APP_STARTUP, the background should not start
  await promiseRestartManager({ lateStartup: false });
  await extension.awaitStartup();
  assertPersistentListeners(extension, "webRequest", "onBeforeRequest", {
    primed: false,
  });

  // Test an early startup event
  let events = trackEvents(extension);

  await ExtensionTestUtils.fetch(
    "http://example.com/",
    "http://example.com/data/file_sample.html"
  );

  await testPersistentRequestStartup(extension, events);

  await AddonTestUtils.notifyLateStartup();
  // After late startup, event page listeners should be primed.
  assertPersistentListeners(extension, "webRequest", "onBeforeRequest", {
    primed: true,
  });

  // We should not have seen any events yet.
  await testPersistentRequestStartup(extension, events);

  // Test an event after startup
  await ExtensionTestUtils.fetch(
    "http://example.com/",
    "http://example.com/data/file_sample.html"
  );

  // Now the event page should be started and we'll see the request.
  await testPersistentRequestStartup(extension, events, {
    background: true,
    started: true,
    request: true,
  });

  await extension.unload();

  await promiseShutdownManager();
  Services.prefs.setBoolPref("extensions.eventPages.enabled", false);
});

// Tests that filters are handled properly: if we have a blocking listener
// with a filter, a request that does not match the filter does not get
// suspended and does not start the background page.
add_task(async function test_persistent_blocking() {
  await promiseStartupManager();

  let extension = ExtensionTestUtils.loadExtension({
    useAddonManager: "permanent",
    manifest: {
      permissions: [
        "webRequest",
        "webRequestBlocking",
        "http://test1.example.com/",
      ],
    },

    background() {
      browser.webRequest.onBeforeRequest.addListener(
        () => {
          browser.test.fail("Listener should not have been called");
        },
        { urls: ["http://test1.example.com/*"] },
        ["blocking"]
      );
    },
  });

  await extension.startup();
  assertPersistentListeners(extension, "webRequest", "onBeforeRequest", {
    primed: false,
  });

  await promiseRestartManager({ lateStartup: false });
  await extension.awaitStartup();
  assertPersistentListeners(extension, "webRequest", "onBeforeRequest", {
    primed: true,
  });

  let events = trackEvents(extension);

  await ExtensionTestUtils.fetch(
    "http://example.com/",
    "http://example.com/data/file_sample.html"
  );

  await testPersistentRequestStartup(extension, events, {
    background: false,
    delayedStart: false,
    request: false,
  });

  AddonTestUtils.notifyLateStartup();

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

// Tests that moving permission to optional retains permission and that the
// persistent listeners are used as expected.
add_task(async function test_persistent_listener_after_sideload_upgrade() {
  let id = "permission-sideload-upgrade@test";
  let extensionData = {
    useAddonManager: "permanent",
    manifest: {
      version: "1.0",
      browser_specific_settings: { gecko: { id } },
      permissions: ["webRequest", "webRequestBlocking", "http://example.com/"],
    },

    background() {
      browser.webRequest.onBeforeRequest.addListener(
        () => {
          browser.test.sendMessage("got-request");
        },
        { urls: ["http://example.com/data/file_sample.html"] },
        ["blocking"]
      );
    },
  };
  let xpi = AddonTestUtils.createTempWebExtensionFile(extensionData);

  let extension = ExtensionTestUtils.expectExtension(id);
  await AddonTestUtils.manuallyInstall(xpi);
  await promiseStartupManager();
  await extension.awaitStartup();
  // Sideload install does not prime listeners
  assertPersistentListeners(extension, "webRequest", "onBeforeRequest", {
    primed: false,
  });

  await ExtensionTestUtils.fetch(
    "http://example.com/",
    "http://example.com/data/file_sample.html"
  );
  await extension.awaitMessage("got-request");

  await promiseShutdownManager();

  // Prepare a sideload update for the extension.
  extensionData.manifest.version = "2.0";
  extensionData.manifest.permissions = ["http://example.com/"];
  extensionData.manifest.optional_permissions = [
    "webRequest",
    "webRequestBlocking",
  ];
  xpi = AddonTestUtils.createTempWebExtensionFile(extensionData);
  await AddonTestUtils.manuallyInstall(xpi);

  await promiseStartupManager();
  await extension.awaitStartup();
  // Upgrades start the background when the extension is loaded, so
  // primed listeners are cleared already and background events are
  // already completed.
  assertPersistentListeners(extension, "webRequest", "onBeforeRequest", {
    primed: false,
    persisted: true,
  });

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

// Utility to install builtin addon
async function installBuiltinExtension(extensionData) {
  let xpi = await AddonTestUtils.createTempWebExtensionFile(extensionData);

  // The built-in location requires a resource: URL that maps to a
  // jar: or file: URL.  This would typically be something bundled
  // into omni.ja but for testing we just use a temp file.
  let base = Services.io.newURI(`jar:file:${xpi.path}!/`);
  let resProto = Services.io
    .getProtocolHandler("resource")
    .QueryInterface(Ci.nsIResProtocolHandler);
  resProto.setSubstitution("ext-test", base);
  return AddonManager.installBuiltinAddon("resource://ext-test/");
}

function promisePostponeInstall(install) {
  return new Promise((resolve, reject) => {
    let listener = {
      onInstallFailed: () => {
        install.removeListener(listener);
        reject(new Error("extension installation should not have failed"));
      },
      onInstallEnded: () => {
        install.removeListener(listener);
        reject(
          new Error(
            `extension installation should not have ended for ${install.addon.id}`
          )
        );
      },
      onInstallPostponed: () => {
        install.removeListener(listener);
        resolve();
      },
    };

    install.addListener(listener);
    install.install();
  });
}

// Tests that moving permission to optional retains permission and that the
// persistent listeners are used as expected.
add_task(
  async function test_persistent_listener_after_builtin_location_upgrade() {
    let id = "permission-builtin-upgrade@test";
    let extensionData = {
      useAddonManager: "permanent",
      manifest: {
        version: "1.0",
        browser_specific_settings: { gecko: { id } },
        permissions: [
          "webRequest",
          "webRequestBlocking",
          "http://example.com/",
        ],
      },

      async background() {
        browser.runtime.onUpdateAvailable.addListener(() => {
          browser.test.sendMessage("postponed");
        });

        browser.webRequest.onBeforeRequest.addListener(
          () => {
            browser.test.sendMessage("got-request");
          },
          { urls: ["http://example.com/data/file_sample.html"] },
          ["blocking"]
        );
      },
    };
    await promiseStartupManager();
    // If we use an extension wrapper via ExtensionTestUtils.expectExtension
    // it will continue to handle messages even after the update, resulting
    // in errors when it receives additional messages without any awaitMessage.
    let promiseExtension = AddonTestUtils.promiseWebExtensionStartup(id);
    await installBuiltinExtension(extensionData);
    let extv1 = await promiseExtension;
    assertPersistentListeners(
      { extension: extv1 },
      "webRequest",
      "onBeforeRequest",
      {
        primed: false,
      }
    );

    // Prepare an update for the extension.
    extensionData.manifest.version = "2.0";
    let xpi = AddonTestUtils.createTempWebExtensionFile(extensionData);
    let install = await AddonManager.getInstallForFile(xpi);

    // Install the update and wait for the onUpdateAvailable event to complete.
    let promiseUpdate = new Promise(resolve =>
      extv1.once("test-message", (kind, msg) => {
        if (msg == "postponed") {
          resolve();
        }
      })
    );
    await Promise.all([promisePostponeInstall(install), promiseUpdate]);
    await promiseShutdownManager();

    // restarting allows upgrade to proceed
    let extension = ExtensionTestUtils.expectExtension(id);
    await promiseStartupManager();
    await extension.awaitStartup();
    // Upgrades start the background when the extension is loaded, so
    // primed listeners are cleared already and background events are
    // already completed.
    assertPersistentListeners(extension, "webRequest", "onBeforeRequest", {
      primed: false,
      persisted: true,
    });

    await extension.unload();

    // remove the builtin addon which will have restarted now.
    let addon = await AddonManager.getAddonByID(id);
    await addon.uninstall();

    await promiseShutdownManager();
  }
);

// Tests that moving permission to optional during a staged upgrade retains permission
// and that the persistent listeners are used as expected.
add_task(async function test_persistent_listener_after_staged_upgrade() {
  AddonManager.checkUpdateSecurity = false;
  let id = "persistent-staged-upgrade@test";

  // register an update file.
  AddonTestUtils.registerJSON(server, "/test_update.json", {
    addons: {
      "persistent-staged-upgrade@test": {
        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` },
      },
      permissions: ["http://example.com/"],
      optional_permissions: ["webRequest", "webRequestBlocking"],
    },

    background() {
      browser.webRequest.onBeforeRequest.addListener(
        () => {
          browser.test.sendMessage("got-request");
        },
        { urls: ["http://example.com/data/file_sample.html"] },
        ["blocking"]
      );
      browser.webRequest.onSendHeaders.addListener(
        () => {
          browser.test.sendMessage("got-sendheaders");
        },
        { urls: ["http://example.com/data/file_sample.html"] }
      );
      // Force a staged updated.
      browser.runtime.onUpdateAvailable.addListener(async details => {
        if (details && details.version) {
          // This should be the version of the pending update.
          browser.test.assertEq("2.0", details.version, "correct version");
          browser.test.sendMessage("delay");
        }
      });
    },
  };

  // 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.manifest.permissions = [
    "webRequest",
    "webRequestBlocking",
    "http://example.com/",
  ];
  delete extensionData.manifest.optional_permissions;
  extensionData.background = function () {
    browser.webRequest.onBeforeRequest.addListener(
      () => {
        browser.test.sendMessage("got-request");
      },
      { urls: ["http://example.com/data/file_sample.html"] },
      ["blocking"]
    );
    browser.webRequest.onBeforeSendHeaders.addListener(
      () => {
        browser.test.sendMessage("got-beforesendheaders");
      },
      { urls: ["http://example.com/data/file_sample.html"] }
    );
    browser.webRequest.onSendHeaders.addListener(
      () => {
        browser.test.sendMessage("got-sendheaders");
      },
      { urls: ["http://example.com/data/file_sample.html"] }
    );
    // Force a staged updated.
    browser.runtime.onUpdateAvailable.addListener(async details => {
      if (details && details.version) {
        // This should be the version of the pending update.
        browser.test.assertEq("2.0", details.version, "correct version");
        browser.test.sendMessage("delay");
      }
    });
  };

  await promiseStartupManager();
  let extension = ExtensionTestUtils.loadExtension(extensionData);
  await extension.startup();
  assertPersistentListeners(extension, "webRequest", "onBeforeRequest", {
    primed: false,
  });
  assertPersistentListeners(extension, "webRequest", "onBeforeSendHeaders", {
    primed: false,
  });
  assertPersistentListeners(extension, "webRequest", "onSendHeaders", {
    primed: false,
  });

  await ExtensionTestUtils.fetch(
    "http://example.com/",
    "http://example.com/data/file_sample.html"
  );
  await extension.awaitMessage("got-request");
  await extension.awaitMessage("got-beforesendheaders");
  await extension.awaitMessage("got-sendheaders");
  ok(true, "Initial version received webRequest event");

  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 promiseShutdownManager();

  // restarting allows upgrade to proceed
  await promiseStartupManager();
  await extension.awaitStartup();

  // Upgrades start the background when the extension is loaded, so
  // primed listeners are cleared already and background events are
  // already completed.
  assertPersistentListeners(extension, "webRequest", "onBeforeRequest", {
    primed: false,
    persisted: true,
  });
  // this was removed in the upgrade background, should not be persisted.
  assertPersistentListeners(extension, "webRequest", "onBeforeSendHeaders", {
    primed: false,
    persisted: false,
  });
  assertPersistentListeners(extension, "webRequest", "onSendHeaders", {
    primed: false,
    persisted: true,
  });

  await extension.unload();
  await promiseShutdownManager();
  AddonManager.checkUpdateSecurity = true;
});

// Tests that removing the permission releases the persistent listener.
add_task(async function test_persistent_listener_after_permission_removal() {
  AddonManager.checkUpdateSecurity = false;
  let id = "persistent-staged-remove@test";

  // register an update file.
  AddonTestUtils.registerJSON(server, "/test_remove.json", {
    addons: {
      "persistent-staged-remove@test": {
        updates: [
          {
            version: "2.0",
            update_link:
              "http://example.com/addons/test_settings_staged_remove.xpi",
          },
        ],
      },
    },
  });

  let extensionData = {
    useAddonManager: "permanent",
    manifest: {
      version: "2.0",
      browser_specific_settings: {
        gecko: { id, update_url: `http://example.com/test_remove.json` },
      },
      permissions: ["tabs", "http://example.com/"],
    },

    background() {
      browser.test.sendMessage("loaded");
    },
  };

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

  await promiseStartupManager();
  let extension = ExtensionTestUtils.loadExtension({
    useAddonManager: "permanent",
    manifest: {
      version: "1.0",
      browser_specific_settings: {
        gecko: { id, update_url: `http://example.com/test_remove.json` },
      },
      permissions: ["webRequest", "webRequestBlocking", "http://example.com/"],
    },

    background() {
      browser.webRequest.onBeforeRequest.addListener(
        () => {
          browser.test.sendMessage("got-request");
        },
        { urls: ["http://example.com/data/file_sample.html"] },
        ["blocking"]
      );
      // Force a staged updated.
      browser.runtime.onUpdateAvailable.addListener(async details => {
        if (details && details.version) {
          // This should be the version of the pending update.
          browser.test.assertEq("2.0", details.version, "correct version");
          browser.test.sendMessage("delay");
        }
      });
    },
  });

  await extension.startup();

  await ExtensionTestUtils.fetch(
    "http://example.com/",
    "http://example.com/data/file_sample.html"
  );
  await extension.awaitMessage("got-request");
  ok(true, "Initial version received webRequest event");

  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 promiseShutdownManager();

  // restarting allows upgrade to proceed
  await promiseStartupManager({ lateStartup: false });
  await extension.awaitStartup();
  await extension.awaitMessage("loaded");

  // Upgrades start the background when the extension is loaded, so
  // primed listeners are cleared already and background events are
  // already completed.
  assertPersistentListeners(extension, "webRequest", "onBeforeRequest", {
    primed: false,
    persisted: false,
  });

  await extension.unload();
  await promiseShutdownManager();
  AddonManager.checkUpdateSecurity = true;
});
