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

// This tests the public Telemetry API for submitting pings.

"use strict";

const { TelemetryController } = ChromeUtils.importESModule(
  "resource://gre/modules/TelemetryController.sys.mjs"
);
const { MockRegistrar } = ChromeUtils.importESModule(
  "resource://testing-common/MockRegistrar.sys.mjs"
);
const { TelemetrySend } = ChromeUtils.importESModule(
  "resource://gre/modules/TelemetrySend.sys.mjs"
);
const { TelemetryStorage } = ChromeUtils.importESModule(
  "resource://gre/modules/TelemetryStorage.sys.mjs"
);
const { TelemetryUtils } = ChromeUtils.importESModule(
  "resource://gre/modules/TelemetryUtils.sys.mjs"
);

ChromeUtils.defineESModuleGetters(this, {
  TelemetryHealthPing: "resource://gre/modules/HealthPing.sys.mjs",
});

const MS_IN_A_MINUTE = 60 * 1000;

function countPingTypes(pings) {
  let countByType = new Map();
  for (let p of pings) {
    countByType.set(p.type, 1 + (countByType.get(p.type) || 0));
  }
  return countByType;
}

function setPingLastModified(id, timestamp) {
  const path = PathUtils.join(TelemetryStorage.pingDirectoryPath, id);
  return IOUtils.setModificationTime(path, timestamp);
}

// Mock out the send timer activity.
function waitForTimer() {
  return new Promise(resolve => {
    fakePingSendTimer(
      (callback, timeout) => {
        resolve([callback, timeout]);
      },
      () => {}
    );
  });
}

function sendPing(aSendClientId, aSendEnvironment) {
  const TEST_PING_TYPE = "test-ping-type";

  if (PingServer.started) {
    TelemetrySend.setServer("http://localhost:" + PingServer.port);
  } else {
    TelemetrySend.setServer("http://doesnotexist");
  }

  let options = {
    addClientId: aSendClientId,
    addEnvironment: aSendEnvironment,
  };
  return TelemetryController.submitExternalPing(TEST_PING_TYPE, {}, options);
}

// Allow easy faking of readable ping ids.
// This helps with debugging issues with e.g. ordering in the send logic.
function fakePingId(type, number) {
  const HEAD = "93bd0011-2c8f-4e1c-bee0-";
  const TAIL = "000000000000";
  const N = String(number);
  const id = HEAD + type + TAIL.slice(type.length, -N.length) + N;
  fakeGeneratePingId(() => id);
  return id;
}

var checkPingsSaved = async function (pingIds) {
  let allFound = true;
  for (let id of pingIds) {
    const path = PathUtils.join(TelemetryStorage.pingDirectoryPath, id);
    let exists = false;
    try {
      exists = await IOUtils.exists(path);
    } catch (ex) {}

    if (!exists) {
      dump("checkPingsSaved - failed to find ping: " + path + "\n");
      allFound = false;
    }
  }

  return allFound;
};

function histogramValueCount(h) {
  return Object.values(h.values).reduce((a, b) => a + b, 0);
}

add_task(async function test_setup() {
  // Trigger a proper telemetry init.
  do_get_profile(true);

  // Addon manager needs a profile directory.
  await loadAddonManager(
    "xpcshell@tests.mozilla.org",
    "XPCShell",
    "1",
    "1.9.2"
  );
  finishAddonManagerStartup();
  fakeIntlReady();

  // Make sure we don't generate unexpected pings due to pref changes.
  await setEmptyPrefWatchlist();
  Services.prefs.setBoolPref(
    TelemetryUtils.Preferences.HealthPingEnabled,
    true
  );
  TelemetryStopwatch.setTestModeEnabled(true);
});

