/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";

// This file tests the functionality of the preference service when using a
// shared memory snapshot. In this configuration, a snapshot of the initial
// state of the preferences database is made when we first spawn a child
// process, and changes after that point are stored as entries in a dynamic hash
// table, on top of the snapshot.

ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://testing-common/ExtensionXPCShellUtils.jsm");

ExtensionTestUtils.init(this);

let contentPage;

const {prefs} = Services;
const defaultPrefs = prefs.getDefaultBranch("");

const FRAME_SCRIPT_INIT = `
  ChromeUtils.import("resource://gre/modules/Services.jsm");
  const {prefs} = Services;
  const defaultPrefs = prefs.getDefaultBranch("");
`;

function try_(fn) {
  try {
    return fn();
  } catch (e) {
    return undefined;
  }
}

function getPref(pref) {
  let flags = {
    locked: try_(() => prefs.prefIsLocked(pref)),
    hasUser: try_(() => prefs.prefHasUserValue(pref)),
  };

  switch (prefs.getPrefType(pref)) {
    case prefs.PREF_INT:
      return {
        ...flags,
        type: "Int",
        user: try_(() => prefs.getIntPref(pref)),
        default: try_(() => defaultPrefs.getIntPref(pref)),
      };
    case prefs.PREF_BOOL:
      return {
        ...flags,
        type: "Bool",
        user: try_(() => prefs.getBoolPref(pref)),
        default: try_(() => defaultPrefs.getBoolPref(pref)),
      };
    case prefs.PREF_STRING:
      return {
        ...flags,
        type: "String",
        user: try_(() => prefs.getStringPref(pref)),
        default: try_(() => defaultPrefs.getStringPref(pref)),
      };
  }
  return {};
}

function getPrefs(prefNames) {
  let result = {};
  for (let pref of prefNames) {
    result[pref] = getPref(pref);
  }
  result.childList = prefs.getChildList("");
  return result;
}

function checkPref(pref, proc, val, type, userVal, defaultVal, expectedFlags = {}) {
  info(`Check "${pref}" ${proc} value`);

  equal(val.type, type, `Expected type for "${pref}"`);
  equal(val.user, userVal, `Expected user value for "${pref}"`);

  // We only send changes to the content process when they'll make a visible
  // difference, so ignore content process default values when we have a defined
  // user value.
  if (proc !== "content" || val.user === undefined) {
    equal(val.default, defaultVal, `Expected default value for "${pref}"`);
  }

  for (let [flag, value] of Object.entries(expectedFlags)) {
    equal(val[flag], value, `Expected ${flag} value for "${pref}"`);
  }
}

function getPrefList() {
  return prefs.getChildList("");
}

const TESTS = {
  "exists.thenDoesNot": {
    beforeContent(PREF) {
      prefs.setBoolPref(PREF, true);

      ok(getPrefList().includes(PREF), `Parent list includes "${PREF}"`);
    },
    contentStartup(PREF, val, childList) {
      ok(getPrefList().includes(PREF), `Parent list includes "${PREF}"`);
      ok(childList.includes(PREF), `Child list includes "${PREF}"`);

      prefs.clearUserPref(PREF);
      ok(!getPrefList().includes(PREF), `Parent list doesn't include "${PREF}"`);
    },
    contentUpdate1(PREF, val, childList) {
      ok(!getPrefList().includes(PREF), `Parent list doesn't include "${PREF}"`);
      ok(!childList.includes(PREF), `Child list doesn't include "${PREF}"`);

      prefs.setCharPref(PREF, "foo");
      ok(getPrefList().includes(PREF), `Parent list includes "${PREF}"`);
      checkPref(PREF, "parent", getPref(PREF), "String", "foo");
    },
    contentUpdate2(PREF, val, childList) {
      ok(getPrefList().includes(PREF), `Parent list includes "${PREF}"`);
      ok(childList.includes(PREF), `Child list includes "${PREF}"`);

      checkPref(PREF, "parent", getPref(PREF), "String", "foo");
      checkPref(PREF, "child", val, "String", "foo");
    },
  },
  "doesNotExists.thenDoes": {
    contentStartup(PREF, val, childList) {
      ok(!getPrefList().includes(PREF), `Parent list doesn't include "${PREF}"`);
      ok(!childList.includes(PREF), `Child list doesn't include "${PREF}"`);

      prefs.setIntPref(PREF, 42);
      ok(getPrefList().includes(PREF), `Parent list includes "${PREF}"`);
    },
    contentUpdate1(PREF, val, childList) {
      ok(getPrefList().includes(PREF), `Parent list includes "${PREF}"`);
      ok(childList.includes(PREF), `Child list includes "${PREF}"`);

      checkPref(PREF, "parent", getPref(PREF), "Int", 42);
      checkPref(PREF, "child", val, "Int", 42);
    },
  },
};

const PREFS = [
  {type: "Bool", values: [true, false, true]},
  {type: "Int", values: [24, 42, 73]},
  {type: "String", values: ["meh", "hem", "hrm"]},
];

