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

ChromeUtils.defineESModuleGetters(this, {
  TestUtils: "resource://testing-common/TestUtils.sys.mjs",
});
const { TelemetryTestUtils } = ChromeUtils.importESModule(
  "resource://testing-common/TelemetryTestUtils.sys.mjs"
);

const PRERELEASE_CHANNELS = Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS;
const ALL_CHANNELS = Ci.nsITelemetry.DATASET_ALL_CHANNELS;

function checkEventFormat(events) {
  Assert.ok(Array.isArray(events), "Events should be serialized to an array.");
  for (let e of events) {
    Assert.ok(Array.isArray(e), "Event should be an array.");
    Assert.greaterOrEqual(
      e.length,
      4,
      "Event should have at least 4 elements."
    );
    Assert.lessOrEqual(e.length, 6, "Event should have at most 6 elements.");

    Assert.equal(typeof e[0], "number", "Element 0 should be a number.");
    Assert.equal(typeof e[1], "string", "Element 1 should be a string.");
    Assert.equal(typeof e[2], "string", "Element 2 should be a string.");
    Assert.equal(typeof e[3], "string", "Element 3 should be a string.");

    if (e.length > 4) {
      Assert.ok(
        e[4] === null || typeof e[4] == "string",
        "Event element 4 should be null or a string."
      );
    }
    if (e.length > 5) {
      Assert.ok(
        e[5] === null || typeof e[5] == "object",
        "Event element 5 should be null or an object."
      );
    }

    let extra = e[5];
    if (extra) {
      Assert.ok(
        Object.keys(extra).every(k => typeof k == "string"),
        "All extra keys should be strings."
      );
      Assert.ok(
        Object.values(extra).every(v => typeof v == "string"),
        "All extra values should be strings."
      );
    }
  }
}

/**
 * @param summaries is of the form
 *   [{process, [event category, event object, event method], count}]
 * @param clearScalars - true if you want to clear the scalars
 */
function checkEventSummary(summaries, clearScalars) {
  let scalars = Telemetry.getSnapshotForKeyedScalars("main", clearScalars);

  for (let [process, [category, eObject, method], count] of summaries) {
    let uniqueEventName = `${category}#${eObject}#${method}`;
    let summaryCount;
    if (process === "dynamic") {
      summaryCount =
        scalars.dynamic["telemetry.dynamic_event_counts"][uniqueEventName];
    } else {
      summaryCount =
        scalars[process]["telemetry.event_counts"][uniqueEventName];
    }
    Assert.equal(
      summaryCount,
      count,
      `${uniqueEventName} had wrong summary count`
    );
  }
}

function checkRegistrationFailure(failureType) {
  let snapshot = Telemetry.getSnapshotForHistograms("main", true);
  Assert.ok(
    "parent" in snapshot,
    "There should be at least one parent histogram when checking for registration failures."
  );
  Assert.ok(
    "TELEMETRY_EVENT_REGISTRATION_ERROR" in snapshot.parent,
    "TELEMETRY_EVENT_REGISTRATION_ERROR should exist when checking for registration failures."
  );
  let values = snapshot.parent.TELEMETRY_EVENT_REGISTRATION_ERROR.values;
  Assert.ok(
    !!values,
    "TELEMETRY_EVENT_REGISTRATION_ERROR's values should exist when checking for registration failures."
  );
  Assert.equal(
    values[failureType],
    1,
    `Event registration ought to have failed due to type ${failureType}`
  );
}

function checkRecordingFailure(failureType) {
  let snapshot = Telemetry.getSnapshotForHistograms("main", true);
  Assert.ok(
    "parent" in snapshot,
    "There should be at least one parent histogram when checking for recording failures."
  );
  Assert.ok(
    "TELEMETRY_EVENT_RECORDING_ERROR" in snapshot.parent,
    "TELEMETRY_EVENT_RECORDING_ERROR should exist when checking for recording failures."
  );
  let values = snapshot.parent.TELEMETRY_EVENT_RECORDING_ERROR.values;
  Assert.ok(
    !!values,
    "TELEMETRY_EVENT_RECORDING_ERROR's values should exist when checking for recording failures."
  );
  Assert.equal(
    values[failureType],
    1,
    `Event recording ought to have failed due to type ${failureType}`
  );
}

