"use strict";

const { ContentTaskUtils } = ChromeUtils.import("resource://testing-common/ContentTaskUtils.jsm", {});
const { TelemetryController } = ChromeUtils.import("resource://gre/modules/TelemetryController.jsm", {});
const { TelemetryUtils } = ChromeUtils.import("resource://gre/modules/TelemetryUtils.jsm", {});

const CONTENT_CREATED = "ipc:content-created";

async function waitForProcessesScalars(aProcesses, aKeyed,
                                       aAdditionalCondition = (data) => true) {
  await ContentTaskUtils.waitForCondition(() => {
    const scalars = aKeyed ?
      Services.telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN) :
      Services.telemetry.snapshotScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN);
    return aProcesses.every(p => Object.keys(scalars).includes(p))
           && aAdditionalCondition(scalars);
  });
}

add_task(async function test_setup() {
  // Make sure the newly spawned content processes will have extended Telemetry enabled.
  await SpecialPowers.pushPrefEnv({
    set: [[TelemetryUtils.Preferences.OverridePreRelease, true]]
  });
  // And take care of the already initialized one as well.
  let canRecordExtended = Services.telemetry.canRecordExtended;
  Services.telemetry.canRecordExtended = true;
  registerCleanupFunction(() => Services.telemetry.canRecordExtended = canRecordExtended);
});

add_task(async function test_recording() {
  let currentPid = gBrowser.selectedBrowser.frameLoader.tabParent.osPid;

  // Register test scalars before spawning the content process: the scalar
  // definitions will propagate to it.
  Services.telemetry.registerScalars("telemetry.test.dynamic", {
    "pre_content_spawn": {
      kind: Ci.nsITelemetry.SCALAR_TYPE_COUNT,
      keyed: false,
      record_on_release: true
    },
    "pre_content_spawn_expiration": {
      kind: Ci.nsITelemetry.SCALAR_TYPE_COUNT,
      keyed: false,
      release_channel_collection: true
    },
  });

  Services.telemetry.scalarSet("telemetry.test.dynamic.pre_content_spawn_expiration", 3);

  let processCreated = TestUtils.topicObserved(CONTENT_CREATED);
  await BrowserTestUtils.withNewTab({ gBrowser, url: "about:blank", forceNewProcess: true },
                                    async function(browser) {
      // Make sure our new browser is in its own process. The processCreated
      // promise should have already resolved by this point.
      await processCreated;
      let newPid = browser.frameLoader.tabParent.osPid;
      ok(currentPid != newPid, "The new tab must spawn its own process");

      // Register test scalars after spawning the content process: the scalar
      // definitions will propagate to it.
      // Also attempt to register again "pre_content_spawn_expiration" and set
      // it to expired.
      Services.telemetry.registerScalars("telemetry.test.dynamic", {
        "post_content_spawn": {
          kind: Ci.nsITelemetry.SCALAR_TYPE_BOOLEAN,
          keyed: false,
          release_channel_collection: false
        },
        "post_content_spawn_keyed": {
          kind: Ci.nsITelemetry.SCALAR_TYPE_COUNT,
          keyed: true,
          release_channel_collection: true
        },
        "pre_content_spawn_expiration": {
          kind: Ci.nsITelemetry.SCALAR_TYPE_COUNT,
          keyed: false,
          release_channel_collection: true,
          expired: true
        },
      });

      // Accumulate from the content process into both dynamic scalars.
      await ContentTask.spawn(browser, {}, async function() {
        Services.telemetry.scalarAdd("telemetry.test.dynamic.pre_content_spawn_expiration", 1);
        Services.telemetry.scalarSet("telemetry.test.dynamic.pre_content_spawn", 3);
        Services.telemetry.scalarSet("telemetry.test.dynamic.post_content_spawn", true);
        Services.telemetry.keyedScalarSet("telemetry.test.dynamic.post_content_spawn_keyed",
                                          "testKey", 3);
      });
  });

  // Wait for the dynamic scalars to appear non-keyed snapshots.
  await waitForProcessesScalars(["dynamic"], false, scalars => {
    // Wait for the scalars set in the content process to be available.
    return "telemetry.test.dynamic.pre_content_spawn" in scalars.dynamic;
  });

  // Verify the content of the snapshots.
  const scalars =
      Services.telemetry.snapshotScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN);
  ok("dynamic" in scalars,
     "The scalars must contain the 'dynamic' process section");
  ok("telemetry.test.dynamic.pre_content_spawn" in scalars.dynamic,
     "Dynamic scalars registered before a process spawns must be present.");
  is(scalars.dynamic["telemetry.test.dynamic.pre_content_spawn"], 3,
    "The dynamic scalar must contain the expected value.");
  is(scalars.dynamic["telemetry.test.dynamic.pre_content_spawn_expiration"], 3,
    "The dynamic scalar must not be updated after being expired.");
  ok("telemetry.test.dynamic.post_content_spawn" in scalars.dynamic,
     "Dynamic scalars registered after a process spawns must be present.");
  is(scalars.dynamic["telemetry.test.dynamic.post_content_spawn"], true,
     "The dynamic scalar must contain the expected value.");

  // Wait for the dynamic scalars to appear in the keyed snapshots.
  await waitForProcessesScalars(["dynamic"], true);

  const keyedScalars =
      Services.telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN);
  ok("dynamic" in keyedScalars,
     "The keyed scalars must contain the 'dynamic' process section");
  ok("telemetry.test.dynamic.post_content_spawn_keyed" in keyedScalars.dynamic,
     "Dynamic keyed scalars registered after a process spawns must be present.");
  is(keyedScalars.dynamic["telemetry.test.dynamic.post_content_spawn_keyed"].testKey, 3,
     "The dynamic keyed scalar must contain the expected value.");
});

