/* 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/. */

var { interfaces: Ci, classes: Cc, results: Cr, utils: Cu } = Components;

Cu.import("resource://gre/modules/Services.jsm");

var cps;
var asyncRunner;
var next;

let loadContext = Cc["@mozilla.org/loadcontext;1"].
                    createInstance(Ci.nsILoadContext);
let privateLoadContext = Cc["@mozilla.org/privateloadcontext;1"].
                           createInstance(Ci.nsILoadContext);

(function init() {
  // There has to be a profile directory before the CPS service is gotten.
  do_get_profile();
})();

function runAsyncTests(tests, dontResetBefore = false) {
  do_test_pending();

  cps = Cc["@mozilla.org/content-pref/service;1"].
        getService(Ci.nsIContentPrefService2);

  let s = {};
  Cu.import("resource://test/AsyncRunner.jsm", s);
  asyncRunner = new s.AsyncRunner({
    done: do_test_finished,
    error(err) {
      // xpcshell test functions like equal throw NS_ERROR_ABORT on
      // failure.  Ignore those and catch only uncaught exceptions.
      if (err !== Cr.NS_ERROR_ABORT) {
        if (err.stack) {
          err = err + "\n\nTraceback (most recent call first):\n" + err.stack +
                      "\nUseless do_throw stack:";
        }
        do_throw(err);
      }
    },
    consoleError(scriptErr) {
      // Previously, this code checked for console errors related to the test,
      // and treated them as failures. This was problematic, because our current
      // very-broken exception reporting machinery in XPCWrappedJSClass reports
      // errors to the console even if there's actually JS on the stack above
      // that will catch them. And a lot of the tests here intentionally trigger
      // error conditions on the JS-implemented XPCOM component (see erroneous()
      // in test_getSubdomains.js, for example). In the old world, we got lucky,
      // and the errors were never reported to the console due to happenstantial
      // JSContext reasons that aren't really worth going into.
      //
      // So. We make sure to dump this stuff so that it shows up in the logs, but
      // don't turn them into duplicate failures of the exception that was already
      // propagated to the caller.
      dump("AsyncRunner.jsm observed console error: " + scriptErr + "\n");
    }
  });

  next = asyncRunner.next.bind(asyncRunner);

  do_register_cleanup(function() {
    asyncRunner.destroy();
    asyncRunner = null;
  });

  tests.forEach(function(test) {
    function* gen() {
      do_print("Running " + test.name);
      yield test();
      yield reset();
    }
    asyncRunner.appendIterator(gen());
  });

  // reset() ends up calling asyncRunner.next(), starting the tests.
  if (dontResetBefore) {
    next();
  } else {
    reset();
  }
}

function makeCallback(callbacks, success = null) {
  callbacks = callbacks || {};
  if (!callbacks.handleError) {
    callbacks.handleError = function(error) {
      do_throw("handleError call was not expected, error: " + error);
    };
  }
  if (!callbacks.handleResult) {
    callbacks.handleResult = function() {
      do_throw("handleResult call was not expected");
    };
  }
  if (!callbacks.handleCompletion)
    callbacks.handleCompletion = function(reason) {
      equal(reason, Ci.nsIContentPrefCallback2.COMPLETE_OK);
      if (success) {
        success();
      } else {
        next();
      }
    };
  return callbacks;
}

function do_check_throws(fn) {
  let threw = false;
  try {
    fn();
  } catch (err) {
    threw = true;
  }
  ok(threw);
}

function sendMessage(msg, callback) {
  let obj = callback || {};
  let ref = Cu.getWeakReference(obj);
  cps.QueryInterface(Ci.nsIObserver).observe(ref, "test:" + msg, null);
  return "value" in obj ? obj.value : undefined;
}

function reset() {
  sendMessage("reset", next);
}

