/* Any copyright is dedicated to the Public Domain.
   http://creativecommons.org/publicdomain/zero/1.0/
*/
/* This testcase triggers two telemetry pings.
 *
 * Telemetry code keeps histograms of past telemetry pings. The first
 * ping populates these histograms. One of those histograms is then
 * checked in the second request.
 */

const { ClientID } = ChromeUtils.importESModule(
  "resource://gre/modules/ClientID.sys.mjs"
);
const { TelemetrySession } = ChromeUtils.importESModule(
  "resource://gre/modules/TelemetrySession.sys.mjs"
);
const { TelemetryEnvironment } = ChromeUtils.importESModule(
  "resource://gre/modules/TelemetryEnvironment.sys.mjs"
);
const { TelemetryReportingPolicy } = ChromeUtils.importESModule(
  "resource://gre/modules/TelemetryReportingPolicy.sys.mjs"
);

const PING_FORMAT_VERSION = 4;
const PING_TYPE_MAIN = "main";
const PING_TYPE_SAVED_SESSION = "saved-session";

const REASON_ABORTED_SESSION = "aborted-session";
const REASON_SAVED_SESSION = "saved-session";
const REASON_SHUTDOWN = "shutdown";
const REASON_TEST_PING = "test-ping";
const REASON_DAILY = "daily";
const REASON_ENVIRONMENT_CHANGE = "environment-change";

const IGNORE_CLONED_HISTOGRAM = "test::ignore_me_also";
// Add some unicode characters here to ensure that sending them works correctly.
const SHUTDOWN_TIME = 10000;
const FAILED_PROFILE_LOCK_ATTEMPTS = 2;

// Constants from prio.h for nsIFileOutputStream.init
const PR_WRONLY = 0x2;
const PR_CREATE_FILE = 0x8;
const PR_TRUNCATE = 0x20;
const RW_OWNER = parseInt("0600", 8);

const MS_IN_ONE_HOUR = 60 * 60 * 1000;
const MS_IN_ONE_DAY = 24 * MS_IN_ONE_HOUR;

const DATAREPORTING_DIR = "datareporting";
const ABORTED_PING_FILE_NAME = "aborted-session-ping";
const ABORTED_SESSION_UPDATE_INTERVAL_MS = 5 * 60 * 1000;

ChromeUtils.defineLazyGetter(this, "DATAREPORTING_PATH", function () {
  return PathUtils.join(PathUtils.profileDir, DATAREPORTING_DIR);
});

var gClientID = null;
var gMonotonicNow = 0;

function sendPing() {
  TelemetrySession.gatherStartup();
  if (PingServer.started) {
    TelemetrySend.setServer("http://localhost:" + PingServer.port);
    return TelemetrySession.testPing();
  }
  TelemetrySend.setServer("http://doesnotexist");
  return TelemetrySession.testPing();
}

function fakeGenerateUUID(sessionFunc, subsessionFunc) {
  const { Policy } = ChromeUtils.importESModule(
    "resource://gre/modules/TelemetrySession.sys.mjs"
  );
  Policy.generateSessionUUID = sessionFunc;
  Policy.generateSubsessionUUID = subsessionFunc;
}

function fakeIdleNotification(topic) {
  return TelemetryScheduler.observe(null, topic, null);
}

function setupTestData() {
  Services.startup.interrupted = true;
  let h2 = Telemetry.getHistogramById("TELEMETRY_TEST_COUNT");
  h2.add();

  let k1 = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_COUNT");
  k1.add("a");
  k1.add("a");
  k1.add("b");
}

function checkPingFormat(aPing, aType, aHasClientId, aHasEnvironment) {
  const MANDATORY_PING_FIELDS = [
    "type",
    "id",
    "creationDate",
    "version",
    "application",
    "payload",
  ];

  const APPLICATION_TEST_DATA = {
    buildId: gAppInfo.appBuildID,
    name: APP_NAME,
    version: APP_VERSION,
    vendor: "Mozilla",
    platformVersion: PLATFORM_VERSION,
    xpcomAbi: "noarch-spidermonkey",
  };

  // Check that the ping contains all the mandatory fields.
  for (let f of MANDATORY_PING_FIELDS) {
    Assert.ok(f in aPing, f + " must be available.");
  }

  Assert.equal(aPing.type, aType, "The ping must have the correct type.");
  Assert.equal(
    aPing.version,
    PING_FORMAT_VERSION,
    "The ping must have the correct version."
  );

  // Test the application section.
  for (let f in APPLICATION_TEST_DATA) {
    Assert.equal(
      aPing.application[f],
      APPLICATION_TEST_DATA[f],
      f + " must have the correct value."
    );
  }

  // We can't check the values for channel and architecture. Just make
  // sure they are in.
  Assert.ok(
    "architecture" in aPing.application,
    "The application section must have an architecture field."
  );
  Assert.ok(
    "channel" in aPing.application,
    "The application section must have a channel field."
  );

  // Check the clientId and environment fields, as needed.
  Assert.equal("clientId" in aPing, aHasClientId);
  Assert.equal("environment" in aPing, aHasEnvironment);
}

function checkPayloadInfo(data, reason) {
  const ALLOWED_REASONS = [
    "environment-change",
    "shutdown",
    "daily",
    "saved-session",
    "test-ping",
  ];
  let numberCheck = arg => {
    return typeof arg == "number";
  };
  let positiveNumberCheck = arg => {
    return numberCheck(arg) && arg >= 0;
  };
  let stringCheck = arg => {
    return typeof arg == "string" && arg != "";
  };
  let revisionCheck = arg => {
    return AppConstants.MOZILLA_OFFICIAL
      ? stringCheck(arg)
      : typeof arg == "string";
  };
  let uuidCheck = arg => {
    return UUID_REGEX.test(arg);
  };
  let isoDateCheck = arg => {
    // We expect use of this version of the ISO format:
    // 2015-04-12T18:51:19.1+00:00
    const isoDateRegEx =
      /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+[+-]\d{2}:\d{2}$/;
    return (
      stringCheck(arg) &&
      !Number.isNaN(Date.parse(arg)) &&
      isoDateRegEx.test(arg)
    );
  };

  const EXPECTED_INFO_FIELDS_TYPES = {
    reason: stringCheck,
    revision: revisionCheck,
    timezoneOffset: numberCheck,
    sessionId: uuidCheck,
    subsessionId: uuidCheck,
    // Special cases: previousSessionId and previousSubsessionId are null on first run.
    previousSessionId: arg => {
      return arg ? uuidCheck(arg) : true;
    },
    previousSubsessionId: arg => {
      return arg ? uuidCheck(arg) : true;
    },
    subsessionCounter: positiveNumberCheck,
    profileSubsessionCounter: positiveNumberCheck,
    sessionStartDate: isoDateCheck,
    subsessionStartDate: isoDateCheck,
    subsessionLength: positiveNumberCheck,
  };

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

    let checkFunc = EXPECTED_INFO_FIELDS_TYPES[f];
    Assert.ok(
      checkFunc(data[f]),
      f + " must have the correct type and valid data " + data[f]
    );
  }

  // Check for a valid revision.
  if (data.revision != "") {
    const revisionUrlRegEx =
      /^http[s]?:\/\/hg.mozilla.org(\/[a-z\S]+)+(\/rev\/[0-9a-z]+)$/g;
    Assert.ok(revisionUrlRegEx.test(data.revision));
  }

  // Previous buildId is not mandatory.
  if (data.previousBuildId) {
    Assert.ok(stringCheck(data.previousBuildId));
  }

  Assert.ok(
    ALLOWED_REASONS.find(r => r == data.reason),
    "Payload must contain an allowed reason."
  );
  Assert.equal(data.reason, reason, "Payload reason must match expected.");

  Assert.ok(
    Date.parse(data.subsessionStartDate) >= Date.parse(data.sessionStartDate)
  );
  Assert.ok(data.profileSubsessionCounter >= data.subsessionCounter);

  // According to https://en.wikipedia.org/wiki/List_of_UTC_time_offsets,
  // UTC offsets range from -12 to +14 hours.
  // Don't think the extremes of the range are affected by further
  // daylight-savings adjustments, but it is possible.
  Assert.ok(
    data.timezoneOffset >= -12 * 60,
    "The timezone must be in a valid range."
  );
  Assert.ok(
    data.timezoneOffset <= 14 * 60,
    "The timezone must be in a valid range."
  );
}

function checkScalars(processes) {
  // Check that the scalars section is available in the ping payload.
  const parentProcess = processes.parent;
  Assert.ok(
    "scalars" in parentProcess,
    "The scalars section must be available in the parent process."
  );
  Assert.ok(
    "keyedScalars" in parentProcess,
    "The keyedScalars section must be available in the parent process."
  );
  Assert.equal(
    typeof parentProcess.scalars,
    "object",
    "The scalars entry must be an object."
  );
  Assert.equal(
    typeof parentProcess.keyedScalars,
    "object",
    "The keyedScalars entry must be an object."
  );

  let checkScalar = function (scalar, name) {
    // Check if the value is of a supported type.
    const valueType = typeof scalar;
    switch (valueType) {
      case "string":
        Assert.ok(
          scalar.length <= 50,
          "String values can't have more than 50 characters"
        );
        break;
      case "number":
        Assert.ok(
          scalar >= 0,
          "We only support unsigned integer values in scalars."
        );
        break;
      case "boolean":
        Assert.ok(true, "Boolean scalar found.");
        break;
      default:
        Assert.ok(
          false,
          name + " contains an unsupported value type (" + valueType + ")"
        );
    }
  };

  // Check that we have valid scalar entries.
  const scalars = parentProcess.scalars;
  for (let name in scalars) {
    Assert.equal(typeof name, "string", "Scalar names must be strings.");
    checkScalar(scalars[name], name);
  }

  // Check that we have valid keyed scalar entries.
  const keyedScalars = parentProcess.keyedScalars;
  for (let name in keyedScalars) {
    Assert.equal(typeof name, "string", "Scalar names must be strings.");
    Assert.ok(
      Object.keys(keyedScalars[name]).length,
      "The reported keyed scalars must contain at least 1 key."
    );
    for (let key in keyedScalars[name]) {
      Assert.equal(typeof key, "string", "Keyed scalar keys must be strings.");
      Assert.ok(
        key.length <= 70,
        "Keyed scalar keys can't have more than 70 characters."
      );
      checkScalar(scalars[name][key], name);
    }
  }
}

