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

"use strict";

const { AppConstants } = ChromeUtils.importESModule(
  "resource://gre/modules/AppConstants.sys.mjs"
);
const { setTimeout } = ChromeUtils.importESModule(
  "resource://gre/modules/Timer.sys.mjs"
);

function sleep(ms) {
  /* eslint-disable mozilla/no-arbitrary-setTimeout */
  return new Promise(resolve => setTimeout(resolve, ms));
}

add_setup(
  /* on Android FOG is set up through head.js */
  { skip_if: () => AppConstants.platform == "android" },
  function test_setup() {
    // FOG needs a profile directory to put its data in.
    do_get_profile();

    // We need to initialize it once, otherwise operations will be stuck in the pre-init queue.
    Services.fog.initializeFOG();
  }
);

add_task(function test_fog_counter_works() {
  Glean.testOnly.badCode.add(31);
  Assert.equal(31, Glean.testOnly.badCode.testGetValue("test-ping"));
});

add_task(async function test_fog_string_works() {
  Assert.equal(null, Glean.testOnly.cheesyString.testGetValue());

  // Setting `undefined` will be ignored.
  Glean.testOnly.cheesyString.set(undefined);
  Assert.equal(null, Glean.testOnly.cheesyString.testGetValue());

  const value = "a cheesy string!";
  Glean.testOnly.cheesyString.set(value);

  Assert.equal(value, Glean.testOnly.cheesyString.testGetValue("test-ping"));
});

add_task(async function test_fog_string_list_works() {
  const value = "a cheesy string!";
  const value2 = "a cheesier string!";
  const value3 = "the cheeziest of strings.";

  const cheeseList = [value, value2];
  Glean.testOnly.cheesyStringList.set(cheeseList);

  let val = Glean.testOnly.cheesyStringList.testGetValue();
  // Note: This is incredibly fragile and will break if we ever rearrange items
  // in the string list.
  Assert.deepEqual(cheeseList, val);

  Glean.testOnly.cheesyStringList.add(value3);
  Assert.ok(Glean.testOnly.cheesyStringList.testGetValue().includes(value3));
});

add_task(async function test_fog_timespan_works() {
  Glean.testOnly.canWeTimeIt.start();
  Glean.testOnly.canWeTimeIt.cancel();
  Assert.equal(undefined, Glean.testOnly.canWeTimeIt.testGetValue());

  // We start, briefly sleep and then stop.
  // That guarantees some time to measure.
  Glean.testOnly.canWeTimeIt.start();
  await sleep(10);
  Glean.testOnly.canWeTimeIt.stop();

  Assert.greater(Glean.testOnly.canWeTimeIt.testGetValue("test-ping"), 0);
});

add_task(async function test_fog_timespan_throws_on_stop_wout_start() {
  Glean.testOnly.canWeTimeIt.stop();
  Assert.throws(
    () => Glean.testOnly.canWeTimeIt.testGetValue(),
    /DataError/,
    "Should throw because stop was called without start."
  );
});

add_task(async function test_fog_uuid_works() {
  const kTestUuid = "decafdec-afde-cafd-ecaf-decafdecafde";
  Glean.testOnly.whatIdIt.set(kTestUuid);
  Assert.equal(kTestUuid, Glean.testOnly.whatIdIt.testGetValue("test-ping"));

  Glean.testOnly.whatIdIt.generateAndSet();
  // Since we generate v4 UUIDs, and the first character of the third group
  // isn't 4, this won't ever collide with kTestUuid.
  Assert.notEqual(kTestUuid, Glean.testOnly.whatIdIt.testGetValue("test-ping"));
});

add_task(function test_fog_datetime_works() {
  const value = new Date("2020-06-11T12:00:00");

  Glean.testOnly.whatADate.set(value.getTime() * 1000);

  const received = Glean.testOnly.whatADate.testGetValue("test-ping");
  Assert.equal(received.getTime(), value.getTime());
});

add_task(function test_fog_boolean_works() {
  Glean.testOnly.canWeFlagIt.set(false);
  Assert.equal(false, Glean.testOnly.canWeFlagIt.testGetValue("test-ping"));
  // While you're here, might as well test that the ping name's optional.
  Assert.equal(false, Glean.testOnly.canWeFlagIt.testGetValue());
});

