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

const { ExtensionUtils } = ChromeUtils.importESModule(
  "resource://gre/modules/ExtensionUtils.sys.mjs"
);

ChromeUtils.defineLazyGetter(this, "resourceProtocol", () =>
  Services.io
    .getProtocolHandler("resource")
    .QueryInterface(Ci.nsIResProtocolHandler)
);

Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false);

const ID = "langpack-und@test.mozilla.org";

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

// Langpacks versions follow the following convention:
// <firefox major>.<firefox minor>.YYYYMMDD.HHmmss
// with no leading zeros allowed (as enforced per version format, see MDN doc page at https://mzl.la/3M6L15y).
//
// See https://searchfox.org/mozilla-central/rev/26790fe/python/mozbuild/mozbuild/action/langpack_manifest.py#388-398
var server = AddonTestUtils.createHttpServer({ hosts: ["example.com"] });
AddonTestUtils.registerJSON(server, "/test_update_langpack.json", {
  addons: {
    "langpack-und@test.mozilla.org": {
      updates: [
        {
          version: "58.0.20230105.121014",
          applications: {
            gecko: {
              strict_min_version: "58.0",
              strict_max_version: "58.*",
            },
          },
        },
        {
          version: "60.0.20230207.112555",
          update_link:
            "http://example.com/addons/langpack-und@test.mozilla.org.xpi",
          applications: {
            gecko: {
              strict_min_version: "60.0",
              strict_max_version: "60.*",
            },
          },
        },
        {
          version: "60.1.20230309.91233",
          update_link:
            "http://example.com/addons/dotrelease/langpack-und@test.mozilla.org.xpi",
          applications: {
            gecko: {
              strict_min_version: "60.0",
              strict_max_version: "60.*",
            },
          },
        },
        {
          version: "145.0.20251124.145406",
          update_link:
            "http://example.com/addons/latermajor/langpack-und@test.mozilla.org.xpi",
          applications: {
            gecko: {
              strict_min_version: "145.0",
              strict_max_version: "145.*",
            },
          },
        },
      ],
    },
  },
});

// A second update url, which is included in the last of the langpack
// version from the previous one (and used to cover the staging of a
// langpack from one dotrelease to another).
AddonTestUtils.registerJSON(server, "/test_update_langpack2.json", {
  addons: {
    "langpack-und@test.mozilla.org": {
      updates: [
        {
          version: "60.2.20230319.94511",
          update_link:
            "http://example.com/addons/dotrelease2/langpack-und@test.mozilla.org.xpi",
          applications: {
            gecko: {
              strict_min_version: "60.0",
              strict_max_version: "60.*",
            },
          },
        },
      ],
    },
  },
});

function promisePostponeInstall(install) {
  return new Promise((resolve, reject) => {
    let listener = {
      onDownloadEnded: () => {
        install.postpone();
      },
      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);
  });
}

createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "58");

const ADDONS = {
  langpack_1: {
    "browser/localization/und/browser.ftl":
      "message-browser = Value from Browser\n",
    "localization/und/toolkit_test.ftl": "message-id1 = Value 1\n",
    "chrome/und/locale/und/global/test.properties":
      "message = Value from .properties\n",
    "manifest.json": {
      name: "und Language Pack",
      version: "58.0.20230105.121014",
      manifest_version: 2,
      browser_specific_settings: {
        gecko: {
          id: ID,
          strict_min_version: "58.0",
          strict_max_version: "58.*",
          update_url: "http://example.com/test_update_langpack.json",
        },
      },
      sources: {
        browser: {
          base_path: "browser/",
        },
      },
      langpack_id: "und",
      languages: {
        und: {
          chrome_resources: {
            global: "chrome/und/locale/und/global/",
          },
          version: "20171001190118",
        },
      },
      author: "Mozilla Localization Task Force",
      description: "Language pack for Testy for und",
    },
  },
};