function checkPayload(payload, reason, successfulPings) {
  Assert.ok("info" in payload, "Payload must contain an info section.");
  checkPayloadInfo(payload.info, reason);

  Assert.ok(payload.simpleMeasurements.totalTime >= 0);
  Assert.equal(payload.simpleMeasurements.startupInterrupted, 1);
  Assert.equal(payload.simpleMeasurements.shutdownDuration, SHUTDOWN_TIME);

  let activeTicks = payload.simpleMeasurements.activeTicks;
  Assert.ok(activeTicks >= 0);

  if ("browser.timings.last_shutdown" in payload.processes.parent.scalars) {
    Assert.equal(
      payload.processes.parent.scalars["browser.timings.last_shutdown"],
      SHUTDOWN_TIME
    );
  }

  Assert.equal(
    payload.simpleMeasurements.failedProfileLockCount,
    FAILED_PROFILE_LOCK_ATTEMPTS
  );
  let profileDirectory = Services.dirsvc.get("ProfD", Ci.nsIFile);
  let failedProfileLocksFile = profileDirectory.clone();
  failedProfileLocksFile.append("Telemetry.FailedProfileLocks.txt");
  Assert.ok(!failedProfileLocksFile.exists());

  let isWindows = "@mozilla.org/windows-registry-key;1" in Cc;
  if (isWindows) {
    Assert.ok(payload.simpleMeasurements.startupSessionRestoreReadBytes > 0);
    Assert.ok(payload.simpleMeasurements.startupSessionRestoreWriteBytes > 0);
  }

  const TELEMETRY_SEND_SUCCESS = "TELEMETRY_SEND_SUCCESS";
  const TELEMETRY_SUCCESS = "TELEMETRY_SUCCESS";
  const TELEMETRY_TEST_FLAG = "TELEMETRY_TEST_FLAG";
  const TELEMETRY_TEST_COUNT = "TELEMETRY_TEST_COUNT";
  const TELEMETRY_TEST_KEYED_FLAG = "TELEMETRY_TEST_KEYED_FLAG";
  const TELEMETRY_TEST_KEYED_COUNT = "TELEMETRY_TEST_KEYED_COUNT";

  if (successfulPings > 0) {
    Assert.ok(TELEMETRY_SEND_SUCCESS in payload.histograms);
  }
  Assert.ok(TELEMETRY_TEST_FLAG in payload.histograms);
  Assert.ok(TELEMETRY_TEST_COUNT in payload.histograms);

  Assert.ok(!(IGNORE_CLONED_HISTOGRAM in payload.histograms));

  // Flag histograms should automagically spring to life.
  const expected_flag = {
    range: [1, 2],
    bucket_count: 3,
    histogram_type: 3,
    values: { 0: 1, 1: 0 },
    sum: 0,
  };
  let flag = payload.histograms[TELEMETRY_TEST_FLAG];
  Assert.deepEqual(flag, expected_flag);

  // We should have a test count.
  const expected_count = {
    range: [1, 2],
    bucket_count: 3,
    histogram_type: 4,
    values: { 0: 1, 1: 0 },
    sum: 1,
  };
  let count = payload.histograms[TELEMETRY_TEST_COUNT];
  Assert.deepEqual(count, expected_count);

  // There should be one successful report from the previous telemetry ping.
  if (successfulPings > 0) {
    const expected_tc = {
      range: [1, 2],
      bucket_count: 3,
      histogram_type: 2,
      values: { 0: 2, 1: successfulPings, 2: 0 },
      sum: successfulPings,
    };
    let tc = payload.histograms[TELEMETRY_SUCCESS];
    Assert.deepEqual(tc, expected_tc);
  }

  // The ping should include data from memory reporters.  We can't check that
  // this data is correct, because we can't control the values returned by the
  // memory reporters.  But we can at least check that the data is there.
  //
  // It's important to check for the presence of reporters with a mix of units,
  // because MemoryTelemetry has separate logic for each one.  But we can't
  // currently check UNITS_COUNT_CUMULATIVE or UNITS_PERCENTAGE because
  // Telemetry doesn't touch a memory reporter with these units that's
  // available on all platforms.

  Assert.ok("MEMORY_TOTAL" in payload.histograms); // UNITS_BYTES
  Assert.ok("MEMORY_JS_GC_HEAP" in payload.histograms); // UNITS_BYTES
  Assert.ok("MEMORY_JS_COMPARTMENTS_SYSTEM" in payload.histograms); // UNITS_COUNT

  Assert.ok(
    "mainThread" in payload.slowSQL && "otherThreads" in payload.slowSQL
  );

  // Check keyed histogram payload.

  Assert.ok("keyedHistograms" in payload);
  let keyedHistograms = payload.keyedHistograms;
  Assert.ok(!(TELEMETRY_TEST_KEYED_FLAG in keyedHistograms));
  Assert.ok(TELEMETRY_TEST_KEYED_COUNT in keyedHistograms);

  const expected_keyed_count = {
    a: {
      range: [1, 2],
      bucket_count: 3,
      histogram_type: 4,
      values: { 0: 2, 1: 0 },
      sum: 2,
    },
    b: {
      range: [1, 2],
      bucket_count: 3,
      histogram_type: 4,
      values: { 0: 1, 1: 0 },
      sum: 1,
    },
  };
  Assert.deepEqual(
    expected_keyed_count,
    keyedHistograms[TELEMETRY_TEST_KEYED_COUNT]
  );

  Assert.ok(
    "processes" in payload,
    "The payload must have a processes section."
  );
  Assert.ok(
    "parent" in payload.processes,
    "There must be at least a parent process."
  );

  checkScalars(payload.processes);
}

function writeStringToFile(file, contents) {
  let ostream = Cc[
    "@mozilla.org/network/safe-file-output-stream;1"
  ].createInstance(Ci.nsIFileOutputStream);
  ostream.init(
    file,
    PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE,
    RW_OWNER,
    ostream.DEFER_OPEN
  );
  ostream.write(contents, contents.length);
  ostream.QueryInterface(Ci.nsISafeOutputStream).finish();
  ostream.close();
}

function write_fake_shutdown_file() {
  let profileDirectory = Services.dirsvc.get("ProfD", Ci.nsIFile);
  let file = profileDirectory.clone();
  file.append("Telemetry.ShutdownTime.txt");
  let contents = "" + SHUTDOWN_TIME;
  writeStringToFile(file, contents);
}

function write_fake_failedprofilelocks_file() {
  let profileDirectory = Services.dirsvc.get("ProfD", Ci.nsIFile);
  let file = profileDirectory.clone();
  file.append("Telemetry.FailedProfileLocks.txt");
  let contents = "" + FAILED_PROFILE_LOCK_ATTEMPTS;
  writeStringToFile(file, contents);
}

add_task(async function test_setup() {
  // Addon manager needs a profile directory
  do_get_profile();
  await loadAddonManager(APP_ID, APP_NAME, APP_VERSION, PLATFORM_VERSION);
  finishAddonManagerStartup();
  fakeIntlReady();
  // Make sure we don't generate unexpected pings due to pref changes.
  await setEmptyPrefWatchlist();

  Services.prefs.setBoolPref(TelemetryUtils.Preferences.FhrUploadEnabled, true);

  // Make it look like we've previously failed to lock a profile a couple times.
  write_fake_failedprofilelocks_file();

  // Make it look like we've shutdown before.
  write_fake_shutdown_file();

  await new Promise(resolve =>
    Telemetry.asyncFetchTelemetryData(wrapWithExceptionHandler(resolve))
  );
});

add_task(async function asyncSetup() {
  await TelemetryController.testSetup();
  // Load the client ID from the client ID provider to check for pings sanity.
  gClientID = await ClientID.getClientID();
});

// Ensures that expired histograms are not part of the payload.
add_task(async function test_expiredHistogram() {
  let dummy = Telemetry.getHistogramById("TELEMETRY_TEST_EXPIRED");

  dummy.add(1);

  Assert.equal(
    TelemetrySession.getPayload().histograms.TELEMETRY_TEST_EXPIRED,
    undefined
  );
});

add_task(async function sessionTimeExcludingAndIncludingSuspend() {
  if (gIsAndroid) {
    // We don't support this new probe on android at the moment.
    return;
  }
  Services.prefs.setBoolPref(
    "toolkit.telemetry.testing.overrideProductsCheck",
    true
  );
  await TelemetryController.testReset();
  let subsession = TelemetrySession.getPayload("environment-change", true);
  let parentScalars = subsession.processes.parent.scalars;

  let withSuspend =
    parentScalars["browser.engagement.session_time_including_suspend"];
  let withoutSuspend =
    parentScalars["browser.engagement.session_time_excluding_suspend"];

  Assert.ok(
    withSuspend > 0,
    "The session time including suspend should be positive"
  );

  Assert.ok(
    withoutSuspend > 0,
    "The session time excluding suspend should be positive"
  );

  // Two things about the next assertion:
  // 1. The two calls to get the two different uptime values are made
  //    separately, so we can't guarantee equality, even if we know the machine
  //    has not been suspended (for example because it's running in infra and
  //    was just booted). In this case the value should be close to each other.
  // 2. This test will fail if the device running this has been suspended in
  //    between booting the Firefox process running this test, and doing the
  //    following assertion test, but that's unlikely in practice.
  const max_delta_ms = 100;

  Assert.ok(
    withSuspend - withoutSuspend <= max_delta_ms,
    "In test condition, the two uptimes should be close to each other"
  );

  // This however should always hold.
  Assert.greaterOrEqual(
    withSuspend,
    withoutSuspend,
    `The uptime with suspend must always been greater or equal to the uptime
     without suspend`
  );

  Services.prefs.setBoolPref(
    "toolkit.telemetry.testing.overrideProductsCheck",
    false
  );
});

// Sends a ping to a non existing server. If we remove this test, we won't get
// all the histograms we need in the main ping.
add_task(async function test_noServerPing() {
  await sendPing();
  // We need two pings in order to make sure STARTUP_MEMORY_STORAGE_SQLIE histograms
  // are initialised. See bug 1131585.
  await sendPing();
  // Allowing Telemetry to persist unsent pings as pending. If omitted may cause
  // problems to the consequent tests.
  await TelemetryController.testShutdown();
});

