"use strict";

ChromeUtils.import("resource://normandy/Normandy.jsm", this);
ChromeUtils.import("resource://normandy/lib/AddonStudies.jsm", this);
ChromeUtils.import("resource://normandy/lib/PreferenceExperiments.jsm", this);
ChromeUtils.import("resource://normandy/lib/PreferenceRollouts.jsm", this);
ChromeUtils.import("resource://normandy/lib/RecipeRunner.jsm", this);
ChromeUtils.import("resource://normandy/lib/TelemetryEvents.jsm", this);
ChromeUtils.import("resource://normandy-content/AboutPages.jsm", this);

const experimentPref1 = "test.initExperimentPrefs1";
const experimentPref2 = "test.initExperimentPrefs2";
const experimentPref3 = "test.initExperimentPrefs3";
const experimentPref4 = "test.initExperimentPrefs4";

function withStubInits(testFunction) {
  return decorate(
    withStub(AboutPages, "init"),
    withStub(AddonStudies, "init"),
    withStub(PreferenceRollouts, "init"),
    withStub(PreferenceExperiments, "init"),
    withStub(RecipeRunner, "init"),
    withStub(TelemetryEvents, "init"),
    () => testFunction(),
  );
}

decorate_task(
  withPrefEnv({
    set: [
      [`app.normandy.startupExperimentPrefs.${experimentPref1}`, true],
      [`app.normandy.startupExperimentPrefs.${experimentPref2}`, 2],
      [`app.normandy.startupExperimentPrefs.${experimentPref3}`, "string"],
    ],
  }),
  async function testApplyStartupPrefs() {
    const defaultBranch = Services.prefs.getDefaultBranch("");
    for (const pref of [experimentPref1, experimentPref2, experimentPref3]) {
      is(
        defaultBranch.getPrefType(pref),
        defaultBranch.PREF_INVALID,
        `Pref ${pref} don't exist before being initialized.`,
      );
    }

    Normandy.applyStartupPrefs("app.normandy.startupExperimentPrefs.");

    ok(
      defaultBranch.getBoolPref(experimentPref1),
      `Pref ${experimentPref1} has a default value after being initialized.`,
    );
    is(
      defaultBranch.getIntPref(experimentPref2),
      2,
      `Pref ${experimentPref2} has a default value after being initialized.`,
    );
    is(
      defaultBranch.getCharPref(experimentPref3),
      "string",
      `Pref ${experimentPref3} has a default value after being initialized.`,
    );

    for (const pref of [experimentPref1, experimentPref2, experimentPref3]) {
      ok(
        !defaultBranch.prefHasUserValue(pref),
        `Pref ${pref} doesn't have a user value after being initialized.`,
      );
      Services.prefs.clearUserPref(pref);
      defaultBranch.deleteBranch(pref);
    }
  },
);

decorate_task(
  withPrefEnv({
    set: [
      ["app.normandy.startupExperimentPrefs.test.existingPref", "experiment"],
    ],
  }),
  async function testApplyStartupPrefsExisting() {
    const defaultBranch = Services.prefs.getDefaultBranch("");
    defaultBranch.setCharPref("test.existingPref", "default");
    Normandy.applyStartupPrefs("app.normandy.startupExperimentPrefs.");
    is(
      defaultBranch.getCharPref("test.existingPref"),
      "experiment",
      "applyStartupPrefs overwrites the default values of existing preferences.",
    );
  },
);

decorate_task(
  withPrefEnv({
    set: [
      ["app.normandy.startupExperimentPrefs.test.mismatchPref", "experiment"],
    ],
  }),
  async function testApplyStartupPrefsMismatch() {
    const defaultBranch = Services.prefs.getDefaultBranch("");
    defaultBranch.setIntPref("test.mismatchPref", 2);
    Normandy.applyStartupPrefs("app.normandy.startupExperimentPrefs.");
    is(
      defaultBranch.getPrefType("test.mismatchPref"),
      Services.prefs.PREF_INT,
      "applyStartupPrefs skips prefs that don't match the existing default value's type.",
    );
  },
);

decorate_task(
  withStub(Normandy, "finishInit"),
  async function testStartupDelayed(finishInitStub) {
    Normandy.init();
    ok(
      !finishInitStub.called,
      "When initialized, do not call finishInit immediately.",
    );

    Normandy.observe(null, "sessionstore-windows-restored");
    ok(
      finishInitStub.called,
      "Once the sessionstore-windows-restored event is observed, finishInit should be called.",
    );
  },
);

// During startup, preferences that are changed for experiments should
// be record by calling PreferenceExperiments.recordOriginalValues.
decorate_task(
  withStub(PreferenceExperiments, "recordOriginalValues"),
  withStub(PreferenceRollouts, "recordOriginalValues"),
  async function testApplyStartupPrefs(experimentsRecordOriginalValuesStub, rolloutsRecordOriginalValueStub) {
    const defaultBranch = Services.prefs.getDefaultBranch("");

    defaultBranch.setBoolPref(experimentPref1, false);
    defaultBranch.setIntPref(experimentPref2, 1);
    defaultBranch.setCharPref(experimentPref3, "original string");
    // experimentPref4 is left unset

    Normandy.applyStartupPrefs("app.normandy.startupExperimentPrefs.");
    Normandy.studyPrefsChanged = {"test.study-pref": 1};
    Normandy.rolloutPrefsChanged = {"test.rollout-pref": 1};
    await Normandy.finishInit();

    Assert.deepEqual(
      experimentsRecordOriginalValuesStub.args,
      [[{"test.study-pref": 1}]],
      "finishInit should record original values of the study prefs",
    );
    Assert.deepEqual(
      rolloutsRecordOriginalValueStub.args,
      [[{"test.rollout-pref": 1}]],
      "finishInit should record original values of the study prefs",
    );

    // cleanup
    defaultBranch.deleteBranch(experimentPref1);
    defaultBranch.deleteBranch(experimentPref2);
    defaultBranch.deleteBranch(experimentPref3);
  },
);

