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

const { AddonManager, AddonManagerPrivate } = ChromeUtils.import(
  "resource://gre/modules/AddonManager.jsm"
);
ChromeUtils.import("resource://gre/modules/TelemetryEnvironment.jsm", this);
ChromeUtils.import("resource://gre/modules/Preferences.jsm", this);
ChromeUtils.import("resource://gre/modules/PromiseUtils.jsm", this);
ChromeUtils.import("resource://gre/modules/Timer.jsm", this);
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm", this);
ChromeUtils.import("resource://testing-common/ContentTaskUtils.jsm", this);
const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
ChromeUtils.import("resource://testing-common/MockRegistrar.jsm", this);
const { FileUtils } = ChromeUtils.import(
  "resource://gre/modules/FileUtils.jsm"
);
const { CommonUtils } = ChromeUtils.import(
  "resource://services-common/utils.js"
);
const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
const { SearchTestUtils } = ChromeUtils.import(
  "resource://testing-common/SearchTestUtils.jsm"
);

// AttributionCode is only needed for Firefox
ChromeUtils.defineModuleGetter(
  this,
  "AttributionCode",
  "resource:///modules/AttributionCode.jsm"
);

ChromeUtils.defineModuleGetter(
  this,
  "ExtensionTestUtils",
  "resource://testing-common/ExtensionXPCShellUtils.jsm"
);

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

function promiseNextTick() {
  return new Promise(resolve => executeSoon(resolve));
}

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

const PLATFORM_VERSION = "1.9.2";
const APP_VERSION = "1";
const APP_ID = "xpcshell@tests.mozilla.org";
const APP_NAME = "XPCShell";

const DISTRIBUTION_ID = "distributor-id";
const DISTRIBUTION_VERSION = "4.5.6b";
const DISTRIBUTOR_NAME = "Some Distributor";
const DISTRIBUTOR_CHANNEL = "A Channel";
const PARTNER_NAME = "test";
const PARTNER_ID = "NicePartner-ID-3785";
const DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC =
  "distribution-customization-complete";

const GFX_VENDOR_ID = "0xabcd";
const GFX_DEVICE_ID = "0x1234";

// The profile reset date, in milliseconds (Today)
const PROFILE_RESET_DATE_MS = Date.now();
// The profile creation date, in milliseconds (Yesterday).
const PROFILE_FIRST_USE_MS = PROFILE_RESET_DATE_MS - MILLISECONDS_PER_DAY;
const PROFILE_CREATION_DATE_MS = PROFILE_FIRST_USE_MS - MILLISECONDS_PER_DAY;

const FLASH_PLUGIN_NAME = "Shockwave Flash";
const FLASH_PLUGIN_DESC = "A mock flash plugin";
const FLASH_PLUGIN_VERSION = "\u201c1.1.1.1\u201d";
const PLUGIN_MIME_TYPE1 = "application/x-shockwave-flash";
const PLUGIN_MIME_TYPE2 = "text/plain";

const PLUGIN2_NAME = "Quicktime";
const PLUGIN2_DESC = "A mock Quicktime plugin";
const PLUGIN2_VERSION = "2.3";

const PLUGIN_UPDATED_TOPIC = "plugins-list-updated";

// system add-ons are enabled at startup, so record date when the test starts
const SYSTEM_ADDON_INSTALL_DATE = Date.now();

const EXPECTED_HDD_FIELDS = ["profile", "binary", "system"];

// Valid attribution code to write so that settings.attribution can be tested.
const ATTRIBUTION_CODE = "source%3Dgoogle.com";

const pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(
  Ci.nsIPluginHost
);

/**
 * Used to mock plugin tags in our fake plugin host.
 */
function PluginTag(aName, aDescription, aVersion, aEnabled) {
  this.pluginTag = pluginHost.createFakePlugin({
    handlerURI: "resource://fake-plugin/${Math.random()}.xhtml",
    mimeEntries: this.mimeTypes.map(type => ({ type })),
    name: aName,
    description: aDescription,
    fileName: `${aName}.so`,
    version: aVersion,
  });
  this.name = aName;
  this.description = aDescription;
  this.version = aVersion;
  this.disabled = !aEnabled;
}

PluginTag.prototype = {
  name: null,
  description: null,
  version: null,
  filename: null,
  fullpath: null,
  blocklisted: false,
  clicktoplay: true,

  get disabled() {
    return this.pluginTag.enabledState == Ci.nsIPluginTag.STATE_DISABLED;
  },
  set disabled(val) {
    this.pluginTag.enabledState =
      Ci.nsIPluginTag[val ? "STATE_DISABLED" : "STATE_CLICKTOPLAY"];
  },

  mimeTypes: [PLUGIN_MIME_TYPE1, PLUGIN_MIME_TYPE2],

  getMimeTypes() {
    return this.mimeTypes;
  },
};

// A container for the plugins handled by the fake plugin host.
var gInstalledPlugins = [
  new PluginTag("Java", "A mock Java plugin", "1.0", false /* Disabled */),
  new PluginTag(
    FLASH_PLUGIN_NAME,
    FLASH_PLUGIN_DESC,
    FLASH_PLUGIN_VERSION,
    true
  ),
];

// A fake plugin host for testing plugin telemetry environment.
var PluginHost = {
  getPluginTags() {
    return gInstalledPlugins.map(plugin => plugin.pluginTag);
  },

  QueryInterface: ChromeUtils.generateQI(["nsIPluginHost"]),
};

function registerFakePluginHost() {
  MockRegistrar.register("@mozilla.org/plugin/host;1", PluginHost);
}

var SysInfo = {
  overrides: {},

  getProperty(name) {
    // Assert.ok(false, "Mock SysInfo: " + name + ", " + JSON.stringify(this.overrides));
    if (name in this.overrides) {
      return this.overrides[name];
    }

    return this._genuine.QueryInterface(Ci.nsIPropertyBag).getProperty(name);
  },

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

  get(name) {
    return this._genuine.QueryInterface(Ci.nsIPropertyBag2).get(name);
  },

  get diskInfo() {
    return this._genuine.QueryInterface(Ci.nsISystemInfo).diskInfo;
  },

  get osInfo() {
    return this._genuine.QueryInterface(Ci.nsISystemInfo).osInfo;
  },

  get processInfo() {
    return this._genuine.QueryInterface(Ci.nsISystemInfo).processInfo;
  },

  QueryInterface: ChromeUtils.generateQI(["nsIPropertyBag2", "nsISystemInfo"]),
};