// Checks that a sent ping is correctly received by a dummy http server.
add_task(async function test_simplePing() {
  await TelemetryStorage.testClearPendingPings();
  PingServer.start();
  Services.prefs.setStringPref(
    TelemetryUtils.Preferences.Server,
    "http://localhost:" + PingServer.port
  );

  let now = new Date(2020, 1, 1, 12, 5, 6);
  let expectedDate = new Date(2020, 1, 1, 12, 0, 0);
  fakeNow(now);
  gMonotonicNow = fakeMonotonicNow(gMonotonicNow + 5000);

  const expectedSessionUUID = "bd314d15-95bf-4356-b682-b6c4a8942202";
  const expectedSubsessionUUID = "3e2e5f6c-74ba-4e4d-a93f-a48af238a8c7";
  fakeGenerateUUID(
    () => expectedSessionUUID,
    () => expectedSubsessionUUID
  );
  await TelemetryController.testReset();

  // Session and subsession start dates are faked during TelemetrySession setup. We can
  // now fake the session duration.
  const SESSION_DURATION_IN_MINUTES = 15;
  fakeNow(new Date(2020, 1, 1, 12, SESSION_DURATION_IN_MINUTES, 0));
  gMonotonicNow = fakeMonotonicNow(
    gMonotonicNow + SESSION_DURATION_IN_MINUTES * 60 * 1000
  );

  await sendPing();
  let ping = await PingServer.promiseNextPing();

  checkPingFormat(ping, PING_TYPE_MAIN, true, true);

  // Check that we get the data we expect.
  let payload = ping.payload;
  Assert.equal(payload.info.sessionId, expectedSessionUUID);
  Assert.equal(payload.info.subsessionId, expectedSubsessionUUID);
  let sessionStartDate = new Date(payload.info.sessionStartDate);
  Assert.equal(sessionStartDate.toISOString(), expectedDate.toISOString());
  let subsessionStartDate = new Date(payload.info.subsessionStartDate);
  Assert.equal(subsessionStartDate.toISOString(), expectedDate.toISOString());
  Assert.equal(payload.info.subsessionLength, SESSION_DURATION_IN_MINUTES * 60);

  // Restore the UUID generator so we don't mess with other tests.
  fakeGenerateUUID(TelemetryUtils.generateUUID, TelemetryUtils.generateUUID);

  await TelemetryController.testShutdown();
});

// Saves the current session histograms, reloads them, performs a ping
// and checks that the dummy http server received both the previously
// saved ping and the new one.
add_task(async function test_saveLoadPing() {
  // Let's start out with a defined state.
  await TelemetryStorage.testClearPendingPings();
  await TelemetryController.testReset();
  PingServer.clearRequests();

  // Setup test data and trigger pings.
  setupTestData();
  await TelemetrySession.testSavePendingPing();
  await sendPing();

  // Get requests received by dummy server.
  const requests = await PingServer.promiseNextRequests(2);

  for (let req of requests) {
    Assert.equal(
      req.getHeader("content-type"),
      "application/json; charset=UTF-8",
      "The request must have the correct content-type."
    );
  }

  // We decode both requests to check for the |reason|.
  let pings = Array.from(requests, decodeRequestPayload);

  // Check we have the correct two requests. Ordering is not guaranteed. The ping type
  // is encoded in the URL.
  if (pings[0].type != PING_TYPE_MAIN) {
    pings.reverse();
  }

  checkPingFormat(pings[0], PING_TYPE_MAIN, true, true);
  checkPayload(pings[0].payload, REASON_TEST_PING, 0);
  checkPingFormat(pings[1], PING_TYPE_SAVED_SESSION, true, true);
  checkPayload(pings[1].payload, REASON_SAVED_SESSION, 0);

  await TelemetryController.testShutdown();
});

add_task(async function test_checkSubsessionScalars() {
  if (gIsAndroid) {
    // We don't support subsessions yet on Android.
    return;
  }

  // Clear the scalars.
  Telemetry.clearScalars();
  await TelemetryController.testReset();

  // Set some scalars.
  const UINT_SCALAR = "telemetry.test.unsigned_int_kind";
  const STRING_SCALAR = "telemetry.test.string_kind";
  let expectedUint = 37;
  let expectedString = "Test value. Yay.";
  Telemetry.scalarSet(UINT_SCALAR, expectedUint);
  Telemetry.scalarSet(STRING_SCALAR, expectedString);

  // Check that scalars are not available in classic pings but are in subsession
  // pings. Also clear the subsession.
  let classic = TelemetrySession.getPayload();
  let subsession = TelemetrySession.getPayload("environment-change", true);

  const TEST_SCALARS = [UINT_SCALAR, STRING_SCALAR];
  for (let name of TEST_SCALARS) {
    // Scalar must be reported in subsession pings (e.g. main).
    Assert.ok(
      name in subsession.processes.parent.scalars,
      name + " must be reported in a subsession ping."
    );
  }
  // No scalar must be reported in classic pings (e.g. saved-session).
  Assert.ok(
    !Object.keys(classic.processes.parent.scalars).length,
    "Scalars must not be reported in a classic ping."
  );

  // And make sure that we're getting the right values in the
  // subsession ping.
  Assert.equal(
    subsession.processes.parent.scalars[UINT_SCALAR],
    expectedUint,
    UINT_SCALAR + " must contain the expected value."
  );
  Assert.equal(
    subsession.processes.parent.scalars[STRING_SCALAR],
    expectedString,
    STRING_SCALAR + " must contain the expected value."
  );

  // Since we cleared the subsession in the last getPayload(), check that
  // breaking subsessions clears the scalars.
  subsession = TelemetrySession.getPayload("environment-change");
  for (let name of TEST_SCALARS) {
    Assert.ok(
      !(name in subsession.processes.parent.scalars),
      name + " must be cleared with the new subsession."
    );
  }

  // Check if setting the scalars again works as expected.
  expectedUint = 85;
  expectedString = "A creative different value";
  Telemetry.scalarSet(UINT_SCALAR, expectedUint);
  Telemetry.scalarSet(STRING_SCALAR, expectedString);
  subsession = TelemetrySession.getPayload("environment-change");
  Assert.equal(
    subsession.processes.parent.scalars[UINT_SCALAR],
    expectedUint,
    UINT_SCALAR + " must contain the expected value."
  );
  Assert.equal(
    subsession.processes.parent.scalars[STRING_SCALAR],
    expectedString,
    STRING_SCALAR + " must contain the expected value."
  );

  await TelemetryController.testShutdown();
});

add_task(async function test_dailyCollection() {
  if (gIsAndroid) {
    // We don't do daily collections yet on Android.
    return;
  }

  let now = new Date(2030, 1, 1, 12, 0, 0);
  let nowHour = new Date(2030, 1, 1, 12, 0, 0);
  let schedulerTickCallback = null;

  PingServer.clearRequests();

  fakeNow(now);

  // Fake scheduler functions to control daily collection flow in tests.
  fakeSchedulerTimer(
    callback => (schedulerTickCallback = callback),
    () => {}
  );

  // Init and check timer.
  await TelemetryStorage.testClearPendingPings();
  await TelemetryController.testReset();
  TelemetrySend.setServer("http://localhost:" + PingServer.port);

  // Set histograms to expected state.
  const COUNT_ID = "TELEMETRY_TEST_COUNT";
  const KEYED_ID = "TELEMETRY_TEST_KEYED_COUNT";
  const count = Telemetry.getHistogramById(COUNT_ID);
  const keyed = Telemetry.getKeyedHistogramById(KEYED_ID);

  count.clear();
  keyed.clear();
  count.add(1);
  keyed.add("a", 1);
  keyed.add("b", 1);
  keyed.add("b", 1);

  // Make sure the daily ping gets triggered.
  let expectedDate = nowHour;
  now = futureDate(nowHour, MS_IN_ONE_DAY);
  fakeNow(now);

  Assert.ok(!!schedulerTickCallback);
  // Run a scheduler tick: it should trigger the daily ping.
  await schedulerTickCallback();

  // Collect the daily ping.
  let ping = await PingServer.promiseNextPing();
  Assert.ok(!!ping);

  Assert.equal(ping.type, PING_TYPE_MAIN);
  Assert.equal(ping.payload.info.reason, REASON_DAILY);
  let subsessionStartDate = new Date(ping.payload.info.subsessionStartDate);
  Assert.equal(subsessionStartDate.toISOString(), expectedDate.toISOString());

  Assert.equal(ping.payload.histograms[COUNT_ID].sum, 1);
  Assert.equal(ping.payload.keyedHistograms[KEYED_ID].a.sum, 1);
  Assert.equal(ping.payload.keyedHistograms[KEYED_ID].b.sum, 2);

  // The daily ping is rescheduled for "tomorrow".
  expectedDate = futureDate(expectedDate, MS_IN_ONE_DAY);
  now = futureDate(now, MS_IN_ONE_DAY);
  fakeNow(now);

  // Run a scheduler tick. Trigger and collect another ping. The histograms should be reset.
  await schedulerTickCallback();

  ping = await PingServer.promiseNextPing();
  Assert.ok(!!ping);

  Assert.equal(ping.type, PING_TYPE_MAIN);
  Assert.equal(ping.payload.info.reason, REASON_DAILY);
  subsessionStartDate = new Date(ping.payload.info.subsessionStartDate);
  Assert.equal(subsessionStartDate.toISOString(), expectedDate.toISOString());

  Assert.ok(!(COUNT_ID in ping.payload.histograms));
  Assert.ok(!(KEYED_ID in ping.payload.keyedHistograms));

  // Trigger and collect another daily ping, with the histograms being set again.
  count.add(1);
  keyed.add("a", 1);
  keyed.add("b", 1);

  // The daily ping is rescheduled for "tomorrow".
  expectedDate = futureDate(expectedDate, MS_IN_ONE_DAY);
  now = futureDate(now, MS_IN_ONE_DAY);
  fakeNow(now);

  await schedulerTickCallback();
  ping = await PingServer.promiseNextPing();
  Assert.ok(!!ping);

  Assert.equal(ping.type, PING_TYPE_MAIN);
  Assert.equal(ping.payload.info.reason, REASON_DAILY);
  subsessionStartDate = new Date(ping.payload.info.subsessionStartDate);
  Assert.equal(subsessionStartDate.toISOString(), expectedDate.toISOString());

  Assert.equal(ping.payload.histograms[COUNT_ID].sum, 1);
  Assert.equal(ping.payload.keyedHistograms[KEYED_ID].a.sum, 1);
  Assert.equal(ping.payload.keyedHistograms[KEYED_ID].b.sum, 1);

  // Shutdown to cleanup the aborted-session if it gets created.
  await TelemetryController.testShutdown();
});

