"use strict";

// Enable SCOPE_APPLICATION for builtin testing.  Default in tests is only SCOPE_PROFILE.
let scopes = AddonManager.SCOPE_PROFILE | AddonManager.SCOPE_APPLICATION;
Services.prefs.setIntPref("extensions.enabledScopes", scopes);

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

// A test directory for default/builtin system addons.
const systemDefaults = FileUtils.getDir("ProfD", [
  "app-system-defaults",
  "features",
]);
systemDefaults.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
registerDirectory("XREAppFeat", systemDefaults);

AddonTestUtils.usePrivilegedSignatures = () => "system";

const ADDON_ID = "updates@test";

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

let server = createHttpServer();

server.registerPathHandler("/upgrade.json", (request, response) => {
  response.setStatusLine(request.httpVersion, 200, "ok");
  response.write(
    JSON.stringify({
      addons: {
        [ADDON_ID]: {
          updates: [
            {
              version: "4.0",
              update_link: `http://localhost:${server.identity.primaryPort}/${ADDON_ID}.xpi`,
            },
          ],
        },
      },
    })
  );
});

function createWebExtensionFile(id, version, update_url) {
  return AddonTestUtils.createTempWebExtensionFile({
    manifest: {
      version,
      browser_specific_settings: {
        gecko: { id, update_url },
      },
    },
  });
}

let xpiUpdate = createWebExtensionFile(ADDON_ID, "4.0");

server.registerFile(`/${ADDON_ID}.xpi`, xpiUpdate);

async function promiseInstallDefaultSystemAddon(id, version) {
  let xpi = createWebExtensionFile(id, version);
  await AddonTestUtils.manuallyInstall(xpi, systemDefaults);
  return xpi;
}

async function promiseInstallProfileExtension(id, version, update_url) {
  return promiseInstallWebExtension({
    manifest: {
      version,
      browser_specific_settings: {
        gecko: { id, update_url },
      },
    },
  });
}

async function promiseInstallSystemProfileAddon(id, version) {
  let xpi = createWebExtensionFile(id, version);
  const install = await AddonManager.getInstallForURL(`file://${xpi.path}`, {
    useSystemLocation: true, // KEY_APP_SYSTEM_PROFILE
  });

  return install.install();
}

async function promiseUpdateSystemAddon(id, version, waitForStartup = true) {
  let xpi = createWebExtensionFile(id, version);
  let xml = buildSystemAddonUpdates([
    {
      id: ADDON_ID,
      version,
      path: xpi.leafName,
      xpi,
    },
  ]);
  // If we're not expecting a startup we need to wait for install to end.
  let promises = [];
  if (!waitForStartup) {
    promises.push(AddonTestUtils.promiseAddonEvent("onInstalled"));
  }
  promises.push(installSystemAddons(xml, waitForStartup ? [ADDON_ID] : []));
  return Promise.all(promises);
}

async function promiseClearSystemAddons() {
  let xml = buildSystemAddonUpdates([]);
  return installSystemAddons(xml, []);
}

const builtInOverride = { system: [ADDON_ID] };

async function checkAddon(version, reason, startReason = reason) {
  let addons = await AddonManager.getAddonsByTypes(["extension"]);
  Assert.equal(addons.length, 1, "one addon expected to be installed");
  Assert.equal(addons[0].version, version, `addon ${version} is installed`);
  Assert.ok(addons[0].isActive, `addon ${version} is active`);
  Assert.ok(!addons[0].disabled, `addon ${version} is enabled`);

  let installInfo = BootstrapMonitor.checkInstalled(ADDON_ID, version);
  equal(
    installInfo.reason,
    reason,
    `bootstrap monitor verified install reason for ${version}`
  );
  let started = BootstrapMonitor.checkStarted(ADDON_ID, version);
  equal(
    started.reason,
    startReason,
    `bootstrap monitor verified started reason for ${version}`
  );

  return addons[0];
}

/**
 * This test function starts after a 1.0 version of an addon is installed
 * either as a builtin ("app-builtin") or as a builtin system addon ("app-system-defaults").
 *
 * This tests the precedence chain works as expected through upgrades and
 * downgrades.
 *
 * Upgrade to a system addon in the profile location, "app-system-addons"
 * Upgrade to a temporary addon
 * Uninstalling the temporary addon downgrades to the system addon in "app-system-addons".
 * Upgrade to a system addon in the "app-system-profile" location.
 * Uninstalling the "app-system-profile" addon downgrades to the one in "app-system-addons".
 * Upgrade to a user-installed addon
 * Upgrade the addon in "app-system-addons", verify user-install is still active
 * Uninstall the addon in "app-system-addons", verify user-install is active
 * Test that the update_url upgrades the user-install and becomes active
 * Disable the user-install, verify the disabled addon retains precedence
 * Uninstall the disabled user-install, verify system addon in "app-system-defaults" is active and enabled
 * Upgrade the system addon again, then user-install a lower version, verify downgrade happens.
 * Uninstall user-install, verify upgrade happens when the system addon in "app-system-addons" is activated.
 */