// clone the extension so we can create an update.
const langpack_update = JSON.parse(JSON.stringify(ADDONS.langpack_1));
langpack_update["manifest.json"].version = "60.0.20230207.112555";
langpack_update["manifest.json"].browser_specific_settings.gecko = {
  id: ID,
  strict_min_version: "60.0",
  strict_max_version: "60.*",
  update_url: "http://example.com/test_update_langpack.json",
};

const langpack_update_dotrelease = JSON.parse(
  JSON.stringify(ADDONS.langpack_1)
);
langpack_update_dotrelease["manifest.json"].version = "60.1.20230309.91233";
langpack_update_dotrelease["manifest.json"].browser_specific_settings.gecko = {
  id: ID,
  strict_min_version: "60.0",
  strict_max_version: "60.*",
  update_url: "http://example.com/test_update_langpack2.json",
};

// Another langpack for another dot release part of the same major version as the previous one.
const langpack_update_dotrelease2 = JSON.parse(
  JSON.stringify(ADDONS.langpack_1)
);
langpack_update_dotrelease2["manifest.json"].version = "60.2.20230319.94511";
langpack_update_dotrelease2["manifest.json"].browser_specific_settings.gecko = {
  id: ID,
  strict_min_version: "60.0",
  strict_max_version: "60.*",
  update_url: "http://example.com/test_update_langpack2.json",
};

const langpack_update_later_major = structuredClone(ADDONS.langpack_1);
langpack_update_later_major["manifest.json"].version = "145.0.20251124.145406";
langpack_update_later_major["manifest.json"].browser_specific_settings.gecko = {
  id: ID,
  strict_min_version: "145.0",
  strict_max_version: "145.*",
  // no update_url, the test using it doesn't care about updating after that.
};

let xpi = AddonTestUtils.createTempXPIFile(langpack_update);
server.registerFile(`/addons/${ID}.xpi`, xpi);

let xpiDotRelease = AddonTestUtils.createTempXPIFile(
  langpack_update_dotrelease
);
server.registerFile(`/addons/dotrelease/${ID}.xpi`, xpiDotRelease);

let xpiDotRelease2 = AddonTestUtils.createTempXPIFile(
  langpack_update_dotrelease2
);
server.registerFile(`/addons/dotrelease2/${ID}.xpi`, xpiDotRelease2);

let xpiLaterMajor = AddonTestUtils.createTempXPIFile(
  langpack_update_later_major
);
server.registerFile(`/addons/latermajor/${ID}.xpi`, xpiLaterMajor);

function promiseLangpackStartup() {
  return new Promise(resolve => {
    const EVENT = "webextension-langpack-startup";
    Services.obs.addObserver(function observer() {
      Services.obs.removeObserver(observer, EVENT);
      resolve();
    }, EVENT);
  });
}

add_task(async function setup() {
  Services.prefs.clearUserPref("extensions.startupScanScopes");
});

/**
 * This is a basic life-cycle test which verifies that
 * the language pack registers and unregisters correct
 * languages at various stages.
 */
add_task(async function test_basic_lifecycle() {
  await promiseStartupManager();

  // Make sure that `und` locale is not installed.
  equal(
    L10nRegistry.getInstance().getAvailableLocales().includes("und"),
    false,
    "und not installed"
  );
  equal(
    Services.locale.availableLocales.includes("und"),
    false,
    "und not available"
  );

  let [, { addon }] = await Promise.all([
    promiseLangpackStartup(),
    AddonTestUtils.promiseInstallXPI(ADDONS.langpack_1),
  ]);

  // Now make sure that `und` locale is available.
  equal(
    L10nRegistry.getInstance().getAvailableLocales().includes("und"),
    true,
    "und is installed"
  );
  equal(
    Services.locale.availableLocales.includes("und"),
    true,
    "und is available"
  );

  await addon.disable();

  // It is not available after the langpack has been disabled.
  equal(
    L10nRegistry.getInstance().getAvailableLocales().includes("und"),
    false,
    "und not installed"
  );
  equal(
    Services.locale.availableLocales.includes("und"),
    false,
    "und not available"
  );

  // This quirky code here allows us to handle a scenario where enabling the
  // addon is synchronous or asynchronous.
  await Promise.all([promiseLangpackStartup(), addon.enable()]);

  // After re-enabling it, the `und` locale is available again.
  equal(
    L10nRegistry.getInstance().getAvailableLocales().includes("und"),
    true,
    "und is installed"
  );
  equal(
    Services.locale.availableLocales.includes("und"),
    true,
    "und is available"
  );

  await addon.uninstall();

  // After the langpack has been uninstalled, no more `und` in locales.
  equal(
    L10nRegistry.getInstance().getAvailableLocales().includes("und"),
    false,
    "und not installed"
  );
  equal(
    Services.locale.availableLocales.includes("und"),
    false,
    "und not available"
  );
});

