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

"use strict";

// NOTE: this head support file should be used along with the
// one we are importing eslint globals from, and to be listed
// in the xpcshell.toml `head` property value right after it for
// the xpcshell test file that are meant to test Nimbus-driven
// train-hopping.

/* import-globals-from ../../../../extensions/newtab/test/xpcshell/head.js */

/* exported assertNewTabResourceMapping, assertTrainhopAddonNimbusExposure,
 *          assertTrainhopAddonVersionPref,
 *          asyncAssertNewTabAddon, asyncAssertNimbusTrainhopAddonStaged,
 *          cancelPendingInstall, mockAboutNewTabUninit, server,
 *          BUILTIN_ADDON_ID, BUILTIN_ADDON_VERSION,
 *          BUILTIN_LOCATION_NAME, PROFILE_LOCATION_NAME,
 *          TRAINHOP_NIMBUS_FEATURE_ID,
 *          TRAINHOP_SCHEDULED_UPDATE_STATE_DELAY_PREF,
 *          TRAINHOP_SCHEDULED_UPDATE_STATE_TIMEOUT_PREF */

const {
  AboutNewTabResourceMapping,
  BUILTIN_ADDON_ID,
  TRAINHOP_NIMBUS_FEATURE_ID,
  TRAINHOP_NIMBUS_FIRST_STARTUP_FEATURE_ID,
  TRAINHOP_XPI_BASE_URL_PREF,
  TRAINHOP_SCHEDULED_UPDATE_STATE_TIMEOUT_PREF,
  TRAINHOP_SCHEDULED_UPDATE_STATE_DELAY_PREF,
} = ChromeUtils.importESModule(
  "resource:///modules/AboutNewTabResourceMapping.sys.mjs"
);
const { ExperimentAPI, NimbusFeatures } = ChromeUtils.importESModule(
  "resource://nimbus/ExperimentAPI.sys.mjs"
);
const { NimbusTestUtils } = ChromeUtils.importESModule(
  "resource://testing-common/NimbusTestUtils.sys.mjs"
);

const BUILTIN_LOCATION_NAME = "app-builtin-addons";
const PROFILE_LOCATION_NAME = "app-profile";

// Set from the setup task based on the currently installed
// newtab built-in add-on version.
let BUILTIN_ADDON_VERSION;

NimbusTestUtils.init(this);

const server = AddonTestUtils.createHttpServer({ hosts: ["example.com"] });
Services.prefs.setStringPref(TRAINHOP_XPI_BASE_URL_PREF, "http://example.com/");

// Defaults to "system" signedState for xpi with the newtab builtin add-id.
AddonTestUtils.usePrivilegedSignatures = addonId =>
  addonId === BUILTIN_ADDON_ID ? "system" : false;

add_setup(async function nimbusTestsSetup() {
  const { cleanup: nimbusTestCleanup } = await NimbusTestUtils.setupTest();
  registerCleanupFunction(nimbusTestCleanup);

  const builtinAddon = await asyncAssertNewTabAddon({
    locationName: "app-builtin-addons",
  });
  Assert.ok(builtinAddon?.version, "Got a builtin add-on version");
  BUILTIN_ADDON_VERSION = builtinAddon.version;

  // When we stage installs and then cancel them, `XPIInstall` won't be able to
  // remove the staging directory (which is expected to be empty) until the
  // next restart. This causes an `AddonTestUtils` assertion to fail because we
  // don't expect any staging directory at the end of the tests. That's why we
  // remove this directory in the cleanup function defined below.
  //
  // We only remove the staging directory and that will only works if the
  // directory is empty, otherwise an unchaught error will be thrown (on
  // purpose).
  registerCleanupFunction(() => {
    const profileDir = do_get_profile();
    const stagingDir = profileDir.clone();
    stagingDir.append("extensions");
    stagingDir.append("staged");
    stagingDir.exists() && stagingDir.remove(/* recursive */ false);
  });
});

/**
 * Setting up a simulated train-hop add-on version for testing.
 *
 * @param {object} params
 * @param {string} [params.updateAddonVersion]
 *   The version of the train-hop add-on to set in the simulated feature enrolling.
 * @returns {Promise<object>} A promise that resolves with an object containing:
 *   - fakeNimbusVariables: The fake Nimbus feature variables used in the simulated Nimbus feature enrolling.
 *   - nimbusFeatureCleanup: The cleanup function for the Nimbus feature.
 */
async function setupNimbusTrainhopAddon({ updateAddonVersion }) {
  info(`Setting up simulated train-hop add-on version ${updateAddonVersion}`);
  let fakeNewTabXPI = AddonTestUtils.createTempWebExtensionFile({
    manifest: {
      version: updateAddonVersion,
      browser_specific_settings: {
        gecko: { id: BUILTIN_ADDON_ID },
      },
    },
    files: {
      "lib/NewTabGleanUtils.sys.mjs": `
        export const NewTabGleanUtils = {
          registerMetricsAndPings() {},
        };
      `,
    },
  });

  const xpi_download_path = "data/newtab.xpi";

  server.registerFile(`/${xpi_download_path}`, fakeNewTabXPI, () => {
    info(`Server got request for ${xpi_download_path}`);
  });

  const fakeNimbusVariables = {
    xpi_download_path,
    addon_version: updateAddonVersion,
  };

  await ExperimentAPI.ready();
  const nimbusFeatureCleanup = await NimbusTestUtils.enrollWithFeatureConfig(
    {
      featureId: TRAINHOP_NIMBUS_FEATURE_ID,
      value: fakeNimbusVariables,
    },
    { isRollout: true }
  );
  // Sanity checks.
  Assert.deepEqual(
    NimbusFeatures[TRAINHOP_NIMBUS_FEATURE_ID].getAllVariables(),
    fakeNimbusVariables,
    "Got the expected variables from the nimbus feature"
  );

  return { fakeNimbusVariables, nimbusFeatureCleanup };
}