// Test the ping sending logic.
add_task(async function test_sendPendingPings() {
  const TYPE_PREFIX = "test-sendPendingPings-";
  const TEST_TYPE_A = TYPE_PREFIX + "A";
  const TEST_TYPE_B = TYPE_PREFIX + "B";

  const TYPE_A_COUNT = 20;
  const TYPE_B_COUNT = 5;

  let histSuccess = Telemetry.getHistogramById("TELEMETRY_SUCCESS");
  let histSendTimeSuccess = Telemetry.getHistogramById(
    "TELEMETRY_SEND_SUCCESS"
  );
  let histSendTimeFail = Telemetry.getHistogramById("TELEMETRY_SEND_FAILURE");
  histSuccess.clear();
  histSendTimeSuccess.clear();
  histSendTimeFail.clear();

  // Fake a current date.
  let now = TelemetryUtils.truncateToDays(new Date());
  now = fakeNow(futureDate(now, 10 * 60 * MS_IN_A_MINUTE));

  // Enable test-mode for TelemetrySend, otherwise we won't store pending pings
  // before the module is fully initialized later.
  TelemetrySend.setTestModeEnabled(true);

  // Submit some pings without the server and telemetry started yet.
  for (let i = 0; i < TYPE_A_COUNT; ++i) {
    fakePingId("a", i);
    const id = await TelemetryController.submitExternalPing(TEST_TYPE_A, {});
    await setPingLastModified(id, now.getTime() + i * 1000);
  }

  Assert.equal(
    TelemetrySend.pendingPingCount,
    TYPE_A_COUNT,
    "Should have correct pending ping count"
  );

  // Submit some more pings of a different type.
  now = fakeNow(futureDate(now, 5 * MS_IN_A_MINUTE));
  for (let i = 0; i < TYPE_B_COUNT; ++i) {
    fakePingId("b", i);
    const id = await TelemetryController.submitExternalPing(TEST_TYPE_B, {});
    await setPingLastModified(id, now.getTime() + i * 1000);
  }

  Assert.equal(
    TelemetrySend.pendingPingCount,
    TYPE_A_COUNT + TYPE_B_COUNT,
    "Should have correct pending ping count"
  );

  Assert.deepEqual(
    histSuccess.snapshot().values,
    {},
    "Should not have recorded any sending in histograms yet."
  );
  Assert.equal(
    histSendTimeSuccess.snapshot().sum,
    0,
    "Should not have recorded any sending in histograms yet."
  );
  Assert.equal(
    histSendTimeFail.snapshot().sum,
    0,
    "Should not have recorded any sending in histograms yet."
  );

  // Now enable sending to the ping server.
  now = fakeNow(futureDate(now, MS_IN_A_MINUTE));
  PingServer.start();
  Services.prefs.setStringPref(
    TelemetryUtils.Preferences.Server,
    "http://localhost:" + PingServer.port
  );

  let timerPromise = waitForTimer();
  await TelemetryController.testReset();
  let [pingSendTimerCallback, pingSendTimeout] = await timerPromise;
  Assert.ok(!!pingSendTimerCallback, "Should have a timer callback");

  // We should have received 10 pings from the first send batch:
  // 5 of type B and 5 of type A, as sending is newest-first.
  // The other pings should be delayed by the 10-pings-per-minute limit.
  let pings = await PingServer.promiseNextPings(10);
  Assert.equal(
    TelemetrySend.pendingPingCount,
    TYPE_A_COUNT - 5,
    "Should have correct pending ping count"
  );
  PingServer.registerPingHandler(() =>
    Assert.ok(false, "Should not have received any pings now")
  );
  let countByType = countPingTypes(pings);

  Assert.equal(
    countByType.get(TEST_TYPE_B),
    TYPE_B_COUNT,
    "Should have received the correct amount of type B pings"
  );
  Assert.equal(
    countByType.get(TEST_TYPE_A),
    10 - TYPE_B_COUNT,
    "Should have received the correct amount of type A pings"
  );

  Assert.deepEqual(
    histSuccess.snapshot().values,
    { 0: 0, 1: 10, 2: 0 },
    "Should have recorded sending success in histograms."
  );
  Assert.equal(
    histogramValueCount(histSendTimeSuccess.snapshot()),
    10,
    "Should have recorded successful send times in histograms."
  );
  Assert.equal(
    histogramValueCount(histSendTimeFail.snapshot()),
    0,
    "Should not have recorded any failed sending in histograms yet."
  );

  // As we hit the ping send limit and still have pending pings, a send tick should
  // be scheduled in a minute.
  Assert.ok(!!pingSendTimerCallback, "Timer callback should be set");
  Assert.equal(
    pingSendTimeout,
    MS_IN_A_MINUTE,
    "Send tick timeout should be correct"
  );

  // Trigger the next tick - we should receive the next 10 type A pings.
  PingServer.resetPingHandler();
  now = fakeNow(futureDate(now, pingSendTimeout));
  timerPromise = waitForTimer();
  pingSendTimerCallback();
  [pingSendTimerCallback, pingSendTimeout] = await timerPromise;

  pings = await PingServer.promiseNextPings(10);
  PingServer.registerPingHandler(() =>
    Assert.ok(false, "Should not have received any pings now")
  );
  countByType = countPingTypes(pings);

  Assert.equal(
    countByType.get(TEST_TYPE_A),
    10,
    "Should have received the correct amount of type A pings"
  );

  // We hit the ping send limit again and still have pending pings, a send tick should
  // be scheduled in a minute.
  Assert.equal(
    pingSendTimeout,
    MS_IN_A_MINUTE,
    "Send tick timeout should be correct"
  );

  // Trigger the next tick - we should receive the remaining type A pings.
  PingServer.resetPingHandler();
  now = fakeNow(futureDate(now, pingSendTimeout));
  await pingSendTimerCallback();

  pings = await PingServer.promiseNextPings(5);
  PingServer.registerPingHandler(() =>
    Assert.ok(false, "Should not have received any pings now")
  );
  countByType = countPingTypes(pings);

  Assert.equal(
    countByType.get(TEST_TYPE_A),
    5,
    "Should have received the correct amount of type A pings"
  );

  await TelemetrySend.testWaitOnOutgoingPings();
  PingServer.resetPingHandler();
  // Restore the default ping id generator.
  fakeGeneratePingId(() => TelemetryUtils.generateUUID());
});