add_task(async function test_event_summary_limit() {
  Telemetry.clearEvents();
  Telemetry.clearScalars();

  const limit = 500; // matches kMaxEventSummaryKeys in TelemetryScalar.cpp.
  let objects = [];
  for (let i = 0; i < limit + 1; i++) {
    objects.push("object" + i);
  }
  // Using "telemetry.test.dynamic" as using "telemetry.test" will enable
  // the "telemetry.test" category.
  Telemetry.registerEvents("telemetry.test.dynamic", {
    test_method: {
      methods: ["testMethod"],
      objects,
      record_on_release: true,
    },
  });
  for (let object of objects) {
    Telemetry.recordEvent("telemetry.test.dynamic", "testMethod", object);
  }

  TelemetryTestUtils.assertNumberOfEvents(
    limit + 1,
    {},
    { process: "dynamic" }
  );
  let scalarSnapshot = Telemetry.getSnapshotForKeyedScalars("main", true);
  Assert.equal(
    Object.keys(scalarSnapshot.dynamic["telemetry.dynamic_event_counts"])
      .length,
    limit,
    "Should not have recorded more than `limit` events"
  );
});

add_task(async function test_recording_state() {
  Telemetry.clearEvents();
  Telemetry.clearScalars();

  const events = [
    ["telemetry.test", "test1", "object1"],
    ["telemetry.test.second", "test", "object1"],
  ];

  // Both test categories should be off by default.
  events.forEach(e => Telemetry.recordEvent(...e));
  TelemetryTestUtils.assertEvents([]);
  checkEventSummary(
    events.map(e => ["parent", e, 1]),
    true
  );

  // Enable one test category and see that we record correctly.
  Telemetry.setEventRecordingEnabled("telemetry.test", true);
  events.forEach(e => Telemetry.recordEvent(...e));
  TelemetryTestUtils.assertEvents([events[0]]);
  checkEventSummary(
    events.map(e => ["parent", e, 1]),
    true
  );

  // Also enable the other test category and see that we record correctly.
  Telemetry.setEventRecordingEnabled("telemetry.test.second", true);
  events.forEach(e => Telemetry.recordEvent(...e));
  TelemetryTestUtils.assertEvents(events);
  checkEventSummary(
    events.map(e => ["parent", e, 1]),
    true
  );

  // Now turn of one category again and check that this works as expected.
  Telemetry.setEventRecordingEnabled("telemetry.test", false);
  events.forEach(e => Telemetry.recordEvent(...e));
  TelemetryTestUtils.assertEvents([events[1]]);
  checkEventSummary(
    events.map(e => ["parent", e, 1]),
    true
  );
});

add_task(async function recording_setup() {
  // Make sure both test categories are enabled for the remaining tests.
  // Otherwise their event recording won't work.
  Telemetry.setEventRecordingEnabled("telemetry.test", true);
  Telemetry.setEventRecordingEnabled("telemetry.test.second", true);
});