add_task(async function test_fog_event_works() {
  Glean.testOnlyIpc.noExtraEvent.record();
  var events = Glean.testOnlyIpc.noExtraEvent.testGetValue();
  Assert.equal(1, events.length);
  Assert.equal("test_only.ipc", events[0].category);
  Assert.equal("no_extra_event", events[0].name);

  let extra = { extra1: "can set extras", extra2: "passing more data" };
  Glean.testOnlyIpc.anEvent.record(extra);
  events = Glean.testOnlyIpc.anEvent.testGetValue();
  Assert.equal(1, events.length);
  Assert.equal("test_only.ipc", events[0].category);
  Assert.equal("an_event", events[0].name);
  Assert.deepEqual(extra, events[0].extra);

  // Corner case: Event with extra with `undefined` value.
  // Should pretend that extra key isn't there.
  extra = { extra1: undefined, extra2: "defined" };
  Glean.testOnlyIpc.anEvent.record(extra);
  events = Glean.testOnlyIpc.anEvent.testGetValue();
  Assert.equal(2, events.length);
  Assert.deepEqual({ extra2: "defined" }, events[1].extra);

  let extra2 = {
    extra1: "can set extras",
    extra2: 37,
    extra3_longer_name: false,
  };
  Glean.testOnlyIpc.eventWithExtra.record(extra2);
  events = Glean.testOnlyIpc.eventWithExtra.testGetValue();
  Assert.equal(1, events.length);
  Assert.equal("test_only.ipc", events[0].category);
  Assert.equal("event_with_extra", events[0].name);
  let expectedExtra = {
    extra1: "can set extras",
    extra2: "37",
    extra3_longer_name: "false",
  };
  Assert.deepEqual(expectedExtra, events[0].extra);

  // camelCase extras work.
  let extra5 = {
    extra4CamelCase: false,
  };
  Glean.testOnlyIpc.eventWithExtra.record(extra5);
  events = Glean.testOnlyIpc.eventWithExtra.testGetValue();
  Assert.equal(2, events.length, "Recorded one event too many.");
  expectedExtra = {
    extra4CamelCase: "false",
  };
  Assert.deepEqual(expectedExtra, events[1].extra);

  // Passing `null` works.
  Glean.testOnlyIpc.eventWithExtra.record(null);
  events = Glean.testOnlyIpc.eventWithExtra.testGetValue();
  Assert.equal(3, events.length, "Recorded another event.");
  Assert.equal(events[2].extra, null);

  // Invalid extra keys don't crash, the event is not recorded,
  // but an error is recorded.
  let extra3 = {
    extra1_nonexistent_extra: "this does not crash",
  };
  Glean.testOnlyIpc.eventWithExtra.record(extra3);
  Assert.throws(
    () => Glean.testOnlyIpc.eventWithExtra.testGetValue(),
    /DataError/,
    "Should throw because of a recording error."
  );

  // Supplying extras when there aren't any defined results in the event not
  // being recorded, but an error is.
  Glean.testOnlyIpc.noExtraEvent.record(extra3);
  Assert.throws(
    () => Glean.testOnlyIpc.eventWithExtra.testGetValue(),
    /DataError/,
    "Should throw because of a recording error."
  );
});

add_task(async function test_fog_memory_distribution_works() {
  Glean.testOnly.doYouRemember.accumulate(7);
  Glean.testOnly.doYouRemember.accumulate(17);

  let data = Glean.testOnly.doYouRemember.testGetValue("test-ping");
  Assert.equal(2, data.count, "Count of entries is correct");
  // `data.sum` is in bytes, but the metric is in MB.
  Assert.equal(24 * 1024 * 1024, data.sum, "Sum's correct");
  for (let [bucket, count] of Object.entries(data.values)) {
    Assert.ok(
      count == 0 || (count == 1 && (bucket == 17520006 || bucket == 7053950)),
      "Only two buckets have a sample"
    );
  }
});

add_task(async function test_fog_custom_distribution_works() {
  Glean.testOnlyIpc.aCustomDist.accumulateSamples([7, 268435458]);

  let data = Glean.testOnlyIpc.aCustomDist.testGetValue("test-ping");
  Assert.equal(2, data.count, "Count of entries is correct");
  Assert.equal(7 + 268435458, data.sum, "Sum's correct");
  for (let [bucket, count] of Object.entries(data.values)) {
    Assert.ok(
      count == 0 || (count == 1 && (bucket == 1 || bucket == 268435456)),
      `Only two buckets have a sample ${bucket} ${count}`
    );
  }

  // Negative values will not be recorded, instead an error is recorded.
  Glean.testOnlyIpc.aCustomDist.accumulateSamples([-7]);
  Assert.throws(
    () => Glean.testOnlyIpc.aCustomDist.testGetValue(),
    /DataError/
  );
});

add_task(function test_fog_custom_pings() {
  Assert.ok("onePingOnly" in GleanPings);
  let submitted = false;
  Glean.testOnly.onePingOneBool.set(false);
  GleanPings.onePingOnly.testBeforeNextSubmit(() => {
    submitted = true;
    Assert.equal(false, Glean.testOnly.onePingOneBool.testGetValue());
  });
  GleanPings.onePingOnly.submit();
  Assert.ok(submitted, "Ping was submitted, callback was called.");
});

add_task(function test_recursive_testBeforeNextSubmit() {
  Assert.ok("onePingOnly" in GleanPings);
  let submitted = 0;
  let rec = () => {
    submitted++;
    GleanPings.onePingOnly.testBeforeNextSubmit(rec);
  };
  GleanPings.onePingOnly.testBeforeNextSubmit(rec);
  GleanPings.onePingOnly.submit();
  GleanPings.onePingOnly.submit();
  GleanPings.onePingOnly.submit();
  Assert.equal(3, submitted, "Ping was submitted 3 times");
  // Be kind and remove the callback.
  GleanPings.onePingOnly.testBeforeNextSubmit(() => {});
});