add_task(async function test_sendDateHeader() {
  fakeNow(new Date(Date.UTC(2011, 1, 1, 11, 0, 0)));
  await TelemetrySend.reset();

  let pingId = await TelemetryController.submitExternalPing(
    "test-send-date-header",
    {}
  );
  let req = await PingServer.promiseNextRequest();
  let ping = decodeRequestPayload(req);
  Assert.equal(
    req.getHeader("Date"),
    "Tue, 01 Feb 2011 11:00:00 GMT",
    "Telemetry should send the correct Date header with requests."
  );
  Assert.equal(ping.id, pingId, "Should have received the correct ping id.");
});

// Test the backoff timeout behavior after send failures.
add_task(async function test_backoffTimeout() {
  const TYPE_PREFIX = "test-backoffTimeout-";
  const TEST_TYPE_C = TYPE_PREFIX + "C";
  const TEST_TYPE_D = TYPE_PREFIX + "D";
  const TEST_TYPE_E = TYPE_PREFIX + "E";

  let histSuccess = Telemetry.getHistogramById("TELEMETRY_SUCCESS");
  let histSendTimeSuccess = Telemetry.getHistogramById(
    "TELEMETRY_SEND_SUCCESS"
  );
  let histSendTimeFail = Telemetry.getHistogramById("TELEMETRY_SEND_FAILURE");

  // Failing a ping send now should trigger backoff behavior.
  let now = fakeNow(2010, 1, 1, 11, 0, 0);
  await TelemetrySend.reset();
  PingServer.stop();

  histSuccess.clear();
  histSendTimeSuccess.clear();
  histSendTimeFail.clear();

  fakePingId("c", 0);
  now = fakeNow(futureDate(now, MS_IN_A_MINUTE));
  let sendAttempts = 0;
  let timerPromise = waitForTimer();
  await TelemetryController.submitExternalPing(TEST_TYPE_C, {});
  let [pingSendTimerCallback, pingSendTimeout] = await timerPromise;
  Assert.equal(
    TelemetrySend.pendingPingCount,
    1,
    "Should have one pending ping."
  );
  ++sendAttempts;

  const MAX_BACKOFF_TIMEOUT = 120 * MS_IN_A_MINUTE;
  for (
    let timeout = 2 * MS_IN_A_MINUTE;
    timeout <= MAX_BACKOFF_TIMEOUT;
    timeout *= 2
  ) {
    Assert.ok(!!pingSendTimerCallback, "Should have received a timer callback");
    Assert.equal(
      pingSendTimeout,
      timeout,
      "Send tick timeout should be correct"
    );

    let callback = pingSendTimerCallback;
    now = fakeNow(futureDate(now, pingSendTimeout));
    timerPromise = waitForTimer();
    await callback();
    [pingSendTimerCallback, pingSendTimeout] = await timerPromise;
    ++sendAttempts;
  }

  timerPromise = waitForTimer();
  await pingSendTimerCallback();
  [pingSendTimerCallback, pingSendTimeout] = await timerPromise;
  Assert.equal(
    pingSendTimeout,
    MAX_BACKOFF_TIMEOUT,
    "Tick timeout should be capped"
  );
  ++sendAttempts;

  Assert.deepEqual(
    histSuccess.snapshot().values,
    { 0: sendAttempts, 1: 0 },
    "Should have recorded sending failure in histograms."
  );
  Assert.equal(
    histSendTimeSuccess.snapshot().sum,
    0,
    "Should not have recorded any sending success in histograms yet."
  );
  Assert.greaterOrEqual(
    histSendTimeFail.snapshot().sum,
    0,
    "Should have recorded send failure times in histograms."
  );
  Assert.equal(
    histogramValueCount(histSendTimeFail.snapshot()),
    sendAttempts,
    "Should have recorded send failure times in histograms."
  );

  // Submitting a new ping should reset the backoff behavior.
  fakePingId("d", 0);
  now = fakeNow(futureDate(now, MS_IN_A_MINUTE));
  timerPromise = waitForTimer();
  await TelemetryController.submitExternalPing(TEST_TYPE_D, {});
  [pingSendTimerCallback, pingSendTimeout] = await timerPromise;
  Assert.equal(
    pingSendTimeout,
    2 * MS_IN_A_MINUTE,
    "Send tick timeout should be correct"
  );
  sendAttempts += 2;

  // With the server running again, we should send out the pending pings immediately
  // when a new ping is submitted.
  PingServer.start();
  TelemetrySend.setServer("http://localhost:" + PingServer.port);
  fakePingId("e", 0);
  now = fakeNow(futureDate(now, MS_IN_A_MINUTE));
  timerPromise = waitForTimer();
  await TelemetryController.submitExternalPing(TEST_TYPE_E, {});

  let pings = await PingServer.promiseNextPings(3);
  let countByType = countPingTypes(pings);

  Assert.equal(
    countByType.get(TEST_TYPE_C),
    1,
    "Should have received the correct amount of type C pings"
  );
  Assert.equal(
    countByType.get(TEST_TYPE_D),
    1,
    "Should have received the correct amount of type D pings"
  );
  Assert.equal(
    countByType.get(TEST_TYPE_E),
    1,
    "Should have received the correct amount of type E pings"
  );

  await TelemetrySend.testWaitOnOutgoingPings();
  Assert.equal(
    TelemetrySend.pendingPingCount,
    0,
    "Should have no pending pings left"
  );

  Assert.deepEqual(
    histSuccess.snapshot().values,
    { 0: sendAttempts, 1: 3, 2: 0 },
    "Should have recorded sending failure in histograms."
  );
  Assert.greaterOrEqual(
    histSendTimeSuccess.snapshot().sum,
    0,
    "Should have recorded sending success in histograms."
  );
  Assert.equal(
    histogramValueCount(histSendTimeSuccess.snapshot()),
    3,
    "Should have recorded sending success in histograms."
  );
  Assert.equal(
    histogramValueCount(histSendTimeFail.snapshot()),
    sendAttempts,
    "Should have recorded send failure times in histograms."
  );

  // Restore the default ping id generator.
  fakeGeneratePingId(() => TelemetryUtils.generateUUID());
});

