"use strict";

const { ExperimentStore } = ChromeUtils.importESModule(
  "resource://nimbus/lib/ExperimentStore.sys.mjs"
);

const { SYNC_DATA_PREF_BRANCH, SYNC_DEFAULTS_PREF_BRANCH } = ExperimentStore;

add_setup(function () {
  Services.fog.initializeFOG();
});

async function setupTest({ ...args } = {}) {
  const ctx = await NimbusTestUtils.setupTest({ ...args });

  return {
    ...ctx,
    store: ctx.manager.store,
  };
}

add_task(async function test_sharedDataMap_key() {
  const store = new ExperimentStore();

  // Outside of tests we use sharedDataKey for the profile dir filepath
  // where we store experiments
  Assert.ok(store._sharedDataKey, "Make sure it's defined");
});

add_task(async function test_usageBeforeInitialization() {
  const { store, initExperimentAPI, cleanup } = await setupTest({
    init: false,
  });
  const recipe = NimbusTestUtils.factories.recipe("foo");

  Assert.equal(store.getAll().length, 0, "It should not fail");

  await initExperimentAPI();

  const experiment = NimbusTestUtils.addEnrollmentForRecipe(recipe, {
    branchSlug: "control",
  });

  Assert.equal(
    store.getExperimentForFeature("testFeature"),
    experiment,
    "should return a matching experiment for the given feature"
  );

  store.deactivateEnrollment(recipe.slug);

  await cleanup();
});

async function test_initOnUpdateEventsFire() {
  let storePath;

  {
    const store = NimbusTestUtils.stubs.store();
    await store.init();

    NimbusTestUtils.addEnrollmentForRecipe(
      NimbusTestUtils.factories.recipe.withFeatureConfig("testFeature-1", {
        featureId: "testFeature",
      }),
      { store }
    );
    NimbusTestUtils.addEnrollmentForRecipe(
      NimbusTestUtils.factories.recipe.withFeatureConfig(
        "testFeature-2",
        {
          featureId: "testFeature",
        },
        { isRollout: true }
      ),
      { store }
    );
    NimbusTestUtils.addEnrollmentForRecipe(
      NimbusTestUtils.factories.recipe.withFeatureConfig("nimbus-qa-1", {
        featureId: "nimbus-qa-1",
      }),
      {
        store,
        extra: { active: false },
      }
    );
    NimbusTestUtils.addEnrollmentForRecipe(
      NimbusTestUtils.factories.recipe.withFeatureConfig(
        "nimbus-qa-2",
        { featureId: "nimbus-qa-2" },
        { isRollout: true }
      ),
      {
        store,
        extra: { active: false },
      }
    );

    NimbusTestUtils.addEnrollmentForRecipe(
      NimbusTestUtils.factories.recipe.withFeatureConfig("coenroll-1", {
        featureId: "no-feature-firefox-desktop",
      }),
      { store }
    );
    NimbusTestUtils.addEnrollmentForRecipe(
      NimbusTestUtils.factories.recipe.withFeatureConfig("coenroll-2", {
        featureId: "no-feature-firefox-desktop",
      }),
      { store }
    );
    NimbusTestUtils.addEnrollmentForRecipe(
      NimbusTestUtils.factories.recipe.withFeatureConfig("coenroll-3", {
        featureId: "no-feature-firefox-desktop",
      }),
      { store }
    );
    NimbusTestUtils.addEnrollmentForRecipe(
      NimbusTestUtils.factories.recipe.withFeatureConfig("coenroll-4", {
        featureId: "no-feature-firefox-desktop",
      }),
      { store }
    );

    storePath = await NimbusTestUtils.saveStore(store);
  }

  const { sandbox, initExperimentAPI, cleanup } = await setupTest({
    storePath,
    init: false,
    migrationState: NimbusTestUtils.migrationState.IMPORTED_ENROLLMENTS_TO_SQL,
  });

  const onFeatureUpdate = sandbox.stub();

  NimbusFeatures.testFeature.onUpdate(onFeatureUpdate);
  NimbusFeatures["nimbus-qa-1"].onUpdate(onFeatureUpdate);
  NimbusFeatures["nimbus-qa-2"].onUpdate(onFeatureUpdate);
  NimbusFeatures["no-feature-firefox-desktop"].onUpdate(onFeatureUpdate);

  await initExperimentAPI();

  Assert.ok(
    onFeatureUpdate.calledWithExactly(
      "featureUpdate:testFeature",
      "feature-enrollments-loaded"
    )
  );
  Assert.ok(
    onFeatureUpdate.calledWithExactly(
      "featureUpdate:no-feature-firefox-desktop",
      "feature-enrollments-loaded"
    )
  );
  Assert.equal(
    onFeatureUpdate.callCount,
    2,
    "onFeatureUpdate called once per active feature ID"
  );

  NimbusFeatures.testFeature.offUpdate(onFeatureUpdate);

  await NimbusTestUtils.cleanupManager([
    "testFeature-1",
    "testFeature-2",
    "coenroll-1",
    "coenroll-2",
    "coenroll-3",
    "coenroll-4",
  ]);
  await cleanup();
}