add_task(async function test_recording() {
  Telemetry.clearScalars();
  Telemetry.clearEvents();

  // Record some events.
  let expected = [
    { optout: false, event: ["telemetry.test", "test1", "object1"] },
    { optout: false, event: ["telemetry.test", "test2", "object2"] },

    { optout: false, event: ["telemetry.test", "test1", "object1", "value"] },
    {
      optout: false,
      event: ["telemetry.test", "test1", "object1", "value", null],
    },
    {
      optout: false,
      event: ["telemetry.test", "test1", "object1", null, { key1: "value1" }],
    },
    {
      optout: false,
      event: [
        "telemetry.test",
        "test1",
        "object1",
        "value",
        { key1: "value1", key2: "value2" },
      ],
    },

    { optout: true, event: ["telemetry.test", "optout", "object1"] },
    { optout: false, event: ["telemetry.test.second", "test", "object1"] },
    {
      optout: false,
      event: [
        "telemetry.test.second",
        "test",
        "object1",
        null,
        { key1: "value1" },
      ],
    },
  ];

  for (let entry of expected) {
    entry.tsBefore = Math.floor(Telemetry.msSinceProcessStart());
    try {
      Telemetry.recordEvent(...entry.event);
    } catch (ex) {
      Assert.ok(
        false,
        `Failed to record event ${JSON.stringify(entry.event)}: ${ex}`
      );
    }
    entry.tsAfter = Math.floor(Telemetry.msSinceProcessStart());
  }

  // Strip off trailing null values to match the serialized events.
  for (let entry of expected) {
    let e = entry.event;
    while (e.length >= 3 && e[e.length - 1] === null) {
      e.pop();
    }
  }

  // Check that the events were summarized properly.
  let summaries = {};
  expected.forEach(({ event }) => {
    let [category, eObject, method] = event;
    let uniqueEventName = `${category}#${eObject}#${method}`;
    if (!(uniqueEventName in summaries)) {
      summaries[uniqueEventName] = ["parent", event, 1];
    } else {
      summaries[uniqueEventName][2]++;
    }
  });
  checkEventSummary(Object.values(summaries), true);

  // The following should not result in any recorded events.
  Telemetry.recordEvent("unknown.category", "test1", "object1");
  checkRecordingFailure(0 /* UnknownEvent */);
  Telemetry.recordEvent("telemetry.test", "unknown", "object1");
  checkRecordingFailure(0 /* UnknownEvent */);
  Telemetry.recordEvent("telemetry.test", "test1", "unknown");
  checkRecordingFailure(0 /* UnknownEvent */);

  let checkEvents = (events, expectedEvents) => {
    checkEventFormat(events);
    Assert.equal(
      events.length,
      expectedEvents.length,
      "Snapshot should have the right number of events."
    );

    for (let i = 0; i < events.length; ++i) {
      let { tsBefore, tsAfter } = expectedEvents[i];
      let ts = events[i][0];
      Assert.greaterOrEqual(
        ts,
        tsBefore,
        "The recorded timestamp should be greater than the one before recording."
      );
      Assert.lessOrEqual(
        ts,
        tsAfter,
        "The recorded timestamp should be less than the one after recording."
      );

      let recordedData = events[i].slice(1);
      let expectedData = expectedEvents[i].event.slice();
      Assert.deepEqual(
        recordedData,
        expectedData,
        "The recorded event data should match."
      );
    }
  };

  // Check that the expected events were recorded.
  let snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, false);
  Assert.ok("parent" in snapshot, "Should have entry for main process.");
  checkEvents(snapshot.parent, expected);

  // Check serializing only opt-out events.
  snapshot = Telemetry.snapshotEvents(ALL_CHANNELS, false);
  Assert.ok("parent" in snapshot, "Should have entry for main process.");
  let filtered = expected.filter(e => !!e.optout);
  checkEvents(snapshot.parent, filtered);
});

add_task(async function test_clear() {
  Telemetry.clearEvents();

  const COUNT = 10;
  for (let i = 0; i < COUNT; ++i) {
    Telemetry.recordEvent("telemetry.test", "test1", "object1");
    Telemetry.recordEvent("telemetry.test.second", "test", "object1");
  }

  // Check that events were recorded.
  // The events are cleared by passing the respective flag.
  let snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true);
  Assert.ok("parent" in snapshot, "Should have entry for main process.");
  Assert.equal(
    snapshot.parent.length,
    2 * COUNT,
    `Should have recorded ${2 * COUNT} events.`
  );

  // Now the events should be cleared.
  snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, false);
  Assert.equal(
    Object.keys(snapshot).length,
    0,
    `Should have cleared the events.`
  );

  for (let i = 0; i < COUNT; ++i) {
    Telemetry.recordEvent("telemetry.test", "test1", "object1");
    Telemetry.recordEvent("telemetry.test.second", "test", "object1");
  }
  snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true, 5);
  Assert.ok("parent" in snapshot, "Should have entry for main process.");
  Assert.equal(snapshot.parent.length, 5, "Should have returned 5 events");
  snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, false);
  Assert.ok("parent" in snapshot, "Should have entry for main process.");
  Assert.equal(
    snapshot.parent.length,
    2 * COUNT - 5,
    `Should have returned ${2 * COUNT - 5} events`
  );

  Telemetry.recordEvent("telemetry.test", "test1", "object1");
  snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, false, 5);
  Assert.ok("parent" in snapshot, "Should have entry for main process.");
  Assert.equal(snapshot.parent.length, 5, "Should have returned 5 events");
  snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true);
  Assert.ok("parent" in snapshot, "Should have entry for main process.");
  Assert.equal(
    snapshot.parent.length,
    2 * COUNT - 5 + 1,
    `Should have returned ${2 * COUNT - 5 + 1} events`
  );
});

