"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 promiseClearSystemAddons() {
  let xml = buildSystemAddonUpdates([]);
  return installSystemAddons(xml, []);
}

const builtInOverride = { system: [ADDON_ID], builtins: [] };

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

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

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

  // cleanup the builtin addon manually installed in the builtin location.
  let addon = await AddonManager.getAddonByID(ADDON_ID);
  await addon.uninstall();
  await AddonTestUtils.promiseShutdownManager();
  BootstrapMonitor.clear(ADDON_ID);
});

// Run the test again, but starting from a "builtin" addon location
add_task(async function test_auto_installed_builtin_addon_upgrades() {
  const ADDON_VERSION = "1.0";
  const builtinExtensionDefinitions = {
    manifest: {
      version: ADDON_VERSION,
      browser_specific_settings: {
        gecko: { id: ADDON_ID },
      },
    },
  };
  await setupBuiltinExtension(builtinExtensionDefinitions, "test-builtin-ext");

  builtInOverride.system = [];
  builtInOverride.builtins = [
    {
      addon_id: ADDON_ID,
      addon_version: ADDON_VERSION,
      res_url: "resource://test-builtin-ext/",
    },
  ];
  await AddonTestUtils.overrideBuiltIns(builtInOverride);

  await Promise.all([
    AddonTestUtils.promiseWebExtensionStartup(ADDON_ID),
    AddonTestUtils.promiseStartupManager(),
  ]);
  await checkAddon(ADDON_VERSION, BOOTSTRAP_REASONS.ADDON_INSTALL);

  await _test_builtin_addon_override();

  // cleanup the system addon auto-installed in the locked system builtin location
  await AddonTestUtils.overrideBuiltIns({ system: [], builtins: [] });
  await AddonTestUtils.promiseRestartManager();
  await AddonTestUtils.promiseShutdownManager();
  BootstrapMonitor.clear(ADDON_ID);
});

add_task(async function test_builtin_addon_version_precedence() {
  builtInOverride.system = [];
  builtInOverride.builtins = [];
  await AddonTestUtils.overrideBuiltIns(builtInOverride);

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

  // Uninstall the system addon update.
  let addon = await AddonManager.getAddonByID(ADDON_ID);
  await addon.uninstall();
  // Uninstall the addon version 1.5 manually installed in the builtin location.
  addon = await AddonManager.getAddonByID(ADDON_ID);
  Assert.equal(
    addon?.version,
    "1.5",
    "Expect builtin addon v1.5 to become visible"
  );
  await addon.uninstall();
  await AddonTestUtils.promiseShutdownManager();
  BootstrapMonitor.clear(ADDON_ID);
});

// Same as test_system_addon_precedence but with the system addon
// bundled in the omni jar instead of an xpi installed in the
// app-system-defaults directory location.
add_task(async function test_systembuiltin_addon_version_precedence() {
  const ADDON_VERSION = "1.0";
  const builtinExtensionDefinitions = {
    manifest: {
      version: ADDON_VERSION,
      browser_specific_settings: {
        gecko: { id: ADDON_ID },
      },
    },
  };
  await setupBuiltinExtension(builtinExtensionDefinitions, "test-builtin-ext");

  builtInOverride.system = [];
  builtInOverride.builtins = [
    {
      addon_id: ADDON_ID,
      addon_version: ADDON_VERSION,
      res_url: "resource://test-builtin-ext/",
    },
  ];
  await AddonTestUtils.overrideBuiltIns(builtInOverride);

  await Promise.all([
    AddonTestUtils.promiseWebExtensionStartup(ADDON_ID),
    AddonTestUtils.promiseStartupManager("1"),
  ]);

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

  /////
  // Prepare an updated system builtin version to mock an app update.
  /////
  builtinExtensionDefinitions.manifest.version = "1.5";
  await setupBuiltinExtension(builtinExtensionDefinitions, "test-builtin-ext");
  builtInOverride.builtins[0].addon_version = "1.5";
  await AddonTestUtils.overrideBuiltIns(builtInOverride);

  /////
  // 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
  );

  // Mock a new system addon update check
  await promiseUpdateSystemAddon(ADDON_ID, "2.0");
  await checkAddon("2.0", BOOTSTRAP_REASONS.ADDON_UPGRADE);

  // cleanup the system addon auto-installed in the locked system builtin location
  await AddonTestUtils.overrideBuiltIns({ system: [], builtins: [] });
  await AddonTestUtils.promiseRestartManager();
  await AddonTestUtils.promiseShutdownManager();
  BootstrapMonitor.clear(ADDON_ID);
});