add_task(test_initOnUpdateEventsFire);
add_task(async function test_initOnUpdateEventsFireDb() {
  const resetNimbusEnrollmentPrefs = NimbusTestUtils.enableNimbusEnrollments({
    read: true,
  });
  await test_initOnUpdateEventsFire();
  resetNimbusEnrollmentPrefs();
});

add_task(async function test_getExperimentForGroup() {
  const { store, cleanup } = await setupTest();

  const experiment = NimbusTestUtils.addEnrollmentForRecipe(
    NimbusTestUtils.factories.recipe.withFeatureConfig("foo", {
      featureId: "purple",
    })
  );
  NimbusTestUtils.addEnrollmentForRecipe(
    NimbusTestUtils.factories.recipe("bar"),
    { branchSlug: "control" }
  );

  Assert.equal(
    store.getExperimentForFeature("purple"),
    experiment,
    "should return a matching experiment for the given feature"
  );

  store.deactivateEnrollment("foo");
  store.deactivateEnrollment("bar");

  await cleanup();
});

add_task(async function test_hasExperimentForFeature() {
  const { store, cleanup } = await setupTest();

  NimbusTestUtils.addEnrollmentForRecipe(
    NimbusTestUtils.factories.recipe.withFeatureConfig("foo", {
      featureId: "green",
    })
  );
  NimbusTestUtils.addEnrollmentForRecipe(
    NimbusTestUtils.factories.recipe.withFeatureConfig("foo2", {
      featureId: "yellow",
    })
  );
  NimbusTestUtils.addEnrollmentForRecipe(
    NimbusTestUtils.factories.recipe.withFeatureConfig("bar_expired", {
      featureId: "purple",
    }),
    {
      extra: { active: false },
    }
  );
  Assert.equal(
    store.hasExperimentForFeature(),
    false,
    "should return false if the input is empty"
  );

  Assert.equal(
    store.hasExperimentForFeature(undefined),
    false,
    "should return false if the input is undefined"
  );

  Assert.equal(
    store.hasExperimentForFeature("green"),
    true,
    "should return true if there is an experiment with any of the given groups"
  );

  Assert.equal(
    store.hasExperimentForFeature("purple"),
    false,
    "should return false if there is a non-active experiment with the given groups"
  );

  store.deactivateEnrollment("foo");
  store.deactivateEnrollment("foo2");

  await cleanup();
});

add_task(async function test_getAll() {
  const { store, cleanup } = await setupTest();

  NimbusTestUtils.addEnrollmentForRecipe(
    NimbusTestUtils.factories.recipe("foo"),
    { branchSlug: "control", extra: { active: false } }
  );
  NimbusTestUtils.addEnrollmentForRecipe(
    NimbusTestUtils.factories.recipe("bar"),
    { branchSlug: "control", extra: { active: false } }
  );
  NimbusTestUtils.addEnrollmentForRecipe(
    NimbusTestUtils.factories.recipe("baz"),
    { branchSlug: "control", extra: { active: false } }
  );
  NimbusTestUtils.addEnrollmentForRecipe(
    NimbusTestUtils.factories.recipe("qux"),
    { branchSlug: "control", extra: { active: true } }
  );
  NimbusTestUtils.addEnrollmentForRecipe(
    NimbusTestUtils.factories.recipe("quux", { isRollout: true }),
    { branchSlug: "control", extra: { active: false } }
  );
  NimbusTestUtils.addEnrollmentForRecipe(
    NimbusTestUtils.factories.recipe("corge", { isRollout: true }),
    { branchSlug: "control", extra: { active: false } }
  );
  NimbusTestUtils.addEnrollmentForRecipe(
    NimbusTestUtils.factories.recipe("grault", { isRollout: true }),
    { branchSlug: "control", extra: { active: false } }
  );
  NimbusTestUtils.addEnrollmentForRecipe(
    NimbusTestUtils.factories.recipe("garply", { isRollout: true }),
    { branchSlug: "control", extra: { active: true } }
  );

  Assert.deepEqual(
    store.getAll().map(e => e.slug),
    ["foo", "bar", "baz", "qux", "quux", "corge", "grault", "garply"],
    ".getAll() should return all experiments"
  );
  Assert.deepEqual(
    store.getAllActiveExperiments().map(e => e.slug),
    ["qux"],
    "getAllActiveExperiments() should return all experiments that are active"
  );
  Assert.deepEqual(
    store.getAllActiveRollouts().map(e => e.slug),
    ["garply"],
    "getAllActiveRollouts() should return all experiments that are active"
  );

  store.deactivateEnrollment("qux");
  store.deactivateEnrollment("garply");

  await cleanup();
});