/**
 * This test verifies that registries are able to load and synchronously return
 * correct strings available in the language pack.
 */
add_task(async function test_locale_registries() {
  let [, { addon }] = await Promise.all([
    promiseLangpackStartup(),
    AddonTestUtils.promiseInstallXPI(ADDONS.langpack_1),
  ]);

  {
    // Toolkit string
    let bundles = L10nRegistry.getInstance().generateBundlesSync(
      ["und"],
      ["toolkit_test.ftl"]
    );
    let bundle0 = bundles.next().value;
    ok(bundle0);
    equal(bundle0.hasMessage("message-id1"), true);
  }

  {
    // Browser string
    let bundles = L10nRegistry.getInstance().generateBundlesSync(
      ["und"],
      ["browser.ftl"]
    );
    let bundle0 = bundles.next().value;
    ok(bundle0);
    equal(bundle0.hasMessage("message-browser"), true);
  }

  {
    // Test chrome package
    let reqLocs = Services.locale.requestedLocales;
    Services.locale.requestedLocales = ["und"];

    let bundle = Services.strings.createBundle(
      "chrome://global/locale/test.properties"
    );
    let entry = bundle.GetStringFromName("message");
    equal(entry, "Value from .properties");

    Services.locale.requestedLocales = reqLocs;
  }

  await addon.uninstall();
});

/**
 * This test verifies that registries are able to load and asynchronously return
 * correct strings available in the language pack.
 */
add_task(async function test_locale_registries_async() {
  let [, { addon }] = await Promise.all([
    promiseLangpackStartup(),
    AddonTestUtils.promiseInstallXPI(ADDONS.langpack_1),
  ]);

  {
    // Toolkit string
    let bundles = L10nRegistry.getInstance().generateBundles(
      ["und"],
      ["toolkit_test.ftl"]
    );
    let bundle0 = (await bundles.next()).value;
    equal(bundle0.hasMessage("message-id1"), true);
  }

  {
    // Browser string
    let bundles = L10nRegistry.getInstance().generateBundles(
      ["und"],
      ["browser.ftl"]
    );
    let bundle0 = (await bundles.next()).value;
    equal(bundle0.hasMessage("message-browser"), true);
  }

  await addon.uninstall();
  await promiseShutdownManager();
});

add_task(async function test_langpack_app_shutdown() {
  let langpackId = `langpack-und-${AppConstants.MOZ_BUILD_APP.replace(
    "/",
    "-"
  )}`;
  let check = (yes, msg) => {
    equal(resourceProtocol.hasSubstitution(langpackId), yes, msg);
  };

  await promiseStartupManager();

  check(false, "no initial resource substitution");

  await Promise.all([
    promiseLangpackStartup(),
    AddonTestUtils.promiseInstallXPI(ADDONS.langpack_1),
  ]);

  check(true, "langpack resource available after startup");

  await promiseShutdownManager();

  check(true, "langpack resource available after app shutdown");

  await promiseStartupManager();

  let addon = await AddonManager.getAddonByID(ID);
  await addon.uninstall();

  check(false, "langpack resource removed during shutdown for uninstall");

  await promiseShutdownManager();
});