function registerFakeSysInfo() {
  MockRegistrar.register("@mozilla.org/system-info;1", SysInfo);
}

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

  get type() {
    return "service";
  },

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

function spoofGfxAdapter() {
  try {
    let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfoDebug);
    gfxInfo.fireTestProcess();
    gfxInfo.spoofVendorID(GFX_VENDOR_ID);
    gfxInfo.spoofDeviceID(GFX_DEVICE_ID);
  } catch (x) {
    // If we can't test gfxInfo, that's fine, we'll note it later.
  }
}

function spoofProfileReset() {
  return CommonUtils.writeJSON(
    {
      created: PROFILE_CREATION_DATE_MS,
      reset: PROFILE_RESET_DATE_MS,
      firstUse: PROFILE_FIRST_USE_MS,
    },
    OS.Path.join(OS.Constants.Path.profileDir, "times.json")
  );
}

function spoofPartnerInfo() {
  let prefsToSpoof = {};
  prefsToSpoof["distribution.id"] = DISTRIBUTION_ID;
  prefsToSpoof["distribution.version"] = DISTRIBUTION_VERSION;
  prefsToSpoof["app.distributor"] = DISTRIBUTOR_NAME;
  prefsToSpoof["app.distributor.channel"] = DISTRIBUTOR_CHANNEL;
  prefsToSpoof["app.partner.test"] = PARTNER_NAME;
  prefsToSpoof["mozilla.partner.id"] = PARTNER_ID;

  // Spoof the preferences.
  for (let pref in prefsToSpoof) {
    Preferences.set(pref, prefsToSpoof[pref]);
  }
}

async function spoofAttributionData() {
  if (gIsWindows || gIsMac) {
    AttributionCode._clearCache();
    await AttributionCode.writeAttributionFile(ATTRIBUTION_CODE);
  }
}

function cleanupAttributionData() {
  if (gIsWindows || gIsMac) {
    AttributionCode.attributionFile.remove(false);
    AttributionCode._clearCache();
  }
}

/**
 * Check that a value is a string and not empty.
 *
 * @param aValue The variable to check.
 * @return True if |aValue| has type "string" and is not empty, False otherwise.
 */
function checkString(aValue) {
  return typeof aValue == "string" && aValue != "";
}

/**
 * If value is non-null, check if it's a valid string.
 *
 * @param aValue The variable to check.
 * @return True if it's null or a valid string, false if it's non-null and an invalid
 *         string.
 */
function checkNullOrString(aValue) {
  if (aValue) {
    return checkString(aValue);
  } else if (aValue === null) {
    return true;
  }

  return false;
}

/**
 * If value is non-null, check if it's a boolean.
 *
 * @param aValue The variable to check.
 * @return True if it's null or a valid boolean, false if it's non-null and an invalid
 *         boolean.
 */
function checkNullOrBool(aValue) {
  return aValue === null || typeof aValue == "boolean";
}

function checkBuildSection(data) {
  const expectedInfo = {
    applicationId: APP_ID,
    applicationName: APP_NAME,
    buildId: gAppInfo.appBuildID,
    version: APP_VERSION,
    vendor: "Mozilla",
    platformVersion: PLATFORM_VERSION,
    xpcomAbi: "noarch-spidermonkey",
  };

  Assert.ok("build" in data, "There must be a build section in Environment.");

  for (let f in expectedInfo) {
    Assert.ok(checkString(data.build[f]), f + " must be a valid string.");
    Assert.equal(
      data.build[f],
      expectedInfo[f],
      f + " must have the correct value."
    );
  }

  // Make sure architecture is in the environment.
  Assert.ok(checkString(data.build.architecture));

  Assert.equal(
    data.build.updaterAvailable,
    AppConstants.MOZ_UPDATER,
    "build.updaterAvailable must equal AppConstants.MOZ_UPDATER"
  );
}

function checkSettingsSection(data) {
  const EXPECTED_FIELDS_TYPES = {
    blocklistEnabled: "boolean",
    e10sEnabled: "boolean",
    e10sMultiProcesses: "number",
    fissionEnabled: "boolean",
    intl: "object",
    locale: "string",
    telemetryEnabled: "boolean",
    update: "object",
    userPrefs: "object",
  };

  Assert.ok(
    "settings" in data,
    "There must be a settings section in Environment."
  );

  for (let f in EXPECTED_FIELDS_TYPES) {
    Assert.equal(
      typeof data.settings[f],
      EXPECTED_FIELDS_TYPES[f],
      f + " must have the correct type."
    );
  }

  // This property is not always present, but when it is, it must be a number.
  if ("launcherProcessState" in data.settings) {
    Assert.equal(typeof data.settings.launcherProcessState, "number");
  }

  // Check "addonCompatibilityCheckEnabled" separately.
  Assert.equal(
    data.settings.addonCompatibilityCheckEnabled,
    AddonManager.checkCompatibility
  );

  // Check "isDefaultBrowser" separately, as it is not available on Android an can either be
  // null or boolean on other platforms.
  if (gIsAndroid) {
    Assert.ok(
      !("isDefaultBrowser" in data.settings),
      "Must not be available on Android."
    );
  } else if ("isDefaultBrowser" in data.settings) {
    // isDefaultBrowser might not be available in the payload, since it's
    // gathered after the session was restored.
    Assert.ok(checkNullOrBool(data.settings.isDefaultBrowser));
  }

  // Check "channel" separately, as it can either be null or string.
  let update = data.settings.update;
  Assert.ok(checkNullOrString(update.channel));
  Assert.equal(typeof update.enabled, "boolean");
  Assert.equal(typeof update.autoDownload, "boolean");

  // Check "defaultSearchEngine" separately, as it can either be undefined or string.
  if ("defaultSearchEngine" in data.settings) {
    checkString(data.settings.defaultSearchEngine);
    Assert.equal(typeof data.settings.defaultSearchEngineData, "object");
  }

  if ("defaultPrivateSearchEngineData" in data.settings) {
    Assert.equal(typeof data.settings.defaultPrivateSearchEngineData, "object");
  }

  if ((gIsWindows || gIsMac) && AppConstants.MOZ_BUILD_APP == "browser") {
    Assert.equal(typeof data.settings.attribution, "object");
    Assert.equal(data.settings.attribution.source, "google.com");
  }

  checkIntlSettings(data.settings);
}

