/* global Services */
import {
  actionCreators as ac,
  actionTypes as at,
  actionUtils as au,
} from "common/Actions.sys.mjs";
import {
  ASRouterEventPing,
  BasePing,
  ImpressionStatsPing,
  SessionPing,
  UserEventPing,
} from "test/schemas/pings";
import { FAKE_GLOBAL_PREFS, GlobalOverrider } from "test/unit/utils";
import { ASRouterPreferences } from "lib/ASRouterPreferences.jsm";
import injector from "inject!lib/TelemetryFeed.jsm";
import { MESSAGE_TYPE_HASH as msg } from "common/ActorConstants.sys.mjs";

const FAKE_UUID = "{foo-123-foo}";
const FAKE_ROUTER_MESSAGE_PROVIDER = [{ id: "cfr", enabled: true }];
const FAKE_TELEMETRY_ID = "foo123";

// eslint-disable-next-line max-statements
describe("TelemetryFeed", () => {
  let globals;
  let sandbox;
  let expectedUserPrefs;
  let browser = {
    getAttribute() {
      return "true";
    },
  };
  let instance;
  let clock;
  let fakeHomePageUrl;
  let fakeHomePage;
  let fakeExtensionSettingsStore;
  let ExperimentAPI = { getExperimentMetaData: () => {} };
  class PingCentre {
    sendPing() {}
    uninit() {}
    sendStructuredIngestionPing() {}
  }
  class UTEventReporting {
    sendUserEvent() {}
    sendSessionEndEvent() {}
    uninit() {}
  }

  // Reset the global prefs before importing the `TelemetryFeed` module, to
  // avoid a coverage miss caused by preference pollution when this test and
  // `ActivityStream.test.js` are run together.
  //
  // The `TelemetryFeed` module defines a lazy `contextId` getter, which the
  // `XPCOMUtils.defineLazyGetter` mock (defined in `unit-entry.js`) executes
  // immediately, as soon as the module is imported.
  //
  // If this test runs first, there's no coverage miss: this test will load
  // the `TelemetryFeed` module and run the lazy `contextId` getter, which will
  // generate a fake context ID and store it in `FAKE_GLOBAL_PREFS`, covering
  // all branches in the module. When `ActivityStream.test.js` runs, it'll load
  // `TelemetryFeed` and run the lazy getter a second time, which will use the
  // existing fake context ID from `FAKE_GLOBAL_PREFS` instead of generating a
  // new one.
  //
  // But, if `ActivityStream.test.js` runs first, then loading `TelemetryFeed` a
  // second time as part of this test will use the existing fake context ID from
  // `FAKE_GLOBAL_PREFS`, missing coverage for the branch to generate a new
  // context ID.
  FAKE_GLOBAL_PREFS.clear();

  const {
    TelemetryFeed,
    USER_PREFS_ENCODING,
    PREF_IMPRESSION_ID,
    TELEMETRY_PREF,
    EVENTS_TELEMETRY_PREF,
    STRUCTURED_INGESTION_ENDPOINT_PREF,
  } = injector({
    "lib/UTEventReporting.sys.mjs": { UTEventReporting },
  });

  beforeEach(() => {
    globals = new GlobalOverrider();
    sandbox = globals.sandbox;
    clock = sinon.useFakeTimers();
    fakeHomePageUrl = "about:home";
    fakeHomePage = {
      get() {
        return fakeHomePageUrl;
      },
    };
    fakeExtensionSettingsStore = {
      initialize() {
        return Promise.resolve();
      },
      getSetting() {},
    };
    sandbox.spy(global.console, "error");
    globals.set("AboutNewTab", {
      newTabURLOverridden: false,
      newTabURL: "",
    });
    globals.set("pktApi", {
      isUserLoggedIn: () => true,
    });
    globals.set("HomePage", fakeHomePage);
    globals.set("ExtensionSettingsStore", fakeExtensionSettingsStore);
    globals.set("PingCentre", PingCentre);
    globals.set("UTEventReporting", UTEventReporting);
    globals.set("ClientID", {
      getClientID: sandbox.spy(async () => FAKE_TELEMETRY_ID),
    });
    globals.set("ExperimentAPI", ExperimentAPI);

    sandbox
      .stub(ASRouterPreferences, "providers")
      .get(() => FAKE_ROUTER_MESSAGE_PROVIDER);
    instance = new TelemetryFeed();
  });
  afterEach(() => {
    clock.restore();
    globals.restore();
    FAKE_GLOBAL_PREFS.clear();
    ASRouterPreferences.uninit();
  });
  describe("#init", () => {
    it("should create an instance", () => {
      const testInstance = new TelemetryFeed();
      assert.isDefined(testInstance);
    });
    it("should add .pingCentre, a PingCentre instance", () => {
      assert.instanceOf(instance.pingCentre, PingCentre);
    });
    it("should add .utEvents, a UTEventReporting instance", () => {
      assert.instanceOf(instance.utEvents, UTEventReporting);
    });
    it("should make this.browserOpenNewtabStart() observe browser-open-newtab-start", () => {
      sandbox.spy(Services.obs, "addObserver");

      instance.init();

      assert.calledTwice(Services.obs.addObserver);
      assert.calledWithExactly(
        Services.obs.addObserver,
        instance.browserOpenNewtabStart,
        "browser-open-newtab-start"
      );
    });
    it("should add window open listener", () => {
      sandbox.spy(Services.obs, "addObserver");

      instance.init();

      assert.calledTwice(Services.obs.addObserver);
      assert.calledWithExactly(
        Services.obs.addObserver,
        instance._addWindowListeners,
        "domwindowopened"
      );
    });
    it("should add TabPinned event listener on new windows", () => {
      const stub = { addEventListener: sandbox.stub() };
      sandbox.spy(Services.obs, "addObserver");

      instance.init();

      assert.calledTwice(Services.obs.addObserver);
      const [cb] = Services.obs.addObserver.secondCall.args;
      cb(stub);
      assert.calledTwice(stub.addEventListener);
      assert.calledWithExactly(
        stub.addEventListener,
        "unload",
        instance.handleEvent
      );
      assert.calledWithExactly(
        stub.addEventListener,
        "TabPinned",
        instance.handleEvent
      );
    });
    it("should create impression id if none exists", () => {
      assert.equal(instance._impressionId, FAKE_UUID);
    });
    it("should set impression id if it exists", () => {
      FAKE_GLOBAL_PREFS.set(PREF_IMPRESSION_ID, "fakeImpressionId");
      assert.equal(new TelemetryFeed()._impressionId, "fakeImpressionId");
    });
    it("should register listeners on existing windows", () => {
      const stub = sandbox.stub();
      globals.set({
        Services: {
          ...Services,
          wm: { getEnumerator: () => [{ addEventListener: stub }] },
        },
      });

      instance.init();

      assert.calledTwice(stub);
      assert.calledWithExactly(stub, "unload", instance.handleEvent);
      assert.calledWithExactly(stub, "TabPinned", instance.handleEvent);
    });
    describe("telemetry pref changes from false to true", () => {
      beforeEach(() => {
        FAKE_GLOBAL_PREFS.set(TELEMETRY_PREF, false);
        instance = new TelemetryFeed();

        assert.propertyVal(instance, "telemetryEnabled", false);
      });

      it("should set the enabled property to true", () => {
        instance._prefs.set(TELEMETRY_PREF, true);

        assert.propertyVal(instance, "telemetryEnabled", true);
      });
    });
    describe("events telemetry pref changes from false to true", () => {
      beforeEach(() => {
        FAKE_GLOBAL_PREFS.set(EVENTS_TELEMETRY_PREF, false);
        instance = new TelemetryFeed();

        assert.propertyVal(instance, "eventTelemetryEnabled", false);
      });

      it("should set the enabled property to true", () => {
        instance._prefs.set(EVENTS_TELEMETRY_PREF, true);

        assert.propertyVal(instance, "eventTelemetryEnabled", true);
      });
    });
    it("should set two scalars for deletion-request", () => {
      sandbox.spy(Services.telemetry, "scalarSet");

      instance.init();

      assert.calledTwice(Services.telemetry.scalarSet);

      // impression_id
      let [type, value] = Services.telemetry.scalarSet.firstCall.args;
      assert.equal(type, "deletion.request.impression_id");
      assert.equal(value, instance._impressionId);

      // context_id
      [type, value] = Services.telemetry.scalarSet.secondCall.args;
      assert.equal(type, "deletion.request.context_id");
      assert.equal(value, FAKE_UUID);
    });
    describe("#_beginObservingNewtabPingPrefs", () => {
      it("should record initial metrics from newtab prefs", () => {
        FAKE_GLOBAL_PREFS.set(
          "browser.newtabpage.activity-stream.feeds.topsites",
          true
        );
        FAKE_GLOBAL_PREFS.set(
          "browser.newtabpage.activity-stream.topSitesRows",
          3
        );
        FAKE_GLOBAL_PREFS.set(
          "browser.topsites.blockedSponsors",
          '["mozilla"]'
        );

        sandbox.spy(Glean.topsites.enabled, "set");
        sandbox.spy(Glean.topsites.rows, "set");
        sandbox.spy(Glean.newtab.blockedSponsors, "set");

        instance = new TelemetryFeed();
        instance.init();

        assert.calledOnce(Glean.topsites.enabled.set);
        assert.calledWith(Glean.topsites.enabled.set, true);
        assert.calledOnce(Glean.topsites.rows.set);
        assert.calledWith(Glean.topsites.rows.set, 3);
        assert.calledOnce(Glean.newtab.blockedSponsors.set);
        assert.calledWith(Glean.newtab.blockedSponsors.set, ["mozilla"]);
      });

      it("should not record blocked sponsor metrics when bad json string is passed", () => {
        FAKE_GLOBAL_PREFS.set("browser.topsites.blockedSponsors", "BAD[JSON]");

        sandbox.spy(Glean.newtab.blockedSponsors, "set");

        instance = new TelemetryFeed();
        instance.init();

        assert.notCalled(Glean.newtab.blockedSponsors.set);
      });

      it("should record new metrics for newtab pref changes", () => {
        FAKE_GLOBAL_PREFS.set(
          "browser.newtabpage.activity-stream.topSitesRows",
          3
        );
        FAKE_GLOBAL_PREFS.set("browser.topsites.blockedSponsors", "[]");
        sandbox.spy(Glean.topsites.rows, "set");
        sandbox.spy(Glean.newtab.blockedSponsors, "set");

        instance = new TelemetryFeed();
        instance.init();

        Services.prefs.setIntPref(
          "browser.newtabpage.activity-stream.topSitesRows",
          2
        );

        Services.prefs.setStringPref(
          "browser.topsites.blockedSponsors",
          '["mozilla"]'
        );

        assert.calledTwice(Glean.topsites.rows.set);
        assert.calledWith(Glean.topsites.rows.set.firstCall, 3);
        assert.calledWith(Glean.topsites.rows.set.secondCall, 2);
        assert.calledWith(Glean.newtab.blockedSponsors.set.firstCall, []);
        assert.calledWith(Glean.newtab.blockedSponsors.set.secondCall, [
          "mozilla",
        ]);
      });
      it("should ignore changes to other prefs", () => {
        FAKE_GLOBAL_PREFS.set("some.other.pref", 123);
        FAKE_GLOBAL_PREFS.set(
          "browser.newtabpage.activity-stream.impressionId",
          "{foo-123-foo}"
        );

        instance = new TelemetryFeed();
        instance.init();

        Services.prefs.setIntPref("some.other.pref", 456);
        Services.prefs.setCharPref(
          "browser.newtabpage.activity-stream.impressionId",
          "{foo-456-foo}"
        );
      });
    });
  });
  describe("#handleEvent", () => {
    it("should dispatch a TAB_PINNED_EVENT", () => {
      sandbox.stub(instance, "sendEvent");
      globals.set({
        Services: {
          ...Services,
          wm: {
            getEnumerator: () => [{ gBrowser: { tabs: [{ pinned: true }] } }],
          },
        },
      });

      instance.handleEvent({ type: "TabPinned", target: {} });

      assert.calledOnce(instance.sendEvent);
      const [ping] = instance.sendEvent.firstCall.args;
      assert.propertyVal(ping, "event", "TABPINNED");
      assert.propertyVal(ping, "source", "TAB_CONTEXT_MENU");
      assert.propertyVal(ping, "session_id", "n/a");
      assert.propertyVal(ping.value, "total_pinned_tabs", 1);
    });
    it("should skip private windows", () => {
      sandbox.stub(instance, "sendEvent");
      globals.set({ PrivateBrowsingUtils: { isWindowPrivate: () => true } });

      instance.handleEvent({ type: "TabPinned", target: {} });

      assert.notCalled(instance.sendEvent);
    });
    it("should return the correct value for total_pinned_tabs", () => {
      sandbox.stub(instance, "sendEvent");
      globals.set({
        Services: {
          ...Services,
          wm: {
            getEnumerator: () => [
              {
                gBrowser: { tabs: [{ pinned: true }, { pinned: false }] },
              },
            ],
          },
        },
      });

      instance.handleEvent({ type: "TabPinned", target: {} });

      assert.calledOnce(instance.sendEvent);
      const [ping] = instance.sendEvent.firstCall.args;
      assert.propertyVal(ping, "event", "TABPINNED");
      assert.propertyVal(ping, "source", "TAB_CONTEXT_MENU");
      assert.propertyVal(ping, "session_id", "n/a");
      assert.propertyVal(ping.value, "total_pinned_tabs", 1);
    });
    it("should return the correct value for total_pinned_tabs (when private windows are open)", () => {
      sandbox.stub(instance, "sendEvent");
      const privateWinStub = sandbox
        .stub()
        .onCall(0)
        .returns(false)
        .onCall(1)
        .returns(true);
      globals.set({
        PrivateBrowsingUtils: { isWindowPrivate: privateWinStub },
      });
      globals.set({
        Services: {
          ...Services,
          wm: {
            getEnumerator: () => [
              {
                gBrowser: { tabs: [{ pinned: true }, { pinned: true }] },
              },
            ],
          },
        },
      });

      instance.handleEvent({ type: "TabPinned", target: {} });

      assert.calledOnce(instance.sendEvent);
      const [ping] = instance.sendEvent.firstCall.args;
      assert.propertyVal(ping.value, "total_pinned_tabs", 0);
    });
    it("should unregister the event listeners", () => {
      const stub = { removeEventListener: sandbox.stub() };

      instance.handleEvent({ type: "unload", target: stub });

      assert.calledTwice(stub.removeEventListener);
      assert.calledWithExactly(
        stub.removeEventListener,
        "unload",
        instance.handleEvent
      );
      assert.calledWithExactly(
        stub.removeEventListener,
        "TabPinned",
        instance.handleEvent
      );
    });
  });
  describe("#addSession", () => {
    it("should add a session and return it", () => {
      const session = instance.addSession("foo");

      assert.equal(instance.sessions.get("foo"), session);
    });
    it("should set the session_id", () => {
      sandbox.spy(Services.uuid, "generateUUID");

      const session = instance.addSession("foo");

      assert.calledOnce(Services.uuid.generateUUID);
      assert.equal(
        session.session_id,
        Services.uuid.generateUUID.firstCall.returnValue
      );
    });
    it("should set the page if a url parameter is given", () => {
      const session = instance.addSession("foo", "about:monkeys");

      assert.propertyVal(session, "page", "about:monkeys");
    });
    it("should set the page prop to 'unknown' if no URL parameter given", () => {
      const session = instance.addSession("foo");

      assert.propertyVal(session, "page", "unknown");
    });
    it("should set the perf type when lacking timestamp", () => {
      const session = instance.addSession("foo");

      assert.propertyVal(session.perf, "load_trigger_type", "unexpected");
    });
    it("should set load_trigger_type to first_window_opened on the first about:home seen", () => {
      const session = instance.addSession("foo", "about:home");

      assert.propertyVal(
        session.perf,
        "load_trigger_type",
        "first_window_opened"
      );
    });
    it("should not set load_trigger_type to first_window_opened on the second about:home seen", () => {
      instance.addSession("foo", "about:home");

      const session2 = instance.addSession("foo", "about:home");

      assert.notPropertyVal(
        session2.perf,
        "load_trigger_type",
        "first_window_opened"
      );
    });
    it("should set load_trigger_ts to the value of the process start timestamp", () => {
      const session = instance.addSession("foo", "about:home");

      assert.propertyVal(session.perf, "load_trigger_ts", 1588010448000);
    });
    it("should create a valid session ping on the first about:home seen", () => {
      // Add a session
      const portID = "foo";
      const session = instance.addSession(portID, "about:home");

      // Create a ping referencing the session
      const ping = instance.createSessionEndEvent(session);
      assert.validate(ping, SessionPing);
    });
    it("should be a valid ping with the data_late_by_ms perf", () => {
      // Add a session
      const portID = "foo";
      const session = instance.addSession(portID, "about:home");
      instance.saveSessionPerfData("foo", { topsites_data_late_by_ms: 10 });
      instance.saveSessionPerfData("foo", { highlights_data_late_by_ms: 20 });

      // Create a ping referencing the session
      const ping = instance.createSessionEndEvent(session);
      assert.validate(ping, SessionPing);
      assert.propertyVal(
        instance.sessions.get("foo").perf,
        "highlights_data_late_by_ms",
        20
      );
      assert.propertyVal(
        instance.sessions.get("foo").perf,
        "topsites_data_late_by_ms",
        10
      );
    });
    it("should be a valid ping with the topsites stats perf", () => {
      // Add a session
      const portID = "foo";
      const session = instance.addSession(portID, "about:home");
      instance.saveSessionPerfData("foo", {
        topsites_icon_stats: {
          custom_screenshot: 0,
          screenshot_with_icon: 2,
          screenshot: 1,
          tippytop: 2,
          rich_icon: 1,
          no_image: 0,
        },
        topsites_pinned: 3,
        topsites_search_shortcuts: 2,
      });

      // Create a ping referencing the session
      const ping = instance.createSessionEndEvent(session);
      assert.validate(ping, SessionPing);
      assert.propertyVal(
        instance.sessions.get("foo").perf.topsites_icon_stats,
        "screenshot_with_icon",
        2
      );
      assert.equal(instance.sessions.get("foo").perf.topsites_pinned, 3);
      assert.equal(
        instance.sessions.get("foo").perf.topsites_search_shortcuts,
        2
      );
    });
  });

  describe("#browserOpenNewtabStart", () => {
    it("should call ChromeUtils.addProfilerMarker with browser-open-newtab-start", () => {
      globals.set("ChromeUtils", {
        addProfilerMarker: sandbox.stub(),
      });

      sandbox.stub(global.Cu, "now").returns(12345);

      instance.browserOpenNewtabStart();

      assert.calledOnce(ChromeUtils.addProfilerMarker);
      assert.calledWithExactly(
        ChromeUtils.addProfilerMarker,
        "UserTiming",
        12345,
        "browser-open-newtab-start"
      );
    });
  });

  describe("#endSession", () => {
    it("should not throw if there is no session for the given port ID", () => {
      assert.doesNotThrow(() => instance.endSession("doesn't exist"));
    });
    it("should add a session_duration integer if there is a visibility_event_rcvd_ts", () => {
      sandbox.stub(instance, "sendEvent");
      const session = instance.addSession("foo");
      session.perf.visibility_event_rcvd_ts = 444.4732;

      instance.endSession("foo");

      assert.isNumber(session.session_duration);
      assert.ok(
        Number.isInteger(session.session_duration),
        "session_duration should be an integer"
      );
    });
    it("shouldn't send session ping if there's no visibility_event_rcvd_ts", () => {
      sandbox.stub(instance, "sendEvent");
      instance.addSession("foo");

      instance.endSession("foo");

      assert.notCalled(instance.sendEvent);
      assert.isFalse(instance.sessions.has("foo"));
    });
    it("should remove the session from .sessions", () => {
      sandbox.stub(instance, "sendEvent");
      instance.addSession("foo");

      instance.endSession("foo");

      assert.isFalse(instance.sessions.has("foo"));
    });
    it("should call createSessionSendEvent and sendEvent with the sesssion", () => {
      FAKE_GLOBAL_PREFS.set(TELEMETRY_PREF, true);
      FAKE_GLOBAL_PREFS.set(EVENTS_TELEMETRY_PREF, true);
      instance = new TelemetryFeed();

      sandbox.stub(instance, "sendEvent");
      sandbox.stub(instance, "createSessionEndEvent");
      sandbox.stub(instance.utEvents, "sendSessionEndEvent");
      const session = instance.addSession("foo");
      session.perf.visibility_event_rcvd_ts = 444.4732;

      instance.endSession("foo");

      // Did we call sendEvent with the result of createSessionEndEvent?
      assert.calledWith(instance.createSessionEndEvent, session);

      let sessionEndEvent =
        instance.createSessionEndEvent.firstCall.returnValue;
      assert.calledWith(instance.sendEvent, sessionEndEvent);
      assert.calledWith(instance.utEvents.sendSessionEndEvent, sessionEndEvent);
    });
  });
  describe("ping creators", () => {
    beforeEach(() => {
      for (const pref of Object.keys(USER_PREFS_ENCODING)) {
        FAKE_GLOBAL_PREFS.set(pref, true);
        expectedUserPrefs |= USER_PREFS_ENCODING[pref];
      }
      instance.init();
    });
    describe("#createPing", () => {
      it("should create a valid base ping without a session if no portID is supplied", async () => {
        const ping = await instance.createPing();
        assert.validate(ping, BasePing);
        assert.notProperty(ping, "session_id");
        assert.notProperty(ping, "page");
      });
      it("should create a valid base ping with session info if a portID is supplied", async () => {
        // Add a session
        const portID = "foo";
        instance.addSession(portID, "about:home");
        const sessionID = instance.sessions.get(portID).session_id;

        // Create a ping referencing the session
        const ping = await instance.createPing(portID);
        assert.validate(ping, BasePing);

        // Make sure we added the right session-related stuff to the ping
        assert.propertyVal(ping, "session_id", sessionID);
        assert.propertyVal(ping, "page", "about:home");
      });
      it("should create an unexpected base ping if no session yet portID is supplied", async () => {
        const ping = await instance.createPing("foo");

        assert.validate(ping, BasePing);
        assert.propertyVal(ping, "page", "unknown");
        assert.propertyVal(
          instance.sessions.get("foo").perf,
          "load_trigger_type",
          "unexpected"
        );
      });
      it("should create a base ping with user_prefs", async () => {
        const ping = await instance.createPing("foo");

        assert.validate(ping, BasePing);
        assert.propertyVal(ping, "user_prefs", expectedUserPrefs);
      });
    });
    describe("#createUserEvent", () => {
      it("should create a valid event", async () => {
        const portID = "foo";
        const data = { source: "TOP_SITES", event: "CLICK" };
        const action = ac.AlsoToMain(ac.UserEvent(data), portID);
        const session = instance.addSession(portID);

        const ping = await instance.createUserEvent(action);

        // Is it valid?
        assert.validate(ping, UserEventPing);
        // Does it have the right session_id?
        assert.propertyVal(ping, "session_id", session.session_id);
      });
    });
    describe("#createSessionEndEvent", () => {
      it("should create a valid event", async () => {
        const ping = await instance.createSessionEndEvent({
          session_id: FAKE_UUID,
          page: "about:newtab",
          session_duration: 12345,
          perf: {
            load_trigger_ts: 10,
            load_trigger_type: "menu_plus_or_keyboard",
            visibility_event_rcvd_ts: 20,
            is_preloaded: true,
          },
        });

        // Is it valid?
        assert.validate(ping, SessionPing);
        assert.propertyVal(ping, "session_id", FAKE_UUID);
        assert.propertyVal(ping, "page", "about:newtab");
        assert.propertyVal(ping, "session_duration", 12345);
      });
      it("should create a valid unexpected session event", async () => {
        const ping = await instance.createSessionEndEvent({
          session_id: FAKE_UUID,
          page: "about:newtab",
          session_duration: 12345,
          perf: {
            load_trigger_type: "unexpected",
            is_preloaded: true,
          },
        });

        // Is it valid?
        assert.validate(ping, SessionPing);
        assert.propertyVal(ping, "session_id", FAKE_UUID);
        assert.propertyVal(ping, "page", "about:newtab");
        assert.propertyVal(ping, "session_duration", 12345);
        assert.propertyVal(ping.perf, "load_trigger_type", "unexpected");
      });
    });
  });
  describe("#createImpressionStats", () => {
    it("should create a valid impression stats ping", async () => {
      const tiles = [{ id: 10001 }, { id: 10002 }, { id: 10003 }];
      const action = ac.ImpressionStats({ source: "POCKET", tiles });
      const ping = await instance.createImpressionStats(
        au.getPortIdOfSender(action),
        action.data
      );

      assert.validate(ping, ImpressionStatsPing);
      assert.propertyVal(ping, "source", "POCKET");
      assert.propertyVal(ping, "tiles", tiles);
    });
    it("should create a valid click ping", async () => {
      const tiles = [{ id: 10001, pos: 2 }];
      const action = ac.ImpressionStats({ source: "POCKET", tiles, click: 0 });
      const ping = await instance.createImpressionStats(
        au.getPortIdOfSender(action),
        action.data
      );

      assert.validate(ping, ImpressionStatsPing);
      assert.propertyVal(ping, "click", 0);
      assert.propertyVal(ping, "tiles", tiles);
    });
    it("should create a valid block ping", async () => {
      const tiles = [{ id: 10001, pos: 2 }];
      const action = ac.ImpressionStats({ source: "POCKET", tiles, block: 0 });
      const ping = await instance.createImpressionStats(
        au.getPortIdOfSender(action),
        action.data
      );

      assert.validate(ping, ImpressionStatsPing);
      assert.propertyVal(ping, "block", 0);
      assert.propertyVal(ping, "tiles", tiles);
    });
    it("should create a valid pocket ping", async () => {
      const tiles = [{ id: 10001, pos: 2 }];
      const action = ac.ImpressionStats({ source: "POCKET", tiles, pocket: 0 });
      const ping = await instance.createImpressionStats(
        au.getPortIdOfSender(action),
        action.data
      );

      assert.validate(ping, ImpressionStatsPing);
      assert.propertyVal(ping, "pocket", 0);
      assert.propertyVal(ping, "tiles", tiles);
    });
    it("should pass shim if it is available to impression ping", async () => {
      const tiles = [{ id: 10001, pos: 2, shim: 1234 }];
      const action = ac.ImpressionStats({ source: "POCKET", tiles });
      const ping = await instance.createImpressionStats(
        au.getPortIdOfSender(action),
        action.data
      );

      assert.propertyVal(ping, "tiles", tiles);
      assert.propertyVal(ping.tiles[0], "shim", tiles[0].shim);
    });
    it("should not include client_id and session_id", async () => {
      const tiles = [{ id: 10001 }, { id: 10002 }, { id: 10003 }];
      const action = ac.ImpressionStats({ source: "POCKET", tiles });
      const ping = await instance.createImpressionStats(
        au.getPortIdOfSender(action),
        action.data
      );

      assert.validate(ping, ImpressionStatsPing);
      assert.notProperty(ping, "client_id");
      assert.notProperty(ping, "session_id");
    });
  });
  describe("#applyCFRPolicy", () => {
    it("should use client_id and message_id in prerelease", async () => {
      globals.set("UpdateUtils", {
        getUpdateChannel() {
          return "nightly";
        },
      });
      const data = {
        action: "cfr_user_event",
        event: "IMPRESSION",
        message_id: "cfr_message_01",
        bucket_id: "cfr_bucket_01",
      };
      const { ping, pingType } = await instance.applyCFRPolicy(data);

      assert.equal(pingType, "cfr");
      assert.isUndefined(ping.impression_id);
      assert.propertyVal(ping, "client_id", FAKE_TELEMETRY_ID);
      assert.propertyVal(ping, "bucket_id", "cfr_bucket_01");
      assert.propertyVal(ping, "message_id", "cfr_message_01");
    });
    it("should use impression_id and bucket_id in release", async () => {
      globals.set("UpdateUtils", {
        getUpdateChannel() {
          return "release";
        },
      });
      const data = {
        action: "cfr_user_event",
        event: "IMPRESSION",
        message_id: "cfr_message_01",
        bucket_id: "cfr_bucket_01",
      };
      const { ping, pingType } = await instance.applyCFRPolicy(data);

      assert.equal(pingType, "cfr");
      assert.isUndefined(ping.client_id);
      assert.propertyVal(ping, "impression_id", FAKE_UUID);
      assert.propertyVal(ping, "message_id", "n/a");
      assert.propertyVal(ping, "bucket_id", "cfr_bucket_01");
    });
    it("should use client_id and message_id in the experiment cohort in release", async () => {
      globals.set("UpdateUtils", {
        getUpdateChannel() {
          return "release";
        },
      });
      sandbox.stub(ExperimentAPI, "getExperimentMetaData").returns({
        slug: "SOME-CFR-EXP",
      });
      const data = {
        action: "cfr_user_event",
        event: "IMPRESSION",
        message_id: "cfr_message_01",
        bucket_id: "cfr_bucket_01",
      };
      const { ping, pingType } = await instance.applyCFRPolicy(data);

      assert.equal(pingType, "cfr");
      assert.isUndefined(ping.impression_id);
      assert.propertyVal(ping, "client_id", FAKE_TELEMETRY_ID);
      assert.propertyVal(ping, "bucket_id", "cfr_bucket_01");
      assert.propertyVal(ping, "message_id", "cfr_message_01");
    });
    it("should use impression_id and bucket_id in Private Browsing", async () => {
      globals.set("UpdateUtils", {
        getUpdateChannel() {
          return "release";
        },
      });
      const data = {
        action: "cfr_user_event",
        event: "IMPRESSION",
        is_private: true,
        message_id: "cfr_message_01",
        bucket_id: "cfr_bucket_01",
      };
      const { ping, pingType } = await instance.applyCFRPolicy(data);

      assert.equal(pingType, "cfr");
      assert.isUndefined(ping.client_id);
      assert.propertyVal(ping, "impression_id", FAKE_UUID);
      assert.propertyVal(ping, "message_id", "n/a");
      assert.propertyVal(ping, "bucket_id", "cfr_bucket_01");
    });
    it("should use client_id and message_id in the experiment cohort in Private Browsing", async () => {
      globals.set("UpdateUtils", {
        getUpdateChannel() {
          return "release";
        },
      });
      sandbox.stub(ExperimentAPI, "getExperimentMetaData").returns({
        slug: "SOME-CFR-EXP",
      });
      const data = {
        action: "cfr_user_event",
        event: "IMPRESSION",
        is_private: true,
        message_id: "cfr_message_01",
        bucket_id: "cfr_bucket_01",
      };
      const { ping, pingType } = await instance.applyCFRPolicy(data);

      assert.equal(pingType, "cfr");
      assert.isUndefined(ping.impression_id);
      assert.propertyVal(ping, "client_id", FAKE_TELEMETRY_ID);
      assert.propertyVal(ping, "bucket_id", "cfr_bucket_01");
      assert.propertyVal(ping, "message_id", "cfr_message_01");
    });
  });
  describe("#applyWhatsNewPolicy", () => {
    it("should set client_id and set pingType", async () => {
      const { ping, pingType } = await instance.applyWhatsNewPolicy({});

      assert.propertyVal(ping, "client_id", FAKE_TELEMETRY_ID);
      assert.equal(pingType, "whats-new-panel");
    });
  });
  describe("#applyInfoBarPolicy", () => {
    it("should set client_id and set pingType", async () => {
      const { ping, pingType } = await instance.applyInfoBarPolicy({});

      assert.propertyVal(ping, "client_id", FAKE_TELEMETRY_ID);
      assert.equal(pingType, "infobar");
    });
  });
  describe("#applyToastNotificationPolicy", () => {
    it("should set client_id and set pingType", async () => {
      const { ping, pingType } = await instance.applyToastNotificationPolicy(
        {}
      );

      assert.propertyVal(ping, "client_id", FAKE_TELEMETRY_ID);
      assert.equal(pingType, "toast_notification");
    });
  });
  describe("#applySpotlightPolicy", () => {
    it("should set client_id and set pingType", async () => {
      let pingData = { action: "foo" };
      const { ping, pingType } = await instance.applySpotlightPolicy(pingData);

      assert.propertyVal(ping, "client_id", FAKE_TELEMETRY_ID);
      assert.equal(pingType, "spotlight");
      assert.notProperty(ping, "action");
    });
  });
  describe("#applyMomentsPolicy", () => {
    it("should use client_id and message_id in prerelease", async () => {
      globals.set("UpdateUtils", {
        getUpdateChannel() {
          return "nightly";
        },
      });
      const data = {
        action: "moments_user_event",
        event: "IMPRESSION",
        message_id: "moments_message_01",
        bucket_id: "moments_bucket_01",
      };
      const { ping, pingType } = await instance.applyMomentsPolicy(data);

      assert.equal(pingType, "moments");
      assert.isUndefined(ping.impression_id);
      assert.propertyVal(ping, "client_id", FAKE_TELEMETRY_ID);
      assert.propertyVal(ping, "bucket_id", "moments_bucket_01");
      assert.propertyVal(ping, "message_id", "moments_message_01");
    });
    it("should use impression_id and bucket_id in release", async () => {
      globals.set("UpdateUtils", {
        getUpdateChannel() {
          return "release";
        },
      });
      const data = {
        action: "moments_user_event",
        event: "IMPRESSION",
        message_id: "moments_message_01",
        bucket_id: "moments_bucket_01",
      };
      const { ping, pingType } = await instance.applyMomentsPolicy(data);

      assert.equal(pingType, "moments");
      assert.isUndefined(ping.client_id);
      assert.propertyVal(ping, "impression_id", FAKE_UUID);
      assert.propertyVal(ping, "message_id", "n/a");
      assert.propertyVal(ping, "bucket_id", "moments_bucket_01");
    });
    it("should use client_id and message_id in the experiment cohort in release", async () => {
      globals.set("UpdateUtils", {
        getUpdateChannel() {
          return "release";
        },
      });
      sandbox.stub(ExperimentAPI, "getExperimentMetaData").returns({
        slug: "SOME-CFR-EXP",
      });
      const data = {
        action: "moments_user_event",
        event: "IMPRESSION",
        message_id: "moments_message_01",
        bucket_id: "moments_bucket_01",
      };
      const { ping, pingType } = await instance.applyMomentsPolicy(data);

      assert.equal(pingType, "moments");
      assert.isUndefined(ping.impression_id);
      assert.propertyVal(ping, "client_id", FAKE_TELEMETRY_ID);
      assert.propertyVal(ping, "bucket_id", "moments_bucket_01");
      assert.propertyVal(ping, "message_id", "moments_message_01");
    });
  });
  describe("#applySnippetsPolicy", () => {
    it("should include client_id", async () => {
      const data = {
        action: "snippets_user_event",
        event: "IMPRESSION",
        message_id: "snippets_message_01",
      };
      const { ping, pingType } = await instance.applySnippetsPolicy(data);

      assert.equal(pingType, "snippets");
      assert.propertyVal(ping, "client_id", FAKE_TELEMETRY_ID);
      assert.propertyVal(ping, "message_id", "snippets_message_01");
    });
  });
  describe("#applyOnboardingPolicy", () => {
    it("should include client_id", async () => {
      const data = {
        action: "onboarding_user_event",
        event: "CLICK_BUTTION",
        message_id: "onboarding_message_01",
      };
      const { ping, pingType } = await instance.applyOnboardingPolicy(data);

      assert.equal(pingType, "onboarding");
      assert.propertyVal(ping, "client_id", FAKE_TELEMETRY_ID);
      assert.propertyVal(ping, "message_id", "onboarding_message_01");
      assert.propertyVal(ping, "browser_session_id", "fake_session_id");
    });
    it("should include page to event_context if there is a session", async () => {
      const data = {
        action: "onboarding_user_event",
        event: "CLICK_BUTTION",
        message_id: "onboarding_message_01",
      };
      const session = { page: "about:welcome" };
      const { ping, pingType } = await instance.applyOnboardingPolicy(
        data,
        session
      );

      assert.equal(pingType, "onboarding");
      assert.propertyVal(
        ping,
        "event_context",
        JSON.stringify({ page: "about:welcome" })
      );
      assert.propertyVal(ping, "message_id", "onboarding_message_01");
    });
    it("should not set page if it is not in ONBOARDING_ALLOWED_PAGE_VALUES", async () => {
      const data = {
        action: "onboarding_user_event",
        event: "CLICK_BUTTION",
        message_id: "onboarding_message_01",
      };
      const session = { page: "foo" };
      const { ping, pingType } = await instance.applyOnboardingPolicy(
        data,
        session
      );

      assert.calledOnce(global.console.error);
      assert.equal(pingType, "onboarding");
      assert.propertyVal(ping, "event_context", JSON.stringify({}));
      assert.propertyVal(ping, "message_id", "onboarding_message_01");
    });
    it("should append page to event_context if it is not empty", async () => {
      const data = {
        action: "onboarding_user_event",
        event: "CLICK_BUTTION",
        message_id: "onboarding_message_01",
        event_context: JSON.stringify({ foo: "bar" }),
      };
      const session = { page: "about:welcome" };
      const { ping, pingType } = await instance.applyOnboardingPolicy(
        data,
        session
      );

      assert.equal(pingType, "onboarding");
      assert.propertyVal(
        ping,
        "event_context",
        JSON.stringify({ foo: "bar", page: "about:welcome" })
      );
      assert.propertyVal(ping, "message_id", "onboarding_message_01");
    });
    it("should append page to event_context if it is not a JSON serialized string", async () => {
      const data = {
        action: "onboarding_user_event",
        event: "CLICK_BUTTION",
        message_id: "onboarding_message_01",
        event_context: "foo",
      };
      const session = { page: "about:welcome" };
      const { ping, pingType } = await instance.applyOnboardingPolicy(
        data,
        session
      );

      assert.equal(pingType, "onboarding");
      assert.propertyVal(
        ping,
        "event_context",
        JSON.stringify({ value: "foo", page: "about:welcome" })
      );
      assert.propertyVal(ping, "message_id", "onboarding_message_01");
    });
  });
  describe("#applyUndesiredEventPolicy", () => {
    it("should exclude client_id and use impression_id", () => {
      const data = {
        action: "asrouter_undesired_event",
        event: "RS_MISSING_DATA",
      };
      const { ping, pingType } = instance.applyUndesiredEventPolicy(data);

      assert.equal(pingType, "undesired-events");
      assert.isUndefined(ping.client_id);
      assert.propertyVal(ping, "impression_id", FAKE_UUID);
    });
  });
  describe("#createASRouterEvent", () => {
    it("should create a valid AS Router event", async () => {
      const data = {
        action: "snippets_user_event",
        event: "CLICK",
        message_id: "snippets_message_01",
      };
      const action = ac.ASRouterUserEvent(data);
      const { ping } = await instance.createASRouterEvent(action);

      assert.validate(ping, ASRouterEventPing);
      assert.propertyVal(ping, "event", "CLICK");
    });
    it("should call applyCFRPolicy if action equals to cfr_user_event", async () => {
      const data = {
        action: "cfr_user_event",
        event: "IMPRESSION",
        message_id: "cfr_message_01",
      };
      sandbox.stub(instance, "applyCFRPolicy");
      const action = ac.ASRouterUserEvent(data);
      await instance.createASRouterEvent(action);

      assert.calledOnce(instance.applyCFRPolicy);
    });
    it("should call applySnippetsPolicy if action equals to snippets_user_event", async () => {
      const data = {
        action: "snippets_user_event",
        event: "IMPRESSION",
        message_id: "snippets_message_01",
      };
      sandbox.stub(instance, "applySnippetsPolicy");
      const action = ac.ASRouterUserEvent(data);
      await instance.createASRouterEvent(action);

      assert.calledOnce(instance.applySnippetsPolicy);
    });
    it("should call applySnippetsPolicy if action equals to snippets_local_testing_user_event", async () => {
      const data = {
        action: "snippets_local_testing_user_event",
        event: "IMPRESSION",
        message_id: "snippets_message_01",
      };
      sandbox.stub(instance, "applySnippetsPolicy");
      const action = ac.ASRouterUserEvent(data);
      await instance.createASRouterEvent(action);

      assert.calledOnce(instance.applySnippetsPolicy);
    });
    it("should call applyOnboardingPolicy if action equals to onboarding_user_event", async () => {
      const data = {
        action: "onboarding_user_event",
        event: "CLICK_BUTTON",
        message_id: "onboarding_message_01",
      };
      sandbox.stub(instance, "applyOnboardingPolicy");
      const action = ac.ASRouterUserEvent(data);
      await instance.createASRouterEvent(action);

      assert.calledOnce(instance.applyOnboardingPolicy);
    });
    it("should call applyWhatsNewPolicy if action equals to whats-new-panel_user_event", async () => {
      const data = {
        action: "whats-new-panel_user_event",
        event: "CLICK_BUTTON",
        message_id: "whats-new-panel_message_01",
      };
      sandbox.stub(instance, "applyWhatsNewPolicy");
      const action = ac.ASRouterUserEvent(data);
      await instance.createASRouterEvent(action);

      assert.calledOnce(instance.applyWhatsNewPolicy);
    });
    it("should call applyMomentsPolicy if action equals to moments_user_event", async () => {
      const data = {
        action: "moments_user_event",
        event: "CLICK_BUTTON",
        message_id: "moments_message_01",
      };
      sandbox.stub(instance, "applyMomentsPolicy");
      const action = ac.ASRouterUserEvent(data);
      await instance.createASRouterEvent(action);

      assert.calledOnce(instance.applyMomentsPolicy);
    });
    it("should call applySpotlightPolicy if action equals to spotlight_user_event", async () => {
      const data = {
        action: "spotlight_user_event",
        event: "CLICK",
        message_id: "SPOTLIGHT_MESSAGE_93",
      };
      sandbox.stub(instance, "applySpotlightPolicy");
      const action = ac.ASRouterUserEvent(data);
      await instance.createASRouterEvent(action);

      assert.calledOnce(instance.applySpotlightPolicy);
    });
    it("should call applyToastNotificationPolicy if action equals to toast_notification_user_event", async () => {
      const data = {
        action: "toast_notification_user_event",
        event: "IMPRESSION",
        message_id: "TEST_TOAST_NOTIFICATION1",
      };
      sandbox.stub(instance, "applyToastNotificationPolicy");
      const action = ac.ASRouterUserEvent(data);
      await instance.createASRouterEvent(action);

      assert.calledOnce(instance.applyToastNotificationPolicy);
    });
    it("should call applyUndesiredEventPolicy if action equals to asrouter_undesired_event", async () => {
      const data = {
        action: "asrouter_undesired_event",
        event: "UNDESIRED_EVENT",
      };
      sandbox.stub(instance, "applyUndesiredEventPolicy");
      const action = ac.ASRouterUserEvent(data);
      await instance.createASRouterEvent(action);

      assert.calledOnce(instance.applyUndesiredEventPolicy);
    });
    it("should stringify event_context if it is an Object", async () => {
      const data = {
        action: "asrouter_undesired_event",
        event: "UNDESIRED_EVENT",
        event_context: { foo: "bar" },
      };
      const action = ac.ASRouterUserEvent(data);
      const { ping } = await instance.createASRouterEvent(action);

      assert.propertyVal(ping, "event_context", JSON.stringify({ foo: "bar" }));
    });
    it("should not stringify event_context if it is a String", async () => {
      const data = {
        action: "asrouter_undesired_event",
        event: "UNDESIRED_EVENT",
        event_context: "foo",
      };
      const action = ac.ASRouterUserEvent(data);
      const { ping } = await instance.createASRouterEvent(action);

      assert.propertyVal(ping, "event_context", "foo");
    });
  });
  describe("#sendEventPing", () => {
    it("should call sendStructuredIngestionEvent", async () => {
      const data = {
        action: "activity_stream_user_event",
        event: "CLICK",
      };
      instance = new TelemetryFeed();
      sandbox.spy(instance, "sendStructuredIngestionEvent");

      await instance.sendEventPing(data);

      const expectedPayload = {
        client_id: FAKE_TELEMETRY_ID,
        event: "CLICK",
        browser_session_id: "fake_session_id",
      };
      assert.calledWith(instance.sendStructuredIngestionEvent, expectedPayload);
    });
    it("should stringify value if it is an Object", async () => {
      const data = {
        action: "activity_stream_user_event",
        event: "CLICK",
        value: { foo: "bar" },
      };
      instance = new TelemetryFeed();
      sandbox.spy(instance, "sendStructuredIngestionEvent");

      await instance.sendEventPing(data);

      const expectedPayload = {
        client_id: FAKE_TELEMETRY_ID,
        event: "CLICK",
        browser_session_id: "fake_session_id",
        value: JSON.stringify({ foo: "bar" }),
      };
      assert.calledWith(instance.sendStructuredIngestionEvent, expectedPayload);
    });
  });
  describe("#sendSessionPing", () => {
    it("should call sendStructuredIngestionEvent", async () => {
      const data = {
        action: "activity_stream_session",
        page: "about:home",
        session_duration: 10000,
      };
      instance = new TelemetryFeed();
      sandbox.spy(instance, "sendStructuredIngestionEvent");

      await instance.sendSessionPing(data);

      const expectedPayload = {
        client_id: FAKE_TELEMETRY_ID,
        page: "about:home",
        session_duration: 10000,
      };
      assert.calledWith(instance.sendStructuredIngestionEvent, expectedPayload);
    });
  });
  describe("#sendEvent", () => {
    it("should call sendEventPing on activity_stream_user_event", () => {
      FAKE_GLOBAL_PREFS.set(TELEMETRY_PREF, true);
      const event = { action: "activity_stream_user_event" };
      instance = new TelemetryFeed();
      sandbox.spy(instance, "sendEventPing");

      instance.sendEvent(event);

      assert.calledOnce(instance.sendEventPing);
    });
    it("should call sendSessionPing on activity_stream_session", () => {
      FAKE_GLOBAL_PREFS.set(TELEMETRY_PREF, true);
      const event = { action: "activity_stream_session" };
      instance = new TelemetryFeed();
      sandbox.spy(instance, "sendSessionPing");

      instance.sendEvent(event);

      assert.calledOnce(instance.sendSessionPing);
    });
  });
  describe("#sendUTEvent", () => {
    it("should call the UT event function passed in", async () => {
      FAKE_GLOBAL_PREFS.set(TELEMETRY_PREF, true);
      FAKE_GLOBAL_PREFS.set(EVENTS_TELEMETRY_PREF, true);
      const event = {};
      instance = new TelemetryFeed();
      sandbox.stub(instance.utEvents, "sendUserEvent");

      await instance.sendUTEvent(event, instance.utEvents.sendUserEvent);

      assert.calledWith(instance.utEvents.sendUserEvent, event);
    });
  });
  describe("#sendStructuredIngestionEvent", () => {
    it("should call PingCentre sendStructuredIngestionPing", async () => {
      FAKE_GLOBAL_PREFS.set(TELEMETRY_PREF, true);
      const event = {};
      instance = new TelemetryFeed();
      sandbox.stub(instance.pingCentre, "sendStructuredIngestionPing");

      await instance.sendStructuredIngestionEvent(
        event,
        "http://foo.com/base/"
      );

      assert.calledWith(instance.pingCentre.sendStructuredIngestionPing, event);
    });
  });
  describe("#setLoadTriggerInfo", () => {
    it("should call saveSessionPerfData w/load_trigger_{ts,type} data", () => {
      sandbox.stub(global.Cu, "now").returns(12345);

      globals.set("ChromeUtils", {
        addProfilerMarker: sandbox.stub(),
      });

      instance.browserOpenNewtabStart();

      const stub = sandbox.stub(instance, "saveSessionPerfData");
      instance.addSession("port123");

      instance.setLoadTriggerInfo("port123");

      assert.calledWith(stub, "port123", {
        load_trigger_ts: 1588010448000 + 12345,
        load_trigger_type: "menu_plus_or_keyboard",
      });
    });

    it("should not call saveSessionPerfData when getting mark throws", () => {
      const stub = sandbox.stub(instance, "saveSessionPerfData");
      instance.addSession("port123");

      instance.setLoadTriggerInfo("port123");

      assert.notCalled(stub);
    });
  });

  describe("#saveSessionPerfData", () => {
    it("should update the given session with the given data", () => {
      instance.addSession("port123");
      assert.notProperty(instance.sessions.get("port123"), "fake_ts");
      const data = { fake_ts: 456, other_fake_ts: 789 };

      instance.saveSessionPerfData("port123", data);

      assert.include(instance.sessions.get("port123").perf, data);
    });

    it("should call setLoadTriggerInfo if data has visibility_event_rcvd_ts", () => {
      sandbox.stub(instance, "setLoadTriggerInfo");
      instance.addSession("port123");
      const data = { visibility_event_rcvd_ts: 444455 };

      instance.saveSessionPerfData("port123", data);

      assert.calledOnce(instance.setLoadTriggerInfo);
      assert.calledWithExactly(instance.setLoadTriggerInfo, "port123");
      assert.include(instance.sessions.get("port123").perf, data);
    });

    it("shouldn't call setLoadTriggerInfo if data has no visibility_event_rcvd_ts", () => {
      sandbox.stub(instance, "setLoadTriggerInfo");
      instance.addSession("port123");

      instance.saveSessionPerfData("port123", { monkeys_ts: 444455 });

      assert.notCalled(instance.setLoadTriggerInfo);
    });

    it("should not call setLoadTriggerInfo when url is about:home", () => {
      sandbox.stub(instance, "setLoadTriggerInfo");
      instance.addSession("port123", "about:home");
      const data = { visibility_event_rcvd_ts: 444455 };

      instance.saveSessionPerfData("port123", data);

      assert.notCalled(instance.setLoadTriggerInfo);
    });

    it("should call maybeRecordTopsitesPainted when url is about:home and topsites_first_painted_ts is given", () => {
      const topsites_first_painted_ts = 44455;
      const data = { topsites_first_painted_ts };
      const spy = sandbox.spy();

      sandbox.stub(Services.prefs, "getIntPref").returns(1);
      globals.set("AboutNewTab", {
        maybeRecordTopsitesPainted: spy,
      });
      instance.addSession("port123", "about:home");
      instance.saveSessionPerfData("port123", data);

      assert.calledOnce(spy);
      assert.calledWith(spy, topsites_first_painted_ts);
    });
    it("should record a Glean newtab.opened event with the correct visit_id when visibility event received", () => {
      const session_id = "decafc0ffee";
      const page = "about:newtab";
      const session = { page, perf: {}, session_id };
      const data = { visibility_event_rcvd_ts: 444455 };
      sandbox.stub(instance.sessions, "get").returns(session);

      sandbox.spy(Glean.newtab.opened, "record");
      instance.saveSessionPerfData("port123", data);

      assert.calledOnce(Glean.newtab.opened.record);
      assert.deepEqual(Glean.newtab.opened.record.firstCall.args[0], {
        newtab_visit_id: session_id,
        source: page,
      });
    });
  });
  describe("#uninit", () => {
    it("should call .pingCentre.uninit", () => {
      const stub = sandbox.stub(instance.pingCentre, "uninit");

      instance.uninit();

      assert.calledOnce(stub);
    });
    it("should call .utEvents.uninit", () => {
      const stub = sandbox.stub(instance.utEvents, "uninit");

      instance.uninit();

      assert.calledOnce(stub);
    });
    it("should make this.browserOpenNewtabStart() stop observing browser-open-newtab-start and domwindowopened", async () => {
      await instance.init();
      sandbox.spy(Services.obs, "removeObserver");
      sandbox.stub(instance.pingCentre, "uninit");

      await instance.uninit();

      assert.calledTwice(Services.obs.removeObserver);
      assert.calledWithExactly(
        Services.obs.removeObserver,
        instance.browserOpenNewtabStart,
        "browser-open-newtab-start"
      );
      assert.calledWithExactly(
        Services.obs.removeObserver,
        instance._addWindowListeners,
        "domwindowopened"
      );
    });
  });
  describe("#onAction", () => {
    beforeEach(() => {
      FAKE_GLOBAL_PREFS.clear();
    });
    it("should call .init() on an INIT action", () => {
      const init = sandbox.stub(instance, "init");
      const sendPageTakeoverData = sandbox.stub(
        instance,
        "sendPageTakeoverData"
      );

      instance.onAction({ type: at.INIT });

      assert.calledOnce(init);
      assert.calledOnce(sendPageTakeoverData);
    });
    it("should call .uninit() on an UNINIT action", () => {
      const stub = sandbox.stub(instance, "uninit");

      instance.onAction({ type: at.UNINIT });

      assert.calledOnce(stub);
    });
    it("should call .handleNewTabInit on a NEW_TAB_INIT action", () => {
      sandbox.spy(instance, "handleNewTabInit");

      instance.onAction(
        ac.AlsoToMain({
          type: at.NEW_TAB_INIT,
          data: { url: "about:newtab", browser },
        })
      );

      assert.calledOnce(instance.handleNewTabInit);
    });
    it("should call .addSession() on a NEW_TAB_INIT action", () => {
      const stub = sandbox.stub(instance, "addSession").returns({ perf: {} });
      sandbox.stub(instance, "setLoadTriggerInfo");

      instance.onAction(
        ac.AlsoToMain(
          {
            type: at.NEW_TAB_INIT,
            data: { url: "about:monkeys", browser },
          },
          "port123"
        )
      );

      assert.calledOnce(stub);
      assert.calledWith(stub, "port123", "about:monkeys");
    });
    it("should call .endSession() on a NEW_TAB_UNLOAD action", () => {
      const stub = sandbox.stub(instance, "endSession");

      instance.onAction(ac.AlsoToMain({ type: at.NEW_TAB_UNLOAD }, "port123"));

      assert.calledWith(stub, "port123");
    });
    it("should call .saveSessionPerfData on SAVE_SESSION_PERF_DATA", () => {
      const stub = sandbox.stub(instance, "saveSessionPerfData");
      const data = { some_ts: 10 };
      const action = { type: at.SAVE_SESSION_PERF_DATA, data };

      instance.onAction(ac.AlsoToMain(action, "port123"));

      assert.calledWith(stub, "port123", data);
    });
    it("should send an event on a TELEMETRY_USER_EVENT action", () => {
      FAKE_GLOBAL_PREFS.set(TELEMETRY_PREF, true);
      FAKE_GLOBAL_PREFS.set(EVENTS_TELEMETRY_PREF, true);
      instance = new TelemetryFeed();

      const sendEvent = sandbox.stub(instance, "sendEvent");
      const utSendUserEvent = sandbox.stub(instance.utEvents, "sendUserEvent");
      const eventCreator = sandbox.stub(instance, "createUserEvent");
      const action = { type: at.TELEMETRY_USER_EVENT };

      instance.onAction(action);

      assert.calledWith(eventCreator, action);
      assert.calledWith(sendEvent, eventCreator.returnValue);
      assert.calledWith(utSendUserEvent, eventCreator.returnValue);
    });
    it("should send an event on a DISCOVERY_STREAM_USER_EVENT action", () => {
      FAKE_GLOBAL_PREFS.set(TELEMETRY_PREF, true);
      FAKE_GLOBAL_PREFS.set(EVENTS_TELEMETRY_PREF, true);
      instance = new TelemetryFeed();

      const sendEvent = sandbox.stub(instance, "sendEvent");
      const utSendUserEvent = sandbox.stub(instance.utEvents, "sendUserEvent");
      const eventCreator = sandbox.stub(instance, "createUserEvent");
      const action = { type: at.DISCOVERY_STREAM_USER_EVENT };

      instance.onAction(action);

      assert.calledWith(eventCreator, {
        ...action,
        data: {
          value: {
            pocket_logged_in_status: true,
          },
        },
      });
      assert.calledWith(sendEvent, eventCreator.returnValue);
      assert.calledWith(utSendUserEvent, eventCreator.returnValue);
    });
    describe("should call handleASRouterUserEvent on x action", () => {
      const actions = [
        at.AS_ROUTER_TELEMETRY_USER_EVENT,
        msg.TOOLBAR_BADGE_TELEMETRY,
        msg.TOOLBAR_PANEL_TELEMETRY,
        msg.MOMENTS_PAGE_TELEMETRY,
        msg.DOORHANGER_TELEMETRY,
      ];
      actions.forEach(type => {
        it(`${type} action`, () => {
          FAKE_GLOBAL_PREFS.set(TELEMETRY_PREF, true);
          FAKE_GLOBAL_PREFS.set(EVENTS_TELEMETRY_PREF, true);
          instance = new TelemetryFeed();

          const eventHandler = sandbox.spy(instance, "handleASRouterUserEvent");
          const action = {
            type,
            data: { event: "CLICK" },
          };

          instance.onAction(action);

          assert.calledWith(eventHandler, action);
        });
      });
    });
    it("should send an event on a TELEMETRY_IMPRESSION_STATS action", () => {
      const sendEvent = sandbox.stub(instance, "sendStructuredIngestionEvent");
      const eventCreator = sandbox.stub(instance, "createImpressionStats");
      const tiles = [{ id: 10001 }, { id: 10002 }, { id: 10003 }];
      const action = ac.ImpressionStats({ source: "POCKET", tiles });

      instance.onAction(action);

      assert.calledWith(
        eventCreator,
        au.getPortIdOfSender(action),
        action.data
      );
      assert.calledWith(sendEvent, eventCreator.returnValue);
    });
    it("should call .handleDiscoveryStreamImpressionStats on a DISCOVERY_STREAM_IMPRESSION_STATS action", () => {
      const session = {};
      sandbox.stub(instance.sessions, "get").returns(session);
      const data = { source: "foo", tiles: [{ id: 1 }] };
      const action = { type: at.DISCOVERY_STREAM_IMPRESSION_STATS, data };
      sandbox.spy(instance, "handleDiscoveryStreamImpressionStats");

      instance.onAction(ac.AlsoToMain(action, "port123"));

      assert.calledWith(
        instance.handleDiscoveryStreamImpressionStats,
        "port123",
        data
      );
    });
    it("should call .handleDiscoveryStreamLoadedContent on a DISCOVERY_STREAM_LOADED_CONTENT action", () => {
      const session = {};
      sandbox.stub(instance.sessions, "get").returns(session);
      const data = { source: "foo", tiles: [{ id: 1 }] };
      const action = { type: at.DISCOVERY_STREAM_LOADED_CONTENT, data };
      sandbox.spy(instance, "handleDiscoveryStreamLoadedContent");

      instance.onAction(ac.AlsoToMain(action, "port123"));

      assert.calledWith(
        instance.handleDiscoveryStreamLoadedContent,
        "port123",
        data
      );
    });
    it("should call .handleTopSitesSponsoredImpressionStats on a TOP_SITES_SPONSORED_IMPRESSION_STATS action", () => {
      const session = {};
      sandbox.stub(instance.sessions, "get").returns(session);
      const data = { type: "impression", tile_id: 42, position: 1 };
      const action = { type: at.TOP_SITES_SPONSORED_IMPRESSION_STATS, data };
      sandbox.spy(instance, "handleTopSitesSponsoredImpressionStats");

      instance.onAction(ac.AlsoToMain(action));

      assert.calledOnce(instance.handleTopSitesSponsoredImpressionStats);
      assert.deepEqual(
        instance.handleTopSitesSponsoredImpressionStats.firstCall.args[0].data,
        data
      );
    });
  });
  it("should call .handleTopSitesOrganicImpressionStats on a TOP_SITES_ORGANIC_IMPRESSION_STATS action", () => {
    const session = {};
    sandbox.stub(instance.sessions, "get").returns(session);
    const data = { type: "impression", position: 1 };
    const action = { type: at.TOP_SITES_ORGANIC_IMPRESSION_STATS, data };
    sandbox.spy(instance, "handleTopSitesOrganicImpressionStats");

    instance.onAction(ac.AlsoToMain(action));

    assert.calledOnce(instance.handleTopSitesOrganicImpressionStats);
    assert.deepEqual(
      instance.handleTopSitesOrganicImpressionStats.firstCall.args[0].data,
      data
    );
  });
  describe("#handleNewTabInit", () => {
    it("should set the session as preloaded if the browser is preloaded", () => {
      const session = { perf: {} };
      let preloadedBrowser = {
        getAttribute() {
          return "preloaded";
        },
      };
      sandbox.stub(instance, "addSession").returns(session);

      instance.onAction(
        ac.AlsoToMain({
          type: at.NEW_TAB_INIT,
          data: { url: "about:newtab", browser: preloadedBrowser },
        })
      );

      assert.ok(session.perf.is_preloaded);
    });
    it("should set the session as non-preloaded if the browser is non-preloaded", () => {
      const session = { perf: {} };
      let nonPreloadedBrowser = {
        getAttribute() {
          return "";
        },
      };
      sandbox.stub(instance, "addSession").returns(session);

      instance.onAction(
        ac.AlsoToMain({
          type: at.NEW_TAB_INIT,
          data: { url: "about:newtab", browser: nonPreloadedBrowser },
        })
      );

      assert.ok(!session.perf.is_preloaded);
    });
  });
  describe("#SendASRouterUndesiredEvent", () => {
    it("should call handleASRouterUserEvent", () => {
      let stub = sandbox.stub(instance, "handleASRouterUserEvent");

      instance.SendASRouterUndesiredEvent({ foo: "bar" });

      assert.calledOnce(stub);
      let [payload] = stub.firstCall.args;
      assert.propertyVal(payload.data, "action", "asrouter_undesired_event");
      assert.propertyVal(payload.data, "foo", "bar");
    });
  });
  describe("#sendPageTakeoverData", () => {
    let fakePrefs = { "browser.newtabpage.enabled": true };

    beforeEach(() => {
      globals.set(
        "Services",
        Object.assign({}, Services, {
          prefs: { getBoolPref: key => fakePrefs[key] },
        })
      );
      // Services.prefs = {getBoolPref: key => fakePrefs[key]};
      sandbox.spy(Glean.newtab.newtabCategory, "set");
      sandbox.spy(Glean.newtab.homepageCategory, "set");
    });
    it("should send correct event data for about:home set to custom URL", async () => {
      fakeHomePageUrl = "https://searchprovider.com";
      instance._prefs.set(TELEMETRY_PREF, true);
      instance._classifySite = () => Promise.resolve("other");
      const sendEvent = sandbox.stub(instance, "sendEvent");

      await instance.sendPageTakeoverData();
      assert.calledOnce(sendEvent);
      assert.equal(sendEvent.firstCall.args[0].event, "PAGE_TAKEOVER_DATA");
      assert.deepEqual(sendEvent.firstCall.args[0].value, {
        home_url_category: "other",
      });
      assert.validate(sendEvent.firstCall.args[0], UserEventPing);
      assert.calledOnce(Glean.newtab.homepageCategory.set);
      assert.calledWith(Glean.newtab.homepageCategory.set, "other");
    });
    it("should send correct event data for about:newtab set to custom URL", async () => {
      globals.set("AboutNewTab", {
        newTabURLOverridden: true,
        newTabURL: "https://searchprovider.com",
      });
      instance._prefs.set(TELEMETRY_PREF, true);
      instance._classifySite = () => Promise.resolve("other");
      const sendEvent = sandbox.stub(instance, "sendEvent");

      await instance.sendPageTakeoverData();
      assert.calledOnce(sendEvent);
      assert.equal(sendEvent.firstCall.args[0].event, "PAGE_TAKEOVER_DATA");
      assert.deepEqual(sendEvent.firstCall.args[0].value, {
        newtab_url_category: "other",
      });
      assert.validate(sendEvent.firstCall.args[0], UserEventPing);
      assert.calledOnce(Glean.newtab.newtabCategory.set);
      assert.calledWith(Glean.newtab.newtabCategory.set, "other");
    });
    it("should not send an event if neither about:{home,newtab} are set to custom URL", async () => {
      instance._prefs.set(TELEMETRY_PREF, true);
      const sendEvent = sandbox.stub(instance, "sendEvent");

      await instance.sendPageTakeoverData();
      assert.notCalled(sendEvent);
      assert.calledOnce(Glean.newtab.newtabCategory.set);
      assert.calledOnce(Glean.newtab.homepageCategory.set);
      assert.calledWith(Glean.newtab.newtabCategory.set, "enabled");
      assert.calledWith(Glean.newtab.homepageCategory.set, "enabled");
    });
    it("should send home_extension_id and newtab_extension_id when appropriate", async () => {
      const ID = "{abc-foo-bar}";
      fakeExtensionSettingsStore.getSetting = () => ({ id: ID });
      instance._prefs.set(TELEMETRY_PREF, true);
      instance._classifySite = () => Promise.resolve("other");
      const sendEvent = sandbox.stub(instance, "sendEvent");

      await instance.sendPageTakeoverData();
      assert.calledOnce(sendEvent);
      assert.equal(sendEvent.firstCall.args[0].event, "PAGE_TAKEOVER_DATA");
      assert.deepEqual(sendEvent.firstCall.args[0].value, {
        home_extension_id: ID,
        newtab_extension_id: ID,
      });
      assert.validate(sendEvent.firstCall.args[0], UserEventPing);
      assert.calledOnce(Glean.newtab.newtabCategory.set);
      assert.calledOnce(Glean.newtab.homepageCategory.set);
      assert.equal(Glean.newtab.newtabCategory.set.args[0], "extension");
      assert.equal(Glean.newtab.homepageCategory.set.args[0], "extension");
    });
    it("instruments when newtab is disabled", async () => {
      instance._prefs.set(TELEMETRY_PREF, true);
      fakePrefs["browser.newtabpage.enabled"] = false;
      await instance.sendPageTakeoverData();
      assert.calledOnce(Glean.newtab.newtabCategory.set);
      assert.calledWith(Glean.newtab.newtabCategory.set, "disabled");
    });
    it("instruments when homepage is disabled", async () => {
      instance._prefs.set(TELEMETRY_PREF, true);
      fakeHomePage.overridden = true;
      await instance.sendPageTakeoverData();
      assert.calledOnce(Glean.newtab.homepageCategory.set);
      assert.calledWith(Glean.newtab.homepageCategory.set, "disabled");
    });
    it("should send a 'newtab' ping", async () => {
      instance._prefs.set(TELEMETRY_PREF, true);
      sandbox.spy(GleanPings.newtab, "submit");
      await instance.sendPageTakeoverData();
      assert.calledOnce(GleanPings.newtab.submit);
      assert.calledWithExactly(GleanPings.newtab.submit, "component_init");
    });
  });
  describe("#sendDiscoveryStreamImpressions", () => {
    it("should not send impression pings if there is no impression data", () => {
      const spy = sandbox.spy(instance, "sendEvent");
      const session = {};
      instance.sendDiscoveryStreamImpressions("foo", session);

      assert.notCalled(spy);
    });
    it("should send impression pings if there is impression data", () => {
      const spy = sandbox.spy(instance, "sendStructuredIngestionEvent");
      const session = {
        impressionSets: {
          source_foo: [
            { id: 1, pos: 0 },
            { id: 2, pos: 1 },
          ],
          source_bar: [
            { id: 3, pos: 0 },
            { id: 4, pos: 1 },
          ],
        },
      };
      instance.sendDiscoveryStreamImpressions("foo", session);

      assert.calledTwice(spy);
    });
  });
  describe("#sendDiscoveryStreamLoadedContent", () => {
    it("should not send loaded content pings if there is no loaded content data", () => {
      const spy = sandbox.spy(instance, "sendEvent");
      const session = {};
      instance.sendDiscoveryStreamLoadedContent("foo", session);

      assert.notCalled(spy);
    });
    it("should send loaded content pings if there is loaded content data", () => {
      const spy = sandbox.spy(instance, "sendStructuredIngestionEvent");
      const session = {
        loadedContentSets: {
          source_foo: [
            { id: 1, pos: 0 },
            { id: 2, pos: 1 },
          ],
          source_bar: [
            { id: 3, pos: 0 },
            { id: 4, pos: 1 },
          ],
        },
      };
      instance.sendDiscoveryStreamLoadedContent("foo", session);

      assert.calledTwice(spy);

      let [payload] = spy.firstCall.args;
      let sources = new Set([]);
      sources.add(payload.source);
      assert.equal(payload.loaded, 2);
      assert.deepEqual(
        payload.tiles,
        session.loadedContentSets[payload.source]
      );

      [payload] = spy.secondCall.args;
      sources.add(payload.source);
      assert.equal(payload.loaded, 2);
      assert.deepEqual(
        payload.tiles,
        session.loadedContentSets[payload.source]
      );

      assert.deepEqual(sources, new Set(["source_foo", "source_bar"]));
    });
  });
  describe("#handleDiscoveryStreamImpressionStats", () => {
    it("should throw for a missing session", () => {
      assert.throws(() => {
        instance.handleDiscoveryStreamImpressionStats("a_missing_port", {});
      }, "Session does not exist.");
    });
    it("should store impression to impressionSets", () => {
      const session = instance.addSession("new_session", "about:newtab");
      instance.handleDiscoveryStreamImpressionStats("new_session", {
        source: "foo",
        tiles: [{ id: 1, pos: 0 }],
        window_inner_width: 1000,
        window_inner_height: 900,
      });

      assert.equal(Object.keys(session.impressionSets).length, 1);
      assert.deepEqual(session.impressionSets.foo, {
        tiles: [{ id: 1, pos: 0 }],
        window_inner_width: 1000,
        window_inner_height: 900,
      });

      // Add another ping with the same source
      instance.handleDiscoveryStreamImpressionStats("new_session", {
        source: "foo",
        tiles: [{ id: 2, pos: 1 }],
        window_inner_width: 1000,
        window_inner_height: 900,
      });

      assert.deepEqual(session.impressionSets.foo, {
        tiles: [
          { id: 1, pos: 0 },
          { id: 2, pos: 1 },
        ],
        window_inner_width: 1000,
        window_inner_height: 900,
      });

      // Add another ping with a different source
      instance.handleDiscoveryStreamImpressionStats("new_session", {
        source: "bar",
        tiles: [{ id: 3, pos: 2 }],
        window_inner_width: 1000,
        window_inner_height: 900,
      });

      assert.equal(Object.keys(session.impressionSets).length, 2);
      assert.deepEqual(session.impressionSets.foo, {
        tiles: [
          { id: 1, pos: 0 },
          { id: 2, pos: 1 },
        ],
        window_inner_width: 1000,
        window_inner_height: 900,
      });
      assert.deepEqual(session.impressionSets.bar, {
        tiles: [{ id: 3, pos: 2 }],
        window_inner_width: 1000,
        window_inner_height: 900,
      });
    });
    it("should instrument pocket impressions", () => {
      const session_id = "1337cafe";
      const pos1 = 1;
      const pos2 = 4;
      sandbox.stub(instance.sessions, "get").returns({ session_id });
      sandbox.spy(Glean.pocket.impression, "record");

      instance.handleDiscoveryStreamImpressionStats("_", {
        source: "foo",
        tiles: [
          { id: 1, pos: pos1, type: "organic" },
          { id: 2, pos: pos2, type: "spoc" },
        ],
        window_inner_width: 1000,
        window_inner_height: 900,
      });

      assert.calledTwice(Glean.pocket.impression.record);
      assert.deepEqual(Glean.pocket.impression.record.firstCall.args[0], {
        newtab_visit_id: session_id,
        is_sponsored: false,
        position: pos1,
      });
      assert.deepEqual(Glean.pocket.impression.record.secondCall.args[0], {
        newtab_visit_id: session_id,
        is_sponsored: true,
        position: pos2,
      });
    });
  });
  describe("#handleDiscoveryStreamLoadedContent", () => {
    it("should throw for a missing session", () => {
      assert.throws(() => {
        instance.handleDiscoveryStreamLoadedContent("a_missing_port", {});
      }, "Session does not exist.");
    });
    it("should store loaded content to loadedContentSets", () => {
      const session = instance.addSession("new_session", "about:newtab");
      instance.handleDiscoveryStreamLoadedContent("new_session", {
        source: "foo",
        tiles: [{ id: 1, pos: 0 }],
      });

      assert.equal(Object.keys(session.loadedContentSets).length, 1);
      assert.deepEqual(session.loadedContentSets.foo, [{ id: 1, pos: 0 }]);

      // Add another ping with the same source
      instance.handleDiscoveryStreamLoadedContent("new_session", {
        source: "foo",
        tiles: [{ id: 2, pos: 1 }],
      });

      assert.deepEqual(session.loadedContentSets.foo, [
        { id: 1, pos: 0 },
        { id: 2, pos: 1 },
      ]);

      // Add another ping with a different source
      instance.handleDiscoveryStreamLoadedContent("new_session", {
        source: "bar",
        tiles: [{ id: 3, pos: 2 }],
      });

      assert.equal(Object.keys(session.loadedContentSets).length, 2);
      assert.deepEqual(session.loadedContentSets.foo, [
        { id: 1, pos: 0 },
        { id: 2, pos: 1 },
      ]);
      assert.deepEqual(session.loadedContentSets.bar, [{ id: 3, pos: 2 }]);
    });
  });
  describe("#_generateStructuredIngestionEndpoint", () => {
    it("should generate a valid endpoint", () => {
      const fakeEndpoint = "http://fakeendpoint.com/base/";
      const fakeUUID = "{34f24486-f01a-9749-9c5b-21476af1fa77}";
      const fakeUUIDWithoutBraces = fakeUUID.substring(1, fakeUUID.length - 1);
      FAKE_GLOBAL_PREFS.set(STRUCTURED_INGESTION_ENDPOINT_PREF, fakeEndpoint);
      sandbox.stub(Services.uuid, "generateUUID").returns(fakeUUID);
      const feed = new TelemetryFeed();
      const url = feed._generateStructuredIngestionEndpoint(
        "testNameSpace",
        "testPingType",
        "1"
      );

      assert.equal(
        url,
        `${fakeEndpoint}/testNameSpace/testPingType/1/${fakeUUIDWithoutBraces}`
      );
    });
  });
  describe("#handleASRouterUserEvent", () => {
    it("should call sendStructuredIngestionEvent on known pingTypes", async () => {
      const data = {
        action: "onboarding_user_event",
        event: "IMPRESSION",
        message_id: "12345",
      };
      instance = new TelemetryFeed();
      sandbox.spy(instance, "sendStructuredIngestionEvent");

      await instance.handleASRouterUserEvent({ data });

      assert.calledOnce(instance.sendStructuredIngestionEvent);
    });
    it("should call submitGleanPingForPing on known pingTypes when telemetry is enabled", async () => {
      const data = {
        action: "onboarding_user_event",
        event: "IMPRESSION",
        message_id: "12345",
      };
      instance = new TelemetryFeed();
      instance._prefs.set(TELEMETRY_PREF, true);
      sandbox.spy(
        global.AboutWelcomeTelemetry.prototype,
        "submitGleanPingForPing"
      );

      await instance.handleASRouterUserEvent({ data });

      assert.calledOnce(
        global.AboutWelcomeTelemetry.prototype.submitGleanPingForPing
      );
    });
    it("should console.error and not submit pings on unknown pingTypes", async () => {
      const data = {
        action: "unknown_event",
        event: "IMPRESSION",
        message_id: "12345",
      };
      instance = new TelemetryFeed();
      sandbox.spy(instance, "sendStructuredIngestionEvent");

      await instance.handleASRouterUserEvent({ data });

      assert.calledOnce(global.console.error);
      assert.notCalled(instance.sendStructuredIngestionEvent);
    });
  });
  describe("#isInCFRCohort", () => {
    it("should return false if there is no CFR experiment registered", () => {
      assert.ok(!instance.isInCFRCohort);
    });
    it("should return true if there is a CFR experiment registered", () => {
      sandbox.stub(ExperimentAPI, "getExperimentMetaData").returns({
        slug: "SOME-CFR-EXP",
      });

      assert.ok(instance.isInCFRCohort);
      assert.propertyVal(
        ExperimentAPI.getExperimentMetaData.firstCall.args[0],
        "featureId",
        "cfr"
      );
    });
  });
  describe("#handleTopSitesSponsoredImpressionStats", () => {
    it("should call sendStructuredIngestionEvent on an impression event", async () => {
      const data = {
        type: "impression",
        tile_id: 42,
        source: "newtab",
        position: 0,
        reporting_url: "https://test.reporting.net/",
      };
      instance = new TelemetryFeed();
      sandbox.spy(instance, "sendStructuredIngestionEvent");
      sandbox.spy(Services.telemetry, "keyedScalarAdd");

      await instance.handleTopSitesSponsoredImpressionStats({ data });

      // Scalar should be added
      assert.calledOnce(Services.telemetry.keyedScalarAdd);
      assert.calledWith(
        Services.telemetry.keyedScalarAdd,
        "contextual.services.topsites.impression",
        "newtab_1",
        1
      );

      assert.calledOnce(instance.sendStructuredIngestionEvent);

      const { args } = instance.sendStructuredIngestionEvent.firstCall;
      // payload
      assert.deepEqual(args[0], {
        context_id: FAKE_UUID,
        tile_id: 42,
        source: "newtab",
        position: 1,
        reporting_url: "https://test.reporting.net/",
      });
      // namespace
      assert.equal(args[1], "contextual-services");
      // docType
      assert.equal(args[2], "topsites-impression");
      // version
      assert.equal(args[3], "1");
    });
    it("should call sendStructuredIngestionEvent on a click event", async () => {
      const data = {
        type: "click",
        tile_id: 42,
        source: "newtab",
        position: 0,
        reporting_url: "https://test.reporting.net/",
      };
      instance = new TelemetryFeed();
      sandbox.spy(instance, "sendStructuredIngestionEvent");
      sandbox.spy(Services.telemetry, "keyedScalarAdd");

      await instance.handleTopSitesSponsoredImpressionStats({ data });

      // Scalar should be added
      assert.calledOnce(Services.telemetry.keyedScalarAdd);
      assert.calledWith(
        Services.telemetry.keyedScalarAdd,
        "contextual.services.topsites.click",
        "newtab_1",
        1
      );

      assert.calledOnce(instance.sendStructuredIngestionEvent);

      const { args } = instance.sendStructuredIngestionEvent.firstCall;
      // payload
      assert.deepEqual(args[0], {
        context_id: FAKE_UUID,
        tile_id: 42,
        source: "newtab",
        position: 1,
        reporting_url: "https://test.reporting.net/",
      });
      // namespace
      assert.equal(args[1], "contextual-services");
      // docType
      assert.equal(args[2], "topsites-click");
      // version
      assert.equal(args[3], "1");
    });
    it("should record a Glean topsites.impression event on an impression event", async () => {
      const data = {
        type: "impression",
        tile_id: 42,
        source: "newtab",
        position: 1,
        reporting_url: "https://test.reporting.net/",
        advertiser: "adnoid ads",
      };
      instance = new TelemetryFeed();
      const session_id = "decafc0ffee";
      sandbox.stub(instance.sessions, "get").returns({ session_id });
      sandbox.spy(Glean.topsites.impression, "record");

      await instance.handleTopSitesSponsoredImpressionStats({ data });

      // Event should be recorded
      assert.calledOnce(Glean.topsites.impression.record);
      assert.calledWith(Glean.topsites.impression.record, {
        advertiser_name: "adnoid ads",
        tile_id: "42",
        newtab_visit_id: session_id,
        is_sponsored: true,
        position: 1,
      });
    });
    it("should record a Glean topsites.click event on a click event", async () => {
      const data = {
        type: "click",
        advertiser: "test advertiser",
        tile_id: 42,
        source: "newtab",
        position: 0,
        reporting_url: "https://test.reporting.net/",
      };
      instance = new TelemetryFeed();
      const session_id = "decafc0ffee";
      sandbox.stub(instance.sessions, "get").returns({ session_id });
      sandbox.spy(Glean.topsites.click, "record");

      await instance.handleTopSitesSponsoredImpressionStats({ data });

      // Event should be recorded
      assert.calledOnce(Glean.topsites.click.record);
      assert.calledWith(Glean.topsites.click.record, {
        advertiser_name: "test advertiser",
        tile_id: "42",
        newtab_visit_id: session_id,
        is_sponsored: true,
        position: 0,
      });
    });
    it("should console.error on unknown pingTypes", async () => {
      const data = { type: "unknown_type" };
      instance = new TelemetryFeed();
      sandbox.spy(instance, "sendStructuredIngestionEvent");

      await instance.handleTopSitesSponsoredImpressionStats({ data });

      assert.calledOnce(global.console.error);
      assert.notCalled(instance.sendStructuredIngestionEvent);
    });
  });
  describe("#handleTopSitesOrganicImpressionStats", () => {
    it("should record a Glean topsites.impression event on an impression event", async () => {
      const data = {
        type: "impression",
        source: "newtab",
        position: 0,
      };
      instance = new TelemetryFeed();
      const session_id = "decafc0ffee";
      sandbox.stub(instance.sessions, "get").returns({ session_id });
      sandbox.spy(Glean.topsites.impression, "record");

      await instance.handleTopSitesOrganicImpressionStats({ data });

      assert.calledOnce(Glean.topsites.impression.record);
      assert.calledWith(Glean.topsites.impression.record, {
        newtab_visit_id: session_id,
        is_sponsored: false,
        position: 0,
      });
    });
    it("should record a Glean topsites.click event on a click event", async () => {
      const data = {
        type: "click",
        source: "newtab",
        position: 0,
      };
      instance = new TelemetryFeed();
      const session_id = "decafc0ffee";
      sandbox.stub(instance.sessions, "get").returns({ session_id });
      sandbox.spy(Glean.topsites.click, "record");

      await instance.handleTopSitesOrganicImpressionStats({ data });

      assert.calledOnce(Glean.topsites.click.record);
      assert.calledWith(Glean.topsites.click.record, {
        newtab_visit_id: session_id,
        is_sponsored: false,
        position: 0,
      });
    });
  });
  describe("#handleDiscoveryStreamUserEvent", () => {
    it("correctly handles action with no `data`", () => {
      const action = ac.DiscoveryStreamUserEvent();
      instance = new TelemetryFeed();
      const session_id = "c0ffee";
      sandbox.stub(instance.sessions, "get").returns({ session_id });
      sandbox.spy(Glean.pocket.topicClick, "record");
      sandbox.spy(Glean.pocket.click, "record");
      sandbox.spy(Glean.pocket.save, "record");

      instance.handleDiscoveryStreamUserEvent(action);

      assert.notCalled(Glean.pocket.topicClick.record);
      assert.notCalled(Glean.pocket.click.record);
      assert.notCalled(Glean.pocket.save.record);
    });
    it("correctly handles CLICK data with no value", () => {
      const action = ac.DiscoveryStreamUserEvent({
        event: "CLICK",
        source: "POPULAR_TOPICS",
      });
      instance = new TelemetryFeed();
      const session_id = "c0ffee";
      sandbox.stub(instance.sessions, "get").returns({ session_id });
      sandbox.spy(Glean.pocket.topicClick, "record");

      instance.handleDiscoveryStreamUserEvent(action);

      assert.calledOnce(Glean.pocket.topicClick.record);
      assert.calledWith(Glean.pocket.topicClick.record, {
        newtab_visit_id: session_id,
        topic: undefined,
      });
    });
    it("correctly handles non-POPULAR_TOPICS CLICK data with no value", () => {
      const action = ac.DiscoveryStreamUserEvent({
        event: "CLICK",
        source: "not-POPULAR_TOPICS",
      });
      instance = new TelemetryFeed();
      const session_id = "c0ffee";
      sandbox.stub(instance.sessions, "get").returns({ session_id });
      sandbox.spy(Glean.pocket.topicClick, "record");
      sandbox.spy(Glean.pocket.click, "record");
      sandbox.spy(Glean.pocket.save, "record");

      instance.handleDiscoveryStreamUserEvent(action);

      assert.notCalled(Glean.pocket.topicClick.record);
      assert.notCalled(Glean.pocket.click.record);
      assert.notCalled(Glean.pocket.save.record);
    });
    it("correctly handles CLICK data with non-POPULAR_TOPICS source", () => {
      const topic = "atopic";
      const action = ac.DiscoveryStreamUserEvent({
        event: "CLICK",
        source: "not-POPULAR_TOPICS",
        value: {
          card_type: "topics_widget",
          topic,
        },
      });
      instance = new TelemetryFeed();
      const session_id = "c0ffee";
      sandbox.stub(instance.sessions, "get").returns({ session_id });
      sandbox.spy(Glean.pocket.topicClick, "record");

      instance.handleDiscoveryStreamUserEvent(action);

      assert.calledOnce(Glean.pocket.topicClick.record);
      assert.calledWith(Glean.pocket.topicClick.record, {
        newtab_visit_id: session_id,
        topic,
      });
    });
    it("doesn't instrument a CLICK without a card_type", () => {
      const action = ac.DiscoveryStreamUserEvent({
        event: "CLICK",
        source: "not-POPULAR_TOPICS",
        value: {
          card_type: "not spoc, organic, or topics_widget",
        },
      });
      instance = new TelemetryFeed();
      const session_id = "c0ffee";
      sandbox.stub(instance.sessions, "get").returns({ session_id });
      sandbox.spy(Glean.pocket.topicClick, "record");
      sandbox.spy(Glean.pocket.click, "record");
      sandbox.spy(Glean.pocket.save, "record");

      instance.handleDiscoveryStreamUserEvent(action);

      assert.notCalled(Glean.pocket.topicClick.record);
      assert.notCalled(Glean.pocket.click.record);
      assert.notCalled(Glean.pocket.save.record);
    });
    it("instruments a popular topic click", () => {
      const topic = "entertainment";
      const action = ac.DiscoveryStreamUserEvent({
        event: "CLICK",
        source: "POPULAR_TOPICS",
        value: {
          card_type: "topics_widget",
          topic,
        },
      });
      instance = new TelemetryFeed();
      const session_id = "c0ffee";
      sandbox.stub(instance.sessions, "get").returns({ session_id });
      sandbox.spy(Glean.pocket.topicClick, "record");

      instance.handleDiscoveryStreamUserEvent(action);

      assert.calledOnce(Glean.pocket.topicClick.record);
      assert.calledWith(Glean.pocket.topicClick.record, {
        newtab_visit_id: session_id,
        topic,
      });
    });
    it("instruments an organic top stories click", () => {
      const action_position = 42;
      const action = ac.DiscoveryStreamUserEvent({
        event: "CLICK",
        action_position,
        value: {
          card_type: "organic",
        },
      });
      instance = new TelemetryFeed();
      const session_id = "c0ffee";
      sandbox.stub(instance.sessions, "get").returns({ session_id });
      sandbox.spy(Glean.pocket.click, "record");

      instance.handleDiscoveryStreamUserEvent(action);

      assert.calledOnce(Glean.pocket.click.record);
      assert.calledWith(Glean.pocket.click.record, {
        newtab_visit_id: session_id,
        is_sponsored: false,
        position: action_position,
      });
    });
    it("instruments a sponsored top stories click", () => {
      const action_position = 42;
      const action = ac.DiscoveryStreamUserEvent({
        event: "CLICK",
        action_position,
        value: {
          card_type: "spoc",
        },
      });
      instance = new TelemetryFeed();
      const session_id = "c0ffee";
      sandbox.stub(instance.sessions, "get").returns({ session_id });
      sandbox.spy(Glean.pocket.click, "record");

      instance.handleDiscoveryStreamUserEvent(action);

      assert.calledOnce(Glean.pocket.click.record);
      assert.calledWith(Glean.pocket.click.record, {
        newtab_visit_id: session_id,
        is_sponsored: true,
        position: action_position,
      });
    });
    it("instruments a save of an organic top story", () => {
      const action_position = 42;
      const action = ac.DiscoveryStreamUserEvent({
        event: "SAVE_TO_POCKET",
        action_position,
        value: {
          card_type: "organic",
        },
      });
      instance = new TelemetryFeed();
      const session_id = "c0ffee";
      sandbox.stub(instance.sessions, "get").returns({ session_id });
      sandbox.spy(Glean.pocket.save, "record");

      instance.handleDiscoveryStreamUserEvent(action);

      assert.calledOnce(Glean.pocket.save.record);
      assert.calledWith(Glean.pocket.save.record, {
        newtab_visit_id: session_id,
        is_sponsored: false,
        position: action_position,
      });
    });
    it("instruments a save of a sponsored top story", () => {
      const action_position = 42;
      const action = ac.DiscoveryStreamUserEvent({
        event: "SAVE_TO_POCKET",
        action_position,
        value: {
          card_type: "spoc",
        },
      });
      instance = new TelemetryFeed();
      const session_id = "c0ffee";
      sandbox.stub(instance.sessions, "get").returns({ session_id });
      sandbox.spy(Glean.pocket.save, "record");

      instance.handleDiscoveryStreamUserEvent(action);

      assert.calledOnce(Glean.pocket.save.record);
      assert.calledWith(Glean.pocket.save.record, {
        newtab_visit_id: session_id,
        is_sponsored: true,
        position: action_position,
      });
    });
    it("instruments a save of a sponsored top story, without `value`", () => {
      const action_position = 42;
      const action = ac.DiscoveryStreamUserEvent({
        event: "SAVE_TO_POCKET",
        action_position,
      });
      instance = new TelemetryFeed();
      const session_id = "c0ffee";
      sandbox.stub(instance.sessions, "get").returns({ session_id });
      sandbox.spy(Glean.pocket.save, "record");

      instance.handleDiscoveryStreamUserEvent(action);

      assert.calledOnce(Glean.pocket.save.record);
      assert.calledWith(Glean.pocket.save.record, {
        newtab_visit_id: session_id,
        is_sponsored: false,
        position: action_position,
      });
    });
  });
});