add_task(async function test_expiry() {
  Telemetry.clearEvents();

  // Recording call with event that is expired by version.
  Telemetry.recordEvent("telemetry.test", "expired_version", "object1");
  checkRecordingFailure(1 /* Expired */);
  let snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true);
  Assert.equal(
    Object.keys(snapshot).length,
    0,
    "Should not record event with expired version."
  );

  // Recording call with event that has expiry_version set into the future.
  Telemetry.recordEvent("telemetry.test", "not_expired_optout", "object1");
  TelemetryTestUtils.assertNumberOfEvents(1);
});

add_task(async function test_invalidParams() {
  Telemetry.clearEvents();

  // Recording call with wrong type for value argument.
  Telemetry.recordEvent("telemetry.test", "test1", "object1", 1);
  let snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true);
  Assert.equal(
    Object.keys(snapshot).length,
    0,
    "Should not record event when value argument with invalid type is passed."
  );
  checkRecordingFailure(3 /* Value */);

  // Recording call with wrong type for extra argument.
  Telemetry.recordEvent("telemetry.test", "test1", "object1", null, "invalid");
  snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true);
  Assert.equal(
    Object.keys(snapshot).length,
    0,
    "Should not record event when extra argument with invalid type is passed."
  );
  checkRecordingFailure(4 /* Extra */);

  // Recording call with unknown extra key.
  Telemetry.recordEvent("telemetry.test", "test1", "object1", null, {
    key3: "x",
  });
  snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true);
  Assert.equal(
    Object.keys(snapshot).length,
    0,
    "Should not record event when extra argument with invalid key is passed."
  );
  checkRecordingFailure(2 /* ExtraKey */);

  // Recording call with invalid value type.
  Telemetry.recordEvent("telemetry.test", "test1", "object1", null, {
    key3: 1,
  });
  snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true);
  Assert.equal(
    Object.keys(snapshot).length,
    0,
    "Should not record event when extra argument with invalid value type is passed."
  );
  checkRecordingFailure(4 /* Extra */);
});

add_task(async function test_storageLimit() {
  Telemetry.clearEvents();

  let limitReached = TestUtils.topicObserved(
    "event-telemetry-storage-limit-reached"
  );
  // Record more events than the storage limit allows.
  let LIMIT = 1000;
  let COUNT = LIMIT + 10;
  for (let i = 0; i < COUNT; ++i) {
    Telemetry.recordEvent("telemetry.test", "test1", "object1", String(i));
  }

  await limitReached;
  Assert.ok(true, "Topic was notified when event limit was reached");

  // Check that the right events were recorded.
  let snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true);
  Assert.ok("parent" in snapshot, "Should have entry for main process.");
  let events = snapshot.parent;
  Assert.equal(
    events.length,
    COUNT,
    `Should have only recorded all ${COUNT} events`
  );
  Assert.ok(
    events.every((e, idx) => e[4] === String(idx)),
    "Should have recorded all events."
  );
});

add_task(async function test_valueLimits() {
  Telemetry.clearEvents();

  // Record values that are at or over the limits for string lengths.
  let LIMIT = 80;
  let expected = [
    ["telemetry.test", "test1", "object1", "a".repeat(LIMIT - 10), null],
    ["telemetry.test", "test1", "object1", "a".repeat(LIMIT), null],
    ["telemetry.test", "test1", "object1", "a".repeat(LIMIT + 1), null],
    ["telemetry.test", "test1", "object1", "a".repeat(LIMIT + 10), null],

    [
      "telemetry.test",
      "test1",
      "object1",
      null,
      { key1: "a".repeat(LIMIT - 10) },
    ],
    ["telemetry.test", "test1", "object1", null, { key1: "a".repeat(LIMIT) }],
    [
      "telemetry.test",
      "test1",
      "object1",
      null,
      { key1: "a".repeat(LIMIT + 1) },
    ],
    [
      "telemetry.test",
      "test1",
      "object1",
      null,
      { key1: "a".repeat(LIMIT + 10) },
    ],
  ];

  for (let event of expected) {
    Telemetry.recordEvent(...event);
    if (event[3]) {
      event[3] = event[3].substr(0, LIMIT);
    } else {
      event[3] = undefined;
    }
    if (event[4]) {
      event[4].key1 = event[4].key1.substr(0, LIMIT);
    }
  }

  // Strip off trailing null values to match the serialized events.
  for (let e of expected) {
    while (e.length >= 3 && e[e.length - 1] === null) {
      e.pop();
    }
  }

  // Check that the right events were recorded.
  TelemetryTestUtils.assertEvents(expected);
});