function checkIntlSettings({ intl }) {
  let fields = [
    "requestedLocales",
    "availableLocales",
    "appLocales",
    "acceptLanguages",
  ];

  for (let field of fields) {
    Assert.ok(Array.isArray(intl[field]), `${field} is an array`);
  }

  // These fields may be null if they aren't ready yet. This is mostly to deal
  // with test failures on Android, but they aren't guaranteed to exist.
  let optionalFields = ["systemLocales", "regionalPrefsLocales"];

  for (let field of optionalFields) {
    let isArray = Array.isArray(intl[field]);
    let isNull = intl[field] === null;
    Assert.ok(isArray || isNull, `${field} is an array or null`);
  }
}

function checkProfileSection(data) {
  Assert.ok(
    "profile" in data,
    "There must be a profile section in Environment."
  );
  Assert.equal(
    data.profile.creationDate,
    truncateToDays(PROFILE_CREATION_DATE_MS)
  );
  Assert.equal(data.profile.resetDate, truncateToDays(PROFILE_RESET_DATE_MS));
  Assert.equal(data.profile.firstUseDate, truncateToDays(PROFILE_FIRST_USE_MS));
}

function checkPartnerSection(data, isInitial) {
  const EXPECTED_FIELDS = {
    distributionId: DISTRIBUTION_ID,
    distributionVersion: DISTRIBUTION_VERSION,
    partnerId: PARTNER_ID,
    distributor: DISTRIBUTOR_NAME,
    distributorChannel: DISTRIBUTOR_CHANNEL,
  };

  Assert.ok(
    "partner" in data,
    "There must be a partner section in Environment."
  );

  for (let f in EXPECTED_FIELDS) {
    let expected = isInitial ? null : EXPECTED_FIELDS[f];
    Assert.strictEqual(
      data.partner[f],
      expected,
      f + " must have the correct value."
    );
  }

  // Check that "partnerNames" exists and contains the correct element.
  Assert.ok(Array.isArray(data.partner.partnerNames));
  if (isInitial) {
    Assert.equal(data.partner.partnerNames.length, 0);
  } else {
    Assert.ok(data.partner.partnerNames.includes(PARTNER_NAME));
  }
}

function checkGfxAdapter(data) {
  const EXPECTED_ADAPTER_FIELDS_TYPES = {
    description: "string",
    vendorID: "string",
    deviceID: "string",
    subsysID: "string",
    RAM: "number",
    driver: "string",
    driverVendor: "string",
    driverVersion: "string",
    driverDate: "string",
    GPUActive: "boolean",
  };

  for (let f in EXPECTED_ADAPTER_FIELDS_TYPES) {
    Assert.ok(f in data, f + " must be available.");

    if (data[f]) {
      // Since we have a non-null value, check if it has the correct type.
      Assert.equal(
        typeof data[f],
        EXPECTED_ADAPTER_FIELDS_TYPES[f],
        f + " must have the correct type."
      );
    }
  }
}

