/* Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/publicdomain/zero/1.0/
 */

// This verifies that delaying an update works for WebExtensions.

// The test extension uses an insecure update url.
Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false);

if (AppConstants.platform == "win" && AppConstants.DEBUG) {
  // Shutdown timing is flaky in this test, and remote extensions
  // sometimes wind up leaving the XPI locked at the point when we try
  // to remove it.
  Services.prefs.setBoolPref("extensions.webextensions.remote", false);
}

PromiseTestUtils.allowMatchingRejectionsGlobally(
  /Message manager disconnected/
);

/* globals browser*/

const profileDir = gProfD.clone();
profileDir.append("extensions");
const stageDir = profileDir.clone();
stageDir.append("staged");

const IGNORE_ID = "test_delay_update_ignore_webext@tests.mozilla.org";
const COMPLETE_ID = "test_delay_update_complete_webext@tests.mozilla.org";
const DEFER_ID = "test_delay_update_defer_webext@tests.mozilla.org";
const STAGED_ID = "test_delay_update_staged_webext@tests.mozilla.org";
const STAGED_NO_UPDATE_URL_ID =
  "test_delay_update_staged_webext_no_update_url@tests.mozilla.org";
const NOUPDATE_ID = "test_no_update_webext@tests.mozilla.org";

// Create and configure the HTTP server.
var testserver = AddonTestUtils.createHttpServer({ hosts: ["example.com"] });
testserver.registerDirectory("/data/", do_get_file("data"));

createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "42", "42");
BootstrapMonitor.init();

const ADDONS = {
  test_delay_update_complete_webextension_v2: {
    "manifest.json": {
      manifest_version: 2,
      name: "Delay Upgrade",
      version: "2.0",
      browser_specific_settings: {
        gecko: { id: COMPLETE_ID },
      },
    },
  },
  test_delay_update_defer_webextension_v2: {
    "manifest.json": {
      manifest_version: 2,
      name: "Delay Upgrade",
      version: "2.0",
      browser_specific_settings: {
        gecko: { id: DEFER_ID },
      },
    },
  },
  test_delay_update_staged_webextension_v2: {
    "manifest.json": {
      manifest_version: 2,
      name: "Delay Upgrade",
      version: "2.0",
      browser_specific_settings: {
        gecko: {
          id: STAGED_ID,
          update_url: `http://example.com/data/test_delay_updates_staged.json`,
          strict_min_version: "1",
          strict_max_version: "41",
        },
      },
    },
  },
  test_delay_update_staged_webextension_no_update_url_v2: {
    "manifest.json": {
      manifest_version: 2,
      name: "Delay Upgrade",
      version: "2.0",
      browser_specific_settings: {
        gecko: {
          id: STAGED_NO_UPDATE_URL_ID,
          strict_min_version: "1",
          strict_max_version: "41",
        },
      },
    },
  },
  test_delay_update_ignore_webextension_v2: {
    "manifest.json": {
      manifest_version: 2,
      name: "Delay Upgrade",
      version: "2.0",
      browser_specific_settings: {
        gecko: { id: IGNORE_ID },
      },
    },
  },
};

const XPIS = {};
for (let [name, files] of Object.entries(ADDONS)) {
  XPIS[name] = AddonTestUtils.createTempXPIFile(files);
  testserver.registerFile(`/addons/${name}.xpi`, XPIS[name]);
}