add_task(async function test_dailyDuplication() {
  if (gIsAndroid) {
    // We don't do daily collections yet on Android.
    return;
  }

  await TelemetrySend.reset();
  await TelemetryStorage.testClearPendingPings();
  PingServer.clearRequests();

  let schedulerTickCallback = null;
  let now = new Date(2030, 1, 1, 0, 0, 0);
  fakeNow(now);
  // Fake scheduler functions to control daily collection flow in tests.
  fakeSchedulerTimer(
    callback => (schedulerTickCallback = callback),
    () => {}
  );
  await TelemetryController.testReset();

  // Make sure the daily ping gets triggered at midnight.
  // We need to make sure that we trigger this after the period where we wait for
  // the user to become idle.
  let firstDailyDue = new Date(2030, 1, 2, 0, 0, 0);
  fakeNow(firstDailyDue);

  // Run a scheduler tick: it should trigger the daily ping.
  Assert.ok(!!schedulerTickCallback);
  await schedulerTickCallback();

  // Get the first daily ping.
  let ping = await PingServer.promiseNextPing();
  Assert.ok(!!ping);

  Assert.equal(ping.type, PING_TYPE_MAIN);
  Assert.equal(ping.payload.info.reason, REASON_DAILY);

  // We don't expect to receive any other daily ping in this test, so assert if we do.
  PingServer.registerPingHandler(() => {
    Assert.ok(
      false,
      "No more daily pings should be sent/received in this test."
    );
  });

  // Set the current time to a bit after midnight.
  let secondDailyDue = new Date(firstDailyDue);
  secondDailyDue.setHours(0);
  secondDailyDue.setMinutes(15);
  fakeNow(secondDailyDue);

  // Run a scheduler tick: it should NOT trigger the daily ping.
  Assert.ok(!!schedulerTickCallback);
  await schedulerTickCallback();

  // Shutdown to cleanup the aborted-session if it gets created.
  PingServer.resetPingHandler();
  await TelemetryController.testShutdown();
});

add_task(async function test_dailyOverdue() {
  if (gIsAndroid) {
    // We don't do daily collections yet on Android.
    return;
  }

  let schedulerTickCallback = null;
  let now = new Date(2030, 1, 1, 11, 0, 0);
  fakeNow(now);
  // Fake scheduler functions to control daily collection flow in tests.
  fakeSchedulerTimer(
    callback => (schedulerTickCallback = callback),
    () => {}
  );
  await TelemetryStorage.testClearPendingPings();
  await TelemetryController.testReset();

  // Skip one hour ahead: nothing should be due.
  now.setHours(now.getHours() + 1);
  fakeNow(now);

  // Assert if we receive something!
  PingServer.registerPingHandler(() => {
    Assert.ok(false, "No daily ping should be received if not overdue!.");
  });

  // This tick should not trigger any daily ping.
  Assert.ok(!!schedulerTickCallback);
  await schedulerTickCallback();

  // Restore the non asserting ping handler.
  PingServer.resetPingHandler();
  PingServer.clearRequests();

  // Simulate an overdue ping: we're not close to midnight, but the last daily ping
  // time is too long ago.
  let dailyOverdue = new Date(2030, 1, 2, 13, 0, 0);
  fakeNow(dailyOverdue);

  // Run a scheduler tick: it should trigger the daily ping.
  Assert.ok(!!schedulerTickCallback);
  await schedulerTickCallback();

  // Get the first daily ping.
  let ping = await PingServer.promiseNextPing();
  Assert.ok(!!ping);

  Assert.equal(ping.type, PING_TYPE_MAIN);
  Assert.equal(ping.payload.info.reason, REASON_DAILY);

  // Shutdown to cleanup the aborted-session if it gets created.
  await TelemetryController.testShutdown();
});

add_task(async function test_environmentChange() {
  if (gIsAndroid) {
    // We don't split subsessions on environment changes yet on Android.
    return;
  }

  await TelemetryStorage.testClearPendingPings();
  PingServer.clearRequests();

  let now = fakeNow(2040, 1, 1, 12, 0, 0);
  gMonotonicNow = fakeMonotonicNow(
    gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE
  );

  const PREF_TEST = "toolkit.telemetry.test.pref1";
  Services.prefs.clearUserPref(PREF_TEST);

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

  // Setup.
  await TelemetryController.testReset();
  TelemetrySend.setServer("http://localhost:" + PingServer.port);
  await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);

  // Set histograms to expected state.
  const COUNT_ID = "TELEMETRY_TEST_COUNT";
  const KEYED_ID = "TELEMETRY_TEST_KEYED_COUNT";
  const count = Telemetry.getHistogramById(COUNT_ID);
  const keyed = Telemetry.getKeyedHistogramById(KEYED_ID);

  count.clear();
  keyed.clear();
  count.add(1);
  keyed.add("a", 1);
  keyed.add("b", 1);

  // Trigger and collect environment-change ping.
  gMonotonicNow = fakeMonotonicNow(
    gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE
  );
  let startHour = TelemetryUtils.truncateToHours(now);
  now = fakeNow(futureDate(now, 10 * MILLISECONDS_PER_MINUTE));

  Services.prefs.setIntPref(PREF_TEST, 1);
  let ping = await PingServer.promiseNextPing();
  Assert.ok(!!ping);

  Assert.equal(ping.type, PING_TYPE_MAIN);
  Assert.equal(ping.environment.settings.userPrefs[PREF_TEST], undefined);
  Assert.equal(ping.payload.info.reason, REASON_ENVIRONMENT_CHANGE);
  let subsessionStartDate = new Date(ping.payload.info.subsessionStartDate);
  Assert.equal(subsessionStartDate.toISOString(), startHour.toISOString());

  Assert.equal(ping.payload.histograms[COUNT_ID].sum, 1);
  Assert.equal(ping.payload.keyedHistograms[KEYED_ID].a.sum, 1);

  // Trigger and collect another ping. The histograms should be reset.
  startHour = TelemetryUtils.truncateToHours(now);
  gMonotonicNow = fakeMonotonicNow(
    gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE
  );
  now = fakeNow(futureDate(now, 10 * MILLISECONDS_PER_MINUTE));

  Services.prefs.setIntPref(PREF_TEST, 2);
  ping = await PingServer.promiseNextPing();
  Assert.ok(!!ping);

  Assert.equal(ping.type, PING_TYPE_MAIN);
  Assert.equal(ping.environment.settings.userPrefs[PREF_TEST], 1);
  Assert.equal(ping.payload.info.reason, REASON_ENVIRONMENT_CHANGE);
  subsessionStartDate = new Date(ping.payload.info.subsessionStartDate);
  Assert.equal(subsessionStartDate.toISOString(), startHour.toISOString());

  Assert.ok(!(COUNT_ID in ping.payload.histograms));
  Assert.ok(!(KEYED_ID in ping.payload.keyedHistograms));

  // Trigger and collect another ping. The histograms should be reset.
  startHour = TelemetryUtils.truncateToHours(now);
  gMonotonicNow = fakeMonotonicNow(
    gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE
  );
  now = fakeNow(futureDate(now, 10 * MILLISECONDS_PER_MINUTE));

  await TelemetryController.testShutdown();
});

add_task(async function test_experimentAnnotations_subsession() {
  if (gIsAndroid) {
    // We don't split subsessions on environment changes yet on Android.
    return;
  }

  const EXPERIMENT1 = "experiment-1";
  const EXPERIMENT1_BRANCH = "nice-branch";
  const EXPERIMENT2 = "experiment-2";
  const EXPERIMENT2_BRANCH = "other-branch";

  await TelemetryStorage.testClearPendingPings();
  PingServer.clearRequests();

  let now = fakeNow(2040, 1, 1, 12, 0, 0);
  gMonotonicNow = fakeMonotonicNow(
    gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE
  );

  // Setup.
  await TelemetryController.testReset();
  TelemetrySend.setServer("http://localhost:" + PingServer.port);
  Assert.equal(TelemetrySession.getPayload().info.subsessionCounter, 1);

  // Trigger a subsession split with a telemetry annotation.
  gMonotonicNow = fakeMonotonicNow(
    gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE
  );
  let futureTestDate = futureDate(now, 10 * MILLISECONDS_PER_MINUTE);
  now = fakeNow(futureTestDate);
  TelemetryEnvironment.setExperimentActive(EXPERIMENT1, EXPERIMENT1_BRANCH);

  let ping = await PingServer.promiseNextPing();
  Assert.ok(!!ping, "A ping must be received.");

  Assert.equal(
    ping.type,
    PING_TYPE_MAIN,
    "The received ping must be a 'main' ping."
  );
  Assert.equal(
    ping.payload.info.reason,
    REASON_ENVIRONMENT_CHANGE,
    "The 'main' ping must be triggered by a change in the environment."
  );
  // We expect the current experiments to be reported in the next ping, not this
  // one.
  Assert.ok(
    !("experiments" in ping.environment),
    "The old environment must contain no active experiments."
  );
  // Since this change wasn't throttled, the subsession counter must increase.
  Assert.equal(
    TelemetrySession.getPayload().info.subsessionCounter,
    2,
    "The experiment annotation must trigger a new subsession."
  );

  // Add another annotation to the environment. We're not advancing the fake
  // timer, so no subsession split should happen due to throttling.
  TelemetryEnvironment.setExperimentActive(EXPERIMENT2, EXPERIMENT2_BRANCH);
  Assert.equal(
    TelemetrySession.getPayload().info.subsessionCounter,
    2,
    "The experiment annotation must not trigger a new subsession " +
      "if throttling happens."
  );
  let oldExperiments = TelemetryEnvironment.getActiveExperiments();

  // Fake the timer and remove an annotation, we expect a new subsession split.
  gMonotonicNow = fakeMonotonicNow(
    gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE
  );
  now = fakeNow(futureDate(now, 10 * MILLISECONDS_PER_MINUTE));
  TelemetryEnvironment.setExperimentInactive(EXPERIMENT1, EXPERIMENT1_BRANCH);

  ping = await PingServer.promiseNextPing();
  Assert.ok(!!ping, "A ping must be received.");

  Assert.equal(
    ping.type,
    PING_TYPE_MAIN,
    "The received ping must be a 'main' ping."
  );
  Assert.equal(
    ping.payload.info.reason,
    REASON_ENVIRONMENT_CHANGE,
    "The 'main' ping must be triggered by a change in the environment."
  );
  // We expect both experiments to be in this environment.
  Assert.deepEqual(
    ping.environment.experiments,
    oldExperiments,
    "The environment must contain both the experiments."
  );
  Assert.equal(
    TelemetrySession.getPayload().info.subsessionCounter,
    3,
    "The removing an experiment annotation must trigger a new subsession."
  );

  await TelemetryController.testShutdown();
});

