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

// NOTE: test_EnvironmentAddonBuilder_telemetry.js (part of the AOM
// xpcshell tests) provides the same kind of testing coverage as this test,
// currently only for Android builds.
// Once the TelemetryEnvironment isn't responsible for collecting the
// activeAddons/theme/GMPlugins anymore then we should run
// test_EnvironmentAddonBuilder_telemetry also on Desktop builds and
// we can remove all activeAddons/theme/GMPPlugins test tasks from this
// test.

const { AddonManager, AddonManagerPrivate } = ChromeUtils.importESModule(
  "resource://gre/modules/AddonManager.sys.mjs"
);
const { TelemetryEnvironment } = ChromeUtils.importESModule(
  "resource://gre/modules/TelemetryEnvironment.sys.mjs"
);
const { SearchService } = ChromeUtils.importESModule(
  "moz-src:///toolkit/components/search/SearchService.sys.mjs"
);
const { SearchTestUtils } = ChromeUtils.importESModule(
  "resource://testing-common/SearchTestUtils.sys.mjs"
);
const { TelemetryEnvironmentTesting } = ChromeUtils.importESModule(
  "resource://testing-common/TelemetryEnvironmentTesting.sys.mjs"
);

ChromeUtils.defineESModuleGetters(this, {
  ExtensionTestUtils:
    "resource://testing-common/ExtensionXPCShellUtils.sys.mjs",
});

async function installXPIFromURL(url) {
  let install = await AddonManager.getInstallForURL(url);
  return install.install();
}

// The webserver hosting the addons.
var gHttpServer = null;
// The URL of the webserver root.
var gHttpRoot = null;
// The URL of the data directory, on the webserver.
var gDataRoot = null;

function MockAddonWrapper(aAddon) {
  this.addon = aAddon;
}
MockAddonWrapper.prototype = {
  get id() {
    return this.addon.id;
  },

  get type() {
    return this.addon.type;
  },

  get appDisabled() {
    return false;
  },

  get isCompatible() {
    return true;
  },

  get isPlatformCompatible() {
    return true;
  },

  get scope() {
    return AddonManager.SCOPE_PROFILE;
  },

  get foreignInstall() {
    return false;
  },

  get providesUpdatesSecurely() {
    return true;
  },

  get blocklistState() {
    return 0; // Not blocked.
  },

  get pendingOperations() {
    return AddonManager.PENDING_NONE;
  },

  get permissions() {
    return AddonManager.PERM_CAN_UNINSTALL | AddonManager.PERM_CAN_DISABLE;
  },

  get isActive() {
    return true;
  },

  get name() {
    return this.addon.name;
  },

  get version() {
    return this.addon.version;
  },

  get creator() {
    return new AddonManagerPrivate.AddonAuthor(this.addon.author);
  },

  get userDisabled() {
    return this.appDisabled;
  },
};

function createMockAddonProvider(aName) {
  let mockProvider = {
    _addons: [],

    get name() {
      return aName;
    },

    addAddon(aAddon) {
      this._addons.push(aAddon);
      AddonManagerPrivate.callAddonListeners(
        "onInstalled",
        new MockAddonWrapper(aAddon)
      );
    },

    async getAddonsByTypes(aTypes) {
      return this._addons
        .filter(a => !aTypes || aTypes.includes(a.type))
        .map(a => new MockAddonWrapper(a));
    },

    shutdown() {
      return Promise.resolve();
    },
  };

  return mockProvider;
}

add_setup(async function setup() {
  TelemetryEnvironmentTesting.registerFakeSysInfo();
  TelemetryEnvironmentTesting.spoofGfxAdapter();
  do_get_profile();

  // We need to ensure FOG is initialized, otherwise we will panic trying to get test values.
  Services.fog.initializeFOG();

  await loadAddonManager(APP_ID, APP_NAME, APP_VERSION, PLATFORM_VERSION);

  TelemetryEnvironmentTesting.init(gAppInfo);

  // The test runs in a fresh profile so starting the AddonManager causes
  // the addons database to be created (as does setting new theme).
  // For test_addonsStartup below, we want to test a "warm" startup where
  // there is already a database on disk.  Simulate that here by just
  // restarting the AddonManager.
  await AddonTestUtils.promiseShutdownManager();
  await AddonTestUtils.overrideBuiltIns({ builtins: [] });
  AddonTestUtils.addonStartup.remove(true);
  await AddonTestUtils.promiseStartupManager();

  // Setup a webserver to serve Addons, etc.
  gHttpServer = new HttpServer();
  gHttpServer.start(-1);
  let port = gHttpServer.identity.primaryPort;
  gHttpRoot = "http://localhost:" + port + "/";
  gDataRoot = gHttpRoot + "data/";
  gHttpServer.registerDirectory("/data/", do_get_cwd());
  registerCleanupFunction(() => gHttpServer.stop(() => {}));

  // Create the attribution data file, so that settings.attribution will exist.
  // The attribution functionality only exists in Firefox.
  if (AppConstants.MOZ_BUILD_APP == "browser") {
    TelemetryEnvironmentTesting.spoofAttributionData();
    registerCleanupFunction(async function () {
      await TelemetryEnvironmentTesting.cleanupAttributionData;
    });
  }
  await TelemetryEnvironmentTesting.spoofProfileReset();
  await TelemetryEnvironment.delayedInit();
  // The environment needs the search service initialised, so use a dummy
  // configuration.
  await SearchTestUtils.setRemoteSettingsConfig([{ identifier: "unused" }]);
});

add_task(async function test_checkEnvironment() {
  // During startup we have partial addon records.
  // First make sure we haven't yet read the addons DB, then test that
  // we have some partial addons data.
  Assert.equal(
    AddonManagerPrivate.isDBLoaded(),
    false,
    "addons database is not loaded"
  );

  let data = TelemetryEnvironment.currentEnvironment;

  // NOTE: about:telemetry expects the active theme to always be set, if no theme
  // was enabled and active (eg. because the theme has not been installed yet and
  // enabled yet) then it should be set to an empty object. This assertion is
  // meant to prevent issues like Bug 1994389 to regress without being caught by
  // the TelemetryEnvironment unit tests.
  Assert.deepEqual(
    data.addons.theme,
    {},
    "Expect active theme property to be set to an empty object"
  );

  TelemetryEnvironmentTesting.checkAddonsSection(data, false, true);

  // Check that settings.intl is lazily loaded.
  Assert.equal(
    typeof data.settings.intl,
    "object",
    "intl is initially an object"
  );
  Assert.equal(
    Object.keys(data.settings.intl).length,
    0,
    "intl is initially empty"
  );

  // Now continue with startup.
  let initPromise = TelemetryEnvironment.onInitialized();
  finishAddonManagerStartup();

  // Fake the delayed startup event for intl data to load.
  fakeIntlReady();

  let environmentData = await initPromise;
  TelemetryEnvironmentTesting.checkEnvironmentData(environmentData, {
    isInitial: true,
  });

  TelemetryEnvironmentTesting.spoofPartnerInfo();
  Services.obs.notifyObservers(null, DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC);

  environmentData = TelemetryEnvironment.currentEnvironment;
  TelemetryEnvironmentTesting.checkEnvironmentData(environmentData, {
    assertProcessData: true,
  });
});