add_task(async function test_addEnrollment() {
  const { store, cleanup } = await setupTest();

  const experiment = NimbusTestUtils.addEnrollmentForRecipe(
    NimbusTestUtils.factories.recipe("experiment"),
    { branchSlug: "control" }
  );
  const rollout = NimbusTestUtils.addEnrollmentForRecipe(
    NimbusTestUtils.factories.recipe("rollout", { isRollout: true })
  );

  Assert.equal(
    store.get("experiment"),
    experiment,
    "should save experiment by slug"
  );
  Assert.equal(store.get("rollout"), rollout, "should save experiment by slug");

  store.deactivateEnrollment("experiment");
  store.deactivateEnrollment("rollout");

  await cleanup();
});

add_task(async function test_deactivateEnrollment() {
  const { store, cleanup } = await setupTest();

  const enrollment = NimbusTestUtils.addEnrollmentForRecipe(
    NimbusTestUtils.factories.recipe.withFeatureConfig("foo", {
      branchSlug: "treatment",
      featureId: "no-feature-firefox-desktop",
    })
  );

  store.deactivateEnrollment("foo", "some-reason");

  Assert.deepEqual(
    store.get(enrollment.slug),
    {
      ...enrollment,
      unenrollReason: "some-reason",
      active: false,
      prefs: null,
      prefFlips: null,
    },
    "should only update relevant fields"
  );

  store.deactivateEnrollment("foo");

  await cleanup();
});

add_task(async function test_sync_access_before_init() {
  const { store, cleanup } = await setupTest();

  Assert.equal(store.getAll().length, 0, "Start with an empty store");

  NimbusTestUtils.addEnrollmentForRecipe(
    NimbusTestUtils.factories.recipe.withFeatureConfig("foo", {
      featureId: "newtab",
    })
  );

  const prefValue = JSON.parse(
    Services.prefs.getStringPref(`${SYNC_DATA_PREF_BRANCH}newtab`)
  );

  Assert.ok(prefValue, "Parsed stored experiment");
  Assert.equal(prefValue.slug, "foo", "Got back the experiment");

  // New un-initialized store that should read the pref value
  const newStore = NimbusTestUtils.stubs.store();

  Assert.equal(
    newStore.getExperimentForFeature("newtab").slug,
    "foo",
    "Returns experiment from pref"
  );

  store.deactivateEnrollment("foo");

  await cleanup();

  await NimbusTestUtils.assert.storeIsEmpty(newStore);
});

add_task(async function test_sync_features_only() {
  const { store, cleanup } = await setupTest();

  NimbusTestUtils.addEnrollmentForRecipe(
    NimbusTestUtils.factories.recipe.withFeatureConfig("foo", {
      featureId: "cfr",
    })
  );

  const newStore = NimbusTestUtils.stubs.store();
  Assert.equal(
    newStore.getAll().length,
    0,
    "cfr is not a sync access experiment"
  );

  store.deactivateEnrollment("foo");

  await cleanup();
});

add_task(async function test_sync_access_unenroll() {
  const { store, cleanup } = await setupTest();

  await store.init();

  NimbusTestUtils.addEnrollmentForRecipe(
    NimbusTestUtils.factories.recipe.withFeatureConfig("foo", {
      featureId: "aboutwelcome",
    })
  );
  store.deactivateEnrollment("foo");

  const newStore = NimbusTestUtils.stubs.store();
  Assert.equal(
    newStore.getAll().length,
    0,
    "Unenrolled experiment is not available via sync store"
  );

  await cleanup();
});