function checkSystemSection(data, assertProcessData) {
  const EXPECTED_FIELDS = [
    "memoryMB",
    "cpu",
    "os",
    "hdd",
    "gfx",
    "appleModelId",
  ];

  Assert.ok("system" in data, "There must be a system section in Environment.");

  // Make sure we have all the top level sections and fields.
  for (let f of EXPECTED_FIELDS) {
    Assert.ok(f in data.system, f + " must be available.");
  }

  Assert.ok(
    Number.isFinite(data.system.memoryMB),
    "MemoryMB must be a number."
  );

  if (assertProcessData) {
    if (gIsWindows || gIsMac || gIsLinux) {
      let EXTRA_CPU_FIELDS = [
        "cores",
        "model",
        "family",
        "stepping",
        "l2cacheKB",
        "l3cacheKB",
        "speedMHz",
        "vendor",
      ];

      for (let f of EXTRA_CPU_FIELDS) {
        // Note this is testing TelemetryEnvironment.js only, not that the
        // values are valid - null is the fallback.
        Assert.ok(f in data.system.cpu, f + " must be available under cpu.");
      }

      if (gIsWindows) {
        Assert.equal(
          typeof data.system.isWow64,
          "boolean",
          "isWow64 must be available on Windows and have the correct type."
        );
        Assert.equal(
          typeof data.system.isWowARM64,
          "boolean",
          "isWowARM64 must be available on Windows and have the correct type."
        );
        Assert.ok(
          "virtualMaxMB" in data.system,
          "virtualMaxMB must be available."
        );
        Assert.ok(
          Number.isFinite(data.system.virtualMaxMB),
          "virtualMaxMB must be a number."
        );

        for (let f of [
          "count",
          "model",
          "family",
          "stepping",
          "l2cacheKB",
          "l3cacheKB",
          "speedMHz",
        ]) {
          Assert.ok(
            Number.isFinite(data.system.cpu[f]),
            f + " must be a number if non null."
          );
        }
      }

      // These should be numbers if they are not null
      for (let f of [
        "count",
        "model",
        "family",
        "stepping",
        "l2cacheKB",
        "l3cacheKB",
        "speedMHz",
      ]) {
        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."
        );
      }

      // We insist these are available
      for (let f of ["cores"]) {
        Assert.ok(
          !(f in data.system.cpu) || Number.isFinite(data.system.cpu[f]),
          f + " must be a number if non null."
        );
      }
    }
  }

  let cpuData = data.system.cpu;

  Assert.ok(
    Array.isArray(cpuData.extensions),
    "CPU extensions must be available."
  );

  let osData = data.system.os;
  Assert.ok(checkNullOrString(osData.name));
  Assert.ok(checkNullOrString(osData.version));
  Assert.ok(checkNullOrString(osData.locale));

  // Service pack is only available on Windows.
  if (gIsWindows) {
    Assert.ok(
      Number.isFinite(osData.servicePackMajor),
      "ServicePackMajor must be a number."
    );
    Assert.ok(
      Number.isFinite(osData.servicePackMinor),
      "ServicePackMinor must be a number."
    );
    if ("windowsBuildNumber" in osData) {
      // This might not be available on all Windows platforms.
      Assert.ok(
        Number.isFinite(osData.windowsBuildNumber),
        "windowsBuildNumber must be a number."
      );
    }
    if ("windowsUBR" in osData) {
      // This might not be available on all Windows platforms.
      Assert.ok(
        osData.windowsUBR === null || Number.isFinite(osData.windowsUBR),
        "windowsUBR must be null or a number."
      );
    }
  } else if (gIsAndroid) {
    Assert.ok(checkNullOrString(osData.kernelVersion));
  }

  for (let disk of EXPECTED_HDD_FIELDS) {
    Assert.ok(checkNullOrString(data.system.hdd[disk].model));
    Assert.ok(checkNullOrString(data.system.hdd[disk].revision));
    Assert.ok(checkNullOrString(data.system.hdd[disk].type));
  }

  let gfxData = data.system.gfx;
  Assert.ok("D2DEnabled" in gfxData);
  Assert.ok("DWriteEnabled" in gfxData);
  Assert.ok("Headless" in gfxData);
  Assert.ok("EmbeddedInFirefoxReality" in gfxData);
  // DWriteVersion is disabled due to main thread jank and will be enabled
  // again as part of bug 1154500.
  // Assert.ok("DWriteVersion" in gfxData);
  if (gIsWindows) {
    Assert.equal(typeof gfxData.D2DEnabled, "boolean");
    Assert.equal(typeof gfxData.DWriteEnabled, "boolean");
    Assert.equal(typeof gfxData.EmbeddedInFirefoxReality, "boolean");
    // As above, will be enabled again as part of bug 1154500.
    // Assert.ok(checkString(gfxData.DWriteVersion));
  }

  Assert.ok("adapters" in gfxData);
  Assert.ok(
    !!gfxData.adapters.length,
    "There must be at least one GFX adapter."
  );
  for (let adapter of gfxData.adapters) {
    checkGfxAdapter(adapter);
  }
  Assert.equal(typeof gfxData.adapters[0].GPUActive, "boolean");
  Assert.ok(
    gfxData.adapters[0].GPUActive,
    "The first GFX adapter must be active."
  );

  Assert.ok(Array.isArray(gfxData.monitors));
  if (gIsWindows || gIsMac || gIsLinux) {
    Assert.ok(gfxData.monitors.length >= 1, "There is at least one monitor.");
    Assert.equal(typeof gfxData.monitors[0].screenWidth, "number");
    Assert.equal(typeof gfxData.monitors[0].screenHeight, "number");
    if (gIsWindows) {
      Assert.equal(typeof gfxData.monitors[0].refreshRate, "number");
      Assert.equal(typeof gfxData.monitors[0].pseudoDisplay, "boolean");
    }
    if (gIsMac) {
      Assert.equal(typeof gfxData.monitors[0].scale, "number");
    }
  }

  Assert.equal(typeof gfxData.features, "object");
  Assert.equal(typeof gfxData.features.compositor, "string");

  Assert.equal(typeof gfxData.features.gpuProcess, "object");
  Assert.equal(typeof gfxData.features.gpuProcess.status, "string");

  try {
    // If we've not got nsIGfxInfoDebug, then this will throw and stop us doing
    // this test.
    let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfoDebug);

    if (gIsWindows || gIsMac) {
      Assert.equal(GFX_VENDOR_ID, gfxData.adapters[0].vendorID);
      Assert.equal(GFX_DEVICE_ID, gfxData.adapters[0].deviceID);
    }

    let features = gfxInfo.getFeatures();
    Assert.equal(features.compositor, gfxData.features.compositor);
    Assert.equal(
      features.gpuProcess.status,
      gfxData.features.gpuProcess.status
    );
    Assert.equal(features.opengl, gfxData.features.opengl);
    Assert.equal(features.webgl, gfxData.features.webgl);
  } catch (e) {}

  if (gIsMac) {
    Assert.ok(checkString(data.system.appleModelId));
  } else {
    Assert.ok(checkNullOrString(data.system.appleModelId));
  }

  // This feature is only available on Windows 8+
  if (AppConstants.isPlatformAndVersionAtLeast("win", "6.2")) {
    Assert.ok("sec" in data.system, "sec must be available under data.system");

    let SEC_FIELDS = ["antivirus", "antispyware", "firewall"];
    for (let f of SEC_FIELDS) {
      Assert.ok(
        f in data.system.sec,
        f + " must be available under data.system.sec"
      );

      let value = data.system.sec[f];
      // value is null on Windows Server
      Assert.ok(
        value === null || Array.isArray(value),
        f + " must be either null or an array"
      );
      if (Array.isArray(value)) {
        for (let product of value) {
          Assert.equal(
            typeof product,
            "string",
            "Each element of " + f + " must be a string"
          );
        }
      }
    }
  }
}

function checkActiveAddon(data, partialRecord) {
  let signedState = "number";
  // system add-ons have an undefined signState
  if (data.isSystem) {
    signedState = "undefined";
  }

  const EXPECTED_ADDON_FIELDS_TYPES = {
    version: "string",
    scope: "number",
    type: "string",
    updateDay: "number",
    isSystem: "boolean",
    isWebExtension: "boolean",
    multiprocessCompatible: "boolean",
  };

  const FULL_ADDON_FIELD_TYPES = {
    blocklisted: "boolean",
    name: "string",
    userDisabled: "boolean",
    appDisabled: "boolean",
    foreignInstall: "boolean",
    hasBinaryComponents: "boolean",
    installDay: "number",
    signedState,
  };

  let fields = EXPECTED_ADDON_FIELDS_TYPES;
  if (!partialRecord) {
    fields = Object.assign({}, fields, FULL_ADDON_FIELD_TYPES);
  }

  for (let [name, type] of Object.entries(fields)) {
    Assert.ok(name in data, name + " must be available.");
    Assert.equal(
      typeof data[name],
      type,
      name + " must have the correct type."
    );
  }

  if (!partialRecord) {
    // We check "description" separately, as it can be null.
    Assert.ok(checkNullOrString(data.description));
  }
}