add_task(async function test_prefWatchPolicies() {
  const PREF_TEST_1 = "toolkit.telemetry.test.pref_new";
  const PREF_TEST_2 = "toolkit.telemetry.test.pref1";
  const PREF_TEST_3 = "toolkit.telemetry.test.pref2";
  const PREF_TEST_4 = "toolkit.telemetry.test.pref_old";
  const PREF_TEST_5 = "toolkit.telemetry.test.requiresRestart";

  const expectedValue = "some-test-value";
  const unexpectedValue = "unexpected-test-value";

  const PREFS_TO_WATCH = new Map([
    [PREF_TEST_1, { what: TelemetryEnvironment.RECORD_PREF_VALUE }],
    [PREF_TEST_2, { what: TelemetryEnvironment.RECORD_PREF_STATE }],
    [PREF_TEST_3, { what: TelemetryEnvironment.RECORD_PREF_STATE }],
    [PREF_TEST_4, { what: TelemetryEnvironment.RECORD_PREF_VALUE }],
    [
      PREF_TEST_5,
      { what: TelemetryEnvironment.RECORD_PREF_VALUE, requiresRestart: true },
    ],
  ]);

  Services.prefs.setStringPref(PREF_TEST_4, expectedValue);
  Services.prefs.setStringPref(PREF_TEST_5, expectedValue);

  // Set the Environment preferences to watch.
  await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
  let deferred = Promise.withResolvers();

  // Check that the pref values are missing or present as expected
  Assert.strictEqual(
    TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST_1],
    undefined
  );
  Assert.strictEqual(
    TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST_4],
    expectedValue
  );
  Assert.strictEqual(
    TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST_5],
    expectedValue
  );

  TelemetryEnvironment.registerChangeListener(
    "testWatchPrefs",
    (reason, data) => deferred.resolve(data)
  );
  let oldEnvironmentData = TelemetryEnvironment.currentEnvironment;

  // Trigger a change in the watched preferences.
  Services.prefs.setStringPref(PREF_TEST_1, expectedValue);
  Services.prefs.setBoolPref(PREF_TEST_2, false);
  Services.prefs.setStringPref(PREF_TEST_5, unexpectedValue);
  let eventEnvironmentData = await deferred.promise;

  // Unregister the listener.
  TelemetryEnvironment.unregisterChangeListener("testWatchPrefs");

  // Check environment contains the correct data.
  Assert.deepEqual(oldEnvironmentData, eventEnvironmentData);
  let userPrefs = TelemetryEnvironment.currentEnvironment.settings.userPrefs;

  Assert.equal(
    userPrefs[PREF_TEST_1],
    expectedValue,
    "Environment contains the correct preference value."
  );
  Assert.equal(
    userPrefs[PREF_TEST_2],
    "<user-set>",
    "Report that the pref was user set but the value is not shown."
  );
  Assert.ok(
    !(PREF_TEST_3 in userPrefs),
    "Do not report if preference not user set."
  );
  Assert.equal(
    userPrefs[PREF_TEST_5],
    expectedValue,
    "The pref value in the environment data should still be the same"
  );
});

add_task(async function test_prefWatch_prefReset() {
  const PREF_TEST = "toolkit.telemetry.test.pref1";
  const PREFS_TO_WATCH = new Map([
    [PREF_TEST, { what: TelemetryEnvironment.RECORD_PREF_STATE }],
  ]);

  // Set the preference to a non-default value.
  Services.prefs.setBoolPref(PREF_TEST, false);

  // Set the Environment preferences to watch.
  await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
  let deferred = Promise.withResolvers();
  TelemetryEnvironment.registerChangeListener(
    "testWatchPrefs_reset",
    deferred.resolve
  );

  Assert.strictEqual(
    TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST],
    "<user-set>"
  );

  // Trigger a change in the watched preferences.
  Services.prefs.clearUserPref(PREF_TEST);
  await deferred.promise;

  Assert.strictEqual(
    TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST],
    undefined
  );

  // Unregister the listener.
  TelemetryEnvironment.unregisterChangeListener("testWatchPrefs_reset");
});

add_task(async function test_prefDefault() {
  const PREF_TEST = "toolkit.telemetry.test.defaultpref1";
  const expectedValue = "some-test-value";

  const PREFS_TO_WATCH = new Map([
    [PREF_TEST, { what: TelemetryEnvironment.RECORD_DEFAULTPREF_VALUE }],
  ]);

  // Set the preference to a default value.
  Services.prefs.getDefaultBranch(null).setCharPref(PREF_TEST, expectedValue);

  // Set the Environment preferences to watch.
  // We're not watching, but this function does the setup we need.
  await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);

  Assert.strictEqual(
    TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST],
    expectedValue
  );
});

add_task(async function test_prefDefaultState() {
  const PREF_TEST = "toolkit.telemetry.test.defaultpref2";
  const expectedValue = "some-test-value";

  const PREFS_TO_WATCH = new Map([
    [PREF_TEST, { what: TelemetryEnvironment.RECORD_DEFAULTPREF_STATE }],
  ]);

  await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);

  Assert.equal(
    PREF_TEST in TelemetryEnvironment.currentEnvironment.settings.userPrefs,
    false
  );

  // Set the preference to a default value.
  Services.prefs.getDefaultBranch(null).setCharPref(PREF_TEST, expectedValue);

  Assert.strictEqual(
    TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST],
    "<set>"
  );
});

add_task(async function test_prefInvalid() {
  const PREF_TEST_1 = "toolkit.telemetry.test.invalid1";
  const PREF_TEST_2 = "toolkit.telemetry.test.invalid2";

  const PREFS_TO_WATCH = new Map([
    [PREF_TEST_1, { what: TelemetryEnvironment.RECORD_DEFAULTPREF_VALUE }],
    [PREF_TEST_2, { what: TelemetryEnvironment.RECORD_DEFAULTPREF_STATE }],
  ]);

  await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);

  Assert.strictEqual(
    TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST_1],
    undefined
  );
  Assert.strictEqual(
    TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST_2],
    undefined
  );
});

add_task(async function test_addonsWatch_InterestingChange() {
  const ADDON_INSTALL_URL = gDataRoot + "restartless.xpi";
  const ADDON_ID = "tel-restartless-webext@tests.mozilla.org";
  // We only expect a single notification for each install, uninstall, enable, disable.
  const EXPECTED_NOTIFICATIONS = 4;

  let receivedNotifications = 0;

  let registerCheckpointPromise = aExpected => {
    return new Promise(resolve =>
      TelemetryEnvironment.registerChangeListener(
        "testWatchAddons_Changes" + aExpected,
        reason => {
          Assert.equal(reason, "addons-changed");
          receivedNotifications++;
          resolve();
        }
      )
    );
  };

  let assertCheckpoint = aExpected => {
    Assert.equal(receivedNotifications, aExpected);
    TelemetryEnvironment.unregisterChangeListener(
      "testWatchAddons_Changes" + aExpected
    );
  };

  // Test for receiving one notification after each change.
  let checkpointPromise = registerCheckpointPromise(1);
  await installXPIFromURL(ADDON_INSTALL_URL);
  await checkpointPromise;
  assertCheckpoint(1);
  Assert.ok(
    ADDON_ID in TelemetryEnvironment.currentEnvironment.addons.activeAddons
  );

  checkpointPromise = registerCheckpointPromise(2);
  let addon = await AddonManager.getAddonByID(ADDON_ID);
  await addon.disable();
  await checkpointPromise;
  assertCheckpoint(2);
  Assert.ok(
    !(ADDON_ID in TelemetryEnvironment.currentEnvironment.addons.activeAddons)
  );

  checkpointPromise = registerCheckpointPromise(3);
  let startupPromise = AddonTestUtils.promiseWebExtensionStartup(ADDON_ID);
  await addon.enable();
  await checkpointPromise;
  assertCheckpoint(3);
  Assert.ok(
    ADDON_ID in TelemetryEnvironment.currentEnvironment.addons.activeAddons
  );
  await startupPromise;

  checkpointPromise = registerCheckpointPromise(4);
  (await AddonManager.getAddonByID(ADDON_ID)).uninstall();
  await checkpointPromise;
  assertCheckpoint(4);
  Assert.ok(
    !(ADDON_ID in TelemetryEnvironment.currentEnvironment.addons.activeAddons)
  );

  Assert.equal(
    receivedNotifications,
    EXPECTED_NOTIFICATIONS,
    "We must only receive the notifications we expect."
  );
});