add_task(async function test_amazing_disappearing_langpacks() {
  let check = yes => {
    equal(
      L10nRegistry.getInstance().getAvailableLocales().includes("und"),
      yes,
      "check L10nRegistry"
    );
    equal(
      Services.locale.availableLocales.includes("und"),
      yes,
      "check availableLocales"
    );
  };

  await promiseStartupManager();

  check(false);

  await Promise.all([
    promiseLangpackStartup(),
    AddonTestUtils.promiseInstallXPI(ADDONS.langpack_1),
  ]);

  check(true);

  await promiseShutdownManager();

  check(false);

  await AddonTestUtils.manuallyUninstall(AddonTestUtils.profileExtensions, ID);

  await promiseStartupManager();

  check(false);
});

/**
 * This test verifies that language pack will get disabled after app
 * gets upgraded.
 */
add_task(async function test_disable_after_app_update() {
  let [, { addon }] = await Promise.all([
    promiseLangpackStartup(),
    AddonTestUtils.promiseInstallXPI(ADDONS.langpack_1),
  ]);
  Assert.ok(addon.isActive);

  await promiseRestartManager("59");

  addon = await promiseAddonByID(ID);
  Assert.ok(!addon.isActive);
  Assert.ok(addon.appDisabled);

  await addon.uninstall();
  await promiseShutdownManager();
});

/**
 * This test verifies that a postponed language pack update will be
 * applied after a restart.
 */
add_task(async function test_after_app_update() {
  await promiseStartupManager("58");
  let [, { addon }] = await Promise.all([
    promiseLangpackStartup(),
    AddonTestUtils.promiseInstallXPI(ADDONS.langpack_1),
  ]);
  Assert.ok(addon.isActive);

  await promiseRestartManager("60");

  addon = await promiseAddonByID(ID);
  Assert.ok(!addon.isActive);
  Assert.ok(addon.appDisabled);
  Assert.equal(addon.version, "58.0.20230105.121014");

  let update = await promiseFindAddonUpdates(addon);
  Assert.ok(update.updateAvailable, "update is available");
  let install = update.updateAvailable;
  let postponed = promisePostponeInstall(install);
  install.install();
  await postponed;
  Assert.equal(
    install.state,
    AddonManager.STATE_POSTPONED,
    "install postponed"
  );

  await promiseRestartManager();

  addon = await promiseAddonByID(ID);
  Assert.ok(addon.isActive);
  Assert.equal(addon.version, "60.1.20230309.91233");

  await addon.uninstall();
  await promiseShutdownManager();
});

// Support setting the request locale.
function promiseLocaleChanged(requestedLocales) {
  let changed = ExtensionUtils.promiseObserved(
    "intl:requested-locales-changed"
  );
  Services.locale.requestedLocales = requestedLocales;
  return changed;
}

/**
 * This test verifies that an addon update for the next version can be
 * retrieved and staged for restart.
 */
add_task(async function test_staged_langpack_for_app_update() {
  let originalLocales = Services.locale.requestedLocales;

  await promiseStartupManager("58");
  let [, { addon }] = await Promise.all([
    promiseLangpackStartup(),
    AddonTestUtils.promiseInstallXPI(ADDONS.langpack_1),
  ]);
  Assert.ok(addon.isActive);
  await promiseLocaleChanged(["und"]);

  // Mimick a major release update happening while a
  // langpack from a dotrelease what already available
  // (and then assert that the dotrelease langpack
  // is the one staged and then installed on browser
  // restart)
  await AddonManager.stageLangpacksForAppUpdate("60");
  await promiseRestartManager("60");

  addon = await promiseAddonByID(ID);
  Assert.ok(addon.isActive);
  Assert.equal(addon.version, "60.1.20230309.91233");

  // Mimick a second dotrelease update, along with
  // staging of the langpacks released along with it
  // (then assert that the langpack for the second
  // dotrelease is staged and then installed on
  // browser restart).
  await promiseRestartManager("60.1");
  await AddonManager.stageLangpacksForAppUpdate("60.2");
  await promiseRestartManager("60.2");

  addon = await promiseAddonByID(ID);
  Assert.ok(addon.isActive);
  Assert.equal(addon.version, "60.2.20230319.94511");

  await addon.uninstall();
  await promiseShutdownManager();

  Services.locale.requestedLocales = originalLocales;
});