add_task(async function test_savedPingsOnShutdown() {
  await TelemetryController.testReset();

  // Assure that we store the ping properly when saving sessions on shutdown.
  // We make the TelemetryController shutdown to trigger a session save.
  const dir = TelemetryStorage.pingDirectoryPath;
  await IOUtils.remove(dir, { ignoreAbsent: true, recursive: true });
  await IOUtils.makeDirectory(dir);
  await TelemetryController.testShutdown();

  PingServer.clearRequests();
  await TelemetryController.testReset();

  const ping = await PingServer.promiseNextPing();

  let expectedType = gIsAndroid ? PING_TYPE_SAVED_SESSION : PING_TYPE_MAIN;
  let expectedReason = gIsAndroid ? REASON_SAVED_SESSION : REASON_SHUTDOWN;

  checkPingFormat(ping, expectedType, true, true);
  Assert.equal(ping.payload.info.reason, expectedReason);
  Assert.equal(ping.clientId, gClientID);
});

add_task(async function test_sendShutdownPing() {
  if (
    gIsAndroid ||
    (AppConstants.platform == "linux" && !Services.appinfo.is64Bit)
  ) {
    // We don't support the pingsender on Android, yet, see bug 1335917.
    // We also don't suppor the pingsender testing on Treeherder for
    // Linux 32 bit (due to missing libraries). So skip it there too.
    // See bug 1310703 comment 78.
    return;
  }

  let checkPendingShutdownPing = async function () {
    let pendingPings = await TelemetryStorage.loadPendingPingList();
    Assert.equal(pendingPings.length, 1, "We expect 1 pending ping: shutdown.");
    // Load the pings off the disk.
    const shutdownPing = await TelemetryStorage.loadPendingPing(
      pendingPings[0].id
    );
    Assert.ok(shutdownPing, "The 'shutdown' ping must be saved to disk.");
    Assert.equal(
      "shutdown",
      shutdownPing.payload.info.reason,
      "The 'shutdown' ping must be saved to disk."
    );
  };

  Services.prefs.setBoolPref(
    TelemetryUtils.Preferences.ShutdownPingSender,
    true
  );
  Services.prefs.setBoolPref(TelemetryUtils.Preferences.FirstRun, false);
  // Make sure the reporting policy picks up the updated pref.
  TelemetryReportingPolicy.testUpdateFirstRun();
  PingServer.clearRequests();
  Telemetry.clearScalars();

  // Shutdown telemetry and wait for an incoming ping.
  let nextPing = PingServer.promiseNextPing();
  await TelemetryController.testShutdown();
  let ping = await nextPing;

  // Check that we received a shutdown ping.
  checkPingFormat(ping, ping.type, true, true);
  Assert.equal(ping.payload.info.reason, REASON_SHUTDOWN);
  Assert.equal(ping.clientId, gClientID);
  // Try again, this time disable ping upload. The PingSender
  // should not be sending any ping!
  PingServer.registerPingHandler(() =>
    Assert.ok(false, "Telemetry must not send pings if not allowed to.")
  );
  Services.prefs.setBoolPref(
    TelemetryUtils.Preferences.FhrUploadEnabled,
    false
  );
  await TelemetryController.testReset();
  await TelemetryController.testShutdown();

  // Make sure we have no pending pings between the runs.
  await TelemetryStorage.testClearPendingPings();

  // Enable ping upload and signal an OS shutdown. The pingsender
  // will not be spawned and no ping will be sent.
  Services.prefs.setBoolPref(TelemetryUtils.Preferences.FhrUploadEnabled, true);
  await TelemetryController.testReset();
  Services.obs.notifyObservers(null, "quit-application-forced");
  await TelemetryController.testShutdown();
  // After re-enabling FHR, wait for the new client ID
  gClientID = await ClientID.getClientID();

  // Check that the "shutdown" ping was correctly saved to disk.
  await checkPendingShutdownPing();

  // Make sure we have no pending pings between the runs.
  await TelemetryStorage.testClearPendingPings();
  Telemetry.clearScalars();

  await TelemetryController.testReset();
  Services.obs.notifyObservers(
    null,
    "quit-application-granted",
    "syncShutdown"
  );
  await TelemetryController.testShutdown();
  await checkPendingShutdownPing();

  // Make sure we have no pending pings between the runs.
  await TelemetryStorage.testClearPendingPings();

  // Disable the "submission policy". The shutdown ping must not be sent.
  Services.prefs.setBoolPref(
    TelemetryUtils.Preferences.BypassNotification,
    false
  );
  await TelemetryController.testReset();
  await TelemetryController.testShutdown();

  // Make sure we have no pending pings between the runs.
  await TelemetryStorage.testClearPendingPings();

  // We cannot reset the BypassNotification pref, as we need it to be
  // |true| in tests.
  Services.prefs.setBoolPref(
    TelemetryUtils.Preferences.BypassNotification,
    true
  );

  // With both upload enabled and the policy shown, make sure we don't
  // send the shutdown ping using the pingsender on the first
  // subsession.
  Services.prefs.setBoolPref(TelemetryUtils.Preferences.FirstRun, true);
  // Make sure the reporting policy picks up the updated pref.
  TelemetryReportingPolicy.testUpdateFirstRun();

  await TelemetryController.testReset();
  await TelemetryController.testShutdown();

  // Clear the state and prepare for the next test.
  await TelemetryStorage.testClearPendingPings();
  PingServer.clearRequests();
  PingServer.resetPingHandler();

  // Check that we're able to send the shutdown ping using the pingsender
  // from the first session if the related pref is on.
  Services.prefs.setBoolPref(
    TelemetryUtils.Preferences.ShutdownPingSenderFirstSession,
    true
  );
  Services.prefs.setBoolPref(TelemetryUtils.Preferences.FirstRun, true);
  TelemetryReportingPolicy.testUpdateFirstRun();

  // Restart/shutdown telemetry and wait for an incoming ping.
  await TelemetryController.testReset();
  await TelemetryController.testShutdown();
  ping = await PingServer.promiseNextPing();

  // Check that we received a shutdown ping.
  checkPingFormat(ping, ping.type, true, true);
  Assert.equal(ping.payload.info.reason, REASON_SHUTDOWN);
  Assert.equal(ping.clientId, gClientID);

  // Reset the pref and restart Telemetry.
  Services.prefs.setBoolPref(
    TelemetryUtils.Preferences.ShutdownPingSender,
    false
  );
  Services.prefs.setBoolPref(
    TelemetryUtils.Preferences.ShutdownPingSenderFirstSession,
    false
  );
  Services.prefs.clearUserPref(TelemetryUtils.Preferences.FirstRun);
  PingServer.resetPingHandler();
});

add_task(async function test_sendFirstShutdownPing() {
  if (
    gIsAndroid ||
    (AppConstants.platform == "linux" && !Services.appinfo.is64Bit)
  ) {
    // We don't support the pingsender on Android, yet, see bug 1335917.
    // We also don't suppor the pingsender testing on Treeherder for
    // Linux 32 bit (due to missing libraries). So skip it there too.
    // See bug 1310703 comment 78.
    return;
  }

  let storageContainsFirstShutdown = async function () {
    let pendingPings = await TelemetryStorage.loadPendingPingList();
    let pings = await Promise.all(
      pendingPings.map(async p => {
        return TelemetryStorage.loadPendingPing(p.id);
      })
    );
    return pings.find(p => p.type == "first-shutdown");
  };

  let checkShutdownNotSent = async function () {
    // The failure-mode of the ping-sender is used to check that a ping was
    // *not* sent. This can be combined with the state of the storage to infer
    // the appropriate behavior from the preference flags.

    // Assert failure if we recive a ping.
    PingServer.registerPingHandler(req => {
      const receivedPing = decodeRequestPayload(req);
      Assert.ok(
        false,
        `No ping should be received in this test (got ${receivedPing.id}).`
      );
    });

    // Assert that pings are sent on first run, forcing a forced application
    // quit. This should be equivalent to the first test in this suite.
    Services.prefs.setBoolPref(TelemetryUtils.Preferences.FirstRun, true);
    TelemetryReportingPolicy.testUpdateFirstRun();

    await TelemetryController.testReset();
    Services.obs.notifyObservers(null, "quit-application-forced");
    await TelemetryController.testShutdown();
    Assert.ok(
      await storageContainsFirstShutdown(),
      "The 'first-shutdown' ping must be saved to disk."
    );

    await TelemetryStorage.testClearPendingPings();

    // Assert that it's not sent during subsequent runs
    Services.prefs.setBoolPref(TelemetryUtils.Preferences.FirstRun, false);
    TelemetryReportingPolicy.testUpdateFirstRun();

    await TelemetryController.testReset();
    Services.obs.notifyObservers(null, "quit-application-forced");
    await TelemetryController.testShutdown();
    Assert.ok(
      !(await storageContainsFirstShutdown()),
      "The 'first-shutdown' ping should only be written during first run."
    );

    await TelemetryStorage.testClearPendingPings();

    // Assert that the the ping is only sent if the flag is enabled.
    Services.prefs.setBoolPref(TelemetryUtils.Preferences.FirstRun, true);
    Services.prefs.setBoolPref(
      TelemetryUtils.Preferences.FirstShutdownPingEnabled,
      false
    );
    TelemetryReportingPolicy.testUpdateFirstRun();

    await TelemetryController.testReset();
    await TelemetryController.testShutdown();
    Assert.ok(
      !(await storageContainsFirstShutdown()),
      "The 'first-shutdown' ping should only be written if enabled"
    );

    await TelemetryStorage.testClearPendingPings();

    // Assert that the the ping is not collected when the ping-sender is disabled.
    // The information would be made irrelevant by the main-ping in the second session.
    Services.prefs.setBoolPref(
      TelemetryUtils.Preferences.FirstShutdownPingEnabled,
      true
    );
    Services.prefs.setBoolPref(
      TelemetryUtils.Preferences.ShutdownPingSender,
      false
    );
    TelemetryReportingPolicy.testUpdateFirstRun();

    await TelemetryController.testReset();
    await TelemetryController.testShutdown();
    Assert.ok(
      !(await storageContainsFirstShutdown()),
      "The 'first-shutdown' ping should only be written if ping-sender is enabled"
    );

    // Clear the state and prepare for the next test.
    await TelemetryStorage.testClearPendingPings();
    PingServer.clearRequests();
    PingServer.resetPingHandler();
  };

  // Remove leftover pending pings from other tests
  await TelemetryStorage.testClearPendingPings();
  PingServer.clearRequests();
  Telemetry.clearScalars();

  // Set testing invariants for FirstShutdownPingEnabled
  Services.prefs.setBoolPref(
    TelemetryUtils.Preferences.ShutdownPingSender,
    true
  );
  Services.prefs.setBoolPref(
    TelemetryUtils.Preferences.ShutdownPingSenderFirstSession,
    false
  );

  // Set primary conditions of the 'first-shutdown' ping
  Services.prefs.setBoolPref(
    TelemetryUtils.Preferences.FirstShutdownPingEnabled,
    true
  );
  Services.prefs.setBoolPref(TelemetryUtils.Preferences.FirstRun, true);
  TelemetryReportingPolicy.testUpdateFirstRun();

  // Assert general 'first-shutdown' use-case.
  await TelemetryController.testReset();
  await TelemetryController.testShutdown();
  let ping = await PingServer.promiseNextPing();
  checkPingFormat(ping, "first-shutdown", true, true);
  Assert.equal(ping.payload.info.reason, REASON_SHUTDOWN);
  Assert.equal(ping.clientId, gClientID);

  await TelemetryStorage.testClearPendingPings();

  // Assert that the shutdown is not sent under various conditions
  await checkShutdownNotSent();

  // Reset the pref and restart Telemetry.
  Services.prefs.setBoolPref(
    TelemetryUtils.Preferences.ShutdownPingSender,
    false
  );
  Services.prefs.setBoolPref(
    TelemetryUtils.Preferences.ShutdownPingSenderFirstSession,
    false
  );
  Services.prefs.setBoolPref(
    TelemetryUtils.Preferences.FirstShutdownPingEnabled,
    false
  );
  Services.prefs.clearUserPref(TelemetryUtils.Preferences.FirstRun);
  PingServer.resetPingHandler();
});