function setWithDate(group, name, val, timestamp, context) {
  function updateDate() {
    let db = sendMessage("db");
    let stmt = db.createAsyncStatement(`
      UPDATE prefs SET timestamp = :timestamp
      WHERE
        settingID = (SELECT id FROM settings WHERE name = :name)
        AND groupID = (SELECT id FROM groups WHERE name = :group)
    `);
    stmt.params.timestamp = timestamp / 1000;
    stmt.params.name = name;
    stmt.params.group = group;

    stmt.executeAsync({
      handleCompletion(reason) {
        next();
      },
      handleError(err) {
        do_throw(err);
      }
    });
    stmt.finalize();
  }

  cps.set(group, name, val, context, makeCallback(null, updateDate));
}

function getDate(group, name, context) {
  let db = sendMessage("db");
  let stmt = db.createAsyncStatement(`
    SELECT timestamp FROM prefs
    WHERE
      settingID = (SELECT id FROM settings WHERE name = :name)
      AND groupID = (SELECT id FROM groups WHERE name = :group)
  `);
  stmt.params.name = name;
  stmt.params.group = group;

  let res;
  stmt.executeAsync({
    handleResult(results) {
      let row = results.getNextRow();
      res = row.getResultByName("timestamp");
    },
    handleCompletion(reason) {
      next(res * 1000);
    },
    handleError(err) {
      do_throw(err);
    }
  });
  stmt.finalize();
}

function set(group, name, val, context) {
  cps.set(group, name, val, context, makeCallback());
}

function setGlobal(name, val, context) {
  cps.setGlobal(name, val, context, makeCallback());
}

function prefOK(actual, expected, strict) {
  ok(actual instanceof Ci.nsIContentPref);
  equal(actual.domain, expected.domain);
  equal(actual.name, expected.name);
  if (strict)
    strictEqual(actual.value, expected.value);
  else
    equal(actual.value, expected.value);
}

function* getOK(args, expectedVal, expectedGroup, strict) {
  if (args.length == 2)
    args.push(undefined);
  let expectedPrefs = expectedVal === undefined ? [] :
                      [{ domain: expectedGroup || args[0],
                         name: args[1],
                         value: expectedVal }];
  yield getOKEx("getByDomainAndName", args, expectedPrefs, strict);
}

function* getSubdomainsOK(args, expectedGroupValPairs) {
  if (args.length == 2)
    args.push(undefined);
  let expectedPrefs = expectedGroupValPairs.map(function([group, val]) {
    return { domain: group, name: args[1], value: val };
  });
  yield getOKEx("getBySubdomainAndName", args, expectedPrefs);
}

function* getGlobalOK(args, expectedVal) {
  if (args.length == 1)
    args.push(undefined);
  let expectedPrefs = expectedVal === undefined ? [] :
                      [{ domain: null, name: args[0], value: expectedVal }];
  yield getOKEx("getGlobal", args, expectedPrefs);
}

function* getOKEx(methodName, args, expectedPrefs, strict, context) {
  let actualPrefs = [];
  args.push(makeCallback({
    handleResult: pref => actualPrefs.push(pref)
  }));
  yield cps[methodName].apply(cps, args);
  arraysOfArraysOK([actualPrefs], [expectedPrefs], function(actual, expected) {
    prefOK(actual, expected, strict);
  });
}

function getCachedOK(args, expectedIsCached, expectedVal, expectedGroup,
                     strict) {
  if (args.length == 2)
    args.push(undefined);
  let expectedPref = !expectedIsCached ? null : {
    domain: expectedGroup || args[0],
    name: args[1],
    value: expectedVal
  };
  getCachedOKEx("getCachedByDomainAndName", args, expectedPref, strict);
}

function getCachedSubdomainsOK(args, expectedGroupValPairs) {
  if (args.length == 2)
    args.push(undefined);
  let len = {};
  args.push(len);
  let actualPrefs = cps.getCachedBySubdomainAndName.apply(cps, args);
  actualPrefs = actualPrefs.sort(function(a, b) {
    return a.domain.localeCompare(b.domain);
  });
  equal(actualPrefs.length, len.value);
  let expectedPrefs = expectedGroupValPairs.map(function([group, val]) {
    return { domain: group, name: args[1], value: val };
  });
  arraysOfArraysOK([actualPrefs], [expectedPrefs], prefOK);
}