add_task(function test_testBeforeNextSubmit_error() {
  Assert.ok("onePingOnly" in GleanPings);

  let submitted = false;
  GleanPings.onePingOnly.testBeforeNextSubmit(() => {
    submitted = true;
    throw new Error("oh no");
  });

  Assert.throws(
    () => GleanPings.onePingOnly.submit(),
    /oh no/,
    "testBeforeNextSubmit error thrown from submit"
  );

  Assert.ok(submitted, "Did submit ping");
});

add_task(async function test_testSubmission() {
  Assert.ok("onePingOnly" in GleanPings);

  let submitReason = null;
  await GleanPings.onePingOnly.testSubmission(
    reason => (submitReason = reason),
    () => GleanPings.onePingOnly.submit("raison d'être")
  );

  Assert.equal(
    submitReason,
    "raison d'être",
    "ping callback called with correct reason"
  );
});

add_task(async function test_testSubmission_async() {
  Assert.ok("onePingOnly" in GleanPings);

  const orderOfOperations = [];

  // We are going to intentionally block submission so that we can delay it
  // until after testSubmission returns a promise.
  const blocker = Promise.withResolvers();

  const testPromise = GleanPings.onePingOnly.testSubmission(
    () => orderOfOperations.push("test-callback"),
    async () => {
      orderOfOperations.push("await-blocker");
      await blocker.promise;
      orderOfOperations.push("submit");
      GleanPings.onePingOnly.submit();
    }
  );
  orderOfOperations.push("test-submission-queued");

  blocker.resolve();
  await testPromise;

  Assert.deepEqual(orderOfOperations, [
    "await-blocker",
    "test-submission-queued",
    "submit",
    "test-callback",
  ]);
});

add_task(async function test_testSubmission_error() {
  Assert.ok("onePingOnly" in GleanPings);

  await Assert.rejects(
    GleanPings.onePingOnly.testSubmission(
      () => {
        throw new Error("uh oh");
      },
      () => GleanPings.onePingOnly.submit()
    ),
    /NS_ERROR_XPC_JAVASCRIPT_ERROR_WITH_DETAILS/,
    "testSubmission callback threw"
  );
});

add_task(async function test_testSubmission_unsubmitted() {
  Assert.ok("onePingOnly" in GleanPings);

  let submitted = false;
  await Assert.rejects(
    GleanPings.onePingOnly.testSubmission(
      () => (submitted = true),
      () => {}
    ),
    /Ping did not submit immediately/,
    "Threw immediately because the ping did not submit"
  );

  Assert.ok(!submitted, "callback not called");
});

add_task(async function test_testSubmission_timeout() {
  Assert.ok("onePingOnly" in GleanPings);

  let submitted = false;
  await Assert.rejects(
    GleanPings.onePingOnly.testSubmission(
      () => (submitted = true),
      () => {},
      1
    ),
    /Ping was not submitted after timeout/,
    "Threw after a timeout"
  );

  Assert.ok(!submitted, "callback not called");
});

add_task(async function test_fog_timing_distribution_works() {
  let t1 = Glean.testOnly.whatTimeIsIt.start();
  let t2 = Glean.testOnly.whatTimeIsIt.start();

  await sleep(5);

  let t3 = Glean.testOnly.whatTimeIsIt.start();
  Glean.testOnly.whatTimeIsIt.cancel(t1);

  await sleep(5);

  Glean.testOnly.whatTimeIsIt.stopAndAccumulate(t2); // 10ms
  Glean.testOnly.whatTimeIsIt.stopAndAccumulate(t3); // 5ms
  // samples are measured in microseconds, since that's the unit listed in metrics.yaml
  Glean.testOnly.whatTimeIsIt.accumulateSingleSample(5000); // 5ms
  Glean.testOnly.whatTimeIsIt.accumulateSamples([2000, 8000]); // 10ms

  let data = Glean.testOnly.whatTimeIsIt.testGetValue();

  // Cancelled timers should not be counted.
  Assert.equal(5, data.count, "Count of entries is correct");

  const NANOS_IN_MILLIS = 1e6;
  // bug 1701949 - Sleep gets close, but sometimes doesn't wait long enough.
  const EPSILON = 40000;

  // Variance in timing makes getting the sum impossible to know.
  Assert.greater(data.sum, 30 * NANOS_IN_MILLIS - EPSILON);

  // No guarantees from timers means no guarantees on buckets.
  // But we can guarantee it's only five samples.
  Assert.equal(
    5,
    Object.entries(data.values).reduce((acc, [, count]) => acc + count, 0),
    "Only five buckets with samples"
  );
});