add_task(async function test_addonsWatch_NotInterestingChange() {
  // Plugins from GMPProvider are listed separately in addons.activeGMPlugins.
  // We simulate the "plugin" type in this test and verify that it is excluded.
  const PLUGIN_ID = "tel-fake-gmp-plugin@tests.mozilla.org";
  // "theme" type is already covered by addons.theme, so we aren't interested.
  const THEME_ID = "tel-theme@tests.mozilla.org";
  // "dictionary" type should be in addon.activeAddons.
  const DICT_ID = "tel-dict@tests.mozilla.org";

  let receivedNotification = false;
  let deferred = Promise.withResolvers();
  TelemetryEnvironment.registerChangeListener("testNotInteresting", () => {
    Assert.ok(
      !receivedNotification,
      "Should not receive multiple notifications"
    );
    receivedNotification = true;
    deferred.resolve();
  });

  // "plugin" type, to verify that non-XPIProvider types such as the "plugin"
  // type from GMPProvider are not included in activeAddons.
  let fakePluginProvider = createMockAddonProvider("Fake GMPProvider");
  AddonManagerPrivate.registerProvider(fakePluginProvider);
  fakePluginProvider.addAddon({
    id: PLUGIN_ID,
    name: "Fake plugin",
    version: "1",
    type: "plugin",
  });

  // "theme" type.
  let themeXpi = AddonTestUtils.createTempWebExtensionFile({
    manifest: {
      theme: {},
      browser_specific_settings: { gecko: { id: THEME_ID } },
    },
  });
  let themeAddon = (await AddonTestUtils.promiseInstallFile(themeXpi)).addon;

  let dictXpi = AddonTestUtils.createTempWebExtensionFile({
    manifest: {
      dictionaries: {},
      browser_specific_settings: { gecko: { id: DICT_ID } },
    },
  });
  let dictAddon = (await AddonTestUtils.promiseInstallFile(dictXpi)).addon;

  await deferred.promise;
  Assert.ok(
    !(PLUGIN_ID in TelemetryEnvironment.currentEnvironment.addons.activeAddons),
    "GMP plugins should not appear in active addons."
  );
  Assert.ok(
    !(THEME_ID in TelemetryEnvironment.currentEnvironment.addons.activeAddons),
    "Themes should not appear in active addons."
  );
  Assert.ok(
    DICT_ID in TelemetryEnvironment.currentEnvironment.addons.activeAddons,
    "Dictionaries should appear in active addons."
  );

  TelemetryEnvironment.unregisterChangeListener("testNotInteresting");

  AddonManagerPrivate.unregisterProvider(fakePluginProvider);
  await themeAddon.uninstall();
  await dictAddon.uninstall();
});

add_task(async function test_addons() {
  const ADDON_INSTALL_URL = gDataRoot + "restartless.xpi";
  const ADDON_ID = "tel-restartless-webext@tests.mozilla.org";
  const ADDON_INSTALL_DATE = truncateToDays(Date.now());
  const EXPECTED_ADDON_DATA = {
    blocklisted: false,
    description: "A restartless addon which gets enabled without a reboot.",
    name: "XPI Telemetry Restartless Test",
    userDisabled: false,
    appDisabled: false,
    version: "1.0",
    scope: AddonManager.SCOPE_PROFILE,
    type: "extension",
    foreignInstall: false,
    hasBinaryComponents: false,
    installDay: ADDON_INSTALL_DATE,
    updateDay: ADDON_INSTALL_DATE,
    signedState: AddonManager.SIGNEDSTATE_PRIVILEGED,
    isSystem: false,
    isWebExtension: true,
    multiprocessCompatible: true,
    quarantineIgnoredByUser: false,
    // quarantineIgnoredByApp expected to be true because
    // the test addon is signed as privileged (see signedState).
    quarantineIgnoredByApp: true,
  };
  const SYSTEM_ADDON_ID = "tel-system-xpi@tests.mozilla.org";
  const EXPECTED_SYSTEM_ADDON_DATA = {
    blocklisted: false,
    description: "A system addon which is shipped with Firefox.",
    name: "XPI Telemetry System Add-on Test",
    userDisabled: false,
    appDisabled: false,
    version: "1.0",
    scope: AddonManager.SCOPE_APPLICATION,
    type: "extension",
    foreignInstall: false,
    hasBinaryComponents: false,
    installDay: 0,
    updateDay: 0,
    signedState: undefined,
    isSystem: true,
    isWebExtension: true,
    multiprocessCompatible: true,
    quarantineIgnoredByUser: false,
    // quarantineIgnoredByApp expected to be true because
    // the test addon is a system addon (see isSystem).
    quarantineIgnoredByApp: true,
  };

  const WEBEXTENSION_ADDON_ID = "tel-webextension-xpi@tests.mozilla.org";
  const WEBEXTENSION_ADDON_INSTALL_DATE = truncateToDays(Date.now());
  const EXPECTED_WEBEXTENSION_ADDON_DATA = {
    blocklisted: false,
    description: "A webextension addon.",
    name: "XPI Telemetry WebExtension Add-on Test",
    userDisabled: false,
    appDisabled: false,
    version: "1.0",
    scope: AddonManager.SCOPE_PROFILE,
    type: "extension",
    foreignInstall: false,
    hasBinaryComponents: false,
    installDay: WEBEXTENSION_ADDON_INSTALL_DATE,
    updateDay: WEBEXTENSION_ADDON_INSTALL_DATE,
    signedState: AddonManager.SIGNEDSTATE_PRIVILEGED,
    isSystem: false,
    isWebExtension: true,
    multiprocessCompatible: true,
    quarantineIgnoredByUser: false,
    // quarantineIgnoredByApp expected to be true because
    // the test addon is signed as privileged (see signedState).
    quarantineIgnoredByApp: true,
  };

  let deferred = Promise.withResolvers();
  TelemetryEnvironment.registerChangeListener("test_WebExtension", reason => {
    Assert.equal(reason, "addons-changed");
    deferred.resolve();
  });

  // Install an add-on so we have some data.
  let addon = await installXPIFromURL(ADDON_INSTALL_URL);

  // Install a webextension as well.
  // Note: all addons are webextensions, so doing this again is redundant...
  ExtensionTestUtils.init(this);

  let webextension = ExtensionTestUtils.loadExtension({
    useAddonManager: "permanent",
    manifest: {
      name: "XPI Telemetry WebExtension Add-on Test",
      description: "A webextension addon.",
      version: "1.0",
      browser_specific_settings: {
        gecko: {
          id: WEBEXTENSION_ADDON_ID,
        },
      },
    },
  });

  await webextension.startup();
  await deferred.promise;
  TelemetryEnvironment.unregisterChangeListener("test_WebExtension");

  let data = TelemetryEnvironment.currentEnvironment;
  TelemetryEnvironmentTesting.checkEnvironmentData(data);

  // Check addon data.
  Assert.ok(
    ADDON_ID in data.addons.activeAddons,
    "We must have one active addon."
  );
  let targetAddon = data.addons.activeAddons[ADDON_ID];
  for (let f in EXPECTED_ADDON_DATA) {
    Assert.equal(
      targetAddon[f],
      EXPECTED_ADDON_DATA[f],
      f + " must have the correct value."
    );
  }

  // Check system add-on data.
  Assert.ok(
    SYSTEM_ADDON_ID in data.addons.activeAddons,
    "We must have one active system addon."
  );
  let targetSystemAddon = data.addons.activeAddons[SYSTEM_ADDON_ID];
  for (let f in EXPECTED_SYSTEM_ADDON_DATA) {
    Assert.equal(
      targetSystemAddon[f],
      EXPECTED_SYSTEM_ADDON_DATA[f],
      f + " must have the correct value."
    );
  }

  // Check webextension add-on data.
  Assert.ok(
    WEBEXTENSION_ADDON_ID in data.addons.activeAddons,
    "We must have one active webextension addon."
  );
  let targetWebExtensionAddon = data.addons.activeAddons[WEBEXTENSION_ADDON_ID];
  for (let f in EXPECTED_WEBEXTENSION_ADDON_DATA) {
    Assert.equal(
      targetWebExtensionAddon[f],
      EXPECTED_WEBEXTENSION_ADDON_DATA[f],
      f + " must have the correct value."
    );
  }

  await webextension.unload();

  // Uninstall the addon.
  await addon.startupPromise;
  await addon.uninstall();
});

