"use strict";

ChromeUtils.defineESModuleGetters(this, {
  ExtensionPreferencesManager:
    "resource://gre/modules/ExtensionPreferencesManager.sys.mjs",
  ExtensionSettingsStore:
    "resource://gre/modules/ExtensionSettingsStore.sys.mjs",
});

const { createAppInfo, promiseShutdownManager, promiseStartupManager } =
  AddonTestUtils;

AddonTestUtils.init(this);

createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");

let lastSetPref;

const STORE_TYPE = "prefs";

// Test settings to use with the preferences manager.
const SETTINGS = {
  multiple_prefs: {
    prefNames: ["my.pref.1", "my.pref.2", "my.pref.3"],

    initalValues: ["value1", "value2", "value3"],

    valueFn(pref, value) {
      return `${pref}-${value}`;
    },

    setCallback(value) {
      let prefs = {};
      for (let pref of this.prefNames) {
        prefs[pref] = this.valueFn(pref, value);
      }
      return prefs;
    },
  },

  singlePref: {
    prefNames: ["my.single.pref"],

    initalValues: ["value1"],

    onPrefsChanged(item) {
      lastSetPref = item;
    },

    valueFn(pref, value) {
      return value;
    },

    setCallback(value) {
      return { [this.prefNames[0]]: this.valueFn(null, value) };
    },
  },
};

ExtensionPreferencesManager.addSetting(
  "multiple_prefs",
  SETTINGS.multiple_prefs
);
ExtensionPreferencesManager.addSetting("singlePref", SETTINGS.singlePref);

// Set initial values for prefs.
for (let setting in SETTINGS) {
  setting = SETTINGS[setting];
  for (let i = 0; i < setting.prefNames.length; i++) {
    Services.prefs.setStringPref(setting.prefNames[i], setting.initalValues[i]);
  }
}

function checkPrefs(settingObj, value, msg) {
  for (let pref of settingObj.prefNames) {
    equal(
      Services.prefs.getStringPref(pref),
      settingObj.valueFn(pref, value),
      msg
    );
  }
}

function checkOnPrefsChanged(setting, value, msg) {
  if (value) {
    deepEqual(lastSetPref, value, msg);
    lastSetPref = null;
  } else {
    ok(!lastSetPref, msg);
  }
}