add_task(async function test_fog_labels_conform() {
  Glean.testOnly.mabelsLabelMaker.singleword.set("portmanteau");
  Assert.equal(
    "portmanteau",
    Glean.testOnly.mabelsLabelMaker.singleword.testGetValue()
  );
  Glean.testOnly.mabelsLabelMaker.snake_case.set("snek");
  Assert.equal(
    "snek",
    Glean.testOnly.mabelsLabelMaker.snake_case.testGetValue()
  );
  Glean.testOnly.mabelsLabelMaker["dash-character"].set("Dash Rendar");
  Assert.equal(
    "Dash Rendar",
    Glean.testOnly.mabelsLabelMaker["dash-character"].testGetValue()
  );
  Glean.testOnly.mabelsLabelMaker["dot.separated"].set("dot product");
  Assert.equal(
    "dot product",
    Glean.testOnly.mabelsLabelMaker["dot.separated"].testGetValue()
  );
  Glean.testOnly.mabelsLabelMaker.camelCase.set("wednesday");
  Assert.equal(
    "wednesday",
    Glean.testOnly.mabelsLabelMaker.camelCase.testGetValue()
  );
  const veryLong = "1".repeat(112);
  Glean.testOnly.mabelsLabelMaker[veryLong].set("seventy-two");
  Assert.throws(
    () => Glean.testOnly.mabelsLabelMaker[veryLong].testGetValue(),
    /DataError/,
    "Should throw because of an invalid label."
  );
  // This test should _now_ throw because we are calling data after an invalid
  // label has been set.
  Assert.throws(
    () => Glean.testOnly.mabelsLabelMaker["dot.separated"].testGetValue(),
    /DataError/,
    "Should throw because of an invalid label."
  );
});

add_task(async function test_fog_labeled_boolean_works() {
  Assert.equal(
    undefined,
    Glean.testOnly.mabelsLikeBalloons.at_parties.testGetValue(),
    "New labels with no values should return undefined"
  );
  Glean.testOnly.mabelsLikeBalloons.at_parties.set(true);
  Glean.testOnly.mabelsLikeBalloons.at_funerals.set(false);
  Assert.equal(
    true,
    Glean.testOnly.mabelsLikeBalloons.at_parties.testGetValue()
  );
  Assert.equal(
    false,
    Glean.testOnly.mabelsLikeBalloons.at_funerals.testGetValue()
  );
  // What about invalid/__other__?
  Assert.equal(
    undefined,
    Glean.testOnly.mabelsLikeBalloons.__other__.testGetValue()
  );
  Glean.testOnly.mabelsLikeBalloons["1".repeat(112)].set(true);
  Assert.throws(
    () => Glean.testOnly.mabelsLikeBalloons.__other__.testGetValue(),
    /DataError/,
    "Should throw because of a recording error."
  );
});

add_task(async function test_fog_labeled_counter_works() {
  Assert.equal(
    undefined,
    Glean.testOnly.mabelsKitchenCounters.near_the_sink.testGetValue(),
    "New labels with no values should return undefined"
  );
  Glean.testOnly.mabelsKitchenCounters.near_the_sink.add(1);
  Glean.testOnly.mabelsKitchenCounters.with_junk_on_them.add(2);
  Assert.equal(
    1,
    Glean.testOnly.mabelsKitchenCounters.near_the_sink.testGetValue()
  );
  Assert.equal(
    2,
    Glean.testOnly.mabelsKitchenCounters.with_junk_on_them.testGetValue()
  );
  // What about invalid/__other__?
  Assert.equal(
    undefined,
    Glean.testOnly.mabelsKitchenCounters.__other__.testGetValue()
  );
  Glean.testOnly.mabelsKitchenCounters["1".repeat(112)].add(1);
  Assert.throws(
    () => Glean.testOnly.mabelsKitchenCounters.__other__.testGetValue(),
    /DataError/,
    "Should throw because of a recording error."
  );
});

add_task(async function test_fog_labeled_string_works() {
  Assert.equal(
    undefined,
    Glean.testOnly.mabelsBalloonStrings.colour_of_99.testGetValue(),
    "New labels with no values should return undefined"
  );
  Glean.testOnly.mabelsBalloonStrings.colour_of_99.set("crimson");
  Glean.testOnly.mabelsBalloonStrings.string_lengths.set("various");
  Assert.equal(
    "crimson",
    Glean.testOnly.mabelsBalloonStrings.colour_of_99.testGetValue()
  );
  Assert.equal(
    "various",
    Glean.testOnly.mabelsBalloonStrings.string_lengths.testGetValue()
  );
  // What about invalid/__other__?
  Assert.equal(
    undefined,
    Glean.testOnly.mabelsBalloonStrings.__other__.testGetValue()
  );
  Glean.testOnly.mabelsBalloonStrings["1".repeat(112)].set("valid");
  Assert.throws(
    () => Glean.testOnly.mabelsBalloonStrings.__other__.testGetValue(),
    /DataError/
  );
});