/**
 * This test verifies that an addon update for the next version can be
 * retrieved and staged for restart, but a restart failure falls back.
 */
add_task(async function test_staged_langpack_for_app_update_fail() {
  let originalLocales = Services.locale.requestedLocales;

  await promiseStartupManager("58");
  let [, { addon }] = await Promise.all([
    promiseLangpackStartup(),
    AddonTestUtils.promiseInstallXPI(ADDONS.langpack_1),
  ]);
  Assert.ok(addon.isActive);
  await promiseLocaleChanged(["und"]);

  await AddonManager.stageLangpacksForAppUpdate("60");
  await promiseRestartManager();

  addon = await promiseAddonByID(ID);
  Assert.ok(addon.isActive);
  Assert.equal(addon.version, "58.0.20230105.121014");

  await addon.uninstall();
  await promiseShutdownManager();
  Services.locale.requestedLocales = originalLocales;
});

/**
 * This test verifies that the staged langpack update is available after
 * updating the application, even if another langpack update happened in
 * the meantime.
 * Regression test for https://bugzilla.mozilla.org/show_bug.cgi?id=2006489
 */
add_task(async function test_staged_langpack_preserved_during_addon_update() {
  let originalLocales = Services.locale.requestedLocales;

  await promiseStartupManager("60");
  let [, { addon }] = await Promise.all([
    promiseLangpackStartup(),
    AddonTestUtils.promiseInstallXPI(langpack_update),
  ]);
  Assert.ok(addon.isActive);
  Assert.equal(addon.version, "60.0.20230207.112555");
  await promiseLocaleChanged(["und"]);

  await AddonManager.stageLangpacksForAppUpdate("145");

  {
    // Between stage of langpack for 60 and application update to 145,
    // we trigger an add-on update check, where we discover an updated
    // langpack for the current application version (60). This should not
    // affect the langpack staged for 145.
    let update = await promiseFindAddonUpdates(addon);
    Assert.ok(update.updateAvailable, "update is available");
    await promiseCompleteInstall(update.updateAvailable);
    addon = await promiseAddonByID(ID);
    Assert.equal(addon.version, "60.1.20230309.91233");
    Assert.ok(addon.isActive);
  }

  await promiseRestartManager("145");

  addon = await promiseAddonByID(ID);
  Assert.equal(addon.version, "145.0.20251124.145406");
  Assert.ok(addon.isActive);

  await addon.uninstall();
  await promiseShutdownManager();
  Services.locale.requestedLocales = originalLocales;
});

/**
 * This test verifies that an attempt to stage another langpack, after having
 * already staged one, will cause the last attempt to take precedence.
 */
add_task(async function test_staged_langpack_twice_without_restart() {
  let originalLocales = Services.locale.requestedLocales;

  await promiseStartupManager("58");
  let [, { addon }] = await Promise.all([
    promiseLangpackStartup(),
    AddonTestUtils.promiseInstallXPI(ADDONS.langpack_1),
  ]);
  Assert.ok(addon.isActive);
  Assert.equal(addon.version, "58.0.20230105.121014");
  await promiseLocaleChanged(["und"]);

  info("Staging langpack for next version (60), without application restart");
  const [[installFor60]] = await Promise.all([
    AddonTestUtils.promiseInstallEvent("onInstallPostponed"),
    AddonManager.stageLangpacksForAppUpdate("60"),
  ]);
  Assert.equal(installFor60.version, "60.1.20230309.91233");

  info("Staging langpack for different version (145), will restart after.");
  await AddonManager.stageLangpacksForAppUpdate("145");

  info("cancel() on obsolete install (60) should not break install (145)");
  Assert.equal(installFor60.state, AddonManager.STATE_POSTPONED);
  // Sanity check: we are operating on an active install:
  Assert.ok((await AddonManager.getAllInstalls()).includes(installFor60));
  installFor60.cancel();

  await promiseRestartManager("145");

  addon = await promiseAddonByID(ID);
  Assert.equal(addon.version, "145.0.20251124.145406");
  Assert.ok(addon.isActive);

  await addon.uninstall();
  await promiseShutdownManager();
  Services.locale.requestedLocales = originalLocales;
});