async function _test_builtin_addon_override() {
  /////
  // Upgrade to a system addon in the profile location, "app-system-addons"
  /////
  await promiseUpdateSystemAddon(ADDON_ID, "2.0");
  await checkAddon("2.0", BOOTSTRAP_REASONS.ADDON_UPGRADE);

  /////
  // Upgrade to a temporary addon
  /////
  let tmpAddon = createWebExtensionFile(ADDON_ID, "2.1");
  await Promise.all([
    AddonManager.installTemporaryAddon(tmpAddon),
    AddonTestUtils.promiseWebExtensionStartup(ADDON_ID),
  ]);
  let addon = await checkAddon("2.1", BOOTSTRAP_REASONS.ADDON_UPGRADE);

  /////
  // Downgrade back to the system addon
  /////
  await addon.uninstall();
  await checkAddon("2.0", BOOTSTRAP_REASONS.ADDON_DOWNGRADE);

  /////
  // Install then uninstall an system profile addon
  /////
  info("Install an System Profile Addon, then uninstall it.");
  await Promise.all([
    promiseInstallSystemProfileAddon(ADDON_ID, "2.2"),
    AddonTestUtils.promiseWebExtensionStartup(ADDON_ID),
  ]);
  addon = await checkAddon("2.2", BOOTSTRAP_REASONS.ADDON_UPGRADE);
  await addon.uninstall();
  await checkAddon("2.0", BOOTSTRAP_REASONS.ADDON_DOWNGRADE);

  /////
  // Upgrade to a user-installed addon
  /////
  await Promise.all([
    promiseInstallProfileExtension(
      ADDON_ID,
      "3.0",
      `http://localhost:${server.identity.primaryPort}/upgrade.json`
    ),
    AddonTestUtils.promiseWebExtensionStartup(ADDON_ID),
  ]);
  await checkAddon("3.0", BOOTSTRAP_REASONS.ADDON_UPGRADE);

  /////
  // Upgrade the system addon, verify user-install is still active
  /////
  await promiseUpdateSystemAddon(ADDON_ID, "2.2", false);
  await checkAddon("3.0", BOOTSTRAP_REASONS.ADDON_UPGRADE);

  /////
  // Uninstall the system addon, verify user-install is active
  /////
  await Promise.all([
    promiseClearSystemAddons(),
    AddonTestUtils.promiseAddonEvent("onUninstalled"),
  ]);
  addon = await checkAddon("3.0", BOOTSTRAP_REASONS.ADDON_UPGRADE);

  /////
  // Test that the update_url upgrades the user-install and becomes active
  /////
  let update = await promiseFindAddonUpdates(addon);
  await Promise.all([
    promiseCompleteAllInstalls([update.updateAvailable]),
    AddonTestUtils.promiseWebExtensionStartup(ADDON_ID),
  ]);
  addon = await checkAddon("4.0", BOOTSTRAP_REASONS.ADDON_UPGRADE);

  /////
  // Disable the user-install, verify the disabled addon retains precedence
  /////
  await addon.disable();

  await AddonManager.getAddonByID(ADDON_ID);
  Assert.ok(!addon.isActive, "4.0 is disabled");
  Assert.equal(
    addon.version,
    "4.0",
    "version 4.0 is still the visible version"
  );

  /////
  // Uninstall the disabled user-install, verify system addon is active and enabled
  /////
  await Promise.all([
    addon.uninstall(),
    AddonTestUtils.promiseWebExtensionStartup(ADDON_ID),
  ]);
  // We've downgraded all the way to 1.0 from a user-installed addon
  addon = await checkAddon("1.0", BOOTSTRAP_REASONS.ADDON_DOWNGRADE);

  /////
  // Upgrade the system addon again, then user-install a lower version, verify downgrade happens.
  /////
  await promiseUpdateSystemAddon(ADDON_ID, "5.1");
  addon = await checkAddon("5.1", BOOTSTRAP_REASONS.ADDON_UPGRADE);

  // user-install a lower version, downgrade happens
  await Promise.all([
    promiseInstallProfileExtension(ADDON_ID, "5.0"),
    AddonTestUtils.promiseWebExtensionStartup(ADDON_ID),
  ]);
  addon = await checkAddon("5.0", BOOTSTRAP_REASONS.ADDON_DOWNGRADE);

  /////
  // Uninstall user-install, verify upgrade happens when system addon is activated.
  /////
  await Promise.all([
    addon.uninstall(),
    AddonTestUtils.promiseWebExtensionStartup(ADDON_ID),
  ]);
  // the "system add-on upgrade" is now revealed
  addon = await checkAddon("5.1", BOOTSTRAP_REASONS.ADDON_UPGRADE);

  await Promise.all([
    addon.uninstall(),
    AddonTestUtils.promiseWebExtensionStartup(ADDON_ID),
  ]);
  await checkAddon("1.0", BOOTSTRAP_REASONS.ADDON_DOWNGRADE);

  // Downgrading from an installed system addon to a default system
  // addon also requires the removal of the file on disk, and removing
  // the addon from the pref.
  Services.prefs.clearUserPref(PREF_SYSTEM_ADDON_SET);
}