add_task(function test_fog_quantity_works() {
  Glean.testOnly.meaningOfLife.set(42);
  Assert.equal(42, Glean.testOnly.meaningOfLife.testGetValue());
});

add_task(function test_fog_rate_works() {
  // 1) Standard rate with internal denominator
  Glean.testOnlyIpc.irate.addToNumerator(22);
  Glean.testOnlyIpc.irate.addToDenominator(7);
  Assert.deepEqual(
    { numerator: 22, denominator: 7 },
    Glean.testOnlyIpc.irate.testGetValue()
  );

  // 2) Rate with external denominator
  Glean.testOnlyIpc.anExternalDenominator.add(11);
  Glean.testOnlyIpc.rateWithExternalDenominator.addToNumerator(121);
  Assert.equal(11, Glean.testOnlyIpc.anExternalDenominator.testGetValue());
  Assert.deepEqual(
    { numerator: 121, denominator: 11 },
    Glean.testOnlyIpc.rateWithExternalDenominator.testGetValue()
  );
});

add_task(async function test_fog_url_works() {
  const value = "https://www.example.com/fog";
  Glean.testOnlyIpc.aUrl.set(value);

  Assert.equal(value, Glean.testOnlyIpc.aUrl.testGetValue("test-ping"));
});

add_task(async function test_fog_text_works() {
  const value =
    "Before the risin' sun, we fly, So many roads to choose, We'll start out walkin' and learn to run, (We've only just begun)";
  Glean.testOnlyIpc.aText.set(value);

  let rslt = Glean.testOnlyIpc.aText.testGetValue();

  Assert.equal(value, rslt);

  Assert.equal(121, rslt.length);
});

add_task(async function test_fog_text_works_unusual_character() {
  const value =
    "The secret to Dominique Ansel's viennoiserie is the use of Isigny Sainte-Mère butter and Les Grands Moulins de Paris flour";
  Glean.testOnlyIpc.aText.set(value);

  let rslt = Glean.testOnlyIpc.aText.testGetValue();

  Assert.equal(value, rslt);

  Assert.greater(rslt.length, 100);
});

add_task(async function test_fog_object_works() {
  Assert.equal(
    undefined,
    Glean.testOnly.balloons.testGetValue(),
    "No object stored"
  );

  // Can't store not-objects.
  let invalidValues = [1, "str", false, undefined, null, NaN, Infinity];
  for (let value of invalidValues) {
    Assert.throws(
      () => Glean.testOnly.balloons.set(value),
      /is not an object/,
      "Should throw a type error"
    );
  }

  // No invalid value will be stored.
  Assert.equal(
    undefined,
    Glean.testOnly.balloons.testGetValue(),
    "No object stored"
  );

  // `JS_Stringify` internally throws
  // an `TypeError: cyclic object value` exception.
  // That's cleared and `set` should not throw on it.
  // This eventually should log a proper error in Glean.
  let selfref = {};
  selfref.a = selfref;
  Glean.testOnly.balloons.set(selfref);
  Assert.equal(
    undefined,
    Glean.testOnly.balloons.testGetValue(),
    "No object stored"
  );

  let balloons = [
    { colour: "red", diameter: 5 },
    { colour: "blue", diameter: 7 },
    { colour: "orange" },
  ];
  Glean.testOnly.balloons.set(balloons);

  let result = Glean.testOnly.balloons.testGetValue();
  let expected = [
    { colour: "red", diameter: 5 },
    { colour: "blue", diameter: 7 },
    { colour: "orange" },
  ];
  Assert.deepEqual(expected, result);
});

add_task(
  // FIXME(bug 1947194): JOG object metrics don't do schema validation yet
  {
    skip_if: () =>
      Services.prefs.getBoolPref("telemetry.fog.artifact_build", false),
  },
  async function test_fog_object_verifies_structure() {
    // These values are coerced to null or removed.
    let balloons = [
      { colour: "inf", diameter: Infinity },
      { colour: "negative-inf", diameter: -1 / 0 },
      { colour: "nan", diameter: NaN },
      { colour: "undef", diameter: undefined },
    ];
    Glean.testOnly.balloons.set(balloons);
    let result = Glean.testOnly.balloons.testGetValue();
    let expected = [
      { colour: "inf" },
      { colour: "negative-inf" },
      { colour: "nan" },
      { colour: "undef" },
    ];
    Assert.deepEqual(expected, result);

    // colour != color.
    let invalid = [{ color: "orange" }, { color: "red", diameter: "small" }];
    Glean.testOnly.balloons.set(invalid);
    Assert.throws(
      () => Glean.testOnly.balloons.testGetValue(),
      /invalid_value/,
      "Should throw because last object was invalid."
    );

    Services.fog.testResetFOG();
    // set again to ensure it's stored
    balloons = [
      { colour: "red", diameter: 5 },
      { colour: "blue", diameter: 7 },
    ];
    Glean.testOnly.balloons.set(balloons);
    result = Glean.testOnly.balloons.testGetValue();
    Assert.deepEqual(balloons, result);

    invalid = [{ colour: "red", diameter: 5, extra: "field" }];
    Glean.testOnly.balloons.set(invalid);
    Assert.throws(
      () => Glean.testOnly.balloons.testGetValue(),
      /invalid_value/,
      "Should throw because last object was invalid."
    );
  }
);