add_task(async function test_savedSessionData() {
  // Create the directory which will contain the data file, if it doesn't already
  // exist.
  await IOUtils.makeDirectory(DATAREPORTING_PATH);
  getHistogram("TELEMETRY_SESSIONDATA_FAILED_LOAD").clear();
  getHistogram("TELEMETRY_SESSIONDATA_FAILED_PARSE").clear();
  getHistogram("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").clear();

  // Write test data to the session data file.
  const dataFilePath = PathUtils.join(DATAREPORTING_PATH, "session-state.json");
  const sessionState = {
    sessionId: null,
    subsessionId: null,
    profileSubsessionCounter: 3785,
  };
  await IOUtils.writeJSON(dataFilePath, sessionState);

  const PREF_TEST = "toolkit.telemetry.test.pref1";
  Services.prefs.clearUserPref(PREF_TEST);
  const PREFS_TO_WATCH = new Map([
    [PREF_TEST, { what: TelemetryEnvironment.RECORD_PREF_VALUE }],
  ]);

  // We expect one new subsession when starting TelemetrySession and one after triggering
  // an environment change.
  const expectedSubsessions = sessionState.profileSubsessionCounter + 2;
  const expectedSessionUUID = "ff602e52-47a1-b7e8-4c1a-ffffffffc87a";
  const expectedSubsessionUUID = "009fd1ad-b85e-4817-b3e5-000000003785";
  fakeGenerateUUID(
    () => expectedSessionUUID,
    () => expectedSubsessionUUID
  );

  if (gIsAndroid) {
    // We don't support subsessions yet on Android, so skip the next checks.
    return;
  }

  // Start TelemetrySession so that it loads the session data file.
  await TelemetryController.testReset();
  Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_LOAD").sum);
  Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_PARSE").sum);
  Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").sum);

  // Watch a test preference, trigger and environment change and wait for it to propagate.
  // _watchPreferences triggers a subsession notification
  gMonotonicNow = fakeMonotonicNow(
    gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE
  );
  fakeNow(new Date(2050, 1, 1, 12, 0, 0));
  await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
  let changePromise = new Promise(resolve =>
    TelemetryEnvironment.registerChangeListener("test_fake_change", resolve)
  );
  Services.prefs.setIntPref(PREF_TEST, 1);
  await changePromise;
  TelemetryEnvironment.unregisterChangeListener("test_fake_change");

  let payload = TelemetrySession.getPayload();
  Assert.equal(payload.info.profileSubsessionCounter, expectedSubsessions);
  await TelemetryController.testShutdown();

  // Restore the UUID generator so we don't mess with other tests.
  fakeGenerateUUID(TelemetryUtils.generateUUID, TelemetryUtils.generateUUID);

  // Load back the serialised session data.
  let data = await IOUtils.readJSON(dataFilePath);
  Assert.equal(data.profileSubsessionCounter, expectedSubsessions);
  Assert.equal(data.sessionId, expectedSessionUUID);
  Assert.equal(data.subsessionId, expectedSubsessionUUID);
});