add_task(async function test_discardBigPings() {
  const TEST_PING_TYPE = "test-ping-type";

  let histSizeExceeded = Telemetry.getHistogramById(
    "TELEMETRY_PING_SIZE_EXCEEDED_SEND"
  );
  let histDiscardedSize = Telemetry.getHistogramById(
    "TELEMETRY_DISCARDED_SEND_PINGS_SIZE_MB"
  );
  let histSuccess = Telemetry.getHistogramById("TELEMETRY_SUCCESS");
  let histSendTimeSuccess = Telemetry.getHistogramById(
    "TELEMETRY_SEND_SUCCESS"
  );
  let histSendTimeFail = Telemetry.getHistogramById("TELEMETRY_SEND_FAILURE");
  for (let h of [
    histSizeExceeded,
    histDiscardedSize,
    histSuccess,
    histSendTimeSuccess,
    histSendTimeFail,
  ]) {
    h.clear();
  }

  // Submit a ping of a normal size and check that we don't count it in the histogram.
  await TelemetryController.submitExternalPing(TEST_PING_TYPE, {
    test: "test",
  });
  await TelemetrySend.testWaitOnOutgoingPings();
  await PingServer.promiseNextPing();

  Assert.equal(
    histSizeExceeded.snapshot().sum,
    0,
    "Telemetry must report no oversized ping submitted."
  );
  Assert.equal(
    histDiscardedSize.snapshot().sum,
    0,
    "Telemetry must report no oversized pings."
  );
  Assert.deepEqual(
    histSuccess.snapshot().values,
    { 0: 0, 1: 1, 2: 0 },
    "Should have recorded sending success."
  );
  Assert.equal(
    histogramValueCount(histSendTimeSuccess.snapshot()),
    1,
    "Should have recorded send success time."
  );
  Assert.greaterOrEqual(
    histSendTimeSuccess.snapshot().sum,
    0,
    "Should have recorded send success time."
  );
  Assert.equal(
    histogramValueCount(histSendTimeFail.snapshot()),
    0,
    "Should not have recorded send failure time."
  );

  // Submit an oversized ping and check that it gets discarded.
  TelemetryHealthPing.testReset();
  // Ensure next ping has a 2 MB gzipped payload.
  fakeGzipCompressStringForNextPing(2 * 1024 * 1024);
  const OVERSIZED_PAYLOAD = {
    data: "empty on purpose - policy takes care of size",
  };
  await TelemetryController.submitExternalPing(
    TEST_PING_TYPE,
    OVERSIZED_PAYLOAD
  );
  await TelemetrySend.testWaitOnOutgoingPings();
  let ping = await PingServer.promiseNextPing();

  Assert.equal(
    ping.type,
    TelemetryHealthPing.HEALTH_PING_TYPE,
    "Should have received a health ping."
  );
  Assert.equal(
    ping.payload.reason,
    TelemetryHealthPing.Reason.IMMEDIATE,
    "Health ping should have the right reason."
  );
  Assert.deepEqual(
    ping.payload[TelemetryHealthPing.FailureType.DISCARDED_FOR_SIZE],
    { [TEST_PING_TYPE]: 1 },
    "Should have recorded correct type of oversized ping."
  );
  Assert.deepEqual(
    ping.payload.os,
    TelemetryHealthPing.OsInfo,
    "Should have correct os info."
  );

  Assert.equal(
    histSizeExceeded.snapshot().sum,
    1,
    "Telemetry must report 1 oversized ping submitted."
  );
  Assert.equal(
    histDiscardedSize.snapshot().values[2],
    1,
    "Telemetry must report a 2MB, oversized, ping submitted."
  );

  Assert.deepEqual(
    histSuccess.snapshot().values,
    { 0: 0, 1: 2, 2: 0 },
    "Should have recorded sending success."
  );
  Assert.equal(
    histogramValueCount(histSendTimeSuccess.snapshot()),
    2,
    "Should have recorded send success time."
  );
  Assert.greaterOrEqual(
    histSendTimeSuccess.snapshot().sum,
    0,
    "Should have recorded send success time."
  );
  Assert.equal(
    histogramValueCount(histSendTimeFail.snapshot()),
    0,
    "Should not have recorded send failure time."
  );
});