add_task(async function test_fog_complex_object_works() {
  if (!Glean.testOnly.crashStack) {
    // FIXME(bug 1883857): object metric type not available, e.g. in artifact builds.
    // Skipping this test.
    return;
  }

  Assert.equal(
    undefined,
    Glean.testOnly.crashStack.testGetValue(),
    "No object stored"
  );

  Glean.testOnly.crashStack.set({});
  let result = Glean.testOnly.crashStack.testGetValue();
  Assert.deepEqual({}, result);

  let stack = {
    status: "OK",
    crash_info: {
      typ: "main",
      address: "0xf001ba11",
      crashing_thread: 1,
    },
    main_module: 0,
    modules: [
      {
        base_addr: "0x00000000",
        end_addr: "0x00004000",
      },
    ],
  };

  Glean.testOnly.crashStack.set(stack);
  result = Glean.testOnly.crashStack.testGetValue();
  Assert.deepEqual(stack, result);

  stack = {
    status: "OK",
    modules: [
      {
        base_addr: "0x00000000",
        end_addr: "0x00004000",
      },
    ],
  };
  Glean.testOnly.crashStack.set(stack);
  result = Glean.testOnly.crashStack.testGetValue();
  Assert.deepEqual(stack, result);

  // FIXME(bug 1947194): JOG object metrics don't do schema validation yet
  if (!Services.prefs.getBoolPref("telemetry.fog.artifact_build", false)) {
    stack = {
      status: "OK",
      modules: [],
    };
    Glean.testOnly.crashStack.set(stack);
    result = Glean.testOnly.crashStack.testGetValue();
    Assert.deepEqual({ status: "OK" }, result);

    stack = {
      status: "OK",
    };
    Glean.testOnly.crashStack.set(stack);
    result = Glean.testOnly.crashStack.testGetValue();
    Assert.deepEqual(stack, result);
  }
});

add_task(
  // FIXME(1898464): ride-along pings are not handled correctly in artifact builds.
  {
    skip_if: () =>
      Services.prefs.getBoolPref("telemetry.fog.artifact_build", false),
  },
  function test_fog_ride_along_pings() {
    Assert.equal(null, Glean.testOnly.badCode.testGetValue("test-ping"));
    Assert.equal(null, Glean.testOnly.badCode.testGetValue("ride-along-ping"));

    Glean.testOnly.badCode.add(37);
    Assert.equal(37, Glean.testOnly.badCode.testGetValue("test-ping"));
    Assert.equal(37, Glean.testOnly.badCode.testGetValue("ride-along-ping"));

    let testPingSubmitted = false;

    GleanPings.testPing.testBeforeNextSubmit(() => {
      testPingSubmitted = true;
    });
    // FIXME(bug 1896356):
    // We can't use `testBeforeNextSubmit` for `ride-along-ping`
    // because it's triggered internally, but the callback would only be available
    // in the C++ bits, not in the internal Rust parts.

    // Submit only a single ping, the other will ride along.
    GleanPings.testPing.submit();

    Assert.ok(
      testPingSubmitted,
      "Test ping was submitted, callback was called."
    );

    // Both pings have been submitted, so the values should be cleared.
    Assert.equal(null, Glean.testOnly.badCode.testGetValue("test-ping"));
    Assert.equal(null, Glean.testOnly.badCode.testGetValue("ride-along-ping"));
  }
);

add_task(async function test_fog_labeled_custom_distribution_works() {
  Assert.equal(
    undefined,
    Glean.testOnly.mabelsCustomLabelLengths.monospace.testGetValue(),
    "New labels with no values should return undefined"
  );
  Glean.testOnly.mabelsCustomLabelLengths.monospace.accumulateSamples([1, 42]);
  Glean.testOnly.mabelsCustomLabelLengths.sanserif.accumulateSingleSample(13);
  let monospace =
    Glean.testOnly.mabelsCustomLabelLengths.monospace.testGetValue();
  Assert.equal(2, monospace.count);
  Assert.equal(43, monospace.sum);
  Assert.deepEqual({ 1: 2 }, monospace.values);
  let sanserif =
    Glean.testOnly.mabelsCustomLabelLengths.sanserif.testGetValue();
  Assert.equal(1, sanserif.count);
  Assert.equal(13, sanserif.sum);
  Assert.deepEqual({ 1: 1 }, sanserif.values);
  // What about invalid/__other__?
  Assert.equal(
    undefined,
    Glean.testOnly.mabelsCustomLabelLengths.__other__.testGetValue()
  );
  Glean.testOnly.mabelsCustomLabelLengths[
    "1".repeat(112)
  ].accumulateSingleSample(3);
  Assert.throws(
    () => Glean.testOnly.mabelsCustomLabelLengths.__other__.testGetValue(),
    /DataError/
  );
});