add_task(async function test_unicodeValues() {
  Telemetry.clearEvents();

  // Record string values containing unicode characters.
  let value = "漢語";
  Telemetry.recordEvent("telemetry.test", "test1", "object1", value);
  Telemetry.recordEvent("telemetry.test", "test1", "object1", null, {
    key1: value,
  });

  // Check that the values were correctly recorded.
  TelemetryTestUtils.assertEvents([{ value }, { extra: { key1: value } }]);
});

add_task(async function test_dynamicEvents() {
  Telemetry.clearEvents();
  Telemetry.clearScalars();
  Telemetry.canRecordExtended = true;

  // Register some test events.
  Telemetry.registerEvents("telemetry.test.dynamic", {
    // Event with only required fields.
    test1: {
      methods: ["test1"],
      objects: ["object1"],
    },
    // Event with extra_keys.
    test2: {
      methods: ["test2", "test2b"],
      objects: ["object1"],
      extra_keys: ["key1", "key2"],
    },
    // Expired event.
    test3: {
      methods: ["test3"],
      objects: ["object1"],
      expired: true,
    },
    // A release-channel recording event.
    test4: {
      methods: ["test4"],
      objects: ["object1"],
      record_on_release: true,
    },
  });

  // Record some valid events.
  Telemetry.recordEvent("telemetry.test.dynamic", "test1", "object1");
  Telemetry.recordEvent("telemetry.test.dynamic", "test2", "object1", null, {
    key1: "foo",
    key2: "bar",
  });
  Telemetry.recordEvent("telemetry.test.dynamic", "test2b", "object1", null, {
    key1: "foo",
    key2: "bar",
  });
  Telemetry.recordEvent(
    "telemetry.test.dynamic",
    "test3",
    "object1",
    "some value"
  );
  Telemetry.recordEvent("telemetry.test.dynamic", "test4", "object1", null);

  // Test recording an unknown event.
  Telemetry.recordEvent("telemetry.test.dynamic", "unknown", "unknown");
  checkRecordingFailure(0 /* UnknownEvent */);

  // Now check that the snapshot contains the expected data.
  let snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, false);
  Assert.ok(
    "dynamic" in snapshot,
    "Should have dynamic events in the snapshot."
  );

  let expected = [
    ["telemetry.test.dynamic", "test1", "object1"],
    [
      "telemetry.test.dynamic",
      "test2",
      "object1",
      null,
      { key1: "foo", key2: "bar" },
    ],
    [
      "telemetry.test.dynamic",
      "test2b",
      "object1",
      null,
      { key1: "foo", key2: "bar" },
    ],
    // "test3" is epxired, so it should not be recorded.
    ["telemetry.test.dynamic", "test4", "object1"],
  ];
  let events = snapshot.dynamic;
  Assert.equal(
    events.length,
    expected.length,
    "Should have recorded the right amount of events."
  );
  for (let i = 0; i < expected.length; ++i) {
    Assert.deepEqual(
      events[i].slice(1),
      expected[i],
      "Should have recorded the expected event data."
    );
  }

  // Check that we've summarized the recorded events
  checkEventSummary(
    expected.map(ev => ["dynamic", ev, 1]),
    true
  );

  // Check that the opt-out snapshot contains only the one expected event.
  snapshot = Telemetry.snapshotEvents(ALL_CHANNELS, false);
  Assert.ok(
    "dynamic" in snapshot,
    "Should have dynamic events in the snapshot."
  );
  Assert.equal(
    snapshot.dynamic.length,
    1,
    "Should have one opt-out event in the snapshot."
  );
  expected = ["telemetry.test.dynamic", "test4", "object1"];
  Assert.deepEqual(snapshot.dynamic[0].slice(1), expected);

  // Recording with unknown extra keys should be ignored and print an error.
  Telemetry.clearEvents();
  Telemetry.recordEvent("telemetry.test.dynamic", "test1", "object1", null, {
    key1: "foo",
  });
  Telemetry.recordEvent("telemetry.test.dynamic", "test2", "object1", null, {
    key1: "foo",
    unknown: "bar",
  });
  snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true);
  Assert.ok(
    !("dynamic" in snapshot),
    "Should have not recorded dynamic events with unknown extra keys."
  );

  // Other built-in events should not show up in the "dynamic" bucket of the snapshot.
  Telemetry.recordEvent("telemetry.test", "test1", "object1");
  snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true);
  Assert.ok(
    !("dynamic" in snapshot),
    "Should have not recorded built-in event into dynamic bucket."
  );

  // Test that recording opt-in and opt-out events works as expected.
  Telemetry.clearEvents();
  Telemetry.canRecordExtended = false;

  Telemetry.recordEvent("telemetry.test.dynamic", "test1", "object1");
  Telemetry.recordEvent("telemetry.test.dynamic", "test4", "object1");

  expected = [
    // Only "test4" should have been recorded.
    ["telemetry.test.dynamic", "test4", "object1"],
  ];
  snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true);
  Assert.equal(
    snapshot.dynamic.length,
    1,
    "Should have one opt-out event in the snapshot."
  );
  Assert.deepEqual(
    snapshot.dynamic.map(e => e.slice(1)),
    expected
  );
});

