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

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

"use strict";

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

function checkHealthPingStructure(ping, expectedFailuresDict) {
  let payload = ping.payload;
  Assert.equal(
    ping.type,
    TelemetryHealthPing.HEALTH_PING_TYPE,
    "Should have recorded a health ping."
  );

  for (let [key, value] of Object.entries(expectedFailuresDict)) {
    Assert.deepEqual(
      payload[key],
      value,
      "Should have recorded correct entry with key: " + key
    );
  }
}

function fakeHealthSchedulerTimer(set, clear) {
  let { Policy } = ChromeUtils.importESModule(
    "resource://gre/modules/HealthPing.sys.mjs"
  );
  Policy.setSchedulerTickTimeout = set;
  Policy.clearSchedulerTickTimeout = clear;
}

add_setup(async function setup() {
  // Trigger a proper telemetry init.
  do_get_profile(true);
  // Make sure we don't generate unexpected pings due to pref changes.
  await setEmptyPrefWatchlist();
  Services.prefs.setBoolPref(
    TelemetryUtils.Preferences.HealthPingEnabled,
    true
  );

  await TelemetryController.testSetup();
  PingServer.start();
  TelemetrySend.setServer("http://localhost:" + PingServer.port);
  Services.prefs.setStringPref(
    TelemetryUtils.Preferences.Server,
    "http://localhost:" + PingServer.port
  );
});

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

add_task(async function test_sendImmediately() {
  PingServer.clearRequests();
  TelemetryHealthPing.testReset();

  await TelemetryHealthPing.recordSendFailure("testProblem");
  let ping = await PingServer.promiseNextPing();
  checkHealthPingStructure(ping, {
    [TelemetryHealthPing.FailureType.SEND_FAILURE]: {
      testProblem: 1,
    },
    os: TelemetryHealthPing.OsInfo,
    reason: TelemetryHealthPing.Reason.IMMEDIATE,
  });
});

add_task(async function test_sendOnDelay() {
  PingServer.clearRequests();
  TelemetryHealthPing.testReset();

  // This first failure should immediately trigger a ping. After this, subsequent failures should be throttled.
  await TelemetryHealthPing.recordSendFailure("testFailure");
  let testPing = await PingServer.promiseNextPing();
  Assert.equal(
    testPing.type,
    TelemetryHealthPing.HEALTH_PING_TYPE,
    "Should have recorded a health ping."
  );

  // Retrieve delayed call back.
  let pingSubmissionCallBack = null;
  fakeHealthSchedulerTimer(
    callBack => (pingSubmissionCallBack = callBack),
    () => {}
  );

  // Record two failures, health ping must not be send now.
  await TelemetryHealthPing.recordSendFailure("testFailure");
  await TelemetryHealthPing.recordSendFailure("testFailure");

  // Wait for sending delayed health ping.
  await pingSubmissionCallBack();

  let ping = await PingServer.promiseNextPing();
  checkHealthPingStructure(ping, {
    [TelemetryHealthPing.FailureType.SEND_FAILURE]: {
      testFailure: 2,
    },
    os: TelemetryHealthPing.OsInfo,
    reason: TelemetryHealthPing.Reason.DELAYED,
  });
});

add_task(async function test_sendOverSizedPing() {
  TelemetryHealthPing.testReset();
  PingServer.clearRequests();
  let OVER_SIZED_PING_TYPE = "over-sized-ping";
  let overSizedData = generateRandomString(2 * 1024 * 1024);

  await TelemetryController.submitExternalPing(OVER_SIZED_PING_TYPE, {
    data: overSizedData,
  });
  let ping = await PingServer.promiseNextPing();

  checkHealthPingStructure(ping, {
    [TelemetryHealthPing.FailureType.DISCARDED_FOR_SIZE]: {
      [OVER_SIZED_PING_TYPE]: 1,
    },
    os: TelemetryHealthPing.OsInfo,
    reason: TelemetryHealthPing.Reason.IMMEDIATE,
  });
});