add_task(async function test_largeButWithinLimit() {
  const TEST_PING_TYPE = "test-ping-type";

  let histSuccess = Telemetry.getHistogramById("TELEMETRY_SUCCESS");
  histSuccess.clear();

  // Next ping will have a 900KB gzip payload.
  fakeGzipCompressStringForNextPing(900 * 1024);
  const LARGE_PAYLOAD = {
    data: "empty on purpose - policy takes care of size",
  };

  await TelemetryController.submitExternalPing(TEST_PING_TYPE, LARGE_PAYLOAD);
  await TelemetrySend.testWaitOnOutgoingPings();
  await PingServer.promiseNextRequest();

  Assert.deepEqual(
    histSuccess.snapshot().values,
    { 0: 0, 1: 1, 2: 0 },
    "Should have sent large ping."
  );
});

add_task(async function test_evictedOnServerErrors() {
  const TEST_TYPE = "test-evicted";

  await TelemetrySend.reset();

  let histEvicted = Telemetry.getHistogramById(
    "TELEMETRY_PING_EVICTED_FOR_SERVER_ERRORS"
  );
  let histSuccess = Telemetry.getHistogramById("TELEMETRY_SUCCESS");
  let histSendTimeSuccess = Telemetry.getHistogramById(
    "TELEMETRY_SEND_SUCCESS"
  );
  let histSendTimeFail = Telemetry.getHistogramById("TELEMETRY_SEND_FAILURE");
  for (let h of [
    histEvicted,
    histSuccess,
    histSendTimeSuccess,
    histSendTimeFail,
  ]) {
    h.clear();
  }

  // Write a custom ping handler which will return 403. This will trigger ping eviction
  // on client side.
  PingServer.registerPingHandler((req, res) => {
    res.setStatusLine(null, 403, "Forbidden");
    res.processAsync();
    res.finish();
  });

  // Clear the histogram and submit a ping.
  let pingId = await TelemetryController.submitExternalPing(TEST_TYPE, {});
  await TelemetrySend.testWaitOnOutgoingPings();

  Assert.equal(
    histEvicted.snapshot().sum,
    1,
    "Telemetry must report a ping evicted due to server errors"
  );
  Assert.deepEqual(histSuccess.snapshot().values, { 0: 0, 1: 1, 2: 0 });
  Assert.equal(histogramValueCount(histSendTimeSuccess.snapshot()), 1);
  Assert.greaterOrEqual(histSendTimeSuccess.snapshot().sum, 0);
  Assert.equal(histogramValueCount(histSendTimeFail.snapshot()), 0);

  // The ping should not be persisted.
  await Assert.rejects(
    TelemetryStorage.loadPendingPing(pingId),
    /TelemetryStorage.loadPendingPing - no ping with id/,
    "The ping must not be persisted."
  );

  // Reset the ping handler and submit a new ping.
  PingServer.resetPingHandler();
  pingId = await TelemetryController.submitExternalPing(TEST_TYPE, {});

  let ping = await PingServer.promiseNextPings(1);
  Assert.equal(ping[0].id, pingId, "The correct ping must be received");

  // We should not have updated the error histogram.
  await TelemetrySend.testWaitOnOutgoingPings();
  Assert.equal(
    histEvicted.snapshot().sum,
    1,
    "Telemetry must report only one ping evicted due to server errors"
  );
  Assert.deepEqual(histSuccess.snapshot().values, { 0: 0, 1: 2, 2: 0 });
  Assert.equal(histogramValueCount(histSendTimeSuccess.snapshot()), 2);
  Assert.equal(histogramValueCount(histSendTimeFail.snapshot()), 0);
});