/**
 * Verifies that a newtab add-on exists in the expected location and optionally asserts
 * that the add-on has the expected version.
 *
 * @param {object} params
 * @param {string} params.locationName
 *   XPIProvider location name where the newtab add-on is expected to be installed into.
 * @param {string} [params.version]
 *   The expected newtab add-on version (only verified if set).
 * @returns {Promise<AddonWrapper>}
 *   A promise that resolves to the AddonWrapper instance for the newtab add-on.
 */
async function asyncAssertNewTabAddon({ locationName, version }) {
  const newtabAddon = await AddonManager.getAddonByID(BUILTIN_ADDON_ID);
  Assert.equal(
    newtabAddon?.locationName,
    locationName,
    "Got the expected newtab add-on locationName"
  );
  if (version) {
    Assert.equal(
      newtabAddon?.version,
      version,
      "Got the expected newtab add-on version"
    );
  }
  return newtabAddon;
}

/**
 * Verifies that the expected staged installation for the train-hop add-on exists.
 *
 * @param {object} params
 * @param {string} params.updateAddonVersion
 *   The add-on version expected to be installed by the simulated feature enrolling.
 * @returns {Promise<AddonInstall>}
 *   A promise that resolves with to the AddonInstall instance for the train-hop
 *   add-on version expected to be found staged.
 */
async function asyncAssertNimbusTrainhopAddonStaged({ updateAddonVersion }) {
  const pendingInstall = (await AddonManager.getAllInstalls()).find(
    install => install.addon.id === BUILTIN_ADDON_ID
  );
  Assert.equal(
    pendingInstall?.state,
    AddonManager.STATE_POSTPONED,
    "Expect a pending install for the newtab add-on to be found"
  );

  Assert.deepEqual(
    {
      existingVersion: pendingInstall.existingAddon.version,
      existingLocationName: pendingInstall.existingAddon.locationName,
      updateVersion: pendingInstall.addon.version,
      updateLocationName: pendingInstall.addon.locationName,
    },
    {
      existingVersion: BUILTIN_ADDON_VERSION,
      existingLocationName: BUILTIN_LOCATION_NAME,
      updateVersion: updateAddonVersion,
      updateLocationName: PROFILE_LOCATION_NAME,
    },
    "Got the expected version and locationName pendingInstall existing and updated add-on"
  );
  return { pendingInstall };
}

/**
 * Asserts that an exposure event for train-hop Nimbus feature has been recorded or
 * not recorded yet.
 *
 * @param {object}  params
 * @param {boolean} params.expectedExposure
 */
function assertTrainhopAddonNimbusExposure({ expectedExposure }) {
  const enrollmentMetadata =
    NimbusFeatures[TRAINHOP_NIMBUS_FEATURE_ID].getEnrollmentMetadata();
  Assert.deepEqual(
    Glean.nimbusEvents.exposure
      .testGetValue("events")
      ?.map(ev => ev.extra)
      .filter(ev => ev.feature_id == TRAINHOP_NIMBUS_FEATURE_ID) ?? [],
    expectedExposure
      ? [
          {
            feature_id: TRAINHOP_NIMBUS_FEATURE_ID,
            branch: enrollmentMetadata.branch,
            experiment: enrollmentMetadata.slug,
          },
        ]
      : [],
    expectedExposure
      ? "Got the expected exposure Glean event for the newtabTrainhopAddon Nimbus feature"
      : "Got no exposure Glean event for the newtabTrainhopAddon as expected"
  );
}

function assertTrainhopAddonVersionPref(expectedTrainhopAddonVersion) {
  Assert.equal(
    Services.prefs.getStringPref(
      "browser.newtabpage.trainhopAddon.version",
      ""
    ),
    expectedTrainhopAddonVersion,
    expectedTrainhopAddonVersion
      ? "Expect browser.newtab.trainhopAddon.version about:config pref to be set while client is enrolled"
      : "Expect browser.newtab.trainhopAddon.version about:config pref to be empty while client is unenrolled"
  );
}

/**
 * Cancels a pending add-on installation and awaits the cancellation to be completed.
 *
 * @param {AddonInstall} pendingInstall
 *   Instance of the pending AddonInstall to cancel.
 * @returns {Promise<void>}
 *   A promise that resolves when the AddonInstall instance has been successfully cancelled.
 */
async function cancelPendingInstall(pendingInstall) {
  const cancelDeferred = Promise.withResolvers();
  pendingInstall.addListener({
    onInstallCancelled() {
      cancelDeferred.resolve();
    },
  });
  pendingInstall.cancel();
  await cancelDeferred.promise;
}

// Mock browser restart scenario.
function mockAboutNewTabUninit() {
  AboutNewTab.uninit();
  AboutNewTabResourceMapping.initialized = false;
  AboutNewTabResourceMapping._rootURISpec = null;
  AboutNewTabResourceMapping._addonVersion = null;
  AboutNewTabResourceMapping._addonListener = null;
}