add_task(async function test_signedTheme() {
  AddonTestUtils.useRealCertChecks = true;

  const { PKCS7_WITH_SHA1, COSE_WITH_SHA256 } = Ci.nsIAppSignatureInfo;

  const ADDON_THEME_INSTALL_URL = gDataRoot + "webext-implicit-id.xpi";
  const ADDON_THEME_ID = "{46607a7b-1b2a-40ce-9afe-91cda52c46a6}";

  // Install the theme.
  let deferred = Promise.withResolvers();
  TelemetryEnvironment.registerChangeListener(
    "test_signedAddon",
    deferred.resolve
  );
  let theme = await installXPIFromURL(ADDON_THEME_INSTALL_URL);
  await theme.enable();
  ok(theme.isActive, "Theme should be active");

  // Install an extension to force the telemetry environment to be
  // updated (currently theme add-ons changes do not seem to be
  // notified as changes, see EnvironmentAddonBuilder _updateAddons
  // method for how changes to the environment.addons property are
  // being detected).
  const ADDON_INSTALL_URL = gDataRoot + "amosigned.xpi";
  let addon = await installXPIFromURL(ADDON_INSTALL_URL);

  await deferred.promise;
  TelemetryEnvironment.unregisterChangeListener("test_signedAddon");

  let data = TelemetryEnvironment.currentEnvironment;
  TelemetryEnvironmentTesting.checkEnvironmentData(data);

  // Check signedState and signedTypes on active theme data
  // (NOTE: other properties of active theme are technically
  // not covered by any other test task in this xpcshell test).
  Assert.equal(
    data.addons.theme.id,
    ADDON_THEME_ID,
    "Theme should be in the environment."
  );
  Assert.equal(
    data.addons.theme.signedState,
    AddonManager.SIGNEDSTATE_SIGNED,
    "Got expected signedState on activeTheme"
  );
  Assert.equal(
    data.addons.theme.signedTypes,
    JSON.stringify([COSE_WITH_SHA256, PKCS7_WITH_SHA1]),
    "Got expected signedTypes on activeTheme"
  );

  AddonTestUtils.useRealCertChecks = false;
  await addon.startupPromise;
  await addon.uninstall();
  await theme.startupPromise;
  await theme.uninstall();
});

add_task(async function test_signedAddon() {
  AddonTestUtils.useRealCertChecks = true;

  const { PKCS7_WITH_SHA1, COSE_WITH_SHA256 } = Ci.nsIAppSignatureInfo;

  const ADDON_INSTALL_URL = gDataRoot + "amosigned.xpi";
  const ADDON_ID = "amosigned-xpi@tests.mozilla.org";
  const ADDON_INSTALL_DATE = truncateToDays(Date.now());
  const EXPECTED_ADDON_DATA = {
    blocklisted: false,
    description: null,
    name: "XPI Test",
    userDisabled: false,
    appDisabled: false,
    version: "2.2",
    scope: AddonManager.SCOPE_PROFILE,
    type: "extension",
    foreignInstall: false,
    hasBinaryComponents: false,
    installDay: ADDON_INSTALL_DATE,
    updateDay: ADDON_INSTALL_DATE,
    signedState: AddonManager.SIGNEDSTATE_SIGNED,
    signedTypes: JSON.stringify([COSE_WITH_SHA256, PKCS7_WITH_SHA1]),
    quarantineIgnoredByUser: false,
    // quarantineIgnoredByApp expected to be false because
    // the test addon is signed as a non-privileged (see signedState),
    // and it doesn't include any recommendations.
    quarantineIgnoredByApp: false,
  };

  let deferred = Promise.withResolvers();
  TelemetryEnvironment.registerChangeListener(
    "test_signedAddon",
    deferred.resolve
  );

  // Install the addon.
  let addon = await installXPIFromURL(ADDON_INSTALL_URL);

  await deferred.promise;
  // Unregister the listener.
  TelemetryEnvironment.unregisterChangeListener("test_signedAddon");

  let data = TelemetryEnvironment.currentEnvironment;
  TelemetryEnvironmentTesting.checkEnvironmentData(data);

  // Check addon data.
  Assert.ok(
    ADDON_ID in data.addons.activeAddons,
    "Add-on should be in the environment."
  );
  let targetAddon = data.addons.activeAddons[ADDON_ID];
  for (let f in EXPECTED_ADDON_DATA) {
    Assert.equal(
      targetAddon[f],
      EXPECTED_ADDON_DATA[f],
      f + " must have the correct value."
    );
  }

  // Make sure quarantineIgnoredByUser property is updated also in the
  // telemetry environment in response to the user changing it.
  deferred = Promise.withResolvers();
  TelemetryEnvironment.registerChangeListener(
    "test_quarantineIgnoreByUser_changed",
    deferred.resolve
  );

  addon.quarantineIgnoredByUser = true;
  await deferred.promise;
  // Unregister the listener.
  TelemetryEnvironment.unregisterChangeListener(
    "test_quarantineIgnoreByUser_changed"
  );

  Assert.equal(
    TelemetryEnvironment.currentEnvironment.addons.activeAddons[ADDON_ID]
      .quarantineIgnoredByUser,
    true,
    "Expect quarantineIgnoredByUser to be set to true"
  );

  AddonTestUtils.useRealCertChecks = false;
  await addon.startupPromise;
  await addon.uninstall();
});