add_task(async function test_tooLateToSend() {
  Assert.ok(true, "TEST BEGIN");
  const TEST_TYPE = "test-too-late-to-send";

  await TelemetrySend.reset();
  PingServer.start();
  PingServer.registerPingHandler(() =>
    Assert.ok(false, "Should not have received any pings now")
  );

  Assert.equal(
    TelemetrySend.pendingPingCount,
    0,
    "Should have no pending pings yet"
  );

  TelemetrySend.testTooLateToSend(true);

  const id = await TelemetryController.submitExternalPing(TEST_TYPE, {});

  // Triggering a shutdown should persist the pings
  await TelemetrySend.shutdown();
  const pendingPings = TelemetryStorage.getPendingPingList();
  Assert.equal(pendingPings.length, 1, "Should have a pending ping in storage");
  Assert.equal(pendingPings[0].id, id, "Should have pended our test's ping");

  Assert.equal(
    Telemetry.getHistogramById("TELEMETRY_SEND_FAILURE_TYPE").snapshot()
      .values[7],
    1,
    "Should have registered the failed attempt to send"
  );
  Assert.equal(
    Telemetry.getKeyedHistogramById(
      "TELEMETRY_SEND_FAILURE_TYPE_PER_PING"
    ).snapshot()[TEST_TYPE].values[7],
    1,
    "Should have registered the failed attempt to send TEST_TYPE ping"
  );
  await TelemetryStorage.reset();
  Assert.equal(
    TelemetrySend.pendingPingCount,
    0,
    "Should clean up after yourself"
  );
});

add_task(
  { skip_if: () => gIsAndroid },
  async function test_pingSenderShutdownBatch() {
    const TEST_TYPE = "test-ping-sender-batch";

    await TelemetrySend.reset();
    PingServer.start();
    PingServer.registerPingHandler(() =>
      Assert.ok(false, "Should not have received any pings at this time.")
    );

    Assert.equal(
      TelemetrySend.pendingPingCount,
      0,
      "Should have no pending pings yet"
    );

    TelemetrySend.testTooLateToSend(true);

    const id = await TelemetryController.submitExternalPing(
      TEST_TYPE,
      { payload: false },
      { usePingSender: true }
    );
    const id2 = await TelemetryController.submitExternalPing(
      TEST_TYPE,
      { payload: false },
      { usePingSender: true }
    );

    Assert.equal(
      TelemetrySend.pendingPingCount,
      2,
      "Should have stored these two pings in pending land."
    );

    // Permit pings to be received.
    PingServer.resetPingHandler();

    TelemetrySend.flushPingSenderBatch();

    // Pings don't have to be sent in the order they're submitted.
    const ping = await PingServer.promiseNextPing();
    const ping2 = await PingServer.promiseNextPing();
    Assert.ok(
      (ping.id == id && ping2.id == id2) || (ping.id == id2 && ping2.id == id)
    );
    Assert.equal(ping.type, TEST_TYPE);
    Assert.equal(ping2.type, TEST_TYPE);

    await TelemetryStorage.reset();
    Assert.equal(
      TelemetrySend.pendingPingCount,
      0,
      "Should clean up after yourself"
    );
  }
);