add_task(async function test_preference_manager() {
  await promiseStartupManager();

  // Create an array of test framework extension wrappers to install.
  let testExtensions = [
    ExtensionTestUtils.loadExtension({
      useAddonManager: "temporary",
      manifest: {},
    }),
    ExtensionTestUtils.loadExtension({
      useAddonManager: "temporary",
      manifest: {},
    }),
  ];

  for (let extension of testExtensions) {
    await extension.startup();
  }

  // Create an array actual Extension objects which correspond to the
  // test framework extension wrappers.
  let extensions = testExtensions.map(extension => extension.extension);

  for (let setting in SETTINGS) {
    let settingObj = SETTINGS[setting];
    let newValue1 = "newValue1";
    let levelOfControl = await ExtensionPreferencesManager.getLevelOfControl(
      extensions[1].id,
      setting
    );
    if (settingObj.onPrefsChanged) {
      checkOnPrefsChanged(
        setting,
        null,
        "onPrefsChanged has not been called yet"
      );
    }
    equal(
      levelOfControl,
      "controllable_by_this_extension",
      "getLevelOfControl returns correct levelOfControl with no settings set."
    );

    let prefsChanged = await ExtensionPreferencesManager.setSetting(
      extensions[1].id,
      setting,
      newValue1
    );
    ok(prefsChanged, "setSetting returns true when the pref(s) have been set.");
    checkPrefs(
      settingObj,
      newValue1,
      "setSetting sets the prefs for the first extension."
    );
    if (settingObj.onPrefsChanged) {
      checkOnPrefsChanged(
        setting,
        { id: extensions[1].id, value: newValue1, key: setting },
        "onPrefsChanged is called when pref changes"
      );
    }
    levelOfControl = await ExtensionPreferencesManager.getLevelOfControl(
      extensions[1].id,
      setting
    );
    equal(
      levelOfControl,
      "controlled_by_this_extension",
      "getLevelOfControl returns correct levelOfControl when a pref has been set."
    );

    let checkSetting = await ExtensionPreferencesManager.getSetting(setting);
    equal(
      checkSetting.value,
      newValue1,
      "getSetting returns the expected value."
    );

    let newValue2 = "newValue2";
    prefsChanged = await ExtensionPreferencesManager.setSetting(
      extensions[0].id,
      setting,
      newValue2
    );
    ok(
      !prefsChanged,
      "setSetting returns false when the pref(s) have not been set."
    );
    checkPrefs(
      settingObj,
      newValue1,
      "setSetting does not set the pref(s) for an earlier extension."
    );
    if (settingObj.onPrefsChanged) {
      checkOnPrefsChanged(
        setting,
        null,
        "onPrefsChanged isn't called without control change"
      );
    }

    prefsChanged = await ExtensionPreferencesManager.disableSetting(
      extensions[0].id,
      setting
    );
    ok(
      !prefsChanged,
      "disableSetting returns false when the pref(s) have not been set."
    );
    checkPrefs(
      settingObj,
      newValue1,
      "disableSetting does not change the pref(s) for the non-top extension."
    );
    if (settingObj.onPrefsChanged) {
      checkOnPrefsChanged(
        setting,
        null,
        "onPrefsChanged isn't called without control change on disable"
      );
    }

    prefsChanged = await ExtensionPreferencesManager.enableSetting(
      extensions[0].id,
      setting
    );
    ok(
      !prefsChanged,
      "enableSetting returns false when the pref(s) have not been set."
    );
    checkPrefs(
      settingObj,
      newValue1,
      "enableSetting does not change the pref(s) for the non-top extension."
    );
    if (settingObj.onPrefsChanged) {
      checkOnPrefsChanged(
        setting,
        null,
        "onPrefsChanged isn't called without control change on enable"
      );
    }

    prefsChanged = await ExtensionPreferencesManager.removeSetting(
      extensions[0].id,
      setting
    );
    ok(
      !prefsChanged,
      "removeSetting returns false when the pref(s) have not been set."
    );
    checkPrefs(
      settingObj,
      newValue1,
      "removeSetting does not change the pref(s) for the non-top extension."
    );
    if (settingObj.onPrefsChanged) {
      checkOnPrefsChanged(
        setting,
        null,
        "onPrefsChanged isn't called without control change on remove"
      );
    }

    prefsChanged = await ExtensionPreferencesManager.setSetting(
      extensions[0].id,
      setting,
      newValue2
    );
    ok(
      !prefsChanged,
      "setSetting returns false when the pref(s) have not been set."
    );
    checkPrefs(
      settingObj,
      newValue1,
      "setSetting does not set the pref(s) for an earlier extension."
    );
    if (settingObj.onPrefsChanged) {
      checkOnPrefsChanged(
        setting,
        null,
        "onPrefsChanged isn't called without control change again"
      );
    }

    prefsChanged = await ExtensionPreferencesManager.disableSetting(
      extensions[1].id,
      setting
    );
    ok(
      prefsChanged,
      "disableSetting returns true when the pref(s) have been set."
    );
    checkPrefs(
      settingObj,
      newValue2,
      "disableSetting sets the pref(s) to the next value when disabling the top extension."
    );
    if (settingObj.onPrefsChanged) {
      checkOnPrefsChanged(
        setting,
        { id: extensions[0].id, key: setting, value: newValue2 },
        "onPrefsChanged is called when control changes on disable"
      );
    }

    prefsChanged = await ExtensionPreferencesManager.enableSetting(
      extensions[1].id,
      setting
    );
    ok(
      prefsChanged,
      "enableSetting returns true when the pref(s) have been set."
    );
    checkPrefs(
      settingObj,
      newValue1,
      "enableSetting sets the pref(s) to the previous value(s)."
    );
    if (settingObj.onPrefsChanged) {
      checkOnPrefsChanged(
        setting,
        { id: extensions[1].id, key: setting, value: newValue1 },
        "onPrefsChanged is called when control changes on enable"
      );
    }

    prefsChanged = await ExtensionPreferencesManager.removeSetting(
      extensions[1].id,
      setting
    );
    ok(
      prefsChanged,
      "removeSetting returns true when the pref(s) have been set."
    );
    checkPrefs(
      settingObj,
      newValue2,
      "removeSetting sets the pref(s) to the next value when removing the top extension."
    );
    if (settingObj.onPrefsChanged) {
      checkOnPrefsChanged(
        setting,
        { id: extensions[0].id, key: setting, value: newValue2 },
        "onPrefsChanged is called when control changes on remove"
      );
    }

    prefsChanged = await ExtensionPreferencesManager.removeSetting(
      extensions[0].id,
      setting
    );
    ok(
      prefsChanged,
      "removeSetting returns true when the pref(s) have been set."
    );
    if (settingObj.onPrefsChanged) {
      checkOnPrefsChanged(
        setting,
        { key: setting, initialValue: { "my.single.pref": "value1" } },
        "onPrefsChanged is called when control is entirely removed"
      );
    }
    for (let i = 0; i < settingObj.prefNames.length; i++) {
      equal(
        Services.prefs.getStringPref(settingObj.prefNames[i]),
        settingObj.initalValues[i],
        "removeSetting sets the pref(s) to the initial value(s) when removing the last extension."
      );
    }

    checkSetting = await ExtensionPreferencesManager.getSetting(setting);
    equal(
      checkSetting,
      null,
      "getSetting returns null when nothing has been set."
    );
  }

  // Tests for unsetAll.
  let newValue3 = "newValue3";
  for (let setting in SETTINGS) {
    let settingObj = SETTINGS[setting];
    await ExtensionPreferencesManager.setSetting(
      extensions[0].id,
      setting,
      newValue3
    );
    checkPrefs(settingObj, newValue3, "setSetting set the pref.");
  }

  let setSettings = await ExtensionSettingsStore.getAllForExtension(
    extensions[0].id,
    STORE_TYPE
  );
  deepEqual(
    setSettings,
    Object.keys(SETTINGS),
    "Expected settings were set for extension."
  );
  await ExtensionPreferencesManager.disableAll(extensions[0].id);

  for (let setting in SETTINGS) {
    let settingObj = SETTINGS[setting];
    for (let i = 0; i < settingObj.prefNames.length; i++) {
      equal(
        Services.prefs.getStringPref(settingObj.prefNames[i]),
        settingObj.initalValues[i],
        "disableAll unset the pref."
      );
    }
  }

  setSettings = await ExtensionSettingsStore.getAllForExtension(
    extensions[0].id,
    STORE_TYPE
  );
  deepEqual(
    setSettings,
    Object.keys(SETTINGS),
    "disableAll retains the settings."
  );

  await ExtensionPreferencesManager.enableAll(extensions[0].id);
  for (let setting in SETTINGS) {
    let settingObj = SETTINGS[setting];
    checkPrefs(settingObj, newValue3, "enableAll re-set the pref.");
  }

  await ExtensionPreferencesManager.removeAll(extensions[0].id);

  for (let setting in SETTINGS) {
    let settingObj = SETTINGS[setting];
    for (let i = 0; i < settingObj.prefNames.length; i++) {
      equal(
        Services.prefs.getStringPref(settingObj.prefNames[i]),
        settingObj.initalValues[i],
        "removeAll unset the pref."
      );
    }
  }

  setSettings = await ExtensionSettingsStore.getAllForExtension(
    extensions[0].id,
    STORE_TYPE
  );
  deepEqual(setSettings, [], "removeAll removed all settings.");

  // Tests for preventing automatic changes to manually edited prefs.
  for (let setting in SETTINGS) {
    let apiValue = "newValue";
    let manualValue = "something different";
    let settingObj = SETTINGS[setting];
    let extension = extensions[1];
    await ExtensionPreferencesManager.setSetting(
      extension.id,
      setting,
      apiValue
    );

    let checkResetPrefs = method => {
      let prefNames = settingObj.prefNames;
      for (let i = 0; i < prefNames.length; i++) {
        if (i === 0) {
          equal(
            Services.prefs.getStringPref(prefNames[0]),
            manualValue,
            `${method} did not change a manually set pref.`
          );
        } else {
          equal(
            Services.prefs.getStringPref(prefNames[i]),
            settingObj.valueFn(prefNames[i], apiValue),
            `${method} did not change another pref when a pref was manually set.`
          );
        }
      }
    };

    // Manually set the preference to a different value.
    Services.prefs.setStringPref(settingObj.prefNames[0], manualValue);

    await ExtensionPreferencesManager.disableAll(extension.id);
    checkResetPrefs("disableAll");

    await ExtensionPreferencesManager.enableAll(extension.id);
    checkResetPrefs("enableAll");

    await ExtensionPreferencesManager.removeAll(extension.id);
    checkResetPrefs("removeAll");
  }

  // Test with an uninitialized pref.
  let setting = "singlePref";
  let settingObj = SETTINGS[setting];
  let pref = settingObj.prefNames[0];
  let newValue = "newValue";
  Services.prefs.clearUserPref(pref);
  await ExtensionPreferencesManager.setSetting(
    extensions[1].id,
    setting,
    newValue
  );
  equal(
    Services.prefs.getStringPref(pref),
    settingObj.valueFn(pref, newValue),
    "Uninitialized pref is set."
  );
  await ExtensionPreferencesManager.removeSetting(extensions[1].id, setting);
  Assert.strictEqual(
    Services.prefs.getPrefType(pref),
    Services.prefs.PREF_INVALID,
    "removeSetting removed the pref."
  );

  // Test levelOfControl with a locked pref.
  setting = "multiple_prefs";
  let prefToLock = SETTINGS[setting].prefNames[0];
  Services.prefs.setStringPref(prefToLock, "1");
  Services.prefs.lockPref(prefToLock);
  ok(
    Services.prefs.prefIsLocked(prefToLock),
    `Preference ${prefToLock} is locked.`
  );
  let levelOfControl = await ExtensionPreferencesManager.getLevelOfControl(
    extensions[1].id,
    setting
  );
  equal(
    levelOfControl,
    "not_controllable",
    "getLevelOfControl returns correct levelOfControl when a pref is locked."
  );

  for (let extension of testExtensions) {
    await extension.unload();
  }

  await promiseShutdownManager();
});

