import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs";
import { GlobalOverrider } from "test/unit/utils";
import { PrefsFeed } from "lib/PrefsFeed.sys.mjs";

let overrider = new GlobalOverrider();

describe("PrefsFeed", () => {
  let feed;
  let FAKE_PREFS;
  let sandbox;
  let ServicesStub;
  let SelectableProfileServiceStub;
  beforeEach(() => {
    sandbox = sinon.createSandbox();
    FAKE_PREFS = new Map([
      ["foo", 1],
      ["bar", 2],
      ["baz", { value: 1, skipBroadcast: true }],
      ["qux", { value: 1, skipBroadcast: true, alsoToPreloaded: true }],
    ]);
    feed = new PrefsFeed(FAKE_PREFS);
    ServicesStub = {
      prefs: {
        clearUserPref: sinon.spy(),
        getStringPref: sinon.spy(),
        getIntPref: sinon.spy(),
        getBoolPref: sinon.spy(),
      },
      obs: {
        removeObserver: sinon.spy(),
        addObserver: sinon.spy(),
      },
    };
    SelectableProfileServiceStub = {
      hasCreatedSelectableProfiles() {
        return false;
      },
    };
    sinon.spy(feed, "_setPref");
    feed.store = {
      dispatch: sinon.spy(),
      getState() {
        return this.state;
      },
    };
    // Setup for tests that don't call `init`
    feed._prefs = {
      get: sinon.spy(item => FAKE_PREFS.get(item)),
      set: sinon.spy((name, value) => FAKE_PREFS.set(name, value)),
      observe: sinon.spy(),
      observeBranch: sinon.spy(),
      ignore: sinon.spy(),
      ignoreBranch: sinon.spy(),
      reset: sinon.stub(),
      _branchStr: "branch.str.",
    };
    overrider.set({
      PrivateBrowsingUtils: { enabled: true },
      Services: ServicesStub,
      SelectableProfileService: SelectableProfileServiceStub,
    });
  });
  afterEach(() => {
    overrider.restore();
    sandbox.restore();
  });

  it("should set a pref when a SET_PREF action is received", () => {
    feed.onAction(ac.SetPref("foo", 2));
    assert.calledWith(feed._prefs.set, "foo", 2);
  });
  it("should call clearUserPref with action CLEAR_PREF", () => {
    feed.onAction({ type: at.CLEAR_PREF, data: { name: "pref.test" } });
    assert.calledWith(ServicesStub.prefs.clearUserPref, "branch.str.pref.test");
  });
  it("should dispatch PREFS_INITIAL_VALUES on init with pref values and .isPrivateBrowsingEnabled", () => {
    feed.onAction({ type: at.INIT });
    assert.calledOnce(feed.store.dispatch);
    assert.equal(
      feed.store.dispatch.firstCall.args[0].type,
      at.PREFS_INITIAL_VALUES
    );
    const [{ data }] = feed.store.dispatch.firstCall.args;
    assert.equal(data.foo, 1);
    assert.equal(data.bar, 2);
    assert.isTrue(data.isPrivateBrowsingEnabled);
  });
  it("should dispatch PREFS_INITIAL_VALUES with a .featureConfig", () => {
    sandbox.stub(global.NimbusFeatures.newtab, "getAllVariables").returns({
      prefsButtonIcon: "icon-foo",
    });
    feed.onAction({ type: at.INIT });
    assert.equal(
      feed.store.dispatch.firstCall.args[0].type,
      at.PREFS_INITIAL_VALUES
    );
    const [{ data }] = feed.store.dispatch.firstCall.args;
    assert.deepEqual(data.featureConfig, { prefsButtonIcon: "icon-foo" });
  });
  it("should dispatch PREFS_INITIAL_VALUES with trainhopConfig", () => {
    const testObject = {
      meta: { isRollout: false },
      value: {
        type: "testExperiment",
        payload: { enabled: true },
      },
    };
    sandbox
      .stub(global.NimbusFeatures.newtabTrainhop, "getAllEnrollments")
      .returns([testObject]);

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

    assert.equal(
      feed.store.dispatch.firstCall.args[0].type,
      at.PREFS_INITIAL_VALUES
    );
    const [{ data }] = feed.store.dispatch.firstCall.args;
    assert.deepEqual(data.trainhopConfig, {
      testExperiment: { enabled: true },
    });
  });
  it("should dispatch PREFS_INITIAL_VALUES with adsBackendConfig", () => {
    const testObject = {
      meta: { isRollout: false },
      value: {
        flags: {
          feature1: true,
        },
      },
    };
    sandbox
      .stub(global.NimbusFeatures.adsBackend, "getAllEnrollments")
      .returns([testObject]);

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

    assert.equal(
      feed.store.dispatch.firstCall.args[0].type,
      at.PREFS_INITIAL_VALUES
    );
    const [{ data }] = feed.store.dispatch.firstCall.args;
    assert.deepEqual(data.adsBackendConfig, {
      feature1: true,
    });
  });
  it("should dispatch PREFS_INITIAL_VALUES with an empty object if no experiment is returned", () => {
    sandbox.stub(global.NimbusFeatures.newtab, "getAllVariables").returns(null);
    feed.onAction({ type: at.INIT });
    assert.equal(
      feed.store.dispatch.firstCall.args[0].type,
      at.PREFS_INITIAL_VALUES
    );
    const [{ data }] = feed.store.dispatch.firstCall.args;
    assert.deepEqual(data.featureConfig, {});
  });
  it("should add one branch observer on init", () => {
    feed.onAction({ type: at.INIT });
    assert.calledOnce(feed._prefs.observeBranch);
    assert.calledWith(feed._prefs.observeBranch, feed);
  });
  it("should handle region on init", () => {
    feed.init();
    assert.equal(feed.geo, "US");
  });
  it("should add region observer on init", () => {
    sandbox.stub(global.Region, "home").get(() => "");
    feed.init();
    assert.equal(feed.geo, "");
    assert.calledWith(
      ServicesStub.obs.addObserver,
      feed,
      global.Region.REGION_TOPIC
    );
  });
  it("should remove the branch observer on uninit", () => {
    feed.onAction({ type: at.UNINIT });
    assert.calledOnce(feed._prefs.ignoreBranch);
    assert.calledWith(feed._prefs.ignoreBranch, feed);
  });
  it("should call removeObserver", () => {
    feed.geo = "";
    feed.uninit();
    assert.calledWith(
      ServicesStub.obs.removeObserver,
      feed,
      global.Region.REGION_TOPIC
    );
  });
  it("should send a PREF_CHANGED action when onPrefChanged is called", () => {
    feed.onPrefChanged("foo", 2);
    assert.calledWith(
      feed.store.dispatch,
      ac.BroadcastToContent({
        type: at.PREF_CHANGED,
        data: { name: "foo", value: 2 },
      })
    );
  });
  it("should send a PREF_CHANGED actions when onPocketExperimentUpdated is called", () => {
    sandbox
      .stub(global.NimbusFeatures.pocketNewtab, "getAllVariables")
      .returns({
        prefsButtonIcon: "icon-new",
      });
    feed.onPocketExperimentUpdated();
    assert.calledWith(
      feed.store.dispatch,
      ac.BroadcastToContent({
        type: at.PREF_CHANGED,
        data: {
          name: "pocketConfig",
          value: {
            prefsButtonIcon: "icon-new",
          },
        },
      })
    );
  });
  it("should not send a PREF_CHANGED actions when onPocketExperimentUpdated is called during startup", () => {
    sandbox
      .stub(global.NimbusFeatures.pocketNewtab, "getAllVariables")
      .returns({
        prefsButtonIcon: "icon-new",
      });
    feed.onPocketExperimentUpdated({}, "feature-experiment-loaded");
    assert.notCalled(feed.store.dispatch);
    feed.onPocketExperimentUpdated({}, "feature-rollout-loaded");
    assert.notCalled(feed.store.dispatch);
  });
  it("should send a PREF_CHANGED actions when onExperimentUpdated is called", () => {
    sandbox.stub(global.NimbusFeatures.newtab, "getAllVariables").returns({
      prefsButtonIcon: "icon-new",
    });
    feed.onExperimentUpdated();
    assert.calledWith(
      feed.store.dispatch,
      ac.BroadcastToContent({
        type: at.PREF_CHANGED,
        data: {
          name: "featureConfig",
          value: {
            prefsButtonIcon: "icon-new",
          },
        },
      })
    );
  });
  describe("newtabTrainhop", () => {
    it("should send a PREF_CHANGED actions when onTrainhopExperimentUpdated is called", () => {
      const testObject = {
        meta: {
          isRollout: false,
        },
        value: {
          type: "testExperiment",
          payload: {
            enabled: true,
          },
        },
      };
      sandbox
        .stub(global.NimbusFeatures.newtabTrainhop, "getAllEnrollments")
        .returns([testObject]);
      feed.onTrainhopExperimentUpdated();
      assert.calledWith(
        feed.store.dispatch,
        ac.BroadcastToContent({
          type: at.PREF_CHANGED,
          data: {
            name: "trainhopConfig",
            value: {
              testExperiment: testObject.value.payload,
            },
          },
        })
      );
    });
    it("should handle and dedupe multiple experiments and rollouts", () => {
      const testObject1 = {
        meta: {
          isRollout: false,
        },
        value: {
          type: "testExperiment1",
          payload: {
            enabled: true,
          },
        },
      };
      const testObject2 = {
        meta: {
          isRollout: false,
        },
        value: {
          type: "testExperiment1",
          payload: {
            enabled: false,
          },
        },
      };
      const testObject3 = {
        meta: {
          isRollout: true,
        },
        value: {
          type: "testExperiment2",
          payload: {
            enabled: true,
          },
        },
      };
      const testObject4 = {
        meta: {
          isRollout: false,
        },
        value: {
          type: "testExperiment2",
          payload: {
            enabled: false,
          },
        },
      };
      sandbox
        .stub(global.NimbusFeatures.newtabTrainhop, "getAllEnrollments")
        .returns([testObject1, testObject2, testObject3, testObject4]);
      feed.onTrainhopExperimentUpdated();
      assert.calledWith(
        feed.store.dispatch,
        ac.BroadcastToContent({
          type: at.PREF_CHANGED,
          data: {
            name: "trainhopConfig",
            value: {
              testExperiment1: testObject1.value.payload,
              testExperiment2: testObject4.value.payload,
            },
          },
        })
      );
    });
    it("should handle multi-payload format with single enrollment", () => {
      const testObject = {
        meta: {
          isRollout: false,
        },
        value: {
          type: "multi-payload",
          payload: [
            {
              type: "testExperiment",
              payload: {
                enabled: true,
                name: "test-name",
              },
            },
          ],
        },
      };
      sandbox
        .stub(global.NimbusFeatures.newtabTrainhop, "getAllEnrollments")
        .returns([testObject]);
      feed.onTrainhopExperimentUpdated();
      assert.calledWith(
        feed.store.dispatch,
        ac.BroadcastToContent({
          type: at.PREF_CHANGED,
          data: {
            name: "trainhopConfig",
            value: {
              testExperiment: {
                enabled: true,
                name: "test-name",
              },
            },
          },
        })
      );
    });
    it("should handle multi-payload format with multiple items in single enrollment", () => {
      const testObject = {
        meta: {
          isRollout: false,
        },
        value: {
          type: "multi-payload",
          payload: [
            {
              type: "testExperiment1",
              payload: {
                enabled: true,
              },
            },
            {
              type: "testExperiment2",
              payload: {
                enabled: false,
              },
            },
          ],
        },
      };
      sandbox
        .stub(global.NimbusFeatures.newtabTrainhop, "getAllEnrollments")
        .returns([testObject]);
      feed.onTrainhopExperimentUpdated();
      assert.calledWith(
        feed.store.dispatch,
        ac.BroadcastToContent({
          type: at.PREF_CHANGED,
          data: {
            name: "trainhopConfig",
            value: {
              testExperiment1: {
                enabled: true,
              },
              testExperiment2: {
                enabled: false,
              },
            },
          },
        })
      );
    });
    it("should dedupe multi-payload format with experiment taking precedence over rollout", () => {
      const rollout = {
        meta: {
          isRollout: true,
        },
        value: {
          type: "multi-payload",
          payload: [
            {
              type: "testExperiment",
              payload: {
                enabled: false,
                name: "rollout-name",
              },
            },
          ],
        },
      };
      const experiment = {
        meta: {
          isRollout: false,
        },
        value: {
          type: "multi-payload",
          payload: [
            {
              type: "testExperiment",
              payload: {
                enabled: true,
                name: "experiment-name",
              },
            },
          ],
        },
      };
      sandbox
        .stub(global.NimbusFeatures.newtabTrainhop, "getAllEnrollments")
        .returns([rollout, experiment]);
      feed.onTrainhopExperimentUpdated();
      assert.calledWith(
        feed.store.dispatch,
        ac.BroadcastToContent({
          type: at.PREF_CHANGED,
          data: {
            name: "trainhopConfig",
            value: {
              testExperiment: {
                enabled: true,
                name: "experiment-name",
              },
            },
          },
        })
      );
    });
  });
  describe("adsBackend", () => {
    it("should send a PREF_CHANGED action when onAdsBackendUpdated is called", () => {
      const testObject = {
        meta: { isRollout: false },
        value: {
          flags: {
            feature1: true,
          },
        },
      };
      sandbox
        .stub(global.NimbusFeatures.adsBackend, "getAllEnrollments")
        .returns([testObject]);
      feed.onAdsBackendUpdated();
      assert.calledWith(
        feed.store.dispatch,
        ac.BroadcastToContent({
          type: at.PREF_CHANGED,
          data: {
            name: "adsBackendConfig",
            value: {
              feature1: true,
            },
          },
        })
      );
    });
    it("should prefer experiments over rollouts for individual flags", () => {
      const testObject1 = {
        meta: { isRollout: false },
        value: {
          flags: {
            feature1: true,
          },
        },
      };
      const testObject2 = {
        meta: { isRollout: true },
        value: {
          flags: {
            feature1: false,
            feature2: true,
          },
        },
      };
      const testObject3 = {
        meta: { isRollout: false },
        value: {
          flags: {
            feature2: false,
          },
        },
      };
      sandbox
        .stub(global.NimbusFeatures.adsBackend, "getAllEnrollments")
        .returns([testObject1, testObject2, testObject3]);
      feed.onAdsBackendUpdated();
      assert.calledWith(
        feed.store.dispatch,
        ac.BroadcastToContent({
          type: at.PREF_CHANGED,
          data: {
            name: "adsBackendConfig",
            value: {
              feature1: true,
              feature2: false,
            },
          },
        })
      );
    });
    it("should handle and merge multiple experiments and rollouts", () => {
      const testObject1 = {
        meta: { isRollout: false },
        value: {
          flags: {
            feature1: true,
            feature2: true,
          },
        },
      };
      const testObject2 = {
        meta: { isRollout: true },
        value: {
          flags: {
            feature1: false,
          },
        },
      };
      const testObject3 = {
        meta: { isRollout: true },
        value: {
          flags: {
            feature3: true,
          },
        },
      };
      const testObject4 = {
        meta: { isRollout: false },
        value: {
          flags: {
            feature3: false,
            feature4: true,
          },
        },
      };
      sandbox
        .stub(global.NimbusFeatures.adsBackend, "getAllEnrollments")
        .returns([testObject1, testObject2, testObject3, testObject4]);
      feed.onAdsBackendUpdated();
      assert.calledWith(
        feed.store.dispatch,
        ac.BroadcastToContent({
          type: at.PREF_CHANGED,
          data: {
            name: "adsBackendConfig",
            value: {
              feature1: true,
              feature2: true,
              feature3: false,
              feature4: true,
            },
          },
        })
      );
    });
    it("should handle no active experiments and rollouts", () => {
      sandbox
        .stub(global.NimbusFeatures.adsBackend, "getAllEnrollments")
        .returns([]);
      feed.onAdsBackendUpdated();
      assert.calledWith(
        feed.store.dispatch,
        ac.BroadcastToContent({
          type: at.PREF_CHANGED,
          data: {
            name: "adsBackendConfig",
            value: {},
          },
        })
      );
    });
  });
  it("should dispatch PREF_CHANGED when onWidgetsUpdated is called", () => {
    sandbox
      .stub(global.NimbusFeatures.newtabWidgets, "getAllVariables")
      .returns({
        enabled: true,
        listsEnabled: true,
        timerEnabled: false,
      });

    feed.onWidgetsUpdated();

    assert.calledWith(
      feed.store.dispatch,
      ac.BroadcastToContent({
        type: at.PREF_CHANGED,
        data: {
          name: "widgetsConfig",
          value: {
            enabled: true,
            listsEnabled: true,
            timerEnabled: false,
          },
        },
      })
    );
  });
  it("should remove all events on removeListeners", () => {
    feed.geo = "";
    sandbox.spy(global.NimbusFeatures.pocketNewtab, "offUpdate");
    sandbox.spy(global.NimbusFeatures.newtab, "offUpdate");
    sandbox.spy(global.NimbusFeatures.newtabTrainhop, "offUpdate");
    sandbox.spy(global.NimbusFeatures.adsBackend, "offUpdate");
    feed.removeListeners();
    assert.calledWith(
      global.NimbusFeatures.pocketNewtab.offUpdate,
      feed.onPocketExperimentUpdated
    );
    assert.calledWith(
      global.NimbusFeatures.newtab.offUpdate,
      feed.onExperimentUpdated
    );
    assert.calledWith(
      global.NimbusFeatures.newtabTrainhop.offUpdate,
      feed.onTrainhopExperimentUpdated
    );
    assert.calledWith(
      global.NimbusFeatures.adsBackend.offUpdate,
      feed.onAdsBackendUpdated
    );
    assert.calledWith(
      ServicesStub.obs.removeObserver,
      feed,
      global.Region.REGION_TOPIC
    );
  });
  it("should send OnlyToMain pref update if config for pref has skipBroadcast: true", async () => {
    feed.onPrefChanged("baz", { value: 2, skipBroadcast: true });
    assert.calledWith(
      feed.store.dispatch,
      ac.OnlyToMain({
        type: at.PREF_CHANGED,
        data: { name: "baz", value: { value: 2, skipBroadcast: true } },
      })
    );
  });
  it("should send AlsoToPreloaded pref update if config for pref has skipBroadcast: true and alsoToPreloaded: true", async () => {
    feed.onPrefChanged("qux", {
      value: 2,
      skipBroadcast: true,
      alsoToPreloaded: true,
    });
    assert.calledWith(
      feed.store.dispatch,
      ac.AlsoToPreloaded({
        type: at.PREF_CHANGED,
        data: {
          name: "qux",
          value: { value: 2, skipBroadcast: true, alsoToPreloaded: true },
        },
      })
    );
  });
  describe("#observe", () => {
    it("should call dispatch from observe", () => {
      feed.observe(undefined, global.Region.REGION_TOPIC);
      assert.calledOnce(feed.store.dispatch);
    });
  });
  describe("#_setStringPref", () => {
    it("should call _setPref and getStringPref from _setStringPref", () => {
      feed._setStringPref({}, "fake.pref", "default");
      assert.calledOnce(feed._setPref);
      assert.calledWith(
        feed._setPref,
        { "fake.pref": undefined },
        "fake.pref",
        "default"
      );
      assert.calledOnce(ServicesStub.prefs.getStringPref);
      assert.calledWith(
        ServicesStub.prefs.getStringPref,
        "browser.newtabpage.activity-stream.fake.pref",
        "default"
      );
    });
  });
  describe("#_setBoolPref", () => {
    it("should call _setPref and getBoolPref from _setBoolPref", () => {
      feed._setBoolPref({}, "fake.pref", false);
      assert.calledOnce(feed._setPref);
      assert.calledWith(
        feed._setPref,
        { "fake.pref": undefined },
        "fake.pref",
        false
      );
      assert.calledOnce(ServicesStub.prefs.getBoolPref);
      assert.calledWith(
        ServicesStub.prefs.getBoolPref,
        "browser.newtabpage.activity-stream.fake.pref",
        false
      );
    });
  });
  describe("#_setIntPref", () => {
    it("should call _setPref and getIntPref from _setIntPref", () => {
      feed._setIntPref({}, "fake.pref", 1);
      assert.calledOnce(feed._setPref);
      assert.calledWith(
        feed._setPref,
        { "fake.pref": undefined },
        "fake.pref",
        1
      );
      assert.calledOnce(ServicesStub.prefs.getIntPref);
      assert.calledWith(
        ServicesStub.prefs.getIntPref,
        "browser.newtabpage.activity-stream.fake.pref",
        1
      );
    });
  });
  describe("#_setPref", () => {
    it("should set pref value with _setPref", () => {
      const getPrefFunctionSpy = sinon.spy();
      const values = {};
      feed._setPref(values, "fake.pref", "default", getPrefFunctionSpy);
      assert.deepEqual(values, { "fake.pref": undefined });
      assert.calledOnce(getPrefFunctionSpy);
      assert.calledWith(
        getPrefFunctionSpy,
        "browser.newtabpage.activity-stream.fake.pref",
        "default"
      );
    });
  });

  describe("Activation Window Evaluation", () => {
    let mockCreatedInstant;
    let mockNowInstant;
    let defaultBranch;

    const TEST_VARIANT = "a";

    beforeEach(() => {
      // Mock Temporal.Instant for time control
      // Create a mock instant representing profile creation time (Jan 1, 2024)
      mockCreatedInstant = {
        toString: () => "2024-01-01T00:00:00Z",
      };

      // Mock "now" as 24 hours after creation
      mockNowInstant = {
        toString: () => "2024-01-02T00:00:00Z",
        subtract: sinon.stub().returns({
          toString: () => "2023-12-30T00:00:00Z",
        }),
      };

      global.Temporal = {
        Instant: {
          compare: sinon.stub(),
        },
        Now: {
          instant: sinon.stub().returns(mockNowInstant),
        },
      };

      global.AboutNewTab = {
        activityStream: {
          createdInstant: mockCreatedInstant,
        },
      };

      defaultBranch = {
        setBoolPref: sinon.spy(),
      };
      ServicesStub.prefs.getDefaultBranch = sinon.stub().returns(defaultBranch);

      // Setup store state with a fake activation window config
      feed.store.state = {
        Prefs: {
          values: {
            trainhopConfig: {
              activationWindowBehavior: {
                enabled: true,
                maxProfileAgeInHours: 48,
                disableTopSites: true,
                disableTopStories: true,
                variant: TEST_VARIANT,
                enterActivationWindowMessageID: "",
                exitActivationWindowMessageID: "",
              },
            },
          },
        },
      };

      sinon.spy(feed, "enterActivationWindowState");
      sinon.spy(feed, "exitActivationWindowState");
    });

    afterEach(() => {
      delete global.Temporal;
      delete global.AboutNewTab;
    });

    describe("#checkForActivationWindow", () => {
      it("should enter activation window state when profile is within window", () => {
        // First call: createdInstant < now (comparison returns -1)
        // Second call: createdInstant > (now - 48 hours) (comparison returns 1)
        global.Temporal.Instant.compare.onFirstCall().returns(-1);
        global.Temporal.Instant.compare.onSecondCall().returns(1);

        feed.checkForActivationWindow(mockNowInstant);

        assert.calledOnce(feed.enterActivationWindowState);
        assert.calledWith(
          feed.enterActivationWindowState,
          TEST_VARIANT,
          true,
          true
        );
      });

      it("should exit activation window state when profile is outside window", () => {
        feed.inActivationWindowState = TEST_VARIANT;
        feed._prefs.isSet = sinon.stub().returns(false);

        // First call: createdInstant < now (comparison returns -1)
        // Second call: createdInstant < (now - 48 hours) (comparison returns -1, meaning too old)
        global.Temporal.Instant.compare.onFirstCall().returns(-1);
        global.Temporal.Instant.compare.onSecondCall().returns(-1);

        feed.checkForActivationWindow(mockNowInstant);

        assert.notCalled(feed.enterActivationWindowState);
        assert.calledOnce(feed.exitActivationWindowState);
      });

      it("should not enter activation window when profile is in the future", () => {
        // First call: createdInstant > now (comparison returns 1)
        global.Temporal.Instant.compare.onFirstCall().returns(1);

        feed.checkForActivationWindow(mockNowInstant);

        assert.notCalled(feed.enterActivationWindowState);
        assert.notCalled(feed.exitActivationWindowState);
      });

      it("should not enter activation window when profile is exactly at boundary", () => {
        // First call: createdInstant < now (comparison returns -1)
        // Second call: createdInstant === (now - 48 hours) (comparison returns 0)
        global.Temporal.Instant.compare.onFirstCall().returns(-1);
        global.Temporal.Instant.compare.onSecondCall().returns(0);

        feed.checkForActivationWindow(mockNowInstant);

        assert.notCalled(feed.enterActivationWindowState);
      });

      it("should not evaluate when config is disabled", () => {
        feed.store.state.Prefs.values.trainhopConfig.activationWindowBehavior.enabled = false;

        feed.checkForActivationWindow(mockNowInstant);

        assert.notCalled(feed.enterActivationWindowState);
        assert.notCalled(feed.exitActivationWindowState);
      });

      it("should not enter the activation window if 1 or more selectable profiles have been created", () => {
        // First call: createdInstant < now (comparison returns -1)
        // Second call: createdInstant > (now - 48 hours) (comparison returns 1)
        global.Temporal.Instant.compare.onFirstCall().returns(-1);
        global.Temporal.Instant.compare.onSecondCall().returns(1);

        sandbox
          .stub(SelectableProfileServiceStub, "hasCreatedSelectableProfiles")
          .returns(true);
        feed.checkForActivationWindow(mockNowInstant);

        assert.notCalled(feed.enterActivationWindowState);
        assert.notCalled(feed.exitActivationWindowState);
      });

      it("should exit activation window state when config is disabled but currently in state", () => {
        feed.inActivationWindowState = TEST_VARIANT;
        feed.store.state.Prefs.values.trainhopConfig.activationWindowBehavior.enabled = false;
        feed._prefs.isSet = sinon.stub().returns(false);

        feed.checkForActivationWindow(mockNowInstant);

        assert.notCalled(feed.enterActivationWindowState);
        assert.calledOnce(feed.exitActivationWindowState);
      });

      it("should not evaluate when createdInstant is missing", () => {
        global.AboutNewTab.activityStream.createdInstant = null;

        feed.checkForActivationWindow(mockNowInstant);

        assert.notCalled(feed.enterActivationWindowState);
        assert.notCalled(feed.exitActivationWindowState);
      });

      it("should exit activation window state when createdInstant is missing but currently in state", () => {
        feed.inActivationWindowState = TEST_VARIANT;
        global.AboutNewTab.activityStream.createdInstant = null;
        feed._prefs.isSet = sinon.stub().returns(false);

        feed.checkForActivationWindow(mockNowInstant);

        assert.notCalled(feed.enterActivationWindowState);
        assert.calledOnce(feed.exitActivationWindowState);
      });

      it("should not evaluate when variant is missing", () => {
        feed.store.state.Prefs.values.trainhopConfig.activationWindowBehavior.variant =
          "";

        feed.checkForActivationWindow(mockNowInstant);

        assert.notCalled(feed.enterActivationWindowState);
        assert.notCalled(feed.exitActivationWindowState);
      });

      it("should exit activation window state when variant is missing but currently in state", () => {
        feed.inActivationWindowState = TEST_VARIANT;
        feed.store.state.Prefs.values.trainhopConfig.activationWindowBehavior.variant =
          "";
        feed._prefs.isSet = sinon.stub().returns(false);

        feed.checkForActivationWindow(mockNowInstant);

        assert.notCalled(feed.enterActivationWindowState);
        assert.calledOnce(feed.exitActivationWindowState);
      });

      it("should return early when store state is missing", () => {
        feed.store.state = null;

        feed.checkForActivationWindow(mockNowInstant);

        assert.notCalled(feed.enterActivationWindowState);
        assert.notCalled(feed.exitActivationWindowState);
      });

      it("should return early when Prefs state is missing", () => {
        feed.store.state = { Prefs: null };

        feed.checkForActivationWindow(mockNowInstant);

        assert.notCalled(feed.enterActivationWindowState);
        assert.notCalled(feed.exitActivationWindowState);
      });

      it("should call enterActivationWindowState even if already in state", () => {
        feed.inActivationWindowState = TEST_VARIANT;

        // First call: createdInstant < now (comparison returns -1)
        // Second call: createdInstant > (now - 48 hours) (comparison returns 1)
        global.Temporal.Instant.compare.onFirstCall().returns(-1);
        global.Temporal.Instant.compare.onSecondCall().returns(1);

        feed.checkForActivationWindow(mockNowInstant);

        assert.calledOnce(feed.enterActivationWindowState);
        assert.notCalled(feed.exitActivationWindowState);
      });

      it("should use default now instant when not provided", () => {
        // Set up for within window
        global.Temporal.Instant.compare.onFirstCall().returns(-1);
        global.Temporal.Instant.compare.onSecondCall().returns(1);

        feed.checkForActivationWindow();

        assert.calledOnce(global.Temporal.Now.instant);
        assert.calledOnce(feed.enterActivationWindowState);
      });

      it("should pass isStartup=true to enterActivationWindowState when called with isStartup=true", () => {
        // Set up for within window
        global.Temporal.Instant.compare.onFirstCall().returns(-1);
        global.Temporal.Instant.compare.onSecondCall().returns(1);

        feed.checkForActivationWindow(mockNowInstant, /* isStartup */ true);

        assert.calledOnce(feed.enterActivationWindowState);
        assert.calledWith(
          feed.enterActivationWindowState,
          TEST_VARIANT,
          true,
          true,
          "",
          true
        );
      });

      it("should pass isStartup=false to enterActivationWindowState when called without isStartup", () => {
        // Set up for within window
        global.Temporal.Instant.compare.onFirstCall().returns(-1);
        global.Temporal.Instant.compare.onSecondCall().returns(1);

        feed.checkForActivationWindow(mockNowInstant);

        assert.calledOnce(feed.enterActivationWindowState);
        assert.calledWith(
          feed.enterActivationWindowState,
          TEST_VARIANT,
          true,
          true,
          "",
          false
        );
      });
    });
  });

  describe("Activation Window Broadcasting", () => {
    const TEST_VARIANT = "a";

    let defaultBranch;
    beforeEach(() => {
      defaultBranch = {
        setBoolPref: sinon.spy(),
      };
      ServicesStub.prefs.getDefaultBranch = sinon.stub().returns(defaultBranch);
      sinon.spy(feed, "onPrefChanged");
    });

    describe("#enterActivationWindowState", () => {
      it("should broadcast pref changes when entering activation window", () => {
        feed.inActivationWindowState = "";

        feed.enterActivationWindowState(TEST_VARIANT, true, true, "");

        assert.calledTwice(feed.onPrefChanged);
        assert.calledWith(feed.onPrefChanged, "feeds.topsites", false);
        assert.calledWith(
          feed.onPrefChanged,
          "feeds.section.topstories",
          false
        );
      });

      it("should not broadcast when already in the same variant", () => {
        feed.inActivationWindowState = TEST_VARIANT;

        feed.enterActivationWindowState(TEST_VARIANT, true, true, "");

        assert.notCalled(feed.onPrefChanged);
        assert.notCalled(defaultBranch.setBoolPref);
      });

      it("should broadcast when entering with different variant", () => {
        feed.inActivationWindowState = "variant-a";

        feed.enterActivationWindowState("variant-b", true, true, "");

        assert.calledTwice(feed.onPrefChanged);
      });

      it("should only broadcast for top sites if only disabling top sites", () => {
        feed.inActivationWindowState = "";

        feed.enterActivationWindowState(TEST_VARIANT, true, false, "");

        assert.calledOnce(feed.onPrefChanged);
        assert.calledWith(feed.onPrefChanged, "feeds.topsites", false);
      });

      it("should only broadcast for top stories if only disabling top stories", () => {
        feed.inActivationWindowState = "";

        feed.enterActivationWindowState(TEST_VARIANT, false, true, "");

        assert.calledOnce(feed.onPrefChanged);
        assert.calledWith(
          feed.onPrefChanged,
          "feeds.section.topstories",
          false
        );
      });

      it("should not broadcast if not disabling anything", () => {
        feed.inActivationWindowState = "";

        feed.enterActivationWindowState(TEST_VARIANT, false, false, "");

        assert.notCalled(feed.onPrefChanged);
      });

      it("should reapply prefs on startup even when already in the same variant", () => {
        feed.inActivationWindowState = TEST_VARIANT;

        feed.enterActivationWindowState(
          TEST_VARIANT,
          true,
          true,
          "",
          /* isStartup */ true
        );

        assert.calledTwice(defaultBranch.setBoolPref);
        assert.calledWith(defaultBranch.setBoolPref, "feeds.topsites", false);
        assert.calledWith(
          defaultBranch.setBoolPref,
          "feeds.section.topstories",
          false
        );
      });

      it("should broadcast on startup even when already in the same variant", () => {
        feed.inActivationWindowState = TEST_VARIANT;

        feed.enterActivationWindowState(
          TEST_VARIANT,
          true,
          true,
          "",
          /* isStartup */ true
        );

        assert.calledTwice(feed.onPrefChanged);
        assert.calledWith(feed.onPrefChanged, "feeds.topsites", false);
        assert.calledWith(
          feed.onPrefChanged,
          "feeds.section.topstories",
          false
        );
      });

      it("should skip idempotent check when isStartup=true", () => {
        feed.inActivationWindowState = TEST_VARIANT;

        feed.enterActivationWindowState(
          TEST_VARIANT,
          true,
          false,
          "",
          /* isStartup */ true
        );

        assert.calledOnce(feed.onPrefChanged);
        assert.calledWith(feed.onPrefChanged, "feeds.topsites", false);
      });

      it("should set enter message ID pref when provided", () => {
        feed.inActivationWindowState = "";

        feed.enterActivationWindowState(
          "test-variant",
          true,
          true,
          "ENTER_MESSAGE_ID"
        );

        assert.calledWith(
          feed._prefs.set,
          "activationWindow.enterMessageID",
          "ENTER_MESSAGE_ID"
        );
      });

      it("should not set enter message ID pref when empty string", () => {
        feed.inActivationWindowState = "";

        feed.enterActivationWindowState("test-variant", true, true, "");

        assert.neverCalledWith(
          feed._prefs.set,
          "activationWindow.enterMessageID"
        );
      });
    });

    describe("#exitActivationWindowState", () => {
      beforeEach(() => {
        feed._prefs.isSet = sinon.stub();
      });

      it("should broadcast pref changes when no user values were set", () => {
        feed._prefs.isSet.returns(false);

        feed.exitActivationWindowState();

        assert.calledTwice(feed.onPrefChanged);
        assert.calledWith(feed.onPrefChanged, "feeds.topsites", true);
        assert.calledWith(feed.onPrefChanged, "feeds.section.topstories", true);
      });

      it("should only broadcast for top stories if top sites had user value", () => {
        feed._prefs.isSet
          .withArgs("activationWindow.temp.topSitesUserValue")
          .returns(true);
        feed._prefs.isSet
          .withArgs("activationWindow.temp.topStoriesUserValue")
          .returns(false);
        FAKE_PREFS.set("activationWindow.temp.topSitesUserValue", false);

        feed.exitActivationWindowState();

        assert.calledOnce(feed.onPrefChanged);
        assert.calledWith(feed.onPrefChanged, "feeds.section.topstories", true);
        assert.neverCalledWith(feed.onPrefChanged, "feeds.topsites", true);
      });

      it("should only broadcast for top sites if top stories had user value", () => {
        feed._prefs.isSet
          .withArgs("activationWindow.temp.topSitesUserValue")
          .returns(false);
        feed._prefs.isSet
          .withArgs("activationWindow.temp.topStoriesUserValue")
          .returns(true);
        FAKE_PREFS.set("activationWindow.temp.topStoriesUserValue", true);

        feed.exitActivationWindowState();

        assert.calledOnce(feed.onPrefChanged);
        assert.calledWith(feed.onPrefChanged, "feeds.topsites", true);
        assert.neverCalledWith(
          feed.onPrefChanged,
          "feeds.section.topstories",
          true
        );
      });

      it("should not broadcast if both prefs had user values", () => {
        feed._prefs.isSet.returns(true);
        FAKE_PREFS.set("activationWindow.temp.topSitesUserValue", false);
        FAKE_PREFS.set("activationWindow.temp.topStoriesUserValue", true);

        feed.exitActivationWindowState();

        assert.notCalled(feed.onPrefChanged);
      });

      it("should clear enter message ID pref on exit", () => {
        feed._prefs.isSet.returns(false);

        feed.exitActivationWindowState();

        assert.calledWith(
          feed._prefs.set,
          "activationWindow.enterMessageID",
          ""
        );
      });

      it("should set exit message ID pref when provided", () => {
        feed._prefs.isSet.returns(false);

        feed.exitActivationWindowState("EXIT_MESSAGE_ID");

        assert.calledWith(
          feed._prefs.set,
          "activationWindow.exitMessageID",
          "EXIT_MESSAGE_ID"
        );
      });

      it("should clear exit message ID pref when not provided", () => {
        feed._prefs.isSet.returns(false);

        feed.exitActivationWindowState();

        assert.calledWith(
          feed._prefs.set,
          "activationWindow.exitMessageID",
          ""
        );
      });
    });

    describe("store.dispatch integration", () => {
      beforeEach(() => {
        feed.inActivationWindowState = "";
        FAKE_PREFS.set("feeds.topsites", { value: true });
        FAKE_PREFS.set("feeds.section.topstories", { value: true });
      });

      it("should dispatch PREF_CHANGED actions when entering activation window", () => {
        feed.enterActivationWindowState(TEST_VARIANT, true, true);

        assert.calledWith(
          feed.store.dispatch,
          sinon.match({
            type: at.PREF_CHANGED,
            data: { name: "feeds.topsites", value: false },
          })
        );
        assert.calledWith(
          feed.store.dispatch,
          sinon.match({
            type: at.PREF_CHANGED,
            data: { name: "feeds.section.topstories", value: false },
          })
        );
      });

      it("should dispatch PREF_CHANGED actions when exiting activation window", () => {
        feed._prefs.isSet = sinon.stub().returns(false);

        feed.exitActivationWindowState();

        assert.calledWith(
          feed.store.dispatch,
          sinon.match({
            type: at.PREF_CHANGED,
            data: { name: "feeds.topsites", value: true },
          })
        );
        assert.calledWith(
          feed.store.dispatch,
          sinon.match({
            type: at.PREF_CHANGED,
            data: { name: "feeds.section.topstories", value: true },
          })
        );
      });
    });
  });

  describe("Activation Window User Preference Tracking", () => {
    describe("#trackActivationWindowPrefChange", () => {
      it("should track top sites user value when changed during activation window", () => {
        feed.trackActivationWindowPrefChange("feeds.topsites", false);
        assert.calledWith(
          feed._prefs.set,
          "activationWindow.temp.topSitesUserValue",
          false
        );
      });

      it("should track top stories user value when changed during activation window", () => {
        feed.trackActivationWindowPrefChange("feeds.section.topstories", true);
        assert.calledWith(
          feed._prefs.set,
          "activationWindow.temp.topStoriesUserValue",
          true
        );
      });

      it("should not track changes for other prefs", () => {
        feed.trackActivationWindowPrefChange("some.other.pref", false);
        assert.neverCalledWith(
          feed._prefs.set,
          "activationWindow.temp.topSitesUserValue"
        );
        assert.neverCalledWith(
          feed._prefs.set,
          "activationWindow.temp.topStoriesUserValue"
        );
      });
    });

    describe("#onPrefChanged with activation window tracking", () => {
      beforeEach(() => {
        feed.inActivationWindowState = "variant-a";
        sinon.spy(feed, "trackActivationWindowPrefChange");
      });

      it("should call trackActivationWindowPrefChange when in activation window", () => {
        feed.onPrefChanged("feeds.topsites", false);
        assert.calledOnce(feed.trackActivationWindowPrefChange);
        assert.calledWith(
          feed.trackActivationWindowPrefChange,
          "feeds.topsites",
          false
        );
      });

      it("should not call trackActivationWindowPrefChange when not in activation window", () => {
        feed.inActivationWindowState = "";
        feed.onPrefChanged("feeds.topsites", false);
        assert.notCalled(feed.trackActivationWindowPrefChange);
      });

      it("should not track when isUserChange=false even if in activation window", () => {
        feed.onPrefChanged("feeds.topsites", false, /* isUserChange */ false);
        assert.notCalled(feed.trackActivationWindowPrefChange);
      });

      it("should track when isUserChange=true (default) and in activation window", () => {
        feed.onPrefChanged("feeds.topsites", false, /* isUserChange */ true);
        assert.calledOnce(feed.trackActivationWindowPrefChange);
      });

      it("should track when isUserChange not specified and in activation window", () => {
        feed.onPrefChanged("feeds.topsites", false);
        assert.calledOnce(feed.trackActivationWindowPrefChange);
      });
    });

    describe("#exitActivationWindowState", () => {
      let defaultBranch;
      beforeEach(() => {
        defaultBranch = {
          setBoolPref: sinon.spy(),
        };
        ServicesStub.prefs.getDefaultBranch = sinon
          .stub()
          .returns(defaultBranch);
      });

      it("should reset defaults to true and restore user's top sites value", () => {
        feed._prefs.isSet = sinon.stub();
        feed._prefs.isSet
          .withArgs("activationWindow.temp.topSitesUserValue")
          .returns(true);
        feed._prefs.isSet
          .withArgs("activationWindow.temp.topStoriesUserValue")
          .returns(false);
        FAKE_PREFS.set("activationWindow.temp.topSitesUserValue", false);

        feed.exitActivationWindowState();

        assert.calledWith(defaultBranch.setBoolPref, "feeds.topsites", true);
        assert.calledWith(
          defaultBranch.setBoolPref,
          "feeds.section.topstories",
          true
        );
        assert.calledWith(feed._prefs.set, "feeds.topsites", false);
        assert.calledWith(
          feed._prefs.reset,
          "activationWindow.temp.topSitesUserValue"
        );
      });

      it("should reset defaults to true and restore user's top stories value", () => {
        feed._prefs.isSet = sinon.stub();
        feed._prefs.isSet
          .withArgs("activationWindow.temp.topSitesUserValue")
          .returns(false);
        feed._prefs.isSet
          .withArgs("activationWindow.temp.topStoriesUserValue")
          .returns(true);
        FAKE_PREFS.set("activationWindow.temp.topStoriesUserValue", true);

        feed.exitActivationWindowState();

        assert.calledWith(defaultBranch.setBoolPref, "feeds.topsites", true);
        assert.calledWith(
          defaultBranch.setBoolPref,
          "feeds.section.topstories",
          true
        );
        assert.calledWith(feed._prefs.set, "feeds.section.topstories", true);
        assert.calledWith(
          feed._prefs.reset,
          "activationWindow.temp.topStoriesUserValue"
        );
      });

      it("should only reset defaults when no user changes were made", () => {
        feed._prefs.isSet = sinon.stub().returns(false);

        feed.exitActivationWindowState();

        assert.calledWith(defaultBranch.setBoolPref, "feeds.topsites", true);
        assert.calledWith(
          defaultBranch.setBoolPref,
          "feeds.section.topstories",
          true
        );
        assert.neverCalledWith(feed._prefs.set, "feeds.topsites");
        assert.neverCalledWith(feed._prefs.set, "feeds.section.topstories");
      });

      it("should clear activation window variant pref", () => {
        feed._prefs.isSet = sinon.stub().returns(false);

        feed.exitActivationWindowState();

        assert.calledWith(feed._prefs.reset, "activationWindow.variant");
      });

      it("should handle user disabling top sites during activation window", () => {
        feed._prefs.isSet = sinon.stub();
        feed._prefs.isSet
          .withArgs("activationWindow.temp.topSitesUserValue")
          .returns(true);
        feed._prefs.isSet
          .withArgs("activationWindow.temp.topStoriesUserValue")
          .returns(false);
        FAKE_PREFS.set("activationWindow.temp.topSitesUserValue", false);

        feed.exitActivationWindowState();

        assert.calledWith(defaultBranch.setBoolPref, "feeds.topsites", true);
        assert.calledWith(feed._prefs.set, "feeds.topsites", false);
      });

      it("should handle user enabling top sites during activation window", () => {
        feed._prefs.isSet = sinon.stub();
        feed._prefs.isSet
          .withArgs("activationWindow.temp.topSitesUserValue")
          .returns(true);
        feed._prefs.isSet
          .withArgs("activationWindow.temp.topStoriesUserValue")
          .returns(false);
        FAKE_PREFS.set("activationWindow.temp.topSitesUserValue", true);

        feed.exitActivationWindowState();

        assert.calledWith(defaultBranch.setBoolPref, "feeds.topsites", true);
        assert.calledWith(feed._prefs.set, "feeds.topsites", true);
      });
    });
  });
});