add_task(async function test_dynamicEventRegistrationValidation() {
  Telemetry.canRecordExtended = true;
  Telemetry.clearEvents();

  // Test registration of invalid categories.
  Telemetry.getSnapshotForHistograms("main", true); // Clear histograms before we begin.
  Assert.throws(
    () =>
      Telemetry.registerEvents("telemetry+test+dynamic", {
        test1: {
          methods: ["test1"],
          objects: ["object1"],
        },
      }),
    /Category parameter should match the identifier pattern\./,
    "Should throw when registering category names with invalid characters."
  );
  checkRegistrationFailure(2 /* Category */);
  Assert.throws(
    () =>
      Telemetry.registerEvents(
        "telemetry.test.test.test.test.test.test.test.test",
        {
          test1: {
            methods: ["test1"],
            objects: ["object1"],
          },
        }
      ),
    /Category parameter should match the identifier pattern\./,
    "Should throw when registering overly long category names."
  );
  checkRegistrationFailure(2 /* Category */);

  // Test registration of invalid event names.
  Assert.throws(
    () =>
      Telemetry.registerEvents("telemetry.test.dynamic1", {
        "test?1": {
          methods: ["test1"],
          objects: ["object1"],
        },
      }),
    /Event names should match the identifier pattern\./,
    "Should throw when registering event names with invalid characters."
  );
  checkRegistrationFailure(1 /* Name */);
  Assert.throws(
    () =>
      Telemetry.registerEvents("telemetry.test.dynamic2", {
        test1test1test1test1test1test1test1: {
          methods: ["test1"],
          objects: ["object1"],
        },
      }),
    /Event names should match the identifier pattern\./,
    "Should throw when registering overly long event names."
  );
  checkRegistrationFailure(1 /* Name */);

  // Test registration of invalid method names.
  Assert.throws(
    () =>
      Telemetry.registerEvents("telemetry.test.dynamic3", {
        test1: {
          methods: ["test?1"],
          objects: ["object1"],
        },
      }),
    /Method names should match the identifier pattern\./,
    "Should throw when registering method names with invalid characters."
  );
  checkRegistrationFailure(3 /* Method */);
  Assert.throws(
    () =>
      Telemetry.registerEvents("telemetry.test.dynamic", {
        test1: {
          methods: ["test1test1test1test1test1test1test1"],
          objects: ["object1"],
        },
      }),
    /Method names should match the identifier pattern\./,
    "Should throw when registering overly long method names."
  );
  checkRegistrationFailure(3 /* Method */);

  // Test registration of invalid object names.
  Assert.throws(
    () =>
      Telemetry.registerEvents("telemetry.test.dynamic4", {
        test1: {
          methods: ["test1"],
          objects: ["object?1"],
        },
      }),
    /Object names should match the identifier pattern\./,
    "Should throw when registering object names with invalid characters."
  );
  checkRegistrationFailure(4 /* Object */);
  Assert.throws(
    () =>
      Telemetry.registerEvents("telemetry.test.dynamic5", {
        test1: {
          methods: ["test1"],
          objects: ["object1object1object1object1object1object1"],
        },
      }),
    /Object names should match the identifier pattern\./,
    "Should throw when registering overly long object names."
  );
  checkRegistrationFailure(4 /* Object */);

  // Test validation of invalid key names.
  Assert.throws(
    () =>
      Telemetry.registerEvents("telemetry.test.dynamic6", {
        test1: {
          methods: ["test1"],
          objects: ["object1"],
          extra_keys: ["a?1"],
        },
      }),
    /Extra key names should match the identifier pattern\./,
    "Should throw when registering extra key names with invalid characters."
  );
  checkRegistrationFailure(5 /* ExtraKeys */);

  // Test validation of key names that are too long - we allow a maximum of 15 characters.
  Assert.throws(
    () =>
      Telemetry.registerEvents("telemetry.test.dynamic7", {
        test1: {
          methods: ["test1"],
          objects: ["object1"],
          extra_keys: ["a012345678901234"],
        },
      }),
    /Extra key names should match the identifier pattern\./,
    "Should throw when registering extra key names which are too long."
  );
  checkRegistrationFailure(5 /* ExtraKeys */);
  Telemetry.registerEvents("telemetry.test.dynamic8", {
    test1: {
      methods: ["test1"],
      objects: ["object1"],
      extra_keys: ["a01234567890123"],
    },
  });

  // Test validation of extra key count - we only allow 10.
  Assert.throws(
    () =>
      Telemetry.registerEvents("telemetry.test.dynamic9", {
        test1: {
          methods: ["test1"],
          objects: ["object1"],
          extra_keys: [
            "a1",
            "a2",
            "a3",
            "a4",
            "a5",
            "a6",
            "a7",
            "a8",
            "a9",
            "a10",
            "a11",
          ],
        },
      }),
    /No more than 10 extra keys can be registered\./,
    "Should throw when registering too many extra keys."
  );
  checkRegistrationFailure(5 /* ExtraKeys */);
  Telemetry.registerEvents("telemetry.test.dynamic10", {
    test1: {
      methods: ["test1"],
      objects: ["object1"],
      extra_keys: ["a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9", "a10"],
    },
  });
});