// add-on registers upgrade listener, and ignores update.
add_task(async function delay_updates_ignore() {
  await promiseStartupManager();

  let extension = ExtensionTestUtils.loadExtension({
    useAddonManager: "permanent",
    manifest: {
      version: "1.0",
      browser_specific_settings: {
        gecko: {
          id: IGNORE_ID,
          update_url: `http://example.com/data/test_delay_updates_ignore.json`,
        },
      },
    },
    background() {
      browser.runtime.onUpdateAvailable.addListener(details => {
        if (details) {
          if (details.version) {
            // This should be the version of the pending update.
            browser.test.assertEq("2.0", details.version, "correct version");
            browser.test.notifyPass("delay");
          }
        } else {
          browser.test.fail("no details object passed");
        }
      });
      browser.test.sendMessage("ready");
    },
  });

  await Promise.all([extension.startup(), extension.awaitMessage("ready")]);
  BootstrapMonitor.checkInstalled(IGNORE_ID, "1.0");

  let addon = await promiseAddonByID(IGNORE_ID);
  Assert.notEqual(addon, null);
  Assert.equal(addon.version, "1.0");
  Assert.equal(addon.name, "Generated extension");
  Assert.ok(addon.isCompatible);
  Assert.ok(!addon.appDisabled);
  Assert.ok(addon.isActive);
  Assert.equal(addon.type, "extension");

  let update = await promiseFindAddonUpdates(addon);
  let install = update.updateAvailable;

  await promiseCompleteAllInstalls([install]);

  Assert.equal(install.state, AddonManager.STATE_POSTPONED);
  BootstrapMonitor.checkInstalled(IGNORE_ID, "1.0");

  // addon upgrade has been delayed
  let addon_postponed = await promiseAddonByID(IGNORE_ID);
  Assert.notEqual(addon_postponed, null);
  Assert.equal(addon_postponed.version, "1.0");
  Assert.equal(addon_postponed.name, "Generated extension");
  Assert.ok(addon_postponed.isCompatible);
  Assert.ok(!addon_postponed.appDisabled);
  Assert.ok(addon_postponed.isActive);
  Assert.equal(addon_postponed.type, "extension");

  await extension.awaitFinish("delay");

  // restarting allows upgrade to proceed
  await promiseRestartManager();

  let addon_upgraded = await promiseAddonByID(IGNORE_ID);
  await extension.awaitStartup();
  BootstrapMonitor.checkUpdated(IGNORE_ID, "2.0");

  Assert.notEqual(addon_upgraded, null);
  Assert.equal(addon_upgraded.version, "2.0");
  Assert.equal(addon_upgraded.name, "Delay Upgrade");
  Assert.ok(addon_upgraded.isCompatible);
  Assert.ok(!addon_upgraded.appDisabled);
  Assert.ok(addon_upgraded.isActive);
  Assert.equal(addon_upgraded.type, "extension");

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

// add-on registers upgrade listener, and allows update.
add_task(async function delay_updates_complete() {
  await promiseStartupManager();

  let extension = ExtensionTestUtils.loadExtension({
    useAddonManager: "permanent",
    manifest: {
      version: "1.0",
      browser_specific_settings: {
        gecko: {
          id: COMPLETE_ID,
          update_url: `http://example.com/data/test_delay_updates_complete.json`,
        },
      },
    },
    background() {
      browser.runtime.onUpdateAvailable.addListener(() => {
        browser.test.notifyPass("reload");
        browser.runtime.reload();
      });
      browser.test.sendMessage("ready");
    },
  });

  await Promise.all([extension.startup(), extension.awaitMessage("ready")]);

  let addon = await promiseAddonByID(COMPLETE_ID);
  Assert.notEqual(addon, null);
  Assert.equal(addon.version, "1.0");
  Assert.equal(addon.name, "Generated extension");
  Assert.ok(addon.isCompatible);
  Assert.ok(!addon.appDisabled);
  Assert.ok(addon.isActive);
  Assert.equal(addon.type, "extension");

  let update = await promiseFindAddonUpdates(addon);
  let install = update.updateAvailable;

  let promiseInstalled = promiseAddonEvent("onInstalled");
  await promiseCompleteAllInstalls([install]);

  await extension.awaitFinish("reload");

  // addon upgrade has been allowed
  let [addon_allowed] = await promiseInstalled;
  await extension.awaitStartup();

  Assert.notEqual(addon_allowed, null);
  Assert.equal(addon_allowed.version, "2.0");
  Assert.equal(addon_allowed.name, "Delay Upgrade");
  Assert.ok(addon_allowed.isCompatible);
  Assert.ok(!addon_allowed.appDisabled);
  Assert.ok(addon_allowed.isActive);
  Assert.equal(addon_allowed.type, "extension");

  await new Promise(executeSoon);

  if (stageDir.exists()) {
    do_throw(
      "Staging directory should not exist for formerly-postponed extension"
    );
  }

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

// add-on registers upgrade listener, initially defers update then allows upgrade
add_task(async function delay_updates_defer() {
  await promiseStartupManager();

  let extension = ExtensionTestUtils.loadExtension({
    useAddonManager: "permanent",
    manifest: {
      version: "1.0",
      browser_specific_settings: {
        gecko: {
          id: DEFER_ID,
          update_url: `http://example.com/data/test_delay_updates_defer.json`,
        },
      },
    },
    background() {
      browser.runtime.onUpdateAvailable.addListener(() => {
        // Upgrade will only proceed when "allow" message received.
        browser.test.onMessage.addListener(msg => {
          if (msg == "allow") {
            browser.test.notifyPass("allowed");
            browser.runtime.reload();
          } else {
            browser.test.fail(`wrong message: ${msg}`);
          }
        });
        browser.test.sendMessage("truly ready");
      });
      browser.test.sendMessage("ready");
    },
  });

  await Promise.all([extension.startup(), extension.awaitMessage("ready")]);

  let addon = await promiseAddonByID(DEFER_ID);
  Assert.notEqual(addon, null);
  Assert.equal(addon.version, "1.0");
  Assert.equal(addon.name, "Generated extension");
  Assert.ok(addon.isCompatible);
  Assert.ok(!addon.appDisabled);
  Assert.ok(addon.isActive);
  Assert.equal(addon.type, "extension");

  let update = await promiseFindAddonUpdates(addon);
  let install = update.updateAvailable;

  let promiseInstalled = promiseAddonEvent("onInstalled");
  await promiseCompleteAllInstalls([install]);

  Assert.equal(install.state, AddonManager.STATE_POSTPONED);

  // upgrade is initially postponed
  let addon_postponed = await promiseAddonByID(DEFER_ID);
  Assert.notEqual(addon_postponed, null);
  Assert.equal(addon_postponed.version, "1.0");
  Assert.equal(addon_postponed.name, "Generated extension");
  Assert.ok(addon_postponed.isCompatible);
  Assert.ok(!addon_postponed.appDisabled);
  Assert.ok(addon_postponed.isActive);
  Assert.equal(addon_postponed.type, "extension");

  // add-on will not allow upgrade until message is received
  await extension.awaitMessage("truly ready");
  extension.sendMessage("allow");
  await extension.awaitFinish("allowed");

  // addon upgrade has been allowed
  let [addon_allowed] = await promiseInstalled;
  await extension.awaitStartup();

  Assert.notEqual(addon_allowed, null);
  Assert.equal(addon_allowed.version, "2.0");
  Assert.equal(addon_allowed.name, "Delay Upgrade");
  Assert.ok(addon_allowed.isCompatible);
  Assert.ok(!addon_allowed.appDisabled);
  Assert.ok(addon_allowed.isActive);
  Assert.equal(addon_allowed.type, "extension");

  await promiseRestartManager();

  // restart changes nothing
  addon_allowed = await promiseAddonByID(DEFER_ID);
  await extension.awaitStartup();

  Assert.notEqual(addon_allowed, null);
  Assert.equal(addon_allowed.version, "2.0");
  Assert.equal(addon_allowed.name, "Delay Upgrade");
  Assert.ok(addon_allowed.isCompatible);
  Assert.ok(!addon_allowed.appDisabled);
  Assert.ok(addon_allowed.isActive);
  Assert.equal(addon_allowed.type, "extension");

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

// add-on registers upgrade listener to deny update, completes after restart,
// even though the updated XPI is incompatible - the information returned
// by the update server defined in its manifest returns a compatible range
add_task(async function delay_updates_staged() {
  await promiseStartupManager();

  let extension = ExtensionTestUtils.loadExtension({
    useAddonManager: "permanent",
    manifest: {
      version: "1.0",
      browser_specific_settings: {
        gecko: {
          id: STAGED_ID,
          update_url: `http://example.com/data/test_delay_updates_staged.json`,
        },
      },
    },
    background() {
      browser.runtime.onUpdateAvailable.addListener(() => {
        browser.test.sendMessage("denied");
      });
      browser.test.sendMessage("ready");
    },
  });

  await Promise.all([extension.startup(), extension.awaitMessage("ready")]);

  let addon = await promiseAddonByID(STAGED_ID);
  Assert.notEqual(addon, null);
  Assert.equal(addon.version, "1.0");
  Assert.equal(addon.name, "Generated extension");
  Assert.ok(addon.isCompatible);
  Assert.ok(!addon.appDisabled);
  Assert.ok(addon.isActive);
  Assert.equal(addon.type, "extension");

  let update = await promiseFindAddonUpdates(addon);
  let install = update.updateAvailable;
  await promiseCompleteAllInstalls([install]);

  Assert.equal(install.state, AddonManager.STATE_POSTPONED);

  // upgrade is initially postponed
  let addon_postponed = await promiseAddonByID(STAGED_ID);
  Assert.notEqual(addon_postponed, null);
  Assert.equal(addon_postponed.version, "1.0");
  Assert.equal(addon_postponed.name, "Generated extension");
  Assert.ok(addon_postponed.isCompatible);
  Assert.ok(!addon_postponed.appDisabled);
  Assert.ok(addon_postponed.isActive);
  Assert.equal(addon_postponed.type, "extension");

  // add-on reports an available upgrade, but denied it till next restart
  await extension.awaitMessage("denied");

  await promiseRestartManager();
  await extension.awaitStartup();

  // add-on should have been updated during restart
  let addon_upgraded = await promiseAddonByID(STAGED_ID);
  Assert.notEqual(addon_upgraded, null);
  Assert.equal(addon_upgraded.version, "2.0");
  Assert.equal(addon_upgraded.name, "Delay Upgrade");
  Assert.ok(addon_upgraded.isCompatible);
  Assert.ok(!addon_upgraded.appDisabled);
  Assert.ok(addon_upgraded.isActive);
  Assert.equal(addon_upgraded.type, "extension");

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

// add-on registers upgrade listener to deny update, does not complete after
// restart, because the updated XPI is incompatible - there is no update server
// defined in its manifest, which could return a compatible range
add_task(async function delay_updates_staged_no_update_url() {
  await promiseStartupManager();

  let extension = ExtensionTestUtils.loadExtension({
    useAddonManager: "permanent",
    manifest: {
      version: "1.0",
      browser_specific_settings: {
        gecko: {
          id: STAGED_NO_UPDATE_URL_ID,
          update_url: `http://example.com/data/test_delay_updates_staged.json`,
        },
      },
    },
    background() {
      browser.runtime.onUpdateAvailable.addListener(() => {
        browser.test.sendMessage("denied");
      });
      browser.test.sendMessage("ready");
    },
  });

  await Promise.all([extension.startup(), extension.awaitMessage("ready")]);

  let addon = await promiseAddonByID(STAGED_NO_UPDATE_URL_ID);
  Assert.notEqual(addon, null);
  Assert.equal(addon.version, "1.0");
  Assert.equal(addon.name, "Generated extension");
  Assert.ok(addon.isCompatible);
  Assert.ok(!addon.appDisabled);
  Assert.ok(addon.isActive);
  Assert.equal(addon.type, "extension");

  let update = await promiseFindAddonUpdates(addon);
  let install = update.updateAvailable;
  await promiseCompleteAllInstalls([install]);

  Assert.equal(install.state, AddonManager.STATE_POSTPONED);

  // upgrade is initially postponed
  let addon_postponed = await promiseAddonByID(STAGED_NO_UPDATE_URL_ID);
  Assert.notEqual(addon_postponed, null);
  Assert.equal(addon_postponed.version, "1.0");
  Assert.equal(addon_postponed.name, "Generated extension");
  Assert.ok(addon_postponed.isCompatible);
  Assert.ok(!addon_postponed.appDisabled);
  Assert.ok(addon_postponed.isActive);
  Assert.equal(addon_postponed.type, "extension");

  // add-on reports an available upgrade, but denied it till next restart
  await extension.awaitMessage("denied");

  await promiseRestartManager();
  await extension.awaitStartup();

  // add-on should not have been updated during restart
  let addon_upgraded = await promiseAddonByID(STAGED_NO_UPDATE_URL_ID);
  Assert.notEqual(addon_upgraded, null);
  Assert.equal(addon_upgraded.version, "1.0");
  Assert.equal(addon_upgraded.name, "Generated extension");
  Assert.ok(addon_upgraded.isCompatible);
  Assert.ok(!addon_upgraded.appDisabled);
  Assert.ok(addon_upgraded.isActive);
  Assert.equal(addon_upgraded.type, "extension");

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

// browser.runtime.reload() without a pending upgrade should just reload.
add_task(async function runtime_reload() {
  await promiseStartupManager();

  let extension = ExtensionTestUtils.loadExtension({
    useAddonManager: "permanent",
    manifest: {
      version: "1.0",
      browser_specific_settings: {
        gecko: {
          id: NOUPDATE_ID,
          update_url: `http://example.com/data/test_no_update.json`,
        },
      },
    },
    background() {
      browser.test.onMessage.addListener(msg => {
        if (msg == "reload") {
          browser.runtime.reload();
        } else {
          browser.test.fail(`wrong message: ${msg}`);
        }
      });
      browser.test.sendMessage("ready");
    },
  });

  await Promise.all([extension.startup(), extension.awaitMessage("ready")]);

  let addon = await promiseAddonByID(NOUPDATE_ID);
  Assert.notEqual(addon, null);
  Assert.equal(addon.version, "1.0");
  Assert.equal(addon.name, "Generated extension");
  Assert.ok(addon.isCompatible);
  Assert.ok(!addon.appDisabled);
  Assert.ok(addon.isActive);
  Assert.equal(addon.type, "extension");

  await promiseFindAddonUpdates(addon);

  extension.sendMessage("reload");
  // Wait for extension to restart, to make sure reload works.
  await AddonTestUtils.promiseWebExtensionStartup(NOUPDATE_ID);
  await extension.awaitMessage("ready");

  addon = await promiseAddonByID(NOUPDATE_ID);
  Assert.notEqual(addon, null);
  Assert.equal(addon.version, "1.0");
  Assert.equal(addon.name, "Generated extension");
  Assert.ok(addon.isCompatible);
  Assert.ok(!addon.appDisabled);
  Assert.ok(addon.isActive);
  Assert.equal(addon.type, "extension");

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