add_task(async function test_preference_manager_set_when_disabled() {
  await promiseStartupManager();

  let id = "@set-disabled-pref";
  let extension = ExtensionTestUtils.loadExtension({
    useAddonManager: "temporary",
    manifest: {
      browser_specific_settings: { gecko: { id } },
    },
  });

  await extension.startup();

  // We test both a default pref and a user-set pref.  Get the default
  // value off the pref we'll use.  We fake the default pref by setting
  // a value on it before creating the setting.
  Services.prefs.setBoolPref("bar", true);

  function isUndefinedPref(pref) {
    try {
      Services.prefs.getStringPref(pref);
      return false;
    } catch (e) {
      return true;
    }
  }
  ok(isUndefinedPref("foo"), "test pref is not set");

  await ExtensionSettingsStore.initialize();
  let lastItemChange = Promise.withResolvers();
  ExtensionPreferencesManager.addSetting("some-pref", {
    prefNames: ["foo", "bar"],
    onPrefsChanged(item) {
      lastItemChange.resolve(item);
      lastItemChange = Promise.withResolvers();
    },
    setCallback(value) {
      return { [this.prefNames[0]]: value, [this.prefNames[1]]: false };
    },
  });

  await ExtensionPreferencesManager.setSetting(id, "some-pref", "my value");

  let item = ExtensionSettingsStore.getSetting("prefs", "some-pref");
  equal(item.value, "my value", "The value has been set");
  equal(
    Services.prefs.getStringPref("foo"),
    "my value",
    "The user pref has been set"
  );
  equal(
    Services.prefs.getBoolPref("bar"),
    false,
    "The default pref has been set"
  );

  await ExtensionPreferencesManager.disableSetting(id, "some-pref");

  // test that a disabled setting has been returned to the default value.  In this
  // case the pref is not a default pref, so it will be undefined.
  item = ExtensionSettingsStore.getSetting("prefs", "some-pref");
  equal(item.value, undefined, "The value is back to default");
  equal(item.initialValue.foo, undefined, "The initialValue is correct");
  ok(isUndefinedPref("foo"), "user pref is not set");
  equal(
    Services.prefs.getBoolPref("bar"),
    true,
    "The default pref has been restored to the default"
  );

  // test that setSetting() will enable a disabled setting
  await ExtensionPreferencesManager.setSetting(id, "some-pref", "new value");

  item = ExtensionSettingsStore.getSetting("prefs", "some-pref");
  equal(item.value, "new value", "The value is set again");
  equal(
    Services.prefs.getStringPref("foo"),
    "new value",
    "The user pref is set again"
  );
  equal(
    Services.prefs.getBoolPref("bar"),
    false,
    "The default pref has been set again"
  );

  // Force settings to be serialized and reloaded to mimick what happens
  // with settings through a restart of Firefox.  Bug 1576266.
  await ExtensionSettingsStore._reloadFile(true);

  // Now unload the extension to test prefs are reset properly.
  let promise = lastItemChange.promise;
  await extension.unload();

  // Test that the pref is unset when an extension is uninstalled.
  item = await promise;
  deepEqual(
    item,
    { key: "some-pref", initialValue: { bar: true } },
    "The value has been reset"
  );
  ok(isUndefinedPref("foo"), "user pref is not set");
  equal(
    Services.prefs.getBoolPref("bar"),
    true,
    "The default pref has been restored to the default"
  );
  Services.prefs.clearUserPref("bar");

  await promiseShutdownManager();
});