// Test that startup prefs are handled correctly when there is a value on the user branch but not the default branch.
decorate_task(
  withPrefEnv({
    set: [
      ["app.normandy.startupExperimentPrefs.testing.does-not-exist", "foo"],
      ["testing.does-not-exist", "foo"],
    ],
  }),
  withStub(PreferenceExperiments, "recordOriginalValues"),
  async function testApplyStartupPrefsNoDefaultValue() {
    Normandy.applyStartupPrefs("app.normandy.startupExperimentPrefs");
    ok(true, "initExperimentPrefs should not throw for prefs that doesn't exist on the default branch");
  },
);

decorate_task(
  withStubInits,
  async function testStartup() {
    const initObserved = TestUtils.topicObserved("shield-init-complete");
    await Normandy.finishInit();
    ok(AboutPages.init.called, "startup calls AboutPages.init");
    ok(AddonStudies.init.called, "startup calls AddonStudies.init");
    ok(PreferenceExperiments.init.called, "startup calls PreferenceExperiments.init");
    ok(RecipeRunner.init.called, "startup calls RecipeRunner.init");
    await initObserved;
  }
);

decorate_task(
  withStubInits,
  async function testStartupPrefInitFail() {
    PreferenceExperiments.init.rejects();

    await Normandy.finishInit();
    ok(AboutPages.init.called, "startup calls AboutPages.init");
    ok(AddonStudies.init.called, "startup calls AddonStudies.init");
    ok(PreferenceExperiments.init.called, "startup calls PreferenceExperiments.init");
    ok(RecipeRunner.init.called, "startup calls RecipeRunner.init");
    ok(TelemetryEvents.init.called, "startup calls TelemetryEvents.init");
    ok(PreferenceRollouts.init.called, "startup calls PreferenceRollouts.init");
  }
);

decorate_task(
  withStubInits,
  async function testStartupAboutPagesInitFail() {
    AboutPages.init.rejects();

    await Normandy.finishInit();
    ok(AboutPages.init.called, "startup calls AboutPages.init");
    ok(AddonStudies.init.called, "startup calls AddonStudies.init");
    ok(PreferenceExperiments.init.called, "startup calls PreferenceExperiments.init");
    ok(RecipeRunner.init.called, "startup calls RecipeRunner.init");
    ok(TelemetryEvents.init.called, "startup calls TelemetryEvents.init");
    ok(PreferenceRollouts.init.called, "startup calls PreferenceRollouts.init");
  }
);

decorate_task(
  withStubInits,
  async function testStartupAddonStudiesInitFail() {
    AddonStudies.init.rejects();

    await Normandy.finishInit();
    ok(AboutPages.init.called, "startup calls AboutPages.init");
    ok(AddonStudies.init.called, "startup calls AddonStudies.init");
    ok(PreferenceExperiments.init.called, "startup calls PreferenceExperiments.init");
    ok(RecipeRunner.init.called, "startup calls RecipeRunner.init");
    ok(TelemetryEvents.init.called, "startup calls TelemetryEvents.init");
    ok(PreferenceRollouts.init.called, "startup calls PreferenceRollouts.init");
  }
);

decorate_task(
  withStubInits,
  async function testStartupTelemetryEventsInitFail() {
    TelemetryEvents.init.throws();

    await Normandy.finishInit();
    ok(AboutPages.init.called, "startup calls AboutPages.init");
    ok(AddonStudies.init.called, "startup calls AddonStudies.init");
    ok(PreferenceExperiments.init.called, "startup calls PreferenceExperiments.init");
    ok(RecipeRunner.init.called, "startup calls RecipeRunner.init");
    ok(TelemetryEvents.init.called, "startup calls TelemetryEvents.init");
    ok(PreferenceRollouts.init.called, "startup calls PreferenceRollouts.init");
  }
);

decorate_task(
  withStubInits,
  async function testStartupPreferenceRolloutsInitFail() {
    PreferenceRollouts.init.throws();

    await Normandy.finishInit();
    ok(AboutPages.init.called, "startup calls AboutPages.init");
    ok(AddonStudies.init.called, "startup calls AddonStudies.init");
    ok(PreferenceExperiments.init.called, "startup calls PreferenceExperiments.init");
    ok(RecipeRunner.init.called, "startup calls RecipeRunner.init");
    ok(TelemetryEvents.init.called, "startup calls TelemetryEvents.init");
    ok(PreferenceRollouts.init.called, "startup calls PreferenceRollouts.init");
  }
);

decorate_task(
  withMockPreferences,
  async function testPrefMigration(mockPreferences) {
    const legacyPref = "extensions.shield-recipe-client.test";
    const migratedPref = "app.normandy.test";
    mockPreferences.set(legacyPref, 1);

    ok(
      Services.prefs.prefHasUserValue(legacyPref),
      "Legacy pref should have a user value before running migration",
    );
    ok(
      !Services.prefs.prefHasUserValue(migratedPref),
      "Migrated pref should not have a user value before running migration",
    );

    Normandy.migrateShieldPrefs();

    ok(
      !Services.prefs.prefHasUserValue(legacyPref),
      "Legacy pref should not have a user value after running migration",
    );
    ok(
      Services.prefs.prefHasUserValue(migratedPref),
      "Migrated pref should have a user value after running migration",
    );
    is(Services.prefs.getIntPref(migratedPref), 1, "Value should have been migrated");

    Services.prefs.clearUserPref(migratedPref);
  },
);