/**
 * Tests that canceling a staged langpack will prevent its installation at
 * the next application startup.
 */
add_task(async function test_staged_langpack_cancel() {
  let originalLocales = Services.locale.requestedLocales;

  await promiseStartupManager("58");
  let [, { addon }] = await Promise.all([
    promiseLangpackStartup(),
    AddonTestUtils.promiseInstallXPI(ADDONS.langpack_1),
  ]);
  Assert.ok(addon.isActive);
  Assert.equal(addon.version, "58.0.20230105.121014");
  await promiseLocaleChanged(["und"]);

  info("Staging langpack for next version (60), without application restart");
  const [[installFor60]] = await Promise.all([
    AddonTestUtils.promiseInstallEvent("onInstallPostponed"),
    AddonManager.stageLangpacksForAppUpdate("60"),
  ]);
  Assert.equal(installFor60.version, "60.1.20230309.91233");
  installFor60.cancel();

  await promiseRestartManager("60");

  addon = await promiseAddonByID(ID);
  Assert.equal(addon.version, "58.0.20230105.121014");
  Assert.ok(!addon.isActive);

  await addon.uninstall();
  await promiseShutdownManager();
  Services.locale.requestedLocales = originalLocales;
});

/**
 * This test verifies that an update restart works when the langpack
 * cannot be updated.
 */
add_task(async function test_staged_langpack_for_app_update_not_found() {
  let originalLocales = Services.locale.requestedLocales;

  await promiseStartupManager("58");
  let [, { addon }] = await Promise.all([
    promiseLangpackStartup(),
    AddonTestUtils.promiseInstallXPI(ADDONS.langpack_1),
  ]);
  Assert.ok(addon.isActive);
  await promiseLocaleChanged(["und"]);

  await AddonManager.stageLangpacksForAppUpdate("59");
  await promiseRestartManager("59");

  addon = await promiseAddonByID(ID);
  Assert.ok(!addon.isActive);
  Assert.equal(addon.version, "58.0.20230105.121014");

  await addon.uninstall();
  await promiseShutdownManager();
  Services.locale.requestedLocales = originalLocales;
});

/**
 * This test verifies that a compat update with an invalid max_version
 * will be disabled, at least allowing Firefox to startup without failures.
 */
add_task(async function test_staged_langpack_compat_startup() {
  let originalLocales = Services.locale.requestedLocales;

  await promiseStartupManager("58");
  let [, { addon }] = await Promise.all([
    promiseLangpackStartup(),
    AddonTestUtils.promiseInstallXPI(ADDONS.langpack_1),
  ]);
  Assert.ok(addon.isActive);
  await promiseLocaleChanged(["und"]);

  // Mimick a compatibility update
  let compatUpdate = {
    targetApplications: [
      {
        id: "toolkit@mozilla.org",
        minVersion: "58",
        maxVersion: "*",
      },
    ],
  };
  addon.__AddonInternal__.applyCompatibilityUpdate(compatUpdate);

  await promiseRestartManager("59");

  addon = await promiseAddonByID(ID);
  Assert.ok(!addon.isActive, "addon is not active after upgrade");
  ok(!addon.isCompatible, "compatibility update fixed");

  await promiseRestartManager("58");

  addon = await promiseAddonByID(ID);
  Assert.ok(addon.isActive, "addon is active after downgrade");
  ok(addon.isCompatible, "compatibility update fixed");

  await addon.uninstall();
  await promiseShutdownManager();
  Services.locale.requestedLocales = originalLocales;
});