add_task(async function test_aggregation() {
  Services.telemetry.clearScalars();

  // Register test scalars before spawning the content process: the scalar
  // definitions will propagate to it. Also cheat TelemetrySession to put
  // the test scalar in the payload by using "cheattest" instead of "test" in
  // the scalar category name.
  Services.telemetry.registerScalars("telemetry.cheattest.dynamic", {
    "test_aggregation": {
      kind: Ci.nsITelemetry.SCALAR_TYPE_COUNT,
      keyed: false,
      record_on_release: true
    },
  });

  const SCALAR_FULL_NAME = "telemetry.cheattest.dynamic.test_aggregation";
  Services.telemetry.scalarAdd(SCALAR_FULL_NAME, 1);

  let processCreated = TestUtils.topicObserved(CONTENT_CREATED);
  await BrowserTestUtils.withNewTab({ gBrowser, url: "about:blank", forceNewProcess: true },
                                    async function(browser) {
      // Accumulate from the content process into both dynamic scalars.
      await ContentTask.spawn(browser, SCALAR_FULL_NAME, async function(aName) {
        Services.telemetry.scalarAdd(aName, 3);
      });
  });

  // Wait for the dynamic scalars to appear. Since we're testing that children
  // and parent data get aggregated, we might need to wait a bit more:
  // TelemetryIPCAccumulator.cpp sends batches to the parent process every 2 seconds.
  await waitForProcessesScalars(["dynamic"], false, scalarData => {
    return "dynamic" in scalarData
      && SCALAR_FULL_NAME in scalarData.dynamic
      && scalarData.dynamic[SCALAR_FULL_NAME] == 4;
  });

  // Check that the definitions made it to the ping payload.
  const pingData = TelemetryController.getCurrentPingData(true);
  ok("dynamic" in pingData.payload.processes,
     "The ping payload must contain the 'dynamic' process section");
  is(pingData.payload.processes.dynamic.scalars[SCALAR_FULL_NAME], 4,
    "The dynamic scalar must contain the aggregated parent and children data.");
});