add_task(async function test_fog_labeled_memory_distribution_works() {
  Glean.testOnly.whatDoYouRemember.twenty_years_ago.accumulate(7);
  Glean.testOnly.whatDoYouRemember.twenty_years_ago.accumulate(17);

  let data = Glean.testOnly.whatDoYouRemember.twenty_years_ago.testGetValue();
  Assert.equal(2, data.count, "Count of entries is correct");
  // `data.sum` is in bytes, but the metric is in MB.
  Assert.equal(24 * 1024 * 1024, data.sum, "Sum's correct");
  for (let [bucket, count] of Object.entries(data.values)) {
    Assert.ok(
      count == 0 || (count == 1 && (bucket == 17520006 || bucket == 7053950)),
      "Only two buckets have a sample"
    );
  }
});

add_task(async function test_labeled_timing_distribution_works() {
  let t1 = Glean.testOnly.whereHasTheTimeGone.west.start();
  let t2 = Glean.testOnly.whereHasTheTimeGone.west.start();

  await sleep(5);

  let t3 = Glean.testOnly.whereHasTheTimeGone.west.start();
  Glean.testOnly.whereHasTheTimeGone.west.cancel(t1);

  await sleep(5);

  Glean.testOnly.whereHasTheTimeGone.west.stopAndAccumulate(t2); // 10ms
  Glean.testOnly.whereHasTheTimeGone.west.stopAndAccumulate(t3); // 5ms

  let data = Glean.testOnly.whereHasTheTimeGone.west.testGetValue();

  // Cancelled timers should not be counted.
  Assert.equal(2, data.count, "Count of entries is correct");

  const NANOS_IN_MILLIS = 1e6;
  // bug 1701949 - Sleep gets close, but sometimes doesn't wait long enough.
  const EPSILON = 40000;

  // Variance in timing makes getting the sum impossible to know.
  Assert.greater(data.sum, 15 * NANOS_IN_MILLIS - EPSILON);

  // No guarantees from timers means no guarantees on buckets.
  // But we can guarantee it's only two samples.
  Assert.equal(
    2,
    Object.entries(data.values).reduce((acc, [, count]) => acc + count, 0),
    "Only two buckets with samples"
  );
});

add_task(async function test_fog_labeled_quantity_works() {
  Assert.equal(
    undefined,
    Glean.testOnly.buttonJars.up.testGetValue(),
    "New labels with no values should return undefined"
  );
  Glean.testOnly.buttonJars.up.set(2);
  Glean.testOnly.buttonJars.curling.set(0);
  Assert.equal(2, Glean.testOnly.buttonJars.up.testGetValue());
  Assert.equal(0, Glean.testOnly.buttonJars.curling.testGetValue());
  // What about invalid/__other__?
  Assert.equal(undefined, Glean.testOnly.buttonJars.__other__.testGetValue());
  Glean.testOnly.buttonJars["1".repeat(112)].set(0);
  Assert.throws(
    () => Glean.testOnly.buttonJars.__other__.testGetValue(),
    /DataError/,
    "Should throw because of a recording error."
  );
});

add_task(async function test_submit_throws() {
  GleanPings.onePingOnly.testBeforeNextSubmit(() => {
    throw new Error("inside callback");
  });

  Assert.throws(
    () => GleanPings.onePingOnly.submit(),
    /inside callback/,
    "Should throw inside callback"
  );
});

add_task(function test_collection_disabled_pings_work() {
  // This test should work equally for full builds and artifact builds.

  Assert.ok("collectionDisabledPing" in GleanPings);

  // collection-enabled=false pings are disabled by default.
  // No data is collected for metrics going into that ping.
  Glean.testOnly.collectionDisabledCounter.add(1);
  Assert.equal(
    undefined,
    Glean.testOnly.collectionDisabledCounter.testGetValue()
  );

  // After enabling a ping we can record data into it
  GleanPings.collectionDisabledPing.setEnabled(true);
  Glean.testOnly.collectionDisabledCounter.add(2);
  Assert.equal(2, Glean.testOnly.collectionDisabledCounter.testGetValue());

  let submitted = false;
  GleanPings.collectionDisabledPing.testBeforeNextSubmit(() => {
    submitted = true;
    Assert.equal(2, Glean.testOnly.collectionDisabledCounter.testGetValue());
  });
  GleanPings.collectionDisabledPing.submit();
  Assert.ok(submitted, "Ping was submitted, callback was called.");
  Assert.equal(
    undefined,
    Glean.testOnly.collectionDisabledCounter.testGetValue()
  );
});

add_task(function test_dual_labeled_counter_works() {
  Glean.testOnly.keyedCategories.get("to the city", "lasered").add(1);
  Glean.testOnly.keyedCategories.get("to the city", "cut").add(4);
  Glean.testOnly.keyedCategories.get("to my heart", "polished").add(1);

  Assert.equal(
    1,
    Glean.testOnly.keyedCategories.get("to the city", "lasered").testGetValue()
  );
  Assert.equal(
    4,
    Glean.testOnly.keyedCategories.get("to the city", "cut").testGetValue()
  );
  Assert.equal(
    1,
    Glean.testOnly.keyedCategories.get("to my heart", "polished").testGetValue()
  );

  Assert.equal(
    undefined,
    Glean.testOnly.keyedCategories
      .get("to the city", "__other__")
      .testGetValue()
  );
  Glean.testOnly.keyedCategories.get("to the city", "cryptographic").add(3);
  Assert.equal(
    3,
    Glean.testOnly.keyedCategories
      .get("to the city", "__other__")
      .testGetValue()
  );
});

add_task(
  {
    skip_if: () =>
      Services.prefs.getBoolPref("telemetry.fog.artifact_build", true),
  },
  async function test_labeled_metrics_test_get_value_works() {
    Services.fog.testResetFOG();

    // LabeledBoolean
    Glean.testOnly.mabelsLikeBalloons.test1.set(true);
    Glean.testOnly.mabelsLikeBalloons.test2.set(false);

    const booleanValue = Glean.testOnly.mabelsLikeBalloons.testGetValue();
    Assert.deepEqual(
      {
        test1: true,
        test2: false,
      },
      booleanValue
    );

    // LabeledCounter
    Glean.testOnly.mabelsLabeledCounters.clean.add(1);

    const counterValue = Glean.testOnly.mabelsLabeledCounters.testGetValue();
    Assert.deepEqual(
      {
        clean: 1,
      },
      counterValue
    );

    // LabeledString
    Glean.testOnly.mabelsBalloonStrings.test.set("string!");

    const stringValue = Glean.testOnly.mabelsBalloonStrings.testGetValue();
    Assert.deepEqual(
      {
        test: "string!",
      },
      stringValue
    );

    // LabeledQuantity
    Glean.testOnly.buttonJars.up.set(1);
    Glean.testOnly.buttonJars.curling.set(10);

    const quantityValue = Glean.testOnly.buttonJars.testGetValue();
    Assert.deepEqual(
      {
        up: 1,
        curling: 10,
      },
      quantityValue
    );

    // LabeledCustomDistribution
    Glean.testOnly.mabelsCustomLabelLengths.monospace.accumulateSamples([
      1, 42,
    ]);
    Glean.testOnly.mabelsCustomLabelLengths.sanserif.accumulateSingleSample(13);

    const customDistributionValue =
      Glean.testOnly.mabelsCustomLabelLengths.testGetValue();
    Assert.deepEqual(
      {
        monospace: {
          count: 2,
          sum: 43,
          values: {
            1: 2,
          },
        },
        sanserif: {
          count: 1,
          sum: 13,
          values: {
            1: 1,
          },
        },
      },
      customDistributionValue
    );

    // LabeledMemoryDistribution
    Glean.testOnly.whatDoYouRemember.twenty_years_ago.accumulate(7);
    Glean.testOnly.whatDoYouRemember.twenty_years_ago.accumulate(17);

    const memoryDistributionValue =
      Glean.testOnly.whatDoYouRemember.testGetValue();
    const [first, second] = Object.keys(
      memoryDistributionValue.twenty_years_ago.values
    );
    Assert.deepEqual(
      {
        twenty_years_ago: {
          count: 2,
          sum: 24 * 1024 * 1024,
          values: {
            [first]: 1,
            [second]: 1,
          },
        },
      },
      memoryDistributionValue
    );

    // LabeledTimingDistribution
    let t1 = Glean.testOnly.whereHasTheTimeGone.west.start();
    let t2 = Glean.testOnly.whereHasTheTimeGone.west.start();
    await sleep(5);
    let t3 = Glean.testOnly.whereHasTheTimeGone.west.start();
    Glean.testOnly.whereHasTheTimeGone.west.cancel(t1);
    await sleep(5);
    Glean.testOnly.whereHasTheTimeGone.west.stopAndAccumulate(t2); // 10ms
    Glean.testOnly.whereHasTheTimeGone.west.stopAndAccumulate(t3); // 5ms

    const timingDistributionValue =
      Glean.testOnly.whereHasTheTimeGone.testGetValue();
    const NANOS_IN_MILLIS = 1e6;
    // bug 1701949 - Sleep gets close, but sometimes doesn't wait long enough.
    const EPSILON = 40000;

    // Variance in timing makes getting the sum impossible to know.
    const sum = timingDistributionValue.west.sum;
    Assert.greater(sum, 15 * NANOS_IN_MILLIS - EPSILON);
    Assert.equal(2, Object.keys(timingDistributionValue.west.values).length);
    Assert.deepEqual(
      {
        west: {
          sum,
          count: 2,
          values: timingDistributionValue.west.values,
        },
      },
      timingDistributionValue
    );
  }
);