add_task(async function test_sync_access_unenroll_2() {
  const { store, cleanup } = await setupTest();

  await store.init();

  NimbusTestUtils.addEnrollmentForRecipe(
    NimbusTestUtils.factories.recipe.withFeatureConfig("foo", {
      featureId: "newtab",
    })
  );
  NimbusTestUtils.addEnrollmentForRecipe(
    NimbusTestUtils.factories.recipe.withFeatureConfig("bar", {
      featureId: "aboutwelcome",
    })
  );

  Assert.equal(store.getAll().length, 2, "2/2 experiments");

  const newStore = NimbusTestUtils.stubs.store();

  Assert.ok(
    newStore.getExperimentForFeature("aboutwelcome"),
    "Fetches experiment from pref cache even before init (aboutwelcome)"
  );

  store.deactivateEnrollment("bar");

  Assert.ok(
    newStore.getExperimentForFeature("newtab").slug,
    "Fetches experiment from pref cache even before init (newtab)"
  );
  Assert.ok(
    !newStore.getExperimentForFeature("aboutwelcome")?.slug,
    "Experiment was updated and should not be found"
  );

  store.deactivateEnrollment("foo");
  Assert.ok(
    !newStore.getExperimentForFeature("newtab")?.slug,
    "Unenrolled from 2/2 experiments"
  );

  Assert.equal(
    Services.prefs.getStringPref(`${SYNC_DATA_PREF_BRANCH}newtab`, "").length,
    0,
    "Cleared pref 1"
  );
  Assert.equal(
    Services.prefs.getStringPref(`${SYNC_DATA_PREF_BRANCH}aboutwelcome`, "")
      .length,
    0,
    "Cleared pref 2"
  );

  await cleanup();
});

add_task(async function test_getRolloutForFeature_fromStore() {
  const { store, cleanup } = await setupTest();

  await store.init();
  const rollout = NimbusTestUtils.addEnrollmentForRecipe(
    NimbusTestUtils.factories.recipe("foo", { isRollout: true })
  );

  Assert.equal(
    store.getRolloutForFeature(rollout.featureIds[0]),
    rollout,
    "Should return back the same rollout"
  );

  store.deactivateEnrollment("foo");

  await cleanup();
});

add_task(async function test_getRolloutForFeature_fromSyncCache() {
  const { store, cleanup } = await setupTest();
  const recipe = NimbusTestUtils.factories.recipe.withFeatureConfig(
    "foo",
    {
      featureId: "aboutwelcome",
      value: { enabled: true },
    },
    { isRollout: true }
  );

  NimbusTestUtils.addEnrollmentForRecipe(recipe);
  // New uninitialized store will return data from sync cache
  // before init
  const newStore = NimbusTestUtils.stubs.store();

  Assert.ok(
    Services.prefs.getStringPref(`${SYNC_DEFAULTS_PREF_BRANCH}aboutwelcome`),
    "Sync cache is set"
  );
  Assert.equal(
    newStore.getRolloutForFeature(recipe.featureIds[0]).slug,
    recipe.slug,
    "Should return back the same rollout"
  );
  Assert.deepEqual(
    newStore.getRolloutForFeature(recipe.featureIds[0]).branch.features[0],
    recipe.branches[0].features[0],
    "Should return back the same feature"
  );

  store.deactivateEnrollment(recipe.slug);

  await cleanup();
});

add_task(async function test_remoteRollout() {
  const { store, initExperimentAPI, cleanup } = await setupTest({
    init: false,
  });
  const featureUpdateStub = sinon.stub();

  const recipe = NimbusTestUtils.factories.recipe.withFeatureConfig(
    "foo",
    {
      featureId: "aboutwelcome",
      value: { enabled: true },
    },
    { isRollout: true }
  );

  store.on("featureUpdate:aboutwelcome", featureUpdateStub);

  await initExperimentAPI();

  NimbusTestUtils.addEnrollmentForRecipe(recipe);

  Assert.ok(
    Services.prefs.getStringPref(`${SYNC_DEFAULTS_PREF_BRANCH}aboutwelcome`),
    "Sync cache is set"
  );

  store.deactivateEnrollment(recipe.slug);

  Assert.ok(featureUpdateStub.calledTwice, "Called for add and remove");
  Assert.ok(
    store.get(recipe.slug),
    "Rollout is still in the store just not active"
  );
  Assert.ok(
    !store.getRolloutForFeature("aboutwelcome"),
    "Feature rollout should not exist"
  );
  Assert.ok(
    !Services.prefs.getStringPref(
      `${SYNC_DEFAULTS_PREF_BRANCH}aboutwelcome`,
      ""
    ),
    "Sync cache is cleared"
  );

  await cleanup();
});