function checkPlugin(data) {
  const EXPECTED_PLUGIN_FIELDS_TYPES = {
    name: "string",
    version: "string",
    description: "string",
    blocklisted: "boolean",
    disabled: "boolean",
    clicktoplay: "boolean",
    updateDay: "number",
  };

  for (let f in EXPECTED_PLUGIN_FIELDS_TYPES) {
    Assert.ok(f in data, f + " must be available.");
    Assert.equal(
      typeof data[f],
      EXPECTED_PLUGIN_FIELDS_TYPES[f],
      f + " must have the correct type."
    );
  }

  Assert.ok(Array.isArray(data.mimeTypes));
  for (let type of data.mimeTypes) {
    Assert.ok(checkString(type));
  }
}

function checkTheme(data) {
  const EXPECTED_THEME_FIELDS_TYPES = {
    id: "string",
    blocklisted: "boolean",
    name: "string",
    userDisabled: "boolean",
    appDisabled: "boolean",
    version: "string",
    scope: "number",
    foreignInstall: "boolean",
    installDay: "number",
    updateDay: "number",
  };

  for (let f in EXPECTED_THEME_FIELDS_TYPES) {
    Assert.ok(f in data, f + " must be available.");
    Assert.equal(
      typeof data[f],
      EXPECTED_THEME_FIELDS_TYPES[f],
      f + " must have the correct type."
    );
  }

  // We check "description" separately, as it can be null.
  Assert.ok(checkNullOrString(data.description));
}

function checkActiveGMPlugin(data) {
  // GMP plugin version defaults to null until GMPDownloader runs to update it.
  if (data.version) {
    Assert.equal(typeof data.version, "string");
  }
  Assert.equal(typeof data.userDisabled, "boolean");
  Assert.equal(typeof data.applyBackgroundUpdates, "number");
}

function checkAddonsSection(data, expectBrokenAddons, partialAddonsRecords) {
  const EXPECTED_FIELDS = [
    "activeAddons",
    "theme",
    "activePlugins",
    "activeGMPlugins",
  ];

  Assert.ok(
    "addons" in data,
    "There must be an addons section in Environment."
  );
  for (let f of EXPECTED_FIELDS) {
    Assert.ok(f in data.addons, f + " must be available.");
  }

  // Check the active addons, if available.
  if (!expectBrokenAddons) {
    let activeAddons = data.addons.activeAddons;
    for (let addon in activeAddons) {
      checkActiveAddon(activeAddons[addon], partialAddonsRecords);
    }
  }

  // Check "theme" structure.
  if (Object.keys(data.addons.theme).length !== 0) {
    checkTheme(data.addons.theme);
  }

  // Check the active plugins.
  Assert.ok(Array.isArray(data.addons.activePlugins));
  for (let plugin of data.addons.activePlugins) {
    checkPlugin(plugin);
  }

  // Check active GMPlugins
  let activeGMPlugins = data.addons.activeGMPlugins;
  for (let gmPlugin in activeGMPlugins) {
    checkActiveGMPlugin(activeGMPlugins[gmPlugin]);
  }
}

function checkExperimentsSection(data) {
  // We don't expect the experiments section to be always available.
  let experiments = data.experiments || {};
  if (!Object.keys(experiments).length) {
    return;
  }

  for (let id in experiments) {
    Assert.ok(checkString(id), id + " must be a valid string.");

    // Check that we have valid experiment info.
    let experimentData = experiments[id];
    Assert.ok(
      "branch" in experimentData,
      "The experiment must have branch data."
    );
    Assert.ok(
      checkString(experimentData.branch),
      "The experiment data must be valid."
    );
    if ("type" in experimentData) {
      Assert.ok(checkString(experimentData.type));
    }
  }
}

function checkEnvironmentData(data, options = {}) {
  const {
    isInitial = false,
    expectBrokenAddons = false,
    assertProcessData = false,
  } = options;

  checkBuildSection(data);
  checkSettingsSection(data);
  checkProfileSection(data);
  checkPartnerSection(data, isInitial);
  checkSystemSection(data, assertProcessData);
  checkAddonsSection(data, expectBrokenAddons);
}