// When add-ons update, they may re-register some of the dynamic events.
// Test through some possible scenarios.
add_task(async function test_dynamicEventRegisterAgain() {
  Telemetry.canRecordExtended = true;
  Telemetry.clearEvents();

  const category = "telemetry.test.register.again";
  let events = {
    test1: {
      methods: ["test1"],
      objects: ["object1"],
    },
  };

  // First register the initial event and make sure it can be recorded.
  Telemetry.registerEvents(category, events);
  let expected = [[category, "test1", "object1"]];
  expected.forEach(e => Telemetry.recordEvent(...e));

  let snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true);
  Assert.equal(
    snapshot.dynamic.length,
    expected.length,
    "Should have right number of events in the snapshot."
  );
  Assert.deepEqual(
    snapshot.dynamic.map(e => e.slice(1)),
    expected
  );

  // Register the same event again and make sure it can still be recorded.
  Telemetry.registerEvents(category, events);
  Telemetry.recordEvent(category, "test1", "object1");

  snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true);
  Assert.equal(
    snapshot.dynamic.length,
    expected.length,
    "Should have right number of events in the snapshot."
  );
  Assert.deepEqual(
    snapshot.dynamic.map(e => e.slice(1)),
    expected
  );

  // Now register another event in the same category and make sure both events can be recorded.
  events.test2 = {
    methods: ["test2"],
    objects: ["object2"],
  };
  Telemetry.registerEvents(category, events);

  expected = [
    [category, "test1", "object1"],
    [category, "test2", "object2"],
  ];
  expected.forEach(e => Telemetry.recordEvent(...e));

  snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true);
  Assert.equal(
    snapshot.dynamic.length,
    expected.length,
    "Should have right number of events in the snapshot."
  );
  Assert.deepEqual(
    snapshot.dynamic.map(e => e.slice(1)),
    expected
  );

  // Check that adding a new object to an event entry works.
  events.test1.methods = ["test1a"];
  events.test2.objects = ["object2", "object2a"];
  Telemetry.registerEvents(category, events);

  expected = [
    [category, "test1", "object1"],
    [category, "test2", "object2"],
    [category, "test1a", "object1"],
    [category, "test2", "object2a"],
  ];
  expected.forEach(e => Telemetry.recordEvent(...e));

  snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true);
  Assert.equal(
    snapshot.dynamic.length,
    expected.length,
    "Should have right number of events in the snapshot."
  );
  Assert.deepEqual(
    snapshot.dynamic.map(e => e.slice(1)),
    expected
  );

  // Make sure that we can expire events that are already registered.
  events.test2.expired = true;
  Telemetry.registerEvents(category, events);

  expected = [[category, "test1", "object1"]];
  expected.forEach(e => Telemetry.recordEvent(...e));

  snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true);
  Assert.equal(
    snapshot.dynamic.length,
    expected.length,
    "Should have right number of events in the snapshot."
  );
  Assert.deepEqual(
    snapshot.dynamic.map(e => e.slice(1)),
    expected
  );
});