add_task(async function test_syncDataStore_setDefault() {
  const { store, cleanup } = await setupTest();

  Assert.equal(
    Services.prefs.getStringPref(
      `${SYNC_DEFAULTS_PREF_BRANCH}aboutwelcome`,
      ""
    ),
    "",
    "Pref is empty"
  );

  NimbusTestUtils.addEnrollmentForRecipe(
    NimbusTestUtils.factories.recipe.withFeatureConfig(
      "foo",
      {
        featureId: "aboutwelcome",
        value: { remote: true },
      },
      { isRollout: true }
    )
  );

  Assert.ok(
    Services.prefs.getStringPref(`${SYNC_DEFAULTS_PREF_BRANCH}aboutwelcome`),
    "Stored in pref"
  );

  store.deactivateEnrollment("foo");

  await cleanup();
});

add_task(async function test_syncDataStore_getDefault() {
  const { store, cleanup } = await setupTest();

  const rollout = await NimbusTestUtils.addEnrollmentForRecipe(
    NimbusTestUtils.factories.recipe.withFeatureConfig(
      "aboutwelcome-slug",
      { featureId: "aboutwelcome", value: { remote: true } },
      { isRollout: true }
    )
  );

  Assert.ok(
    Services.prefs.getStringPref(`${SYNC_DEFAULTS_PREF_BRANCH}aboutwelcome`)
  );

  const restoredRollout = store.getRolloutForFeature("aboutwelcome");

  Assert.ok(restoredRollout);
  Assert.ok(
    restoredRollout.branch.features[0].value.remote,
    "Restore data from pref"
  );

  store.deactivateEnrollment(rollout.slug);

  await cleanup();
});

add_task(async function test_addEnrollment_rollout() {
  const { sandbox, store, initExperimentAPI, cleanup } = await setupTest({
    init: false,
  });

  const stub = sandbox.stub();
  const value = { bar: true };
  const recipe = NimbusTestUtils.factories.recipe.withFeatureConfig(
    "foo",
    {
      featureId: "aboutwelcome",
      value,
    },
    { isRollout: true }
  );

  store._onFeatureUpdate("aboutwelcome", stub);

  await initExperimentAPI();

  NimbusTestUtils.addEnrollmentForRecipe(recipe);

  Assert.ok(
    store.getRolloutForFeature("aboutwelcome"),
    "should return an enrollment"
  );
  Assert.equal(stub.callCount, 1, "Called once on update");
  Assert.equal(
    stub.firstCall.args[1],
    "rollout-updated",
    "Called for correct reason"
  );

  store.deactivateEnrollment("foo");

  await cleanup();
});

add_task(async function test_storeValuePerPref_returnsSameValue_allTypes() {
  const cleanupFeature = NimbusTestUtils.addTestFeatures(
    new ExperimentFeature("purple", {
      isEarlyStartup: true,
      variables: {
        string: { type: "string" },
        bool: { type: "boolean" },
        array: { type: "json" },
        number1: { type: "int" },
        number2: { type: "int" },
        number3: { type: "int" },
        json: { type: "json" },
      },
    })
  );

  const { store, cleanup } = await setupTest();

  const recipe = NimbusTestUtils.factories.recipe.withFeatureConfig("foo", {
    // Ensure it gets saved to prefs
    featureId: "purple",
    value: {
      string: "string",
      bool: true,
      array: [1, 2, 3],
      number1: 42,
      number2: 0,
      number3: -5,
      json: { jsonValue: true },
    },
  });

  NimbusTestUtils.addEnrollmentForRecipe(recipe);
  const branch = Services.prefs.getBranch(`${SYNC_DATA_PREF_BRANCH}purple.`);

  const newStore = NimbusTestUtils.stubs.store();
  Assert.deepEqual(
    newStore.getExperimentForFeature("purple").branch.features[0].value,
    recipe.branches[0].features[0].value,
    "Returns the same value"
  );

  // Cleanup
  store.deactivateEnrollment(recipe.slug);
  Assert.ok(
    !Services.prefs.getStringPref(`${SYNC_DATA_PREF_BRANCH}purple`, ""),
    "Experiment cleanup"
  );
  Assert.deepEqual(branch.getChildList(""), [], "Variables are also removed");

  await cleanup();
  cleanupFeature();
});