add_task(async function setup() {
  registerFakeSysInfo();
  spoofGfxAdapter();
  do_get_profile();

  // The system add-on must be installed before AddonManager is started.
  const distroDir = FileUtils.getDir("ProfD", ["sysfeatures", "app0"], true);
  do_get_file("system.xpi").copyTo(
    distroDir,
    "tel-system-xpi@tests.mozilla.org.xpi"
  );
  let system_addon = FileUtils.File(distroDir.path);
  system_addon.append("tel-system-xpi@tests.mozilla.org.xpi");
  system_addon.lastModifiedTime = SYSTEM_ADDON_INSTALL_DATE;
  loadAddonManager(APP_ID, APP_NAME, APP_VERSION, PLATFORM_VERSION);

  // 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({ system: [] });
  AddonTestUtils.addonStartup.remove(true);
  await AddonTestUtils.promiseStartupManager();
  // Override ExtensionXPCShellUtils.jsm's overriding of the pref as the
  // search service needs it.
  Services.prefs.clearUserPref("services.settings.default_bucket");

  // Register a fake plugin host for consistent flash version data.
  registerFakePluginHost();

  // Setup a webserver to serve Addons, Plugins, 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") {
    spoofAttributionData();
    registerCleanupFunction(cleanupAttributionData);
  }

  await spoofProfileReset();
  await TelemetryEnvironment.delayedInit();
  await SearchTestUtils.useTestEngines("data", "search-extensions");
});

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;
  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;
  checkEnvironmentData(environmentData, { isInitial: true });

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

  environmentData = TelemetryEnvironment.currentEnvironment;
  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 },
    ],
  ]);

  Preferences.set(PREF_TEST_4, expectedValue);
  Preferences.set(PREF_TEST_5, expectedValue);

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

  // 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.
  Preferences.set(PREF_TEST_1, expectedValue);
  Preferences.set(PREF_TEST_2, false);
  Preferences.set(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.
  Preferences.set(PREF_TEST, false);

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

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

  // Trigger a change in the watched preferences.
  Preferences.reset(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, data) => {
          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_pluginsWatch_Add() {
  if (!gIsFirefox) {
    Assert.ok(true, "Skipping: there is no Plugin Manager on Android.");
    return;
  }

  Assert.equal(
    TelemetryEnvironment.currentEnvironment.addons.activePlugins.length,
    1
  );

  let newPlugin = new PluginTag(
    PLUGIN2_NAME,
    PLUGIN2_DESC,
    PLUGIN2_VERSION,
    true
  );
  gInstalledPlugins.push(newPlugin);

  let receivedNotifications = 0;
  let callback = (reason, data) => {
    receivedNotifications++;
  };
  TelemetryEnvironment.registerChangeListener("testWatchPlugins_Add", callback);

  Services.obs.notifyObservers(null, PLUGIN_UPDATED_TOPIC);

  await ContentTaskUtils.waitForCondition(() => {
    return (
      TelemetryEnvironment.currentEnvironment.addons.activePlugins.length == 2
    );
  });

  TelemetryEnvironment.unregisterChangeListener("testWatchPlugins_Add");

  Assert.equal(
    receivedNotifications,
    0,
    "We must not receive any notifications."
  );
});

add_task(async function test_pluginsWatch_Remove() {
  if (!gIsFirefox) {
    Assert.ok(true, "Skipping: there is no Plugin Manager on Android.");
    return;
  }

  Assert.equal(
    TelemetryEnvironment.currentEnvironment.addons.activePlugins.length,
    2
  );

  // Find the test plugin.
  let plugin = gInstalledPlugins.find(p => p.name == PLUGIN2_NAME);
  Assert.ok(plugin, "The test plugin must exist.");

  // Remove it from the PluginHost.
  gInstalledPlugins = gInstalledPlugins.filter(p => p != plugin);

  let receivedNotifications = 0;
  let callback = () => {
    receivedNotifications++;
  };
  TelemetryEnvironment.registerChangeListener(
    "testWatchPlugins_Remove",
    callback
  );

  Services.obs.notifyObservers(null, PLUGIN_UPDATED_TOPIC);

  await ContentTaskUtils.waitForCondition(() => {
    return (
      TelemetryEnvironment.currentEnvironment.addons.activePlugins.length == 1
    );
  });

  TelemetryEnvironment.unregisterChangeListener("testWatchPlugins_Remove");

  Assert.equal(
    receivedNotifications,
    0,
    "We must not receive any notifications."
  );
});

add_task(async function test_addonsWatch_NotInterestingChange() {
  // We are not interested to dictionary addons changes.
  const DICTIONARY_ADDON_INSTALL_URL = gDataRoot + "dictionary.xpi";
  const INTERESTING_ADDON_INSTALL_URL = gDataRoot + "restartless.xpi";

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

  let dictionaryAddon = await installXPIFromURL(DICTIONARY_ADDON_INSTALL_URL);
  let interestingAddon = await installXPIFromURL(INTERESTING_ADDON_INSTALL_URL);

  await deferred.promise;
  Assert.ok(
    !(
      "telemetry-dictionary@tests.mozilla.org" in
      TelemetryEnvironment.currentEnvironment.addons.activeAddons
    ),
    "Dictionaries should not appear in active addons."
  );

  TelemetryEnvironment.unregisterChangeListener("testNotInteresting");

  dictionaryAddon.uninstall();
  await interestingAddon.startupPromise;
  interestingAddon.uninstall();
});

add_task(async function test_addonsAndPlugins() {
  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: 1,
    type: "extension",
    foreignInstall: false,
    hasBinaryComponents: false,
    installDay: ADDON_INSTALL_DATE,
    updateDay: ADDON_INSTALL_DATE,
    signedState: AddonManager.SIGNEDSTATE_PRIVILEGED,
    isSystem: false,
    isWebExtension: true,
    multiprocessCompatible: 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: 1,
    type: "extension",
    foreignInstall: false,
    hasBinaryComponents: false,
    installDay: truncateToDays(SYSTEM_ADDON_INSTALL_DATE),
    updateDay: truncateToDays(SYSTEM_ADDON_INSTALL_DATE),
    signedState: undefined,
    isSystem: true,
    isWebExtension: true,
    multiprocessCompatible: 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: 1,
    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,
  };

  const EXPECTED_PLUGIN_DATA = {
    name: FLASH_PLUGIN_NAME,
    version: FLASH_PLUGIN_VERSION,
    description: FLASH_PLUGIN_DESC,
    blocklisted: false,
    disabled: false,
    clicktoplay: true,
  };

  let deferred = PromiseUtils.defer();
  TelemetryEnvironment.registerChangeListener(
    "test_WebExtension",
    (reason, data) => {
      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.
  ExtensionTestUtils.init(this);

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

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

  let data = TelemetryEnvironment.currentEnvironment;
  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();

  // Check plugin data.
  Assert.equal(
    data.addons.activePlugins.length,
    1,
    "We must have only one active plugin."
  );
  let targetPlugin = data.addons.activePlugins[0];
  for (let f in EXPECTED_PLUGIN_DATA) {
    Assert.equal(
      targetPlugin[f],
      EXPECTED_PLUGIN_DATA[f],
      f + " must have the correct value."
    );
  }

  // Check plugin mime types.
  Assert.ok(targetPlugin.mimeTypes.find(m => m == PLUGIN_MIME_TYPE1));
  Assert.ok(targetPlugin.mimeTypes.find(m => m == PLUGIN_MIME_TYPE2));
  Assert.ok(!targetPlugin.mimeTypes.find(m => m == "Not There."));

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

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

  const ADDON_INSTALL_URL = gDataRoot + "signed-webext.xpi";
  const ADDON_ID = "tel-signed-webext@tests.mozilla.org";
  const ADDON_INSTALL_DATE = truncateToDays(Date.now());
  const EXPECTED_ADDON_DATA = {
    blocklisted: false,
    description: "A signed webextension",
    name: "XPI Telemetry Signed Test",
    userDisabled: false,
    appDisabled: false,
    version: "1.0",
    scope: 1,
    type: "extension",
    foreignInstall: false,
    hasBinaryComponents: false,
    installDay: ADDON_INSTALL_DATE,
    updateDay: ADDON_INSTALL_DATE,
    signedState: AddonManager.SIGNEDSTATE_SIGNED,
  };

  let deferred = PromiseUtils.defer();
  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;
  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."
    );
  }

  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 = PromiseUtils.defer();
  TelemetryEnvironment.registerChangeListener(
    "test_longFieldsAddon",
    deferred.resolve
  );
  let addon = await installXPIFromURL(ADDON_INSTALL_URL);
  await deferred.promise;
  TelemetryEnvironment.unregisterChangeListener("test_longFieldsAddon");

  let data = TelemetryEnvironment.currentEnvironment;
  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: 1,
    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, data) => {
          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 Social addon installed with the broken
  // manifest and the rest of the data.
  let data = TelemetryEnvironment.currentEnvironment;
  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();
});