add_task(async function test_system_addon_upgrades() {
  await AddonTestUtils.overrideBuiltIns(builtInOverride);
  await promiseInstallDefaultSystemAddon(ADDON_ID, "1.0");
  await AddonTestUtils.promiseStartupManager();
  await checkAddon("1.0", BOOTSTRAP_REASONS.ADDON_INSTALL);

  await _test_builtin_addon_override();

  // cleanup the system addon in the default location
  await AddonTestUtils.manuallyUninstall(systemDefaults, ADDON_ID);
  // If we don't restart (to clean up the uninstall above) and we
  // clear BootstrapMonitor, BootstrapMonitor asserts on the next AOM startup.
  await AddonTestUtils.promiseRestartManager();

  await AddonTestUtils.promiseShutdownManager();
  BootstrapMonitor.clear(ADDON_ID);
});

// Run the test again, but starting from a "builtin" addon location
add_task(async function test_builtin_addon_upgrades() {
  builtInOverride.system = [];

  await AddonTestUtils.promiseStartupManager();
  await Promise.all([
    installBuiltinExtension({
      manifest: {
        version: "1.0",
        browser_specific_settings: {
          gecko: { id: ADDON_ID },
        },
      },
    }),
    AddonTestUtils.promiseWebExtensionStartup(ADDON_ID),
  ]);
  await checkAddon("1.0", BOOTSTRAP_REASONS.ADDON_INSTALL);

  await _test_builtin_addon_override();

  let addon = await AddonManager.getAddonByID(ADDON_ID);
  await addon.uninstall();
  await AddonTestUtils.promiseShutdownManager();
  BootstrapMonitor.clear(ADDON_ID);
});

add_task(async function test_system_addon_precedence() {
  builtInOverride.system = [ADDON_ID];
  await AddonTestUtils.overrideBuiltIns(builtInOverride);
  await promiseInstallDefaultSystemAddon(ADDON_ID, "1.0");
  await AddonTestUtils.promiseStartupManager();
  await checkAddon("1.0", BOOTSTRAP_REASONS.ADDON_INSTALL);

  /////
  // Upgrade to a system addon in the profile location, "app-system-addons"
  /////
  await promiseUpdateSystemAddon(ADDON_ID, "2.0");
  await checkAddon("2.0", BOOTSTRAP_REASONS.ADDON_UPGRADE);

  /////
  // Builtin system addon is changed, it has precedence because when this
  // happens we remove all prior system addon upgrades.
  /////
  await AddonTestUtils.promiseShutdownManager();
  await AddonTestUtils.overrideBuiltIns(builtInOverride);
  await promiseInstallDefaultSystemAddon(ADDON_ID, "1.5");
  await AddonTestUtils.promiseStartupManager("2");
  await checkAddon(
    "1.5",
    BOOTSTRAP_REASONS.ADDON_DOWNGRADE,
    BOOTSTRAP_REASONS.APP_STARTUP
  );

  // cleanup the system addon in the default location
  await AddonTestUtils.manuallyUninstall(systemDefaults, ADDON_ID);
  await AddonTestUtils.promiseRestartManager();

  await AddonTestUtils.promiseShutdownManager();
  BootstrapMonitor.clear(ADDON_ID);
});

add_task(async function test_builtin_addon_version_precedence() {
  builtInOverride.system = [];

  await AddonTestUtils.promiseStartupManager();
  await Promise.all([
    installBuiltinExtension({
      manifest: {
        version: "1.0",
        browser_specific_settings: {
          gecko: { id: ADDON_ID },
        },
      },
    }),
    AddonTestUtils.promiseWebExtensionStartup(ADDON_ID),
  ]);
  await checkAddon("1.0", BOOTSTRAP_REASONS.ADDON_INSTALL);

  /////
  // Upgrade to a system addon in the profile location, "app-system-addons"
  /////
  await promiseUpdateSystemAddon(ADDON_ID, "2.0");
  await checkAddon("2.0", BOOTSTRAP_REASONS.ADDON_UPGRADE);

  /////
  // Builtin addon is changed, the system addon should still have precedence.
  /////
  await Promise.all([
    installBuiltinExtension(
      {
        manifest: {
          version: "1.5",
          browser_specific_settings: {
            gecko: { id: ADDON_ID },
          },
        },
      },
      false
    ),
    AddonTestUtils.promiseAddonEvent("onInstalled"),
  ]);
  await checkAddon("2.0", BOOTSTRAP_REASONS.ADDON_UPGRADE);

  let addon = await AddonManager.getAddonByID(ADDON_ID);
  await addon.uninstall();
  await AddonTestUtils.promiseShutdownManager();
  BootstrapMonitor.clear(ADDON_ID);
});