add_task(async function test_sessionData_ShortSession() {
  if (gIsAndroid) {
    // We don't support subsessions yet on Android, so skip the next checks.
    return;
  }

  const SESSION_STATE_PATH = PathUtils.join(
    DATAREPORTING_PATH,
    "session-state.json"
  );

  // Remove the session state file.
  await IOUtils.remove(SESSION_STATE_PATH, { ignoreAbsent: true });
  getHistogram("TELEMETRY_SESSIONDATA_FAILED_LOAD").clear();
  getHistogram("TELEMETRY_SESSIONDATA_FAILED_PARSE").clear();
  getHistogram("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").clear();

  const expectedSessionUUID = "ff602e52-47a1-b7e8-4c1a-ffffffffc87a";
  const expectedSubsessionUUID = "009fd1ad-b85e-4817-b3e5-000000003785";
  fakeGenerateUUID(
    () => expectedSessionUUID,
    () => expectedSubsessionUUID
  );

  // We intentionally don't wait for the setup to complete and shut down to simulate
  // short sessions. We expect the profile subsession counter to be 1.
  TelemetryController.testReset();
  await TelemetryController.testShutdown();

  Assert.equal(1, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_LOAD").sum);
  Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_PARSE").sum);
  Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").sum);

  // Restore the UUID generation functions.
  fakeGenerateUUID(TelemetryUtils.generateUUID, TelemetryUtils.generateUUID);

  // Start TelemetryController so that it loads the session data file. We expect the profile
  // subsession counter to be incremented by 1 again.
  await TelemetryController.testReset();

  // We expect 2 profile subsession counter updates.
  let payload = TelemetrySession.getPayload();
  Assert.equal(payload.info.profileSubsessionCounter, 2);
  Assert.equal(payload.info.previousSessionId, expectedSessionUUID);
  Assert.equal(payload.info.previousSubsessionId, expectedSubsessionUUID);
  Assert.equal(1, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_LOAD").sum);
  Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_PARSE").sum);
  Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").sum);

  await TelemetryController.testShutdown();
});

add_task(async function test_invalidSessionData() {
  // Create the directory which will contain the data file, if it doesn't already
  // exist.
  await IOUtils.makeDirectory(DATAREPORTING_PATH);
  getHistogram("TELEMETRY_SESSIONDATA_FAILED_LOAD").clear();
  getHistogram("TELEMETRY_SESSIONDATA_FAILED_PARSE").clear();
  getHistogram("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").clear();

  // Write test data to the session data file. This should fail to parse.
  const dataFilePath = PathUtils.join(DATAREPORTING_PATH, "session-state.json");
  const unparseableData = "{asdf:@äü";
  await IOUtils.writeUTF8(dataFilePath, unparseableData, {
    tmpPath: `${dataFilePath}.tmp`,
  });

  // Start TelemetryController so that it loads the session data file.
  await TelemetryController.testReset();

  // The session data file should not load. Only expect the current subsession.
  Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_LOAD").sum);
  Assert.equal(1, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_PARSE").sum);
  Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").sum);

  // Write test data to the session data file. This should fail validation.
  const sessionState = {
    profileSubsessionCounter: "not-a-number?",
    someOtherField: 12,
  };
  await IOUtils.writeJSON(dataFilePath, sessionState);

  // The session data file should not load. Only expect the current subsession.
  const expectedSubsessions = 1;
  const expectedSessionUUID = "ff602e52-47a1-b7e8-4c1a-ffffffffc87a";
  const expectedSubsessionUUID = "009fd1ad-b85e-4817-b3e5-000000003785";
  fakeGenerateUUID(
    () => expectedSessionUUID,
    () => expectedSubsessionUUID
  );

  // Start TelemetryController so that it loads the session data file.
  await TelemetryController.testShutdown();
  await TelemetryController.testReset();

  let payload = TelemetrySession.getPayload();
  Assert.equal(payload.info.profileSubsessionCounter, expectedSubsessions);
  Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_LOAD").sum);
  Assert.equal(1, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_PARSE").sum);
  Assert.equal(1, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").sum);

  await TelemetryController.testShutdown();

  // Restore the UUID generator so we don't mess with other tests.
  fakeGenerateUUID(TelemetryUtils.generateUUID, TelemetryUtils.generateUUID);

  // Load back the serialised session data.
  let data = await IOUtils.readJSON(dataFilePath);
  Assert.equal(data.profileSubsessionCounter, expectedSubsessions);
  Assert.equal(data.sessionId, expectedSessionUUID);
  Assert.equal(data.subsessionId, expectedSubsessionUUID);
});

add_task(async function test_abortedSession() {
  if (gIsAndroid) {
    // We don't have the aborted session ping here.
    return;
  }

  const ABORTED_FILE = PathUtils.join(
    DATAREPORTING_PATH,
    ABORTED_PING_FILE_NAME
  );

  // Make sure the aborted sessions directory does not exist to test its creation.
  await IOUtils.remove(DATAREPORTING_PATH, {
    ignoreAbsent: true,
    recursive: true,
  });

  let schedulerTickCallback = null;
  let now = new Date(2040, 1, 1, 0, 0, 0);
  fakeNow(now);
  // Fake scheduler functions to control aborted-session flow in tests.
  fakeSchedulerTimer(
    callback => (schedulerTickCallback = callback),
    () => {}
  );
  await TelemetryController.testReset();

  Assert.ok(
    await IOUtils.exists(DATAREPORTING_PATH),
    "Telemetry must create the aborted session directory when starting."
  );

  // Fake now again so that the scheduled aborted-session save takes place.
  now = futureDate(now, ABORTED_SESSION_UPDATE_INTERVAL_MS);
  fakeNow(now);
  // The first aborted session checkpoint must take place right after the initialisation.
  Assert.ok(!!schedulerTickCallback);
  // Execute one scheduler tick.
  await schedulerTickCallback();
  // Check that the aborted session is due at the correct time.
  Assert.ok(
    await IOUtils.exists(ABORTED_FILE),
    "There must be an aborted session ping."
  );

  // This ping is not yet in the pending pings folder, so we can't access it using
  // TelemetryStorage.popPendingPings().
  let abortedSessionPing = await IOUtils.readJSON(ABORTED_FILE);

  // Validate the ping.
  checkPingFormat(abortedSessionPing, PING_TYPE_MAIN, true, true);
  Assert.equal(abortedSessionPing.payload.info.reason, REASON_ABORTED_SESSION);

  // Trigger a another aborted-session ping and check that it overwrites the previous one.
  now = futureDate(now, ABORTED_SESSION_UPDATE_INTERVAL_MS);
  fakeNow(now);
  await schedulerTickCallback();

  let updatedAbortedSessionPing = await IOUtils.readJSON(ABORTED_FILE);
  checkPingFormat(updatedAbortedSessionPing, PING_TYPE_MAIN, true, true);
  Assert.equal(
    updatedAbortedSessionPing.payload.info.reason,
    REASON_ABORTED_SESSION
  );
  Assert.notEqual(abortedSessionPing.id, updatedAbortedSessionPing.id);
  Assert.notEqual(
    abortedSessionPing.creationDate,
    updatedAbortedSessionPing.creationDate
  );

  await TelemetryController.testShutdown();
  Assert.ok(
    !(await IOUtils.exists(ABORTED_FILE)),
    "No aborted session ping must be available after a shutdown."
  );
});

add_task(async function test_abortedSession_Shutdown() {
  if (gIsAndroid) {
    // We don't have the aborted session ping here.
    return;
  }

  const ABORTED_FILE = PathUtils.join(
    DATAREPORTING_PATH,
    ABORTED_PING_FILE_NAME
  );

  let schedulerTickCallback = null;
  let now = fakeNow(2040, 1, 1, 0, 0, 0);
  // Fake scheduler functions to control aborted-session flow in tests.
  fakeSchedulerTimer(
    callback => (schedulerTickCallback = callback),
    () => {}
  );
  await TelemetryController.testReset();

  Assert.ok(
    await IOUtils.exists(DATAREPORTING_PATH),
    "Telemetry must create the aborted session directory when starting."
  );

  // Fake now again so that the scheduled aborted-session save takes place.
  fakeNow(futureDate(now, ABORTED_SESSION_UPDATE_INTERVAL_MS));
  // The first aborted session checkpoint must take place right after the initialisation.
  Assert.ok(!!schedulerTickCallback);
  // Execute one scheduler tick.
  await schedulerTickCallback();
  // Check that the aborted session is due at the correct time.
  Assert.ok(
    await IOUtils.exists(ABORTED_FILE),
    "There must be an aborted session ping."
  );

  // Remove the aborted session file and then shut down to make sure exceptions (e.g file
  // not found) do not compromise the shutdown.
  await IOUtils.remove(ABORTED_FILE);

  await TelemetryController.testShutdown();
});

add_task(async function test_abortedDailyCoalescing() {
  if (gIsAndroid) {
    // We don't have the aborted session or the daily ping here.
    return;
  }

  const ABORTED_FILE = PathUtils.join(
    DATAREPORTING_PATH,
    ABORTED_PING_FILE_NAME
  );

  // Make sure the aborted sessions directory does not exist to test its creation.
  await IOUtils.remove(DATAREPORTING_PATH, {
    ignoreAbsent: true,
    recursive: true,
  });

  let schedulerTickCallback = null;
  PingServer.clearRequests();

  let nowDate = new Date(2009, 10, 18, 0, 0, 0);
  fakeNow(nowDate);

  // Fake scheduler functions to control aborted-session flow in tests.
  fakeSchedulerTimer(
    callback => (schedulerTickCallback = callback),
    () => {}
  );
  await TelemetryStorage.testClearPendingPings();
  PingServer.clearRequests();
  await TelemetryController.testReset();

  Assert.ok(
    await IOUtils.exists(DATAREPORTING_PATH),
    "Telemetry must create the aborted session directory when starting."
  );

  // Delay the callback around midnight so that the aborted-session ping gets merged with the
  // daily ping.
  let dailyDueDate = futureDate(nowDate, MS_IN_ONE_DAY);
  fakeNow(dailyDueDate);
  // Trigger both the daily ping and the saved-session.
  Assert.ok(!!schedulerTickCallback);
  // Execute one scheduler tick.
  await schedulerTickCallback();

  // Wait for the daily ping.
  let dailyPing = await PingServer.promiseNextPing();
  Assert.equal(dailyPing.payload.info.reason, REASON_DAILY);

  // Check that an aborted session ping was also written to disk.
  Assert.ok(
    await IOUtils.exists(ABORTED_FILE),
    "There must be an aborted session ping."
  );

  // Read aborted session ping and check that the session/subsession ids equal the
  // ones in the daily ping.
  let abortedSessionPing = await IOUtils.readJSON(ABORTED_FILE);
  Assert.equal(
    abortedSessionPing.payload.info.sessionId,
    dailyPing.payload.info.sessionId
  );
  Assert.equal(
    abortedSessionPing.payload.info.subsessionId,
    dailyPing.payload.info.subsessionId
  );

  await TelemetryController.testShutdown();
});

add_task(async function test_schedulerComputerSleep() {
  if (gIsAndroid) {
    // We don't have the aborted session or the daily ping here.
    return;
  }

  const ABORTED_FILE = PathUtils.join(
    DATAREPORTING_PATH,
    ABORTED_PING_FILE_NAME
  );

  await TelemetryController.testReset();
  await TelemetryController.testShutdown();
  await TelemetryStorage.testClearPendingPings();
  PingServer.clearRequests();

  // Remove any aborted-session ping from the previous tests.
  await IOUtils.remove(DATAREPORTING_PATH, {
    ignoreAbsent: true,
    recursive: true,
  });

  // Set a fake current date and start Telemetry.
  let nowDate = fakeNow(2009, 10, 18, 0, 0, 0);
  let schedulerTickCallback = null;
  fakeSchedulerTimer(
    callback => (schedulerTickCallback = callback),
    () => {}
  );
  await TelemetryController.testReset();

  // Set the current time 3 days in the future at midnight, before running the callback.
  nowDate = fakeNow(futureDate(nowDate, 3 * MS_IN_ONE_DAY));
  Assert.ok(!!schedulerTickCallback);
  // Execute one scheduler tick.
  await schedulerTickCallback();

  let dailyPing = await PingServer.promiseNextPing();
  Assert.equal(
    dailyPing.payload.info.reason,
    REASON_DAILY,
    "The wake notification should have triggered a daily ping."
  );
  Assert.equal(
    dailyPing.creationDate,
    nowDate.toISOString(),
    "The daily ping date should be correct."
  );

  Assert.ok(
    await IOUtils.exists(ABORTED_FILE),
    "There must be an aborted session ping."
  );

  // Now also test if we are sending a daily ping if we wake up on the next
  // day even when the timer doesn't trigger.
  // This can happen due to timeouts not running out during sleep times,
  // see bug 1262386, bug 1204823 et al.
  // Note that we don't get wake notifications on Linux due to bug 758848.
  nowDate = fakeNow(futureDate(nowDate, 1 * MS_IN_ONE_DAY));

  // We emulate the mentioned timeout behavior by sending the wake notification
  // instead of triggering the timeout callback.
  // This should trigger a daily ping, because we passed midnight.
  Services.obs.notifyObservers(null, "wake_notification");

  dailyPing = await PingServer.promiseNextPing();
  Assert.equal(
    dailyPing.payload.info.reason,
    REASON_DAILY,
    "The wake notification should have triggered a daily ping."
  );
  Assert.equal(
    dailyPing.creationDate,
    nowDate.toISOString(),
    "The daily ping date should be correct."
  );

  await TelemetryController.testShutdown();
});

add_task(async function test_schedulerEnvironmentReschedules() {
  if (gIsAndroid) {
    // We don't have the aborted session or the daily ping here.
    return;
  }

  // Reset the test preference.
  const PREF_TEST = "toolkit.telemetry.test.pref1";
  Services.prefs.clearUserPref(PREF_TEST);
  const PREFS_TO_WATCH = new Map([
    [PREF_TEST, { what: TelemetryEnvironment.RECORD_PREF_VALUE }],
  ]);

  await TelemetryController.testReset();
  await TelemetryController.testShutdown();
  await TelemetryStorage.testClearPendingPings();
  // bug 1829855 - Sometimes the idle dispatch from a previous test interferes.
  TelemetryScheduler.testReset();
  PingServer.clearRequests();

  // Set a fake current date and start Telemetry.
  let nowDate = fakeNow(2060, 10, 18, 0, 0, 0);
  gMonotonicNow = fakeMonotonicNow(
    gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE
  );
  let schedulerTickCallback = null;
  fakeSchedulerTimer(
    callback => (schedulerTickCallback = callback),
    () => {}
  );
  await TelemetryController.testReset();
  await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);

  // Set the current time at midnight.
  fakeNow(futureDate(nowDate, MS_IN_ONE_DAY));
  gMonotonicNow = fakeMonotonicNow(
    gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE
  );

  // Trigger the environment change.
  Services.prefs.setIntPref(PREF_TEST, 1);

  // Wait for the environment-changed ping.
  let ping = await PingServer.promiseNextPing();
  Assert.equal(ping.type, "main", `Expected 'main' ping on ${ping.id}`);
  Assert.equal(
    ping.payload.info.reason,
    "environment-change",
    `Expected 'environment-change' reason on ${ping.id}`
  );

  // We don't expect to receive any daily ping in this test, so assert if we do.
  PingServer.registerPingHandler(req => {
    const receivedPing = decodeRequestPayload(req);
    Assert.ok(
      false,
      `No ping should be received in this test (got ${receivedPing.id} type: ${receivedPing.type} reason: ${receivedPing.payload.info.reason}).`
    );
  });

  // Execute one scheduler tick. It should not trigger a daily ping.
  Assert.ok(!!schedulerTickCallback);
  await schedulerTickCallback();
  await TelemetryController.testShutdown();
});

add_task(async function test_schedulerNothingDue() {
  if (gIsAndroid) {
    // We don't have the aborted session or the daily ping here.
    return;
  }

  const ABORTED_FILE = PathUtils.join(
    DATAREPORTING_PATH,
    ABORTED_PING_FILE_NAME
  );

  // Remove any aborted-session ping from the previous tests.
  await IOUtils.remove(DATAREPORTING_PATH, {
    ignoreAbsent: true,
    recursive: true,
  });
  await TelemetryStorage.testClearPendingPings();
  await TelemetryController.testReset();

  // We don't expect to receive any ping in this test, so assert if we do.
  PingServer.registerPingHandler(req => {
    const receivedPing = decodeRequestPayload(req);
    Assert.ok(
      false,
      `No ping should be received in this test (got ${receivedPing.id}).`
    );
  });

  // Set a current date/time away from midnight, so that the daily ping doesn't get
  // sent.
  let nowDate = new Date(2009, 10, 18, 11, 0, 0);
  fakeNow(nowDate);
  let schedulerTickCallback = null;
  fakeSchedulerTimer(
    callback => (schedulerTickCallback = callback),
    () => {}
  );
  await TelemetryController.testReset();

  // Delay the callback execution to a time when no ping should be due.
  let nothingDueDate = futureDate(
    nowDate,
    ABORTED_SESSION_UPDATE_INTERVAL_MS / 2
  );
  fakeNow(nothingDueDate);
  Assert.ok(!!schedulerTickCallback);
  // Execute one scheduler tick.
  await schedulerTickCallback();

  // Check that no aborted session ping was written to disk.
  Assert.ok(!(await IOUtils.exists(ABORTED_FILE)));

  await TelemetryController.testShutdown();
  PingServer.resetPingHandler();
});

add_task(async function test_pingExtendedStats() {
  const EXTENDED_PAYLOAD_FIELDS = [
    "log",
    "slowSQL",
    "fileIOReports",
    "lateWrites",
    "addonDetails",
  ];

  // Reset telemetry and disable sending extended statistics.
  await TelemetryStorage.testClearPendingPings();
  PingServer.clearRequests();
  await TelemetryController.testReset();
  Telemetry.canRecordExtended = false;

  await sendPing();

  let ping = await PingServer.promiseNextPing();
  checkPingFormat(ping, PING_TYPE_MAIN, true, true);

  // Check that the payload does not contain extended statistics fields.
  for (let f in EXTENDED_PAYLOAD_FIELDS) {
    Assert.ok(
      !(EXTENDED_PAYLOAD_FIELDS[f] in ping.payload),
      EXTENDED_PAYLOAD_FIELDS[f] +
        " must not be in the payload if the extended set is off."
    );
  }

  // We check this one separately so that we can reuse EXTENDED_PAYLOAD_FIELDS below, since
  // slowSQLStartup might not be there.
  Assert.ok(
    !("slowSQLStartup" in ping.payload),
    "slowSQLStartup must not be sent if the extended set is off"
  );

  Assert.ok(
    !("addonManager" in ping.payload.simpleMeasurements),
    "addonManager must not be sent if the extended set is off."
  );
  Assert.ok(
    !("UITelemetry" in ping.payload.simpleMeasurements),
    "UITelemetry must not be sent."
  );

  // Restore the preference.
  Telemetry.canRecordExtended = true;

  // Send a new ping that should contain the extended data.
  await sendPing();
  ping = await PingServer.promiseNextPing();
  checkPingFormat(ping, PING_TYPE_MAIN, true, true);

  // Check that the payload now contains extended statistics fields.
  for (let f in EXTENDED_PAYLOAD_FIELDS) {
    Assert.ok(
      EXTENDED_PAYLOAD_FIELDS[f] in ping.payload,
      EXTENDED_PAYLOAD_FIELDS[f] +
        " must be in the payload if the extended set is on."
    );
  }

  Assert.ok(
    "addonManager" in ping.payload.simpleMeasurements,
    "addonManager must be sent if the extended set is on."
  );
  Assert.ok(
    !("UITelemetry" in ping.payload.simpleMeasurements),
    "UITelemetry must not be sent."
  );

  await TelemetryController.testShutdown();
});

add_task(async function test_schedulerUserIdle() {
  if (gIsAndroid) {
    // We don't have the aborted session or the daily ping here.
    return;
  }

  const SCHEDULER_TICK_INTERVAL_MS = 5 * 60 * 1000;
  const SCHEDULER_TICK_IDLE_INTERVAL_MS = 60 * 60 * 1000;

  let now = new Date(2010, 1, 1, 11, 0, 0);
  fakeNow(now);

  let schedulerTimeout = 0;
  fakeSchedulerTimer(
    (callback, timeout) => {
      schedulerTimeout = timeout;
    },
    () => {}
  );
  await TelemetryController.testReset();
  await TelemetryStorage.testClearPendingPings();
  PingServer.clearRequests();

  // When not idle, the scheduler should have a 5 minutes tick interval.
  Assert.equal(schedulerTimeout, SCHEDULER_TICK_INTERVAL_MS);

  // Send an "idle" notification to the scheduler.
  fakeIdleNotification("idle");

  // When idle, the scheduler should have a 1hr tick interval.
  Assert.equal(schedulerTimeout, SCHEDULER_TICK_IDLE_INTERVAL_MS);

  // Send an "active" notification to the scheduler.
  await fakeIdleNotification("active");

  // When user is back active, the scheduler tick should be 5 minutes again.
  Assert.equal(schedulerTimeout, SCHEDULER_TICK_INTERVAL_MS);

  // We should not miss midnight when going to idle.
  now.setHours(23);
  now.setMinutes(50);
  fakeNow(now);
  fakeIdleNotification("idle");
  Assert.equal(schedulerTimeout, 10 * 60 * 1000);

  await TelemetryController.testShutdown();
});

add_task(async function test_DailyDueAndIdle() {
  if (gIsAndroid) {
    // We don't have the aborted session or the daily ping here.
    return;
  }

  await TelemetryStorage.testClearPendingPings();
  PingServer.clearRequests();

  let receivedPingRequest = null;
  // Register a ping handler that will assert when receiving multiple daily pings.
  PingServer.registerPingHandler(req => {
    Assert.ok(!receivedPingRequest, "Telemetry must only send one daily ping.");
    receivedPingRequest = req;
  });

  // Faking scheduler timer has to happen before resetting TelemetryController
  // to be effective.
  let schedulerTickCallback = null;
  let now = new Date(2030, 1, 1, 0, 0, 0);
  fakeNow(now);
  // Fake scheduler functions to control daily collection flow in tests.
  fakeSchedulerTimer(
    callback => (schedulerTickCallback = callback),
    () => {}
  );
  await TelemetryController.testReset();

  // Trigger the daily ping.
  let firstDailyDue = new Date(2030, 1, 2, 0, 0, 0);
  fakeNow(firstDailyDue);

  // Run a scheduler tick: it should trigger the daily ping.
  Assert.ok(!!schedulerTickCallback);
  let tickPromise = schedulerTickCallback();

  // Send an idle and then an active user notification.
  fakeIdleNotification("idle");
  fakeIdleNotification("active");

  // Wait on the tick promise.
  await tickPromise;

  await TelemetrySend.testWaitOnOutgoingPings();

  // Decode the ping contained in the request and check that's a daily ping.
  Assert.ok(receivedPingRequest, "Telemetry must send one daily ping.");
  const receivedPing = decodeRequestPayload(receivedPingRequest);
  checkPingFormat(receivedPing, PING_TYPE_MAIN, true, true);
  Assert.equal(receivedPing.payload.info.reason, REASON_DAILY);

  await TelemetryController.testShutdown();
});

add_task(async function test_userIdleAndSchedlerTick() {
  if (gIsAndroid) {
    // We don't have the aborted session or the daily ping here.
    return;
  }

  let receivedPingRequest = null;
  // Register a ping handler that will assert when receiving multiple daily pings.
  PingServer.registerPingHandler(req => {
    Assert.ok(!receivedPingRequest, "Telemetry must only send one daily ping.");
    receivedPingRequest = req;
  });

  let schedulerTickCallback = null;
  let now = new Date(2030, 1, 1, 0, 0, 0);
  fakeNow(now);
  // Fake scheduler functions to control daily collection flow in tests.
  fakeSchedulerTimer(
    callback => (schedulerTickCallback = callback),
    () => {}
  );
  await TelemetryStorage.testClearPendingPings();
  await TelemetryController.testReset();
  PingServer.clearRequests();

  // Move the current date/time to midnight.
  let firstDailyDue = new Date(2030, 1, 2, 0, 0, 0);
  fakeNow(firstDailyDue);

  // The active notification should trigger a scheduler tick. The latter will send the
  // due daily ping.
  fakeIdleNotification("active");

  // Immediately running another tick should not send a daily ping again.
  Assert.ok(!!schedulerTickCallback);
  await schedulerTickCallback();

  // A new "idle" notification should not send a new daily ping.
  fakeIdleNotification("idle");

  await TelemetrySend.testWaitOnOutgoingPings();

  // Decode the ping contained in the request and check that's a daily ping.
  Assert.ok(receivedPingRequest, "Telemetry must send one daily ping.");
  const receivedPing = decodeRequestPayload(receivedPingRequest);
  checkPingFormat(receivedPing, PING_TYPE_MAIN, true, true);
  Assert.equal(receivedPing.payload.info.reason, REASON_DAILY);

  PingServer.resetPingHandler();
  await TelemetryController.testShutdown();
});

add_task(async function test_changeThrottling() {
  if (gIsAndroid) {
    // We don't support subsessions yet on Android.
    return;
  }

  let getSubsessionCount = () => {
    return TelemetrySession.getPayload().info.subsessionCounter;
  };

  let now = fakeNow(2050, 1, 2, 0, 0, 0);
  gMonotonicNow = fakeMonotonicNow(
    gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE
  );
  await TelemetryController.testReset();
  Assert.equal(getSubsessionCount(), 1);

  // The first pref change should not trigger a notification.
  TelemetrySession.testOnEnvironmentChange("test", {});
  Assert.equal(getSubsessionCount(), 1);

  // We should get a change notification after the 5min throttling interval.
  fakeNow(futureDate(now, 5 * MILLISECONDS_PER_MINUTE + 1));
  gMonotonicNow = fakeMonotonicNow(
    gMonotonicNow + 5 * MILLISECONDS_PER_MINUTE + 1
  );
  TelemetrySession.testOnEnvironmentChange("test", {});
  Assert.equal(getSubsessionCount(), 2);

  // After that, changes should be throttled again.
  now = fakeNow(futureDate(now, 1 * MILLISECONDS_PER_MINUTE));
  gMonotonicNow = fakeMonotonicNow(gMonotonicNow + 1 * MILLISECONDS_PER_MINUTE);
  TelemetrySession.testOnEnvironmentChange("test", {});
  Assert.equal(getSubsessionCount(), 2);

  // ... for 5min.
  now = fakeNow(futureDate(now, 4 * MILLISECONDS_PER_MINUTE + 1));
  gMonotonicNow = fakeMonotonicNow(
    gMonotonicNow + 4 * MILLISECONDS_PER_MINUTE + 1
  );
  TelemetrySession.testOnEnvironmentChange("test", {});
  Assert.equal(getSubsessionCount(), 3);
});

add_task(async function stopServer() {
  // It is important to shut down the TelemetryController first as, due to test
  // environment changes, failure to upload pings here during shutdown results
  // in an infinite loop of send failures and retries.
  await TelemetryController.testShutdown();
  await PingServer.stop();
});