async function checkDefaultSearch(privateOn, reInitSearchService) {
  // Start off with separate default engine for private browsing turned off.
  Preferences.set(
    "browser.search.separatePrivateDefault.ui.enabled",
    privateOn
  );
  Preferences.set("browser.search.separatePrivateDefault", privateOn);

  let data = await TelemetryEnvironment.testCleanRestart().onInitialized();
  checkEnvironmentData(data);
  Assert.ok(!("defaultSearchEngine" in data.settings));
  Assert.ok(!("defaultSearchEngineData" in data.settings));
  Assert.ok(!("defaultPrivateSearchEngine" in data.settings));
  Assert.ok(!("defaultPrivateSearchEngineData" in data.settings));

  // Load the engines definitions from a xpcshell data: that's needed so that
  // the search provider reports an engine identifier.

  // Initialize the search service.
  if (reInitSearchService) {
    Services.search.wrappedJSObject.reset();
  }
  await Services.search.init();
  await promiseNextTick();

  // Our default engine from the JAR file has an identifier. Check if it is correctly
  // reported.
  data = TelemetryEnvironment.currentEnvironment;
  checkEnvironmentData(data);
  Assert.equal(data.settings.defaultSearchEngine, "telemetrySearchIdentifier");
  let expectedSearchEngineData = {
    name: "telemetrySearchIdentifier",
    loadPath:
      "[other]addEngineWithDetails:telemetrySearchIdentifier@search.mozilla.org",
    origin: "default",
    submissionURL:
      "https://ar.wikipedia.org/wiki/%D8%AE%D8%A7%D8%B5:%D8%A8%D8%AD%D8%AB?search=&sourceId=Mozilla-search",
  };
  Assert.deepEqual(
    data.settings.defaultSearchEngineData,
    expectedSearchEngineData
  );
  if (privateOn) {
    Assert.equal(
      data.settings.defaultPrivateSearchEngine,
      "telemetrySearchIdentifier"
    );
    Assert.deepEqual(
      data.settings.defaultPrivateSearchEngineData,
      expectedSearchEngineData,
      "Should have the correct data for the private search engine"
    );
  } else {
    Assert.ok(
      !("defaultPrivateSearchEngine" in data.settings),
      "Should not have private name recorded as the pref for separate is off"
    );
    Assert.ok(
      !("defaultPrivateSearchEngineData" in data.settings),
      "Should not have private data recorded as the pref for separate is off"
    );
  }

  // Add a new search engine (this will have no engine identifier).
  const SEARCH_ENGINE_ID = "telemetry_default";
  const SEARCH_ENGINE_URL = `http://www.example.org/${
    privateOn ? "private" : ""
  }?search={searchTerms}`;
  await Services.search.addEngineWithDetails(SEARCH_ENGINE_ID, {
    method: "get",
    template: SEARCH_ENGINE_URL,
  });

  // Register a new change listener and then wait for the search engine change to be notified.
  let deferred = PromiseUtils.defer();
  TelemetryEnvironment.registerChangeListener(
    "testWatch_SearchDefault",
    deferred.resolve
  );
  if (privateOn) {
    // As we had no default and no search engines, the normal mode engine will
    // assume the same as the added engine. To ensure the telemetry is different
    // we enforce a different default here.
    const engine = await Services.search.getEngineByName(
      "telemetrySearchIdentifier"
    );
    engine.hidden = false;
    await Services.search.setDefault(engine);
    await Services.search.setDefaultPrivate(
      Services.search.getEngineByName(SEARCH_ENGINE_ID)
    );
  } else {
    await Services.search.setDefault(
      Services.search.getEngineByName(SEARCH_ENGINE_ID)
    );
  }
  await deferred.promise;

  data = TelemetryEnvironment.currentEnvironment;
  checkEnvironmentData(data);

  const EXPECTED_SEARCH_ENGINE = "other-" + SEARCH_ENGINE_ID;
  const EXPECTED_SEARCH_ENGINE_DATA = {
    name: "telemetry_default",
    loadPath: "[other]addEngineWithDetails:telemetry_default@test.engine",
    origin: "verified",
  };
  if (privateOn) {
    Assert.equal(
      data.settings.defaultSearchEngine,
      "telemetrySearchIdentifier"
    );
    Assert.deepEqual(
      data.settings.defaultSearchEngineData,
      expectedSearchEngineData
    );
    Assert.equal(
      data.settings.defaultPrivateSearchEngine,
      EXPECTED_SEARCH_ENGINE
    );
    Assert.deepEqual(
      data.settings.defaultPrivateSearchEngineData,
      EXPECTED_SEARCH_ENGINE_DATA
    );
  } else {
    Assert.equal(data.settings.defaultSearchEngine, EXPECTED_SEARCH_ENGINE);
    Assert.deepEqual(
      data.settings.defaultSearchEngineData,
      EXPECTED_SEARCH_ENGINE_DATA
    );
  }
  TelemetryEnvironment.unregisterChangeListener("testWatch_SearchDefault");
}