// Test that the current, non-persisted pending pings are properly saved on shutdown.
add_task(async function test_persistCurrentPingsOnShutdown() {
  const TEST_TYPE = "test-persistCurrentPingsOnShutdown";
  const PING_COUNT = 5;
  await TelemetrySend.reset();
  PingServer.stop();
  Assert.equal(
    TelemetrySend.pendingPingCount,
    0,
    "Should have no pending pings yet"
  );

  // Submit new pings that shouldn't be persisted yet.
  let ids = [];
  for (let i = 0; i < 5; ++i) {
    ids.push(fakePingId("f", i));
    TelemetryController.submitExternalPing(TEST_TYPE, {});
  }

  Assert.equal(
    TelemetrySend.pendingPingCount,
    PING_COUNT,
    "Should have the correct pending ping count"
  );

  // Triggering a shutdown should persist the pings.
  await TelemetrySend.shutdown();
  Assert.ok(
    await checkPingsSaved(ids),
    "All pending pings should have been persisted"
  );

  // After a restart the pings should have been found when scanning.
  await TelemetrySend.reset();
  Assert.equal(
    TelemetrySend.pendingPingCount,
    PING_COUNT,
    "Should have the correct pending ping count"
  );

  // Restore the default ping id generator.
  fakeGeneratePingId(() => TelemetryUtils.generateUUID());
});

add_task(async function test_sendCheckOverride() {
  const TEST_PING_TYPE = "test-sendCheckOverride";

  // Disable "health" ping. It can sneak into the test.
  Services.prefs.setBoolPref(
    TelemetryUtils.Preferences.HealthPingEnabled,
    false
  );

  // Clear any pending pings.
  await TelemetryController.testShutdown();
  await TelemetryStorage.testClearPendingPings();

  // Enable the ping server.
  PingServer.start();
  Services.prefs.setStringPref(
    TelemetryUtils.Preferences.Server,
    "http://localhost:" + PingServer.port
  );

  // Start Telemetry and disable the test-mode so pings don't get
  // sent unless we enable the override.
  await TelemetryController.testReset();

  // Submit a test ping and make sure it doesn't get sent. We only do
  // that if we're on unofficial builds: pings will always get sent otherwise.
  if (!Services.telemetry.isOfficialTelemetry) {
    TelemetrySend.setTestModeEnabled(false);
    PingServer.registerPingHandler(() =>
      Assert.ok(false, "Should not have received any pings now")
    );

    await TelemetryController.submitExternalPing(TEST_PING_TYPE, {
      test: "test",
    });
    Assert.equal(
      TelemetrySend.pendingPingCount,
      0,
      "Should have no pending pings"
    );
  }

  // Enable the override and try to send again.
  Services.prefs.setBoolPref(
    TelemetryUtils.Preferences.OverrideOfficialCheck,
    true
  );
  PingServer.resetPingHandler();
  await TelemetrySend.reset();
  await TelemetryController.submitExternalPing(TEST_PING_TYPE, {
    test: "test",
  });

  // Make sure we received the ping.
  const ping = await PingServer.promiseNextPing();
  Assert.equal(
    ping.type,
    TEST_PING_TYPE,
    "Must receive a ping of the expected type"
  );

  // Restore the test mode and disable the override.
  TelemetrySend.setTestModeEnabled(true);
  Services.prefs.clearUserPref(
    TelemetryUtils.Preferences.OverrideOfficialCheck
  );
});

add_task(async function test_submissionPath() {
  const PING_FORMAT_VERSION = 4;
  const TEST_PING_TYPE = "test-ping-type";

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

  await sendPing(false, false);

  // Fetch the request from the server.
  let request = await PingServer.promiseNextRequest();

  // Get the payload.
  let ping = decodeRequestPayload(request);
  checkPingFormat(ping, TEST_PING_TYPE, false, false);

  let app = ping.application;
  let pathComponents = [
    ping.id,
    ping.type,
    app.name,
    app.version,
    app.channel,
    app.buildId,
  ];

  let urlComponents = request.path.split("/");

  for (let i = 0; i < pathComponents.length; i++) {
    Assert.ok(
      urlComponents.includes(pathComponents[i]),
      `Path should include ${pathComponents[i]}`
    );
  }

  // Check that we have a version query parameter in the URL.
  Assert.notEqual(request.queryString, "");

  // Make sure the version in the query string matches the new ping format version.
  let params = request.queryString.split("&");
  Assert.ok(params.find(p => p == "v=" + PING_FORMAT_VERSION));
});