add_task(async function test_preference_default_upgraded() {
  await promiseStartupManager();

  let id = "@upgrade-pref";
  let extension = ExtensionTestUtils.loadExtension({
    useAddonManager: "temporary",
    manifest: {
      browser_specific_settings: { gecko: { id } },
    },
  });

  await extension.startup();

  // We set the default value for a pref here so it will be
  // picked up by EPM.
  let defaultPrefs = Services.prefs.getDefaultBranch(null);
  defaultPrefs.setStringPref("bar", "initial default");

  await ExtensionSettingsStore.initialize();
  ExtensionPreferencesManager.addSetting("some-pref", {
    prefNames: ["bar"],
    setCallback(value) {
      return { [this.prefNames[0]]: value };
    },
  });

  await ExtensionPreferencesManager.setSetting(id, "some-pref", "new value");
  let item = ExtensionSettingsStore.getSetting("prefs", "some-pref");
  equal(item.value, "new value", "The value is set");

  defaultPrefs.setStringPref("bar", "new default");

  item = ExtensionSettingsStore.getSetting("prefs", "some-pref");
  equal(item.value, "new value", "The value is still set");

  let prefsChanged = await ExtensionPreferencesManager.removeSetting(
    id,
    "some-pref"
  );
  ok(prefsChanged, "pref changed on removal of setting.");
  equal(
    Services.prefs.getStringPref("bar"),
    "new default",
    "default value is correct"
  );

  await extension.unload();
  await promiseShutdownManager();
});