add_task(async function test_cleanupOldRecipes() {
  const store = NimbusTestUtils.stubs.store();

  await store.init({ cleanupOldRecipes: false });

  const NOW = Date.now();
  const SIX_HOURS = 6 * 3600 * 1000;
  const ONE_DAY = 4 * SIX_HOURS;
  const ONE_YEAR = 365.25 * 24 * 3600 * 1000;
  const ONE_MONTH = Math.floor(ONE_YEAR / 12);

  const active = NimbusTestUtils.factories.recipe("active-6hrs");
  const inactiveToday = NimbusTestUtils.factories.recipe("inactive-recent");
  const inactiveSixMonths = NimbusTestUtils.factories.recipe("inactive-6mo");
  const inactiveUnderTwelveMonths = NimbusTestUtils.factories.recipe(
    "inactive-under-12mo"
  );
  const inactiveOverTwelveMonths =
    NimbusTestUtils.factories.recipe("inactive-over-12mo");

  const inactiveNoLastSeen = NimbusTestUtils.factories.experiment(
    "inactive-unknown",
    {
      active: false,
      unenrollReason: "unknown",
    }
  );

  delete inactiveNoLastSeen.lastSeen;

  NimbusTestUtils.addEnrollmentForRecipe(active, {
    store,
    branchSlug: "control",
    extra: {
      lastSeen: new Date(NOW - SIX_HOURS).toJSON(),
    },
  });
  NimbusTestUtils.addEnrollmentForRecipe(inactiveToday, {
    store,
    branchSlug: "control",
    extra: {
      active: false,
      unenrollReason: "unknown",
      lastSeen: new Date(NOW - SIX_HOURS).toJSON(),
    },
  });
  NimbusTestUtils.addEnrollmentForRecipe(inactiveSixMonths, {
    store,
    branchSlug: "control",
    extra: {
      active: false,
      unenrollReason: "unknown",
      lastSeen: new Date(NOW - 6 * ONE_MONTH).toJSON(),
    },
  });
  NimbusTestUtils.addEnrollmentForRecipe(inactiveUnderTwelveMonths, {
    store,
    branchSlug: "control",
    extra: {
      active: false,
      unenrollReason: "unknown",
      lastSeen: new Date(NOW - ONE_YEAR + ONE_DAY).toJSON(),
    },
  });
  NimbusTestUtils.addEnrollmentForRecipe(inactiveOverTwelveMonths, {
    store,
    branchSlug: "control",
    extra: {
      active: false,
      unenrollReason: "unknown",
      lastSeen: new Date(NOW - ONE_YEAR - ONE_DAY).toJSON(),
    },
  });

  await NimbusTestUtils.flushStore();

  // There is a NOT NULL constraint that prevents adding this enrollment to the
  // database and addEnrollment() is stubbed to validate the enrollment so we
  // must use set() here.
  store.set(inactiveNoLastSeen.slug, inactiveNoLastSeen);

  // Insert a row belonging to another profile.
  const otherProfileId = Services.uuid.generateUUID().toString().slice(1, -1);

  await NimbusTestUtils.insertEnrollment(inactiveOverTwelveMonths, "control", {
    extra: {
      active: false,
      lastSeen: new Date(NOW - ONE_YEAR - ONE_DAY),
    },
    profileId: otherProfileId,
  });

  store._cleanupOldRecipes();
  await NimbusTestUtils.flushStore(store);

  Assert.equal(
    store.get(inactiveOverTwelveMonths.slug),
    null,
    "Expired enrollment removed from in memory store"
  );
  Assert.equal(
    store.get(inactiveNoLastSeen.slug),
    null,
    "invalid enrollment removed from the store"
  );

  await NimbusTestUtils.assert.enrollmentExists(active.slug, { active: true });

  await NimbusTestUtils.assert.enrollmentExists(inactiveToday.slug, {
    active: false,
  });

  await NimbusTestUtils.assert.enrollmentExists(inactiveSixMonths.slug, {
    active: false,
  });
  await NimbusTestUtils.assert.enrollmentExists(
    inactiveUnderTwelveMonths.slug,
    { active: false }
  );
  await NimbusTestUtils.assert.enrollmentDoesNotExist(
    inactiveOverTwelveMonths.slug
  );

  // Rows in the other profile should not have been changed.
  await NimbusTestUtils.assert.enrollmentExists(inactiveOverTwelveMonths.slug, {
    active: false,
    profileId: otherProfileId,
  });

  store.deactivateEnrollment(active.slug);
  await NimbusTestUtils.flushStore();

  await NimbusTestUtils.deleteEnrollmentsFromProfiles([otherProfileId]);
  await NimbusTestUtils.assert.storeIsEmpty(store, { allProfiles: true });
});