for (let {type, values} of PREFS) {
  let set = `set${type}Pref`;
  let get = `get${type}Pref`;

  function prefTest(opts) {
    function check(pref, proc, val, {expectedVal, defaultVal = undefined, expectedDefault = defaultVal, expectedFlags = {}}) {
      checkPref(pref, proc, val, type, expectedVal, expectedDefault, expectedFlags);
    }

    function updatePref(PREF,
                        {userVal = undefined,
                         defaultVal = undefined,
                         flags = {}}) {
      info(`Update "${PREF}"`);
      if (userVal !== undefined) {
        prefs[set](PREF, userVal);
      }
      if (defaultVal !== undefined) {
        defaultPrefs[set](PREF, defaultVal);
      }
      if (flags.locked === true) {
        prefs.lockPref(PREF);
      } else if (flags.locked === false) {
        prefs.unlockPref(PREF);
      }
    }

    return {
      beforeContent(PREF) {
        updatePref(PREF, opts.initial)
        check(PREF, "parent", getPref(PREF), opts.initial);
      },
      contentStartup(PREF, contentVal) {
        check(PREF, "content", contentVal, opts.initial);
        check(PREF, "parent", getPref(PREF), opts.initial);

        updatePref(PREF, opts.change1)
        check(PREF, "parent", getPref(PREF), opts.change1);
      },
      contentUpdate1(PREF, contentVal) {
        check(PREF, "content", contentVal, opts.change1);
        check(PREF, "parent", getPref(PREF), opts.change1);

        if (opts.change2) {
          updatePref(PREF, opts.change2)
          check(PREF, "parent", getPref(PREF), opts.change2);
        }
      },
      contentUpdate2(PREF, contentVal) {
        if (opts.change2) {
          check(PREF, "content", contentVal, opts.change2);
          check(PREF, "parent", getPref(PREF), opts.change2);
        }
      },
    };
  }

  for (let i of [0, 1]) {
    let userVal = values[i];
    let defaultVal = values[+!i];

    TESTS[`type.${type}.${i}.default`] = prefTest({
      initial: {defaultVal, expectedVal: defaultVal},
      change1: {defaultVal: values[2], expectedVal: values[2]},
    });

    TESTS[`type.${type}.${i}.user`] = prefTest({
      initial: {userVal, expectedVal: userVal},
      change1: {defaultVal: values[2], expectedVal: userVal},
      change2: {userVal: values[2],
                expectedDefault: values[2],
                expectedVal: values[2]},
    });

    TESTS[`type.${type}.${i}.both`] = prefTest({
      initial: {userVal, defaultVal, expectedVal: userVal},
      change1: {defaultVal: values[2], expectedVal: userVal},
      change2: {userVal: values[2],
                expectedDefault: values[2],
                expectedVal: values[2]},
    });

    TESTS[`type.${type}.${i}.both.thenLock`] = prefTest({
      initial: {userVal, defaultVal, expectedVal: userVal},
      change1: {expectedDefault: defaultVal,
                expectedVal: defaultVal,
                flags: {locked: true},
                expectFlags: {locked: true}},
    });

    TESTS[`type.${type}.${i}.both.thenUnlock`] = prefTest({
      initial: {userVal, defaultVal, expectedVal: defaultVal,
                flags: {locked: true}, expectedFlags: {locked: true}},
      change1: {expectedDefault: defaultVal,
                expectedVal: userVal,
                flags: {locked: false},
                expectFlags: {locked: false}},
    });

    TESTS[`type.${type}.${i}.both.locked`] = prefTest({
      initial: {userVal, defaultVal, expectedVal: defaultVal,
                flags: {locked: true}, expectedFlags: {locked: true}},
      change1: {userVal: values[2],
                expectedDefault: defaultVal,
                expectedVal: defaultVal,
                expectedFlags: {locked: true}},
      change2: {defaultVal: values[2],
                expectedDefault: defaultVal,
                expectedVal: defaultVal,
                expectedFlags: {locked: true}},
    });
  }
}

add_task(async function test_sharedMap_prefs() {
  let prefValues = {};

  async function runChecks(op) {
    for (let [pref, ops] of Object.entries(TESTS)) {
      if (ops[op]) {
        info(`Running ${op} for "${pref}"`);
        await ops[op](pref,
                      prefValues[pref] || undefined,
                      prefValues.childList || undefined);
      }
    }
  }

  await runChecks("beforeContent");

  contentPage = await ExtensionTestUtils.loadContentPage("about:blank", {remote: true});
  registerCleanupFunction(() => contentPage.close());

  contentPage.addFrameScriptHelper(FRAME_SCRIPT_INIT);
  contentPage.addFrameScriptHelper(try_);
  contentPage.addFrameScriptHelper(getPref);

  let prefNames = Object.keys(TESTS);
  prefValues = await contentPage.spawn(prefNames, getPrefs);

  await runChecks("contentStartup");

  prefValues = await contentPage.spawn(prefNames, getPrefs);

  await runChecks("contentUpdate1");

  prefValues = await contentPage.spawn(prefNames, getPrefs);

  await runChecks("contentUpdate2");
});