add_task(async function test_preference_select() {
  await promiseStartupManager();

  let extensionData = {
    useAddonManager: "temporary",
    manifest: {
      browser_specific_settings: { gecko: { id: "@one" } },
    },
  };
  let one = ExtensionTestUtils.loadExtension(extensionData);

  await one.startup();

  // We set the default value for a pref here so it will be
  // picked up by EPM.
  let defaultPrefs = Services.prefs.getDefaultBranch(null);
  defaultPrefs.setStringPref("bar", "initial default");

  await ExtensionSettingsStore.initialize();
  ExtensionPreferencesManager.addSetting("some-pref", {
    prefNames: ["bar"],
    setCallback(value) {
      return { [this.prefNames[0]]: value };
    },
  });

  ok(
    await ExtensionPreferencesManager.setSetting(
      one.id,
      "some-pref",
      "new value"
    ),
    "setting was changed"
  );
  let item = await ExtensionPreferencesManager.getSetting("some-pref");
  equal(item.value, "new value", "The value is set");

  // User-set the setting.
  await ExtensionPreferencesManager.selectSetting(null, "some-pref");
  item = await ExtensionPreferencesManager.getSetting("some-pref");
  deepEqual(
    item,
    { key: "some-pref", initialValue: {} },
    "The value is user-set"
  );

  // Extensions installed before cannot gain control again.
  let levelOfControl = await ExtensionPreferencesManager.getLevelOfControl(
    one.id,
    "some-pref"
  );
  equal(
    levelOfControl,
    "not_controllable",
    "getLevelOfControl returns correct levelOfControl when user-set."
  );

  // Enabling the top-precedence addon does not take over a user-set setting.
  await ExtensionPreferencesManager.disableSetting(one.id, "some-pref");
  await ExtensionPreferencesManager.enableSetting(one.id, "some-pref");
  item = await ExtensionPreferencesManager.getSetting("some-pref");
  deepEqual(
    item,
    { key: "some-pref", initialValue: {} },
    "The value is user-set"
  );

  // Upgrading does not override the user-set setting.
  extensionData.manifest.version = "2.0";
  extensionData.manifest.incognito = "not_allowed";
  await one.upgrade(extensionData);
  levelOfControl = await ExtensionPreferencesManager.getLevelOfControl(
    one.id,
    "some-pref"
  );
  equal(
    levelOfControl,
    "not_controllable",
    "getLevelOfControl returns correct levelOfControl when user-set after addon upgrade."
  );

  // We can re-select the extension.
  await ExtensionPreferencesManager.selectSetting(one.id, "some-pref");
  item = await ExtensionPreferencesManager.getSetting("some-pref");
  deepEqual(item.value, "new value", "The value is extension set");

  // An extension installed after user-set can take over the setting.
  await ExtensionPreferencesManager.selectSetting(null, "some-pref");
  item = await ExtensionPreferencesManager.getSetting("some-pref");
  deepEqual(
    item,
    { key: "some-pref", initialValue: {} },
    "The value is user-set"
  );

  let two = ExtensionTestUtils.loadExtension({
    useAddonManager: "temporary",
    manifest: {
      browser_specific_settings: { gecko: { id: "@two" } },
    },
  });

  await two.startup();
  levelOfControl = await ExtensionPreferencesManager.getLevelOfControl(
    two.id,
    "some-pref"
  );
  equal(
    levelOfControl,
    "controllable_by_this_extension",
    "getLevelOfControl returns correct levelOfControl when user-set after addon install."
  );

  await ExtensionPreferencesManager.setSetting(
    two.id,
    "some-pref",
    "another value"
  );
  item = ExtensionSettingsStore.getSetting("prefs", "some-pref");
  equal(item.value, "another value", "The value is set");

  // A new installed extension can override a user selected extension.
  let three = ExtensionTestUtils.loadExtension({
    useAddonManager: "temporary",
    manifest: {
      browser_specific_settings: { gecko: { id: "@three" } },
    },
  });

  // user selects specific extension to take control
  await ExtensionPreferencesManager.selectSetting(one.id, "some-pref");

  // two cannot control
  levelOfControl = await ExtensionPreferencesManager.getLevelOfControl(
    two.id,
    "some-pref"
  );
  equal(
    levelOfControl,
    "not_controllable",
    "getLevelOfControl returns correct levelOfControl when user-set after addon install."
  );

  // three can control after install
  await three.startup();
  levelOfControl = await ExtensionPreferencesManager.getLevelOfControl(
    three.id,
    "some-pref"
  );
  equal(
    levelOfControl,
    "controllable_by_this_extension",
    "getLevelOfControl returns correct levelOfControl when user-set after addon install."
  );

  await ExtensionPreferencesManager.setSetting(
    three.id,
    "some-pref",
    "third value"
  );
  item = ExtensionSettingsStore.getSetting("prefs", "some-pref");
  equal(item.value, "third value", "The value is set");

  // We have returned to precedence based settings.
  await ExtensionPreferencesManager.removeSetting(three.id, "some-pref");
  await ExtensionPreferencesManager.removeSetting(two.id, "some-pref");
  item = await ExtensionPreferencesManager.getSetting("some-pref");
  equal(item.value, "new value", "The value is extension set");

  await one.unload();
  await two.unload();
  await three.unload();
  await promiseShutdownManager();
});

add_task(async function test_preference_select() {
  let prefNames = await ExtensionPreferencesManager.getManagedPrefDetails();
  // Just check a subset of settings that are in this test file.
  Assert.greater(prefNames.size, 0, "some prefs exist");
  for (let settingName in SETTINGS) {
    let setting = SETTINGS[settingName];
    for (let prefName of setting.prefNames) {
      Assert.equal(
        prefNames.get(prefName),
        settingName,
        "setting retrieved prefNames"
      );
    }
  }
});