add_task(async function test_sendOnlyTopTenDiscardedPings() {
  TelemetryHealthPing.testReset();
  await TelemetrySend.reset();
  PingServer.clearRequests();
  let PING_TYPE = "sort-discarded";

  // This first failure should immediately trigger a ping. After this, subsequent failures should be throttled.
  await TelemetryHealthPing.recordSendFailure("testFailure");
  let testPing = await PingServer.promiseNextPing();
  Assert.equal(
    testPing.type,
    TelemetryHealthPing.HEALTH_PING_TYPE,
    "Should have recorded a health ping."
  );

  // Retrieve delayed call back.
  let pingSubmissionCallBack = null;
  fakeHealthSchedulerTimer(
    callBack => (pingSubmissionCallBack = callBack),
    () => {}
  );

  // Add failures
  for (let i = 1; i < 12; i++) {
    for (let j = 1; j < i; j++) {
      TelemetryHealthPing.recordDiscardedPing(PING_TYPE + i);
    }
  }

  await TelemetrySend.reset();
  await pingSubmissionCallBack();
  let ping = await PingServer.promiseNextPing();

  checkHealthPingStructure(ping, {
    os: TelemetryHealthPing.OsInfo,
    reason: TelemetryHealthPing.Reason.DELAYED,
    [TelemetryHealthPing.FailureType.DISCARDED_FOR_SIZE]: {
      [PING_TYPE + 11]: 10,
      [PING_TYPE + 10]: 9,
      [PING_TYPE + 9]: 8,
      [PING_TYPE + 8]: 7,
      [PING_TYPE + 7]: 6,
      [PING_TYPE + 6]: 5,
      [PING_TYPE + 5]: 4,
      [PING_TYPE + 4]: 3,
      [PING_TYPE + 3]: 2,
      [PING_TYPE + 2]: 1,
    },
  });
});

add_task(async function test_discardedForSizePending() {
  TelemetryHealthPing.testReset();
  PingServer.clearRequests();

  const PING_TYPE = "discarded-for-size-pending";

  const OVERSIZED_PING_ID = "9b21ec8f-f762-4d28-a2c1-44e1c4694f24";
  // Create a pending oversized ping.
  let overSizedPayload = generateRandomString(2 * 1024 * 1024);
  const OVERSIZED_PING = {
    id: OVERSIZED_PING_ID,
    type: PING_TYPE,
    creationDate: new Date().toISOString(),
    // Generate a 2MB string to use as the ping payload.
    payload: overSizedPayload,
  };

  // Test loadPendingPing.
  await TelemetryStorage.savePendingPing(OVERSIZED_PING);
  // Try to manually load the oversized ping.
  await Assert.rejects(
    TelemetryStorage.loadPendingPing(OVERSIZED_PING_ID),
    /loadPendingPing - exceeded the maximum ping size/,
    "The oversized ping should have been pruned."
  );

  let ping = await PingServer.promiseNextPing();
  checkHealthPingStructure(ping, {
    [TelemetryHealthPing.FailureType.DISCARDED_FOR_SIZE]: {
      "<unknown>": 1,
    },
    os: TelemetryHealthPing.OsInfo,
    reason: TelemetryHealthPing.Reason.IMMEDIATE,
  });

  // Test _scanPendingPings.
  TelemetryHealthPing.testReset();
  await TelemetryStorage.savePendingPing(OVERSIZED_PING);
  await TelemetryStorage.loadPendingPingList();

  ping = await PingServer.promiseNextPing();
  checkHealthPingStructure(ping, {
    [TelemetryHealthPing.FailureType.DISCARDED_FOR_SIZE]: {
      "<unknown>": 1,
    },
    os: TelemetryHealthPing.OsInfo,
    reason: TelemetryHealthPing.Reason.IMMEDIATE,
  });
});

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

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

  // This first failure should immediately trigger a ping.
  // After this, subsequent failures should be throttled.
  await TelemetryHealthPing.recordSendFailure("testFailure");
  await PingServer.promiseNextPing();

  TelemetryHealthPing.recordSendFailure("testFailure");
  let nextRequest = PingServer.promiseNextRequest();

  await TelemetryController.testReset();
  await TelemetryController.testShutdown();
  let request = await nextRequest;
  let ping = decodeRequestPayload(request);

  checkHealthPingStructure(ping, {
    [TelemetryHealthPing.FailureType.SEND_FAILURE]: {
      testFailure: 1,
    },
    os: TelemetryHealthPing.OsInfo,
    reason: TelemetryHealthPing.Reason.SHUT_DOWN,
  });

  // Check that the health ping is sent at shutdown using the pingsender.
  Assert.equal(
    request.getHeader("User-Agent"),
    "pingsender/1.0",
    "Should have received the correct user agent string."
  );
  Assert.equal(
    request.getHeader("X-PingSender-Version"),
    "1.0",
    "Should have received the correct PingSender version string."
  );
});