function getCachedGlobalOK(args, expectedIsCached, expectedVal) {
  if (args.length == 1)
    args.push(undefined);
  let expectedPref = !expectedIsCached ? null : {
    domain: null,
    name: args[0],
    value: expectedVal
  };
  getCachedOKEx("getCachedGlobal", args, expectedPref);
}

function getCachedOKEx(methodName, args, expectedPref, strict) {
  let actualPref = cps[methodName].apply(cps, args);
  if (expectedPref)
    prefOK(actualPref, expectedPref, strict);
  else
    strictEqual(actualPref, null);
}

function arraysOK(actual, expected, cmp) {
  if (actual.length != expected.length) {
    do_throw("Length is not equal: " + JSON.stringify(actual) + "==" + JSON.stringify(expected));
  } else {
    actual.forEach(function(actualElt, j) {
      let expectedElt = expected[j];
      cmp(actualElt, expectedElt);
    });
  }
}

function arraysOfArraysOK(actual, expected, cmp) {
  cmp = cmp || equal;
  arraysOK(actual, expected, function(act, exp) {
    arraysOK(act, exp, cmp)
  });
}

function dbOK(expectedRows) {
  let db = sendMessage("db");
  let stmt = db.createAsyncStatement(`
    SELECT groups.name AS grp, settings.name AS name, prefs.value AS value
    FROM prefs
    LEFT JOIN groups ON groups.id = prefs.groupID
    LEFT JOIN settings ON settings.id = prefs.settingID
    UNION

    /*
      These second two SELECTs get the rows of the groups and settings tables
      that aren't referenced by the prefs table.  Neither should return any
      rows if the component is working properly.
    */
    SELECT groups.name AS grp, NULL AS name, NULL AS value
    FROM groups
    WHERE id NOT IN (
      SELECT DISTINCT groupID
      FROM prefs
      WHERE groupID NOTNULL
    )
    UNION
    SELECT NULL AS grp, settings.name AS name, NULL AS value
    FROM settings
    WHERE id NOT IN (
      SELECT DISTINCT settingID
      FROM prefs
      WHERE settingID NOTNULL
    )

    ORDER BY value ASC, grp ASC, name ASC
  `);

  let actualRows = [];
  let cols = ["grp", "name", "value"];

  db.executeAsync([stmt], 1, {
    handleCompletion(reason) {
      arraysOfArraysOK(actualRows, expectedRows);
      next();
    },
    handleResult(results) {
      let row = null;
      while ((row = results.getNextRow())) {
        actualRows.push(cols.map(c => row.getResultByName(c)));
      }
    },
    handleError(err) {
      do_throw(err);
    }
  });
  stmt.finalize();
}

function on(event, names, dontRemove) {
  let args = {
    reset() {
      for (let prop in this) {
        if (Array.isArray(this[prop]))
          this[prop].splice(0, this[prop].length);
      }
    },
  };

  let observers = {};

  names.forEach(function(name) {
    let obs = {};
    ["onContentPrefSet", "onContentPrefRemoved"].forEach(function(meth) {
      obs[meth] = () => do_throw(meth + " should not be called");
    });
    obs["onContentPref" + event] = function() {
      args[name].push(Array.slice(arguments));
    };
    observers[name] = obs;
    args[name] = [];
    args[name].observer = obs;
    cps.addObserverForName(name, obs);
  });

  do_execute_soon(function() {
    if (!dontRemove)
      names.forEach(n => cps.removeObserverForName(n, observers[n]));
    next(args);
  });
}

function schemaVersionIs(expectedVersion) {
  let db = sendMessage("db");
  equal(db.schemaVersion, expectedVersion);
}

function wait() {
  do_execute_soon(next);
}

function observerArgsOK(actualArgs, expectedArgs) {
  notEqual(actualArgs, undefined);
  arraysOfArraysOK(actualArgs, expectedArgs);
}