add_task(async function test_addonsFieldsLimit() {
  const ADDON_INSTALL_URL = gDataRoot + "long-fields.xpi";
  const ADDON_ID = "tel-longfields-webext@tests.mozilla.org";

  // Install the addon and wait for the TelemetryEnvironment to pick it up.
  let deferred = Promise.withResolvers();
  TelemetryEnvironment.registerChangeListener(
    "test_longFieldsAddon",
    deferred.resolve
  );
  let addon = await installXPIFromURL(ADDON_INSTALL_URL);
  await deferred.promise;
  TelemetryEnvironment.unregisterChangeListener("test_longFieldsAddon");

  let data = TelemetryEnvironment.currentEnvironment;
  TelemetryEnvironmentTesting.checkEnvironmentData(data);

  // Check that the addon is available and that the string fields are limited.
  Assert.ok(
    ADDON_ID in data.addons.activeAddons,
    "Add-on should be in the environment."
  );
  let targetAddon = data.addons.activeAddons[ADDON_ID];

  // TelemetryEnvironment limits the length of string fields for activeAddons to 100 chars,
  // to mitigate misbehaving addons.
  Assert.lessOrEqual(
    targetAddon.version.length,
    100,
    "The version string must have been limited"
  );
  Assert.lessOrEqual(
    targetAddon.name.length,
    100,
    "The name string must have been limited"
  );
  Assert.lessOrEqual(
    targetAddon.description.length,
    100,
    "The description string must have been limited"
  );

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

add_task(async function test_collectionWithbrokenAddonData() {
  const BROKEN_ADDON_ID = "telemetry-test2.example.com@services.mozilla.org";
  const BROKEN_MANIFEST = {
    id: "telemetry-test2.example.com@services.mozilla.org",
    name: "telemetry broken addon",
    origin: "https://telemetry-test2.example.com",
    version: 1, // This is intentionally not a string.
    signedState: AddonManager.SIGNEDSTATE_SIGNED,
    type: "extension",
  };

  const ADDON_INSTALL_URL = gDataRoot + "restartless.xpi";
  const ADDON_ID = "tel-restartless-webext@tests.mozilla.org";
  const ADDON_INSTALL_DATE = truncateToDays(Date.now());
  const EXPECTED_ADDON_DATA = {
    blocklisted: false,
    description: "A restartless addon which gets enabled without a reboot.",
    name: "XPI Telemetry Restartless Test",
    userDisabled: false,
    appDisabled: false,
    version: "1.0",
    scope: AddonManager.SCOPE_PROFILE,
    type: "extension",
    foreignInstall: false,
    hasBinaryComponents: false,
    installDay: ADDON_INSTALL_DATE,
    updateDay: ADDON_INSTALL_DATE,
    signedState: AddonManager.SIGNEDSTATE_MISSING,
  };

  let receivedNotifications = 0;

  let registerCheckpointPromise = aExpected => {
    return new Promise(resolve =>
      TelemetryEnvironment.registerChangeListener(
        "testBrokenAddon_collection" + aExpected,
        reason => {
          Assert.equal(reason, "addons-changed");
          receivedNotifications++;
          resolve();
        }
      )
    );
  };

  let assertCheckpoint = aExpected => {
    Assert.equal(receivedNotifications, aExpected);
    TelemetryEnvironment.unregisterChangeListener(
      "testBrokenAddon_collection" + aExpected
    );
  };

  // Register the broken provider and install the broken addon.
  let checkpointPromise = registerCheckpointPromise(1);
  let brokenAddonProvider = createMockAddonProvider(
    "Broken Extensions Provider"
  );
  AddonManagerPrivate.registerProvider(brokenAddonProvider);
  brokenAddonProvider.addAddon(BROKEN_MANIFEST);
  await checkpointPromise;
  assertCheckpoint(1);

  // Now install an addon which returns the correct information.
  checkpointPromise = registerCheckpointPromise(2);
  let addon = await installXPIFromURL(ADDON_INSTALL_URL);
  await checkpointPromise;
  assertCheckpoint(2);

  // Check that the new environment contains the info from the broken provider,
  // despite the addon missing some details.
  let data = TelemetryEnvironment.currentEnvironment;
  TelemetryEnvironmentTesting.checkEnvironmentData(data, {
    expectBrokenAddons: true,
  });

  let activeAddons = data.addons.activeAddons;
  Assert.ok(
    BROKEN_ADDON_ID in activeAddons,
    "The addon with the broken manifest must be reported."
  );
  Assert.equal(
    activeAddons[BROKEN_ADDON_ID].version,
    null,
    "null should be reported for invalid data."
  );
  Assert.ok(ADDON_ID in activeAddons, "The valid addon must be reported.");
  Assert.equal(
    activeAddons[ADDON_ID].description,
    EXPECTED_ADDON_DATA.description,
    "The description for the valid addon should be correct."
  );

  // Unregister the broken provider so we don't mess with other tests.
  AddonManagerPrivate.unregisterProvider(brokenAddonProvider);

  // Uninstall the valid addon.
  await addon.startupPromise;
  await addon.uninstall();
});

// NOTE: this test is meant to ensure that non-system built-in add-ons
// changes are not triggering updates to the telemetry environment.
//
// This test is meant to be just dropped once the TelemetryEnvironment
// isn't responsible for collecting the activeAddons metadata (and so
// it is not needed to be preserved in the new AOM xpcshell test,
// test_EnvironmentAddonBuilder_telemetry.js).
add_task(async function test_nonSystemBuiltinAddonDidnChangeEnvironment() {
  let data = TelemetryEnvironment.currentEnvironment;
  TelemetryEnvironmentTesting.checkEnvironmentData(data);

  const LISTENER_NAME = "test_nonSystemBuiltinAddonDidnChangeEnvironment";

  let deferred = Promise.withResolvers();
  let receivedNotifications = 0;
  TelemetryEnvironment.registerChangeListener(LISTENER_NAME, reason => {
    Assert.equal(reason, "addons-changed");
    receivedNotifications++;
    deferred.resolve();
  });

  // Mock a non-system builtin addon being installed.
  const ADDON_ID_NONSYSTEM_BUILTIN =
    "tel-nonsystem-builtin-addon@tests.mozilla.org";
  const addon_res_url_path = "telemetry-test-nonsystem-builtin-addon";
  let xpi = await AddonTestUtils.createTempWebExtensionFile({
    manifest: {
      name: "XPI Telemetry non-System Built-in Add-on Test",
      description: "A system addon which is shipped with Firefox.",
      version: "1.0",
      browser_specific_settings: {
        gecko: { id: ADDON_ID_NONSYSTEM_BUILTIN },
      },
    },
  });
  let base = Services.io.newURI(`jar:file:${xpi.path}!/`);
  let resProto = Services.io
    .getProtocolHandler("resource")
    .QueryInterface(Ci.nsIResProtocolHandler);
  resProto.setSubstitution(addon_res_url_path, base);
  // Install a non-system built-in add-on expected to not be included
  // in the TelemetryEnvironment's activeAddons metadata.
  const builtinAddon = await AddonManager.maybeInstallBuiltinAddon(
    ADDON_ID_NONSYSTEM_BUILTIN,
    "1.0",
    `resource://${addon_res_url_path}/`
  );
  // Sanity-check.
  Assert.equal(
    builtinAddon.isBuiltin,
    true,
    `Expect ${ADDON_ID_NONSYSTEM_BUILTIN} isBuiltin to be true`
  );
  Assert.equal(
    builtinAddon.isSystem,
    false,
    `Expect ${ADDON_ID_NONSYSTEM_BUILTIN} isSystem to be false`
  );
  Assert.equal(
    builtinAddon.scope,
    AddonManager.SCOPE_APPLICATION,
    `Expect ${ADDON_ID_NONSYSTEM_BUILTIN} to be installed in SCOPE_APPLICATION`
  );

  // Install another extension that is expected to be included
  // in the TelemetryEnvironment's activeAddons metadata.
  const ADDON_ID = "test-addon@tests.mozilla.org";
  let webextension = ExtensionTestUtils.loadExtension({
    useAddonManager: "permanent",
    manifest: {
      name: "XPI Telemetry WebExtension Add-on Test",
      description: "A webextension addon.",
      version: "1.0",
      browser_specific_settings: {
        gecko: {
          id: ADDON_ID,
        },
      },
    },
  });

  await webextension.startup();
  await deferred.promise;

  data = TelemetryEnvironment.currentEnvironment;

  Assert.ok(
    ADDON_ID in data.addons.activeAddons,
    "Expect the last test extension installed to be found"
  );

  Assert.ok(
    !(ADDON_ID_NONSYSTEM_BUILTIN in data.addons.activeAddons),
    "Expect the non-system built-in test extension to NOT be found"
  );

  Assert.equal(
    receivedNotifications,
    1,
    "Expect only 1 environment change to be triggered by both extensions installation"
  );

  TelemetryEnvironment.unregisterChangeListener(LISTENER_NAME);

  await builtinAddon.startupPromise;
  await builtinAddon.uninstall();
  await webextension.unload();
});

add_task(
  { skip_if: () => AppConstants.MOZ_APP_NAME == "thunderbird" },
  async function test_delayed_defaultBrowser() {
    // Skip this test on Thunderbird since it is not a browser, so it cannot
    // be the default browser.

    // Make sure we don't have anything already cached for this test.
    await TelemetryEnvironment.testCleanRestart().onInitialized();

    let environmentData = TelemetryEnvironment.currentEnvironment;
    TelemetryEnvironmentTesting.checkEnvironmentData(environmentData);
    Assert.equal(
      environmentData.settings.isDefaultBrowser,
      null,
      "isDefaultBrowser must be null before the session is restored."
    );

    Services.obs.notifyObservers(null, "sessionstore-windows-restored");
    // Session restore triggers search service init asynchronously.
    // If this completes during shutdown, it throws an exception.
    // Await the search service init to make this deterministic (bug 1885310).
    await SearchService.promiseInitialized;

    environmentData = TelemetryEnvironment.currentEnvironment;
    TelemetryEnvironmentTesting.checkEnvironmentData(environmentData);
    Assert.ok(
      "isDefaultBrowser" in environmentData.settings,
      "isDefaultBrowser must be available after the session is restored."
    );
    Assert.equal(
      typeof environmentData.settings.isDefaultBrowser,
      "boolean",
      "isDefaultBrowser must be of the right type."
    );

    // Make sure pref-flipping doesn't overwrite the browser default state.
    const PREF_TEST = "toolkit.telemetry.test.pref1";
    const PREFS_TO_WATCH = new Map([
      [PREF_TEST, { what: TelemetryEnvironment.RECORD_PREF_STATE }],
    ]);
    Services.prefs.clearUserPref(PREF_TEST);

    // Watch the test preference.
    await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
    let deferred = Promise.withResolvers();
    TelemetryEnvironment.registerChangeListener(
      "testDefaultBrowser_pref",
      deferred.resolve
    );
    // Trigger an environment change.
    Services.prefs.setIntPref(PREF_TEST, 1);
    await deferred.promise;
    TelemetryEnvironment.unregisterChangeListener("testDefaultBrowser_pref");

    // Check that the data is still available.
    environmentData = TelemetryEnvironment.currentEnvironment;
    TelemetryEnvironmentTesting.checkEnvironmentData(environmentData);
    Assert.ok(
      "isDefaultBrowser" in environmentData.settings,
      "isDefaultBrowser must still be available after a pref is flipped."
    );
  }
);

add_task(async function test_osstrings() {
  // First test that numbers in sysinfo properties are converted to string fields
  // in system.os.
  TelemetryEnvironmentTesting.setSysInfoOverrides({
    version: 1,
    name: 2,
    kernel_version: 3,
  });

  await TelemetryEnvironment.testCleanRestart().onInitialized();
  let data = TelemetryEnvironment.currentEnvironment;
  TelemetryEnvironmentTesting.checkEnvironmentData(data);

  Assert.equal(data.system.os.version, "1");
  Assert.equal(data.system.os.name, "2");
  if (AppConstants.platform == "android") {
    Assert.equal(data.system.os.kernelVersion, "3");
  }

  // Check that null values are also handled.
  TelemetryEnvironmentTesting.setSysInfoOverrides({
    version: null,
    name: null,
    kernel_version: null,
  });

  await TelemetryEnvironment.testCleanRestart().onInitialized();
  data = TelemetryEnvironment.currentEnvironment;
  TelemetryEnvironmentTesting.checkEnvironmentData(data);

  Assert.equal(data.system.os.version, null);
  Assert.equal(data.system.os.name, null);
  if (AppConstants.platform == "android") {
    Assert.equal(data.system.os.kernelVersion, null);
  }

  // Clean up.
  TelemetryEnvironmentTesting.setSysInfoOverrides({});
  await TelemetryEnvironment.testCleanRestart().onInitialized();
});

add_task(async function test_experimentsAPI() {
  const EXPERIMENT1 = "experiment-1";
  const EXPERIMENT1_BRANCH = "nice-branch";
  const EXPERIMENT2 = "experiment-2";
  const EXPERIMENT2_BRANCH = "other-branch";

  let checkExperiment = (environmentData, id, branch) => {
    Assert.ok(
      "experiments" in environmentData,
      "The current environment must report the experiment annotations."
    );
    Assert.ok(
      id in environmentData.experiments,
      "The experiments section must contain the expected experiment id."
    );
    Assert.equal(
      environmentData.experiments[id].branch,
      branch,
      "The experiment branch must be correct."
    );
  };

  // Clean the environment and check that it's reporting the correct info.
  await TelemetryEnvironment.testCleanRestart().onInitialized();
  let data = TelemetryEnvironment.currentEnvironment;
  TelemetryEnvironmentTesting.checkEnvironmentData(data);

  // We don't expect the experiments section to be there if no annotation
  // happened.
  Assert.ok(
    !("experiments" in data),
    "No experiments section must be reported if nothing was annotated."
  );

  // Add a change listener and add an experiment annotation.
  let deferred = Promise.withResolvers();
  TelemetryEnvironment.registerChangeListener(
    "test_experimentsAPI",
    (reason, env) => {
      deferred.resolve(env);
    }
  );
  TelemetryEnvironment.setExperimentActive(EXPERIMENT1, EXPERIMENT1_BRANCH);
  let eventEnvironmentData = await deferred.promise;

  // Check that the old environment does not contain the experiments.
  TelemetryEnvironmentTesting.checkEnvironmentData(eventEnvironmentData);
  Assert.ok(
    !("experiments" in eventEnvironmentData),
    "No experiments section must be reported in the old environment."
  );

  // Check that the current environment contains the right experiment.
  data = TelemetryEnvironment.currentEnvironment;
  TelemetryEnvironmentTesting.checkEnvironmentData(data);
  checkExperiment(data, EXPERIMENT1, EXPERIMENT1_BRANCH);

  TelemetryEnvironment.unregisterChangeListener("test_experimentsAPI");

  // Add a second annotation and check that both experiments are there.
  deferred = Promise.withResolvers();
  TelemetryEnvironment.registerChangeListener(
    "test_experimentsAPI2",
    (reason, env) => {
      deferred.resolve(env);
    }
  );
  TelemetryEnvironment.setExperimentActive(EXPERIMENT2, EXPERIMENT2_BRANCH);
  eventEnvironmentData = await deferred.promise;

  // Check that the current environment contains both the experiment.
  data = TelemetryEnvironment.currentEnvironment;
  TelemetryEnvironmentTesting.checkEnvironmentData(data);
  checkExperiment(data, EXPERIMENT1, EXPERIMENT1_BRANCH);
  checkExperiment(data, EXPERIMENT2, EXPERIMENT2_BRANCH);

  // The previous environment should only contain the first experiment.
  checkExperiment(eventEnvironmentData, EXPERIMENT1, EXPERIMENT1_BRANCH);
  Assert.ok(
    !(EXPERIMENT2 in eventEnvironmentData),
    "The old environment must not contain the new experiment annotation."
  );

  TelemetryEnvironment.unregisterChangeListener("test_experimentsAPI2");

  // Check that removing an unknown experiment annotation does not trigger
  // a notification.
  TelemetryEnvironment.registerChangeListener("test_experimentsAPI3", () => {
    Assert.ok(
      false,
      "Removing an unknown experiment annotation must not trigger a change."
    );
  });
  TelemetryEnvironment.setExperimentInactive("unknown-experiment-id");
  // Also make sure that passing non-string parameters arguments doesn't throw nor
  // trigger a notification.
  TelemetryEnvironment.setExperimentActive({}, "some-branch");
  TelemetryEnvironment.setExperimentActive("some-id", {});
  TelemetryEnvironment.unregisterChangeListener("test_experimentsAPI3");

  // Check that removing a known experiment leaves the other in place and triggers
  // a change.
  deferred = Promise.withResolvers();
  TelemetryEnvironment.registerChangeListener(
    "test_experimentsAPI4",
    (reason, env) => {
      deferred.resolve(env);
    }
  );
  TelemetryEnvironment.setExperimentInactive(EXPERIMENT1);
  eventEnvironmentData = await deferred.promise;

  // Check that the current environment contains just the second experiment.
  data = TelemetryEnvironment.currentEnvironment;
  TelemetryEnvironmentTesting.checkEnvironmentData(data);
  Assert.ok(
    !(EXPERIMENT1 in data),
    "The current environment must not contain the removed experiment annotation."
  );
  checkExperiment(data, EXPERIMENT2, EXPERIMENT2_BRANCH);

  // The previous environment should contain both annotations.
  checkExperiment(eventEnvironmentData, EXPERIMENT1, EXPERIMENT1_BRANCH);
  checkExperiment(eventEnvironmentData, EXPERIMENT2, EXPERIMENT2_BRANCH);

  // Set an experiment with a type and check that it correctly shows up.
  TelemetryEnvironment.setExperimentActive(
    "typed-experiment",
    "random-branch",
    { type: "ab-test" }
  );
  data = TelemetryEnvironment.currentEnvironment;
  checkExperiment(data, "typed-experiment", "random-branch", "ab-test");
});

add_task(async function test_experimentsAPI_limits() {
  const EXPERIMENT =
    "experiment-2-experiment-2-experiment-2-experiment-2-experiment-2" +
    "-experiment-2-experiment-2-experiment-2-experiment-2";
  const EXPERIMENT_BRANCH =
    "other-branch-other-branch-other-branch-other-branch-other" +
    "-branch-other-branch-other-branch-other-branch-other-branch";
  const EXPERIMENT_TRUNCATED = EXPERIMENT.substring(0, 100);
  const EXPERIMENT_BRANCH_TRUNCATED = EXPERIMENT_BRANCH.substring(0, 100);

  // Clean the environment and check that it's reporting the correct info.
  await TelemetryEnvironment.testCleanRestart().onInitialized();
  let data = TelemetryEnvironment.currentEnvironment;
  TelemetryEnvironmentTesting.checkEnvironmentData(data);

  // We don't expect the experiments section to be there if no annotation
  // happened.
  Assert.ok(
    !("experiments" in data),
    "No experiments section must be reported if nothing was annotated."
  );

  // Add a change listener and wait for the annotation to happen.
  let deferred = Promise.withResolvers();
  TelemetryEnvironment.registerChangeListener("test_experimentsAPI", () =>
    deferred.resolve()
  );
  TelemetryEnvironment.setExperimentActive(EXPERIMENT, EXPERIMENT_BRANCH);
  await deferred.promise;

  // Check that the current environment contains the truncated values
  // for the experiment data.
  data = TelemetryEnvironment.currentEnvironment;
  TelemetryEnvironmentTesting.checkEnvironmentData(data);
  Assert.ok(
    "experiments" in data,
    "The environment must contain an experiments section."
  );
  Assert.ok(
    EXPERIMENT_TRUNCATED in data.experiments,
    "The experiments must be reporting the truncated id."
  );
  Assert.ok(
    !(EXPERIMENT in data.experiments),
    "The experiments must not be reporting the full id."
  );
  Assert.equal(
    EXPERIMENT_BRANCH_TRUNCATED,
    data.experiments[EXPERIMENT_TRUNCATED].branch,
    "The experiments must be reporting the truncated branch."
  );

  TelemetryEnvironment.unregisterChangeListener("test_experimentsAPI");

  // Check that an overly long type is truncated.
  const longType = "a0123456678901234567890123456789";
  TelemetryEnvironment.setExperimentActive("exp", "some-branch", {
    type: longType,
  });
  data = TelemetryEnvironment.currentEnvironment;
  Assert.equal(data.experiments.exp.type, longType.substring(0, 20));
});

if (gIsWindows) {
  add_task(async function test_environmentHDDInfo() {
    await TelemetryEnvironment.testCleanRestart().onInitialized();
    let data = TelemetryEnvironment.currentEnvironment;
    let empty = { model: null, revision: null, type: null };
    Assert.deepEqual(
      data.system.hdd,
      { binary: empty, profile: empty, system: empty },
      "Should have no data yet."
    );
    await TelemetryEnvironment.delayedInit();
    data = TelemetryEnvironment.currentEnvironment;
    for (let k of TelemetryEnvironmentTesting.EXPECTED_HDD_FIELDS) {
      TelemetryEnvironmentTesting.checkString(data.system.hdd[k].model);
      TelemetryEnvironmentTesting.checkString(data.system.hdd[k].revision);
      TelemetryEnvironmentTesting.checkString(data.system.hdd[k].type);
    }
  });

  add_task(async function test_environmentProcessInfo() {
    await TelemetryEnvironment.testCleanRestart().onInitialized();
    let data = TelemetryEnvironment.currentEnvironment;
    Assert.deepEqual(data.system.isWow64, null, "Should have no data yet.");
    await TelemetryEnvironment.delayedInit();
    data = TelemetryEnvironment.currentEnvironment;
    Assert.equal(
      typeof data.system.isWow64,
      "boolean",
      "isWow64 must be a boolean."
    );
    Assert.equal(
      typeof data.system.isWowARM64,
      "boolean",
      "isWowARM64 must be a boolean."
    );
    Assert.equal(
      typeof data.system.hasWinPackageId,
      "boolean",
      "hasWinPackageId must be a boolean."
    );
    // This is only sent for Mozilla produced MSIX packages
    Assert.ok(
      !("winPackageFamilyName" in data.system) ||
        data.system.winPackageFamilyName === null ||
        typeof data.system.winPackageFamilyName === "string",
      "winPackageFamilyName must be a string if non null"
    );
    // These should be numbers if they are not null
    for (let f of [
      "count",
      "model",
      "family",
      "stepping",
      "l2cacheKB",
      "l3cacheKB",
      "speedMHz",
      "cores",
    ]) {
      Assert.ok(
        !(f in data.system.cpu) ||
          data.system.cpu[f] === null ||
          Number.isFinite(data.system.cpu[f]),
        f + " must be a number if non null."
      );
    }
    Assert.ok(
      TelemetryEnvironmentTesting.checkString(data.system.cpu.vendor),
      "vendor must be a valid string."
    );
  });

  add_task(async function test_environmentOSInfo() {
    await TelemetryEnvironment.testCleanRestart().onInitialized();
    let data = TelemetryEnvironment.currentEnvironment;
    Assert.deepEqual(
      data.system.os.installYear,
      null,
      "Should have no data yet."
    );
    await TelemetryEnvironment.delayedInit();
    data = TelemetryEnvironment.currentEnvironment;
    Assert.ok(
      Number.isFinite(data.system.os.installYear),
      "Install year must be a number."
    );
  });
}

add_task(
  { skip_if: () => AppConstants.MOZ_APP_NAME == "thunderbird" },
  async function test_environmentServicesInfo() {
    let cache = TelemetryEnvironment.testCleanRestart();
    await cache.onInitialized();
    let oldGetFxaSignedInUser = cache._getFxaSignedInUser;
    try {
      // Test the 'yes to both' case.

      // This makes the weave service return that the usere is definitely a sync user
      Services.prefs.setStringPref(
        "services.sync.username",
        "c00lperson123@example.com"
      );
      let calledFxa = false;
      cache._getFxaSignedInUser = () => {
        calledFxa = true;
        return null;
      };

      await cache._updateServicesInfo();
      ok(
        !calledFxa,
        "Shouldn't need to ask FxA if they're definitely signed in"
      );
      deepEqual(cache.currentEnvironment.services, {
        accountEnabled: true,
        syncEnabled: true,
      });

      // Test the fxa-but-not-sync case.
      Services.prefs.clearUserPref("services.sync.username");
      // We don't actually inspect the returned object, just t
      cache._getFxaSignedInUser = async () => {
        return {};
      };
      await cache._updateServicesInfo();
      deepEqual(cache.currentEnvironment.services, {
        accountEnabled: true,
        syncEnabled: false,
      });
      // Test the "no to both" case.
      cache._getFxaSignedInUser = async () => {
        return null;
      };
      await cache._updateServicesInfo();
      deepEqual(cache.currentEnvironment.services, {
        accountEnabled: false,
        syncEnabled: false,
      });
      // And finally, the 'fxa is in an error state' case.
      cache._getFxaSignedInUser = () => {
        throw new Error("You'll never know");
      };
      await cache._updateServicesInfo();
      equal(cache.currentEnvironment.services, null);
    } finally {
      cache._getFxaSignedInUser = oldGetFxaSignedInUser;
      Services.prefs.clearUserPref("services.sync.username");
    }
  }
);

add_task(async function test_normandyTestPrefsGoneAfter91() {
  const testPrefBool = "app.normandy.test-prefs.bool";
  const testPrefInteger = "app.normandy.test-prefs.integer";
  const testPrefString = "app.normandy.test-prefs.string";

  Services.prefs.setBoolPref(testPrefBool, true);
  Services.prefs.setIntPref(testPrefInteger, 10);
  Services.prefs.setCharPref(testPrefString, "test-string");

  const data = TelemetryEnvironment.currentEnvironment;

  if (Services.vc.compare(data.build.version, "91") > 0) {
    Assert.equal(
      data.settings.userPrefs["app.normandy.test-prefs.bool"],
      null,
      "This probe should expire in FX91. bug 1686105 "
    );
    Assert.equal(
      data.settings.userPrefs["app.normandy.test-prefs.integer"],
      null,
      "This probe should expire in FX91. bug 1686105 "
    );
    Assert.equal(
      data.settings.userPrefs["app.normandy.test-prefs.string"],
      null,
      "This probe should expire in FX91. bug 1686105 "
    );
  }
});

add_task(async function test_environmentShutdown() {
  // Define and reset the test preference.
  const PREF_TEST = "toolkit.telemetry.test.pref1";
  const PREFS_TO_WATCH = new Map([
    [PREF_TEST, { what: TelemetryEnvironment.RECORD_PREF_STATE }],
  ]);
  Services.prefs.clearUserPref(PREF_TEST);

  // Set up the preferences and listener, then the trigger shutdown
  await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
  TelemetryEnvironment.registerChangeListener(
    "test_environmentShutdownChange",
    () => {
      // Register a new change listener that asserts if change is propogated
      Assert.ok(false, "No change should be propagated after shutdown.");
    }
  );
  TelemetryEnvironment.shutdown();

  // Flipping  the test preference after shutdown should not trigger the listener
  Services.prefs.setIntPref(PREF_TEST, 1);

  // Unregister the listener.
  TelemetryEnvironment.unregisterChangeListener(
    "test_environmentShutdownChange"
  );
});

add_task(async function test_environmentDidntChange() {
  // Clean the environment and check that it's reporting the correct info.
  await TelemetryEnvironment.testCleanRestart().onInitialized();
  let data = TelemetryEnvironment.currentEnvironment;
  TelemetryEnvironmentTesting.checkEnvironmentData(data);

  const LISTENER_NAME = "test_environmentDidntChange";
  TelemetryEnvironment.registerChangeListener(LISTENER_NAME, () => {
    Assert.ok(false, "The environment didn't actually change.");
  });

  // Don't actually change the environment, but notify of a compositor abort.
  const COMPOSITOR_ABORTED_TOPIC = "compositor:process-aborted";
  Services.obs.notifyObservers(null, COMPOSITOR_ABORTED_TOPIC);

  TelemetryEnvironment.unregisterChangeListener(LISTENER_NAME);
});