async function test_restore() {
  let storePath;
  {
    const store = NimbusTestUtils.stubs.store();
    await store.init();

    NimbusTestUtils.addEnrollmentForRecipe(
      NimbusTestUtils.factories.recipe("experiment"),
      { store, branchSlug: "control" }
    );
    NimbusTestUtils.addEnrollmentForRecipe(
      NimbusTestUtils.factories.recipe("rollout", { isRollout: true }),
      { store }
    );

    storePath = await NimbusTestUtils.saveStore(store);
  }

  const { store, cleanup } = await setupTest({
    storePath,
    migrationState: NimbusTestUtils.migrationState.IMPORTED_ENROLLMENTS_TO_SQL,
  });

  Assert.ok(store.get("experiment"));
  Assert.ok(store.get("rollout"));

  await NimbusTestUtils.cleanupManager(["experiment", "rollout"]);
  await cleanup();
}

add_task(test_restore);
add_task(async function test_restore_db() {
  const resetNimbusEnrollmentPrefs = NimbusTestUtils.enableNimbusEnrollments({
    read: true,
  });
  await test_restore();
  resetNimbusEnrollmentPrefs();
});

async function test_restoreDatabaseConsistency(primary = "jsonfile") {
  Services.fog.testResetFOG();

  let storePath;

  {
    const store = await NimbusTestUtils.stubs.store();
    await store.init();

    const experimentRecipe = NimbusTestUtils.factories.recipe.withFeatureConfig(
      "experiment",
      { featureId: "no-feature-firefox-desktop" }
    );

    const rolloutRecipe = NimbusTestUtils.factories.recipe.withFeatureConfig(
      "rollout",
      { featureId: "no-feature-firefox-desktop" },
      { isRollout: true }
    );

    const inactiveRecipe = NimbusTestUtils.factories.recipe.withFeatureConfig(
      "inactive",
      { featureId: "no-feature-firefox-desktop" }
    );

    NimbusTestUtils.addEnrollmentForRecipe(experimentRecipe, { store });
    NimbusTestUtils.addEnrollmentForRecipe(rolloutRecipe, { store });
    NimbusTestUtils.addEnrollmentForRecipe(inactiveRecipe, {
      store,
      extra: { active: false },
    });

    storePath = await NimbusTestUtils.saveStore(store);

    // We should expect to see one successful databaseWrite event.
    const events = Glean.nimbusEvents.databaseWrite
      .testGetValue("events")
      .map(ev => ev.extra);

    Assert.deepEqual(events, [{ success: "true" }]);
  }

  // Initializing the store above will submit the event we care about. Disregard
  // any metrics previously recorded.
  Services.fog.testResetFOG();

  const { cleanup } = await NimbusTestUtils.setupTest({
    storePath,
    clearTelemetry: true,
    migrationState: NimbusTestUtils.migrationState.IMPORTED_ENROLLMENTS_TO_SQL,
  });

  const events = Glean.nimbusEvents.startupDatabaseConsistency
    .testGetValue("events")
    .map(ev => ev.extra);
  Assert.deepEqual(events, [
    {
      total_db_count: "3",
      total_store_count: "3",
      db_active_count: "2",
      store_active_count: "2",
      trigger: "startup",
      primary,
    },
  ]);

  await NimbusTestUtils.cleanupManager(["rollout", "experiment"]);
  await cleanup();
}

add_task(test_restoreDatabaseConsistency);
add_task(async function test_restoreDatabaseConsistencyDb() {
  const resetNimbusEnrollmentPrefs = NimbusTestUtils.enableNimbusEnrollments({
    read: true,
  });
  await test_restoreDatabaseConsistency("database");
  resetNimbusEnrollmentPrefs();
});