add_task(
  {
    skip_if: () => gIsAndroid,
  },
  async function test_productSpecificEvents() {
    const EVENT_CATEGORY = "telemetry.test";
    const DEFAULT_PRODUCTS_EVENT = "default_products";
    const DESKTOP_ONLY_EVENT = "desktop_only";
    const MULTIPRODUCT_EVENT = "multiproduct";
    const MOBILE_ONLY_EVENT = "mobile_only";

    Telemetry.clearEvents();

    // Try to record the desktop and multiproduct event
    Telemetry.recordEvent(EVENT_CATEGORY, DEFAULT_PRODUCTS_EVENT, "object1");
    Telemetry.recordEvent(EVENT_CATEGORY, DESKTOP_ONLY_EVENT, "object1");
    Telemetry.recordEvent(EVENT_CATEGORY, MULTIPRODUCT_EVENT, "object1");

    // Try to record the mobile-only event
    Telemetry.recordEvent(EVENT_CATEGORY, MOBILE_ONLY_EVENT, "object1");

    let events = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true).parent;

    let expected = [
      [EVENT_CATEGORY, DEFAULT_PRODUCTS_EVENT, "object1"],
      [EVENT_CATEGORY, DESKTOP_ONLY_EVENT, "object1"],
      [EVENT_CATEGORY, MULTIPRODUCT_EVENT, "object1"],
    ];
    Assert.equal(
      events.length,
      expected.length,
      "Should have recorded the right amount of events."
    );
    for (let i = 0; i < expected.length; ++i) {
      Assert.deepEqual(
        events[i].slice(1),
        expected[i],
        "Should have recorded the expected event data."
      );
    }
  }
);

add_task(
  {
    skip_if: () => !gIsAndroid,
  },
  async function test_mobileSpecificEvents() {
    const EVENT_CATEGORY = "telemetry.test";
    const DEFAULT_PRODUCTS_EVENT = "default_products";
    const DESKTOP_ONLY_EVENT = "desktop_only";
    const MULTIPRODUCT_EVENT = "multiproduct";
    const MOBILE_ONLY_EVENT = "mobile_only";

    Telemetry.clearEvents();

    // Try to record the mobile-only and multiproduct event
    Telemetry.recordEvent(EVENT_CATEGORY, DEFAULT_PRODUCTS_EVENT, "object1");
    Telemetry.recordEvent(EVENT_CATEGORY, MOBILE_ONLY_EVENT, "object1");
    Telemetry.recordEvent(EVENT_CATEGORY, MULTIPRODUCT_EVENT, "object1");

    // Try to record the mobile-only event
    Telemetry.recordEvent(EVENT_CATEGORY, DESKTOP_ONLY_EVENT, "object1");

    let events = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true).parent;

    let expected = [
      [EVENT_CATEGORY, DEFAULT_PRODUCTS_EVENT, "object1"],
      [EVENT_CATEGORY, MOBILE_ONLY_EVENT, "object1"],
      [EVENT_CATEGORY, MULTIPRODUCT_EVENT, "object1"],
    ];
    Assert.equal(
      events.length,
      expected.length,
      "Should have recorded the right amount of events."
    );
    for (let i = 0; i < expected.length; ++i) {
      Assert.deepEqual(
        events[i].slice(1),
        expected[i],
        "Should have recorded the expected event data."
      );
    }
  }
);