add_task(async function testCookies() {
  const TEST_TYPE = "test-cookies";

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

  let uri = Services.io.newURI("http://localhost:" + PingServer.port);
  let channel = NetUtil.newChannel({
    uri,
    loadUsingSystemPrincipal: true,
    contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
  });
  Services.cookies.QueryInterface(Ci.nsICookieService);
  Services.cookies.setCookieStringFromHttp(uri, "cookie-time=yes", channel);

  const id = await TelemetryController.submitExternalPing(TEST_TYPE, {});
  let foundit = false;
  while (!foundit) {
    var request = await PingServer.promiseNextRequest();
    var ping = decodeRequestPayload(request);
    foundit = id === ping.id;
  }
  Assert.equal(id, ping.id, "We're testing the right ping's request, right?");
  Assert.equal(
    false,
    request.hasHeader("Cookie"),
    "Request should not have Cookie header"
  );
});

add_task(async function test_pref_observer() {
  // This test requires the presence of the crash reporter component.
  let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
  if (
    !registrar.isContractIDRegistered("@mozilla.org/toolkit/crash-reporter;1")
  ) {
    return;
  }

  await TelemetrySend.setup(true);

  const IS_UNIFIED_TELEMETRY = Services.prefs.getBoolPref(
    TelemetryUtils.Preferences.Unified,
    false
  );

  let origTelemetryEnabled = Services.prefs.getBoolPref(
    TelemetryUtils.Preferences.TelemetryEnabled
  );
  let origFhrUploadEnabled = Services.prefs.getBoolPref(
    TelemetryUtils.Preferences.FhrUploadEnabled
  );

  if (!IS_UNIFIED_TELEMETRY) {
    Services.prefs.setBoolPref(
      TelemetryUtils.Preferences.TelemetryEnabled,
      true
    );
  }
  Services.prefs.setBoolPref(TelemetryUtils.Preferences.FhrUploadEnabled, true);

  function waitAnnotateCrashReport(expectedValue, trigger) {
    return new Promise(function (resolve, reject) {
      let keys = new Set(["TelemetryClientId", "TelemetryServerURL"]);

      let crs = {
        QueryInterface: ChromeUtils.generateQI(["nsICrashReporter"]),
        annotateCrashReport(key, value) {
          if (!keys.delete(key)) {
            MockRegistrar.unregister(gMockCrs);
            reject(
              Error(`Crash report annotation with unexpected key: "${key}".`)
            );
          }

          if (expectedValue && value == "") {
            MockRegistrar.unregister(gMockCrs);
            reject(Error("Crash report annotation without expected value."));
          }

          if (keys.size == 0) {
            MockRegistrar.unregister(gMockCrs);
            resolve();
          }
        },
        removeCrashReportAnnotation(key) {
          if (!keys.delete(key)) {
            MockRegistrar.unregister(gMockCrs);
          }

          if (keys.size == 0) {
            MockRegistrar.unregister(gMockCrs);
            resolve();
          }
        },
        UpdateCrashEventsDir() {},
      };

      let gMockCrs = MockRegistrar.register(
        "@mozilla.org/toolkit/crash-reporter;1",
        crs
      );
      registerCleanupFunction(function () {
        MockRegistrar.unregister(gMockCrs);
      });

      trigger();
    });
  }

  await waitAnnotateCrashReport(!IS_UNIFIED_TELEMETRY, () =>
    Services.prefs.setBoolPref(
      TelemetryUtils.Preferences.FhrUploadEnabled,
      false
    )
  );

  await waitAnnotateCrashReport(true, () =>
    Services.prefs.setBoolPref(
      TelemetryUtils.Preferences.FhrUploadEnabled,
      true
    )
  );

  if (!IS_UNIFIED_TELEMETRY) {
    Services.prefs.setBoolPref(
      TelemetryUtils.Preferences.TelemetryEnabled,
      origTelemetryEnabled
    );
  }
  Services.prefs.setBoolPref(
    TelemetryUtils.Preferences.FhrUploadEnabled,
    origFhrUploadEnabled
  );
});

add_task(async function cleanup() {
  await PingServer.stop();
});