add_task(async function test_defaultSearchEngine() {
  await checkDefaultSearch(false);

  // Cleanly install an engine from an xml file, and check if origin is
  // recorded as "verified".
  let promise = new Promise(resolve => {
    TelemetryEnvironment.registerChangeListener(
      "testWatch_SearchDefault",
      resolve
    );
  });
  let engine = await new Promise((resolve, reject) => {
    Services.obs.addObserver(function obs(obsSubject, obsTopic, obsData) {
      try {
        let searchEngine = obsSubject.QueryInterface(Ci.nsISearchEngine);
        info("Observed " + obsData + " for " + searchEngine.name);
        if (
          obsData != "engine-added" ||
          searchEngine.name != "engine-telemetry"
        ) {
          return;
        }

        Services.obs.removeObserver(obs, "browser-search-engine-modified");
        resolve(searchEngine);
      } catch (ex) {
        reject(ex);
      }
    }, "browser-search-engine-modified");
    Services.search.addOpenSearchEngine(
      "file://" + do_get_cwd().path + "/engine.xml",
      null
    );
  });
  await Services.search.setDefault(engine);
  await promise;
  TelemetryEnvironment.unregisterChangeListener("testWatch_SearchDefault");
  let data = TelemetryEnvironment.currentEnvironment;
  checkEnvironmentData(data);
  Assert.deepEqual(data.settings.defaultSearchEngineData, {
    name: "engine-telemetry",
    loadPath: "[other]/engine.xml",
    origin: "verified",
  });

  // Now break this engine's load path hash.
  promise = new Promise(resolve => {
    TelemetryEnvironment.registerChangeListener(
      "testWatch_SearchDefault",
      resolve
    );
  });
  engine.wrappedJSObject.setAttr("loadPathHash", "broken");
  Services.obs.notifyObservers(
    null,
    "browser-search-engine-modified",
    "engine-default"
  );
  await promise;
  TelemetryEnvironment.unregisterChangeListener("testWatch_SearchDefault");
  data = TelemetryEnvironment.currentEnvironment;
  Assert.equal(data.settings.defaultSearchEngineData.origin, "invalid");
  await Services.search.removeEngine(engine);

  const SEARCH_ENGINE_ID = "telemetry_default";
  const EXPECTED_SEARCH_ENGINE = "other-" + SEARCH_ENGINE_ID;
  // Work around bug 1165341: Intentionally set the default engine.
  await Services.search.setDefault(
    Services.search.getEngineByName(SEARCH_ENGINE_ID)
  );

  // Double-check the default for the next part of the test.
  data = TelemetryEnvironment.currentEnvironment;
  checkEnvironmentData(data);
  Assert.equal(data.settings.defaultSearchEngine, EXPECTED_SEARCH_ENGINE);

  // 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 }],
  ]);
  Preferences.reset(PREF_TEST);

  // Watch the test preference.
  await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
  let deferred = PromiseUtils.defer();
  TelemetryEnvironment.registerChangeListener(
    "testSearchEngine_pref",
    deferred.resolve
  );
  // Trigger an environment change.
  Preferences.set(PREF_TEST, 1);
  await deferred.promise;
  TelemetryEnvironment.unregisterChangeListener("testSearchEngine_pref");

  // Check that the search engine information is correctly retained when prefs change.
  data = TelemetryEnvironment.currentEnvironment;
  checkEnvironmentData(data);
  Assert.equal(data.settings.defaultSearchEngine, EXPECTED_SEARCH_ENGINE);
});

add_task(async function test_defaultPrivateSearchEngine() {
  await checkDefaultSearch(true, true);
});

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;
    checkEnvironmentData(environmentData);
    Assert.equal(
      environmentData.settings.isDefaultBrowser,
      null,
      "isDefaultBrowser must be null before the session is restored."
    );

    Services.obs.notifyObservers(null, "sessionstore-windows-restored");

    environmentData = TelemetryEnvironment.currentEnvironment;
    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 }],
    ]);
    Preferences.reset(PREF_TEST);

    // Watch the test preference.
    await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
    let deferred = PromiseUtils.defer();
    TelemetryEnvironment.registerChangeListener(
      "testDefaultBrowser_pref",
      deferred.resolve
    );
    // Trigger an environment change.
    Preferences.set(PREF_TEST, 1);
    await deferred.promise;
    TelemetryEnvironment.unregisterChangeListener("testDefaultBrowser_pref");

    // Check that the data is still available.
    environmentData = TelemetryEnvironment.currentEnvironment;
    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.
  SysInfo.overrides = {
    version: 1,
    name: 2,
    kernel_version: 3,
  };

  await TelemetryEnvironment.testCleanRestart().onInitialized();
  let data = TelemetryEnvironment.currentEnvironment;
  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.
  SysInfo.overrides = {
    version: null,
    name: null,
    kernel_version: null,
  };

  await TelemetryEnvironment.testCleanRestart().onInitialized();
  data = TelemetryEnvironment.currentEnvironment;
  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.
  SysInfo.overrides = {};
  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, type = null) => {
    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;
  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 = PromiseUtils.defer();
  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.
  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;
  checkEnvironmentData(data);
  checkExperiment(data, EXPERIMENT1, EXPERIMENT1_BRANCH);

  TelemetryEnvironment.unregisterChangeListener("test_experimentsAPI");

  // Add a second annotation and check that both experiments are there.
  deferred = PromiseUtils.defer();
  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;
  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 = PromiseUtils.defer();
  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;
  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;
  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 = PromiseUtils.defer();
  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;
  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 EXPECTED_HDD_FIELDS) {
      checkString(data.system.hdd[k].model);
      checkString(data.system.hdd[k].revision);
      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."
    );
    // 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(
      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
      Preferences.set("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.
      Preferences.reset("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;
      Preferences.reset("services.sync.username");
    }
  }
);

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 }],
  ]);
  Preferences.reset(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
  Preferences.set(PREF_TEST, 1);

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