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

/**
 * Return the path to the definitions file for the events.
 */
function getDefinitionsPath() {
  // Write the event definition to the spec file in the binary directory.
  let definitionFile = Cc["@mozilla.org/file/local;1"].createInstance(
    Ci.nsIFile
  );
  definitionFile = Services.dirsvc.get("GreD", Ci.nsIFile);
  definitionFile.append("EventArtifactDefinitions.json");
  return definitionFile.path;
}

add_task(async function test_setup() {
  do_get_profile();
});

add_task(
  {
    // The test needs to write a file, and that fails in tests on Android.
    // We don't really need the Android coverage, so skip on Android.
    skip_if: () => AppConstants.platform == "android",
  },
  async function test_invalidJSON() {
    const INVALID_JSON = "{ invalid,JSON { {1}";
    const FILE_PATH = getDefinitionsPath();

    // Write a corrupted JSON file.
    await IOUtils.writeUTF8(FILE_PATH, INVALID_JSON, {
      mode: "overwrite",
    });

    // Simulate Firefox startup. This should not throw!
    await TelemetryController.testSetup();
    await TelemetryController.testPromiseJsProbeRegistration();

    // Cleanup.
    await TelemetryController.testShutdown();
    await IOUtils.remove(FILE_PATH);
  }
);

add_task(
  {
    // The test needs to write a file, and that fails in tests on Android.
    // We don't really need the Android coverage, so skip on Android.
    skip_if: () => AppConstants.platform == "android",
  },
  async function test_dynamicBuiltin() {
    const DYNAMIC_EVENT_SPEC = {
      "telemetry.test.builtin": {
        test: {
          objects: ["object1", "object2"],
          expires: "never",
          methods: ["test1", "test2"],
          extra_keys: ["key2", "key1"],
          record_on_release: false,
        },
      },
      // Test a new, expired event
      "telemetry.test.expired": {
        expired: {
          objects: ["object1"],
          methods: ["method1"],
          expires: AppConstants.MOZ_APP_VERSION,
          record_on_release: false,
        },
      },
      // Test overwriting static expiries
      "telemetry.test": {
        expired_version: {
          objects: ["object1"],
          methods: ["expired_version"],
          expires: "never",
          record_on_release: false,
        },
        not_expired_optout: {
          objects: ["object1"],
          methods: ["not_expired_optout"],
          expires: AppConstants.MOZ_APP_VERSION,
          record_on_release: true,
        },
      },
    };

    Telemetry.clearEvents();

    // Let's write to the definition file to also cover the file
    // loading part.
    const FILE_PATH = getDefinitionsPath();
    await IOUtils.writeJSON(FILE_PATH, DYNAMIC_EVENT_SPEC);

    // Start TelemetryController to trigger loading the specs.
    await TelemetryController.testReset();
    await TelemetryController.testPromiseJsProbeRegistration();

    // Record the events
    const TEST_EVENT_NAME = "telemetry.test.builtin";
    const DYNAMIC_EVENT_CATEGORY = "telemetry.test.expired";
    const STATIC_EVENT_CATEGORY = "telemetry.test";
    Telemetry.setEventRecordingEnabled(TEST_EVENT_NAME, true);
    Telemetry.setEventRecordingEnabled(DYNAMIC_EVENT_CATEGORY, true);
    Telemetry.setEventRecordingEnabled(STATIC_EVENT_CATEGORY, true);
    Telemetry.recordEvent(TEST_EVENT_NAME, "test1", "object1");
    Telemetry.recordEvent(TEST_EVENT_NAME, "test2", "object1", null, {
      key1: "foo",
      key2: "bar",
    });
    Telemetry.recordEvent(TEST_EVENT_NAME, "test2", "object2", null, {
      key2: "bar",
    });
    Telemetry.recordEvent(DYNAMIC_EVENT_CATEGORY, "method1", "object1");
    Telemetry.recordEvent(STATIC_EVENT_CATEGORY, "expired_version", "object1");
    Telemetry.recordEvent(
      STATIC_EVENT_CATEGORY,
      "not_expired_optout",
      "object1"
    );

    // Check the values we tried to store.
    const snapshot = Telemetry.snapshotEvents(
      Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
      false
    );
    Assert.ok(
      "parent" in snapshot,
      "Should have parent events in the snapshot."
    );

    let expected = [
      [TEST_EVENT_NAME, "test1", "object1"],
      [TEST_EVENT_NAME, "test2", "object1", null, { key1: "foo", key2: "bar" }],
      [TEST_EVENT_NAME, "test2", "object2", null, { key2: "bar" }],
      [STATIC_EVENT_CATEGORY, "expired_version", "object1"],
    ];
    let events = snapshot.parent;
    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."
      );
    }

    // Clean up.
    await TelemetryController.testShutdown();
    await IOUtils.remove(FILE_PATH);
  }
);

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

  const TEST_EVENT_NAME = "telemetry.test.dynamicbuiltin";

  // Register some dynamic builtin test events.
  Telemetry.registerBuiltinEvents(TEST_EVENT_NAME, {
    // Event with only required fields.
    test1: {
      methods: ["test1"],
      objects: ["object1"],
    },
    // Event with extra_keys.
    test2: {
      methods: ["test2", "test2b"],
      objects: ["object1", "object2"],
      extra_keys: ["key1", "key2"],
    },
  });

  // Record some events.
  Telemetry.setEventRecordingEnabled(TEST_EVENT_NAME, true);
  Telemetry.recordEvent(TEST_EVENT_NAME, "test1", "object1");
  Telemetry.recordEvent(TEST_EVENT_NAME, "test2", "object1", null, {
    key1: "foo",
    key2: "bar",
  });
  Telemetry.recordEvent(TEST_EVENT_NAME, "test2b", "object2", null, {
    key2: "bar",
  });
  // Now check that the snapshot contains the expected data.
  let snapshot = Telemetry.snapshotEvents(
    Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
    false
  );
  Assert.ok("parent" in snapshot, "Should have parent events in the snapshot.");

  // For checking event summaries
  const scalars = Telemetry.getSnapshotForKeyedScalars("main", true);
  Assert.ok(
    "parent" in scalars,
    "Should have parent scalars in the main snapshot."
  );

  let expected = [
    [TEST_EVENT_NAME, "test1", "object1"],
    [TEST_EVENT_NAME, "test2", "object1", null, { key1: "foo", key2: "bar" }],
    [TEST_EVENT_NAME, "test2b", "object2", null, { key2: "bar" }],
  ];
  let events = snapshot.parent;
  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."
    );

    const uniqueEventName = `${expected[i][0]}#${expected[i][1]}#${expected[i][2]}`;
    const summaryCount =
      scalars.parent["telemetry.event_counts"][uniqueEventName];
    Assert.equal(1, summaryCount, `${uniqueEventName} had wrong summary count`);
  }
});

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

  const TEST_EVENT_NAME = "telemetry.test.offbydefault";

  // Register some dynamic builtin test events.
  Telemetry.registerBuiltinEvents(TEST_EVENT_NAME, {
    // Event with only required fields.
    test1: {
      methods: ["test1"],
      objects: ["object1"],
    },
  });

  // Record some events.
  // Explicitely _don't_ enable the category
  Telemetry.recordEvent(TEST_EVENT_NAME, "test1", "object1");

  // Now check that the snapshot contains the expected data.
  let snapshot = Telemetry.snapshotEvents(
    Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
    false
  );
  Assert.ok(
    !("parent" in snapshot),
    "Should not have parent events in the snapshot."
  );

  // Now enable the category and record again
  Telemetry.setEventRecordingEnabled(TEST_EVENT_NAME, true);
  Telemetry.recordEvent(TEST_EVENT_NAME, "test1", "object1");

  snapshot = Telemetry.snapshotEvents(
    Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
    false
  );
  Assert.ok("parent" in snapshot, "Should have parent events in the snapshot.");

  let expected = [[TEST_EVENT_NAME, "test1", "object1"]];
  let events = snapshot.parent;
  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(async function test_dynamicBuiltinDontOverwriteStaticData() {
  Telemetry.clearEvents();
  Telemetry.canRecordExtended = true;

  const TEST_STATIC_EVENT_NAME = "telemetry.test";
  const TEST_EVENT_NAME = "telemetry.test.nooverwrite";

  // Register some dynamic builtin test events.
  Telemetry.registerBuiltinEvents(TEST_EVENT_NAME, {
    dynamic: {
      methods: ["dynamic"],
      objects: ["builtin", "anotherone"],
    },
  });

  // First enable the categories we're using
  Telemetry.setEventRecordingEnabled(TEST_STATIC_EVENT_NAME, true);
  Telemetry.setEventRecordingEnabled(TEST_EVENT_NAME, true);

  // Now record some dynamic-builtin and static events
  Telemetry.recordEvent(TEST_EVENT_NAME, "dynamic", "builtin");
  Telemetry.recordEvent(TEST_STATIC_EVENT_NAME, "test1", "object1");
  Telemetry.recordEvent(TEST_EVENT_NAME, "dynamic", "anotherone");

  let snapshot = Telemetry.snapshotEvents(
    Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
    false
  );
  Assert.ok("parent" in snapshot, "Should have parent events in the snapshot.");

  // All events should now be recorded in the right order
  let expected = [
    [TEST_EVENT_NAME, "dynamic", "builtin"],
    [TEST_STATIC_EVENT_NAME, "test1", "object1"],
    [TEST_EVENT_NAME, "dynamic", "anotherone"],
  ];
  let events = snapshot.parent;
  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(async function test_dynamicBuiltinEventsOverridingStatic() {
  Telemetry.clearEvents();
  Telemetry.canRecordExtended = true;

  const TEST_EVENT_NAME = "telemetry.test";

  // Register dynamic builtin test events, overwriting existing one.
  Telemetry.registerBuiltinEvents(TEST_EVENT_NAME, {
    // Event with only required fields.
    test1: {
      methods: ["test1"],
      objects: ["object1", "object2"],
    },
    // Event with extra_keys.
    test2: {
      methods: ["test2"],
      objects: ["object1", "object2", "object3"],
      extra_keys: ["key1", "key2", "newdynamickey"],
    },
  });

  // Record some events that should be available in the static event already .
  Telemetry.setEventRecordingEnabled(TEST_EVENT_NAME, true);
  Telemetry.recordEvent(TEST_EVENT_NAME, "test1", "object1");
  Telemetry.recordEvent(TEST_EVENT_NAME, "test2", "object1", null, {
    key1: "foo",
    key2: "bar",
  });
  // Record events with newly added objects and keys.
  Telemetry.recordEvent(TEST_EVENT_NAME, "test2", "object2", null, {
    newdynamickey: "foo",
  });
  Telemetry.recordEvent(TEST_EVENT_NAME, "test2", "object3", null, {
    key1: "foo",
  });
  // Now check that the snapshot contains the expected data.
  let snapshot = Telemetry.snapshotEvents(
    Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
    false
  );
  Assert.ok("parent" in snapshot, "Should have parent events in the snapshot.");

  let expected = [
    [TEST_EVENT_NAME, "test1", "object1"],
    [TEST_EVENT_NAME, "test2", "object1", null, { key1: "foo", key2: "bar" }],
    [TEST_EVENT_NAME, "test2", "object2", null, { newdynamickey: "foo" }],
    [TEST_EVENT_NAME, "test2", "object3", null, { key1: "foo" }],
  ];
  let events = snapshot.parent;
  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(async function test_realDynamicDontOverwrite() {
  // Real dynamic events follow similar code paths internally.
  // Let's ensure they trigger the right code path and don't overwrite.

  Telemetry.clearEvents();
  Telemetry.canRecordExtended = true;

  const TEST_EVENT_NAME = "telemetry.test";

  // Register dynamic test events, this should not overwrite existing ones.
  Telemetry.registerEvents(TEST_EVENT_NAME, {
    // Event with only required fields.
    test1: {
      methods: ["test1"],
      objects: ["object1", "object2"],
    },
    // Event with extra_keys.
    test2: {
      methods: ["test2"],
      objects: ["object1", "object2", "object3"],
      extra_keys: ["key1", "key2", "realdynamic"],
    },
  });

  // Record some events that should be available in the static event already .
  Telemetry.setEventRecordingEnabled(TEST_EVENT_NAME, true);
  Telemetry.recordEvent(TEST_EVENT_NAME, "test1", "object1");
  Telemetry.recordEvent(TEST_EVENT_NAME, "test2", "object1", null, {
    key1: "foo",
    key2: "bar",
  });
  // Record events with newly added objects and keys.
  Telemetry.recordEvent(TEST_EVENT_NAME, "test2", "object2", null, {
    realdynamic: "foo",
  });
  Telemetry.recordEvent(TEST_EVENT_NAME, "test2", "object3", null, {
    key1: "foo",
  });
  // Now check that the snapshot contains the expected data.
  let snapshot = Telemetry.snapshotEvents(
    Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
    false
  );
  Assert.ok("parent" in snapshot, "Should have parent events in the snapshot.");

  let expected = [
    [TEST_EVENT_NAME, "test1", "object1"],
    [TEST_EVENT_NAME, "test2", "object1", null, { key1: "foo", key2: "bar" }],
    [TEST_EVENT_NAME, "test2", "object3", null, { key1: "foo" }],
  ];
  let events = snapshot.parent;
  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."
    );
  }
});
