/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et: */
/* 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/. */

const apiUtils = require("api-utils/api-utils");

exports.testPublicConstructor = function (test) {
  function PrivateCtor() {}
  PrivateCtor.prototype = {};

  let PublicCtor = apiUtils.publicConstructor(PrivateCtor);
  test.assert(
    PrivateCtor.prototype.isPrototypeOf(PublicCtor.prototype),
    "PrivateCtor.prototype should be prototype of PublicCtor.prototype"
  );

  function testObj(useNew) {
    let obj = useNew ? new PublicCtor() : PublicCtor();
    test.assert(obj instanceof PublicCtor,
                "Object should be instance of PublicCtor");
    test.assert(obj instanceof PrivateCtor,
                "Object should be instance of PrivateCtor");
    test.assert(PublicCtor.prototype.isPrototypeOf(obj),
                "PublicCtor's prototype should be prototype of object");
    test.assertEqual(obj.constructor, PublicCtor,
                     "Object constructor should be PublicCtor");
  }
  testObj(true);
  testObj(false);
};

exports.testValidateOptionsEmpty = function (test) {
  let val = apiUtils.validateOptions(null, {});
  assertObjsEqual(test, val, {});

  val = apiUtils.validateOptions(null, { foo: {} });
  assertObjsEqual(test, val, {});

  val = apiUtils.validateOptions({}, {});
  assertObjsEqual(test, val, {});

  val = apiUtils.validateOptions({}, { foo: {} });
  assertObjsEqual(test, val, {});
};

exports.testValidateOptionsNonempty = function (test) {
  let val = apiUtils.validateOptions({ foo: 123 }, {});
  assertObjsEqual(test, val, {});

  val = apiUtils.validateOptions({ foo: 123, bar: 456 },
                                 { foo: {}, bar: {}, baz: {} });
  assertObjsEqual(test, val, { foo: 123, bar: 456 });
};

exports.testValidateOptionsMap = function (test) {
  let val = apiUtils.validateOptions({ foo: 3, bar: 2 }, {
    foo: { map: function (v) v * v },
    bar: { map: function (v) undefined }
  });
  assertObjsEqual(test, val, { foo: 9, bar: undefined });
};

exports.testValidateOptionsMapException = function (test) {
  let val = apiUtils.validateOptions({ foo: 3 }, {
    foo: { map: function () { throw new Error(); }}
  });
  assertObjsEqual(test, val, { foo: 3 });
};

exports.testValidateOptionsOk = function (test) {
  let val = apiUtils.validateOptions({ foo: 3, bar: 2, baz: 1 }, {
    foo: { ok: function (v) v },
    bar: { ok: function (v) v }
  });
  assertObjsEqual(test, val, { foo: 3, bar: 2 });

  test.assertRaises(
    function () apiUtils.validateOptions({ foo: 2, bar: 2 }, {
      bar: { ok: function (v) v > 2 }
    }),
    'The option "bar" is invalid.',
    "ok should raise exception on invalid option"
  );

  test.assertRaises(
    function () apiUtils.validateOptions(null, { foo: { ok: function (v) v }}),
    'The option "foo" is invalid.',
    "ok should raise exception on invalid option"
  );
};

exports.testValidateOptionsIs = function (test) {
  let opts = {
    array: [],
    boolean: true,
    func: function () {},
    nul: null,
    number: 1337,
    object: {},
    string: "foo",
    undef1: undefined
  };
  let requirements = {
    array: { is: ["array"] },
    boolean: { is: ["boolean"] },
    func: { is: ["function"] },
    nul: { is: ["null"] },
    number: { is: ["number"] },
    object: { is: ["object"] },
    string: { is: ["string"] },
    undef1: { is: ["undefined"] },
    undef2: { is: ["undefined"] }
  };
  let val = apiUtils.validateOptions(opts, requirements);
  assertObjsEqual(test, val, opts);

  test.assertRaises(
    function () apiUtils.validateOptions(null, {
      foo: { is: ["object", "number"] }
    }),
    'The option "foo" must be one of the following types: object, number',
    "Invalid type should raise exception"
  );
};

exports.testValidateOptionsMapIsOk = function (test) {
  let [map, is, ok] = [false, false, false];
  let val = apiUtils.validateOptions({ foo: 1337 }, {
    foo: {
      map: function (v) v.toString(),
      is: ["string"],
      ok: function (v) v.length > 0
    }
  });
  assertObjsEqual(test, val, { foo: "1337" });

  let requirements = {
    foo: {
      is: ["object"],
      ok: function () test.fail("is should have caused us to throw by now")
    }
  };
  test.assertRaises(
    function () apiUtils.validateOptions(null, requirements),
    'The option "foo" must be one of the following types: object',
    "is should be used before ok is called"
  );
};

exports.testValidateOptionsErrorMsg = function (test) {
  test.assertRaises(
    function () apiUtils.validateOptions(null, {
      foo: { ok: function (v) v, msg: "foo!" }
    }),
    "foo!",
    "ok should raise exception with customized message"
  );
};

exports.testValidateMapWithMissingKey = function (test) {
  let val = apiUtils.validateOptions({ }, {
    foo: {
      map: function (v) v || "bar"
    }
  });
  assertObjsEqual(test, val, { foo: "bar" });

  val = apiUtils.validateOptions({ }, {
    foo: {
      map: function (v) { throw "bar" }
    }
  });
  assertObjsEqual(test, val, { });
};

exports.testAddIterator = function testAddIterator(test) {
  let obj = {};
  let keys = ["foo", "bar", "baz"];
  let vals = [1, 2, 3];
  let keysVals = [["foo", 1], ["bar", 2], ["baz", 3]];
  apiUtils.addIterator(
    obj,
    function keysValsGen() {
      for each (let keyVal in keysVals)
        yield keyVal;
    }
  );

  let keysItr = [];
  for (let key in obj)
    keysItr.push(key);
  test.assertEqual(keysItr.length, keys.length,
                   "the keys iterator returns the correct number of items");
  for (let i = 0; i < keys.length; i++)
    test.assertEqual(keysItr[i], keys[i], "the key is correct");

  let valsItr = [];
  for each (let val in obj)
    valsItr.push(val);
  test.assertEqual(valsItr.length, vals.length,
                   "the vals iterator returns the correct number of items");
  for (let i = 0; i < vals.length; i++)
    test.assertEqual(valsItr[i], vals[i], "the val is correct");

  let keysValsItr = [];
  for (let keyVal in Iterator(obj))
    keysValsItr.push(keyVal);
  test.assertEqual(keysValsItr.length, keysVals.length, "the keys/vals " +
                   "iterator returns the correct number of items");
  for (let i = 0; i < keysVals.length; i++) {
    test.assertEqual(keysValsItr[i][0], keysVals[i][0], "the key is correct");
    test.assertEqual(keysValsItr[i][1], keysVals[i][1], "the val is correct");
  }

  let keysOnlyItr = [];
  for (let key in Iterator(obj, true /* keysonly */))
    keysOnlyItr.push(key);
  test.assertEqual(keysOnlyItr.length, keysVals.length, "the keys only " +
                   "iterator returns the correct number of items");
  for (let i = 0; i < keysVals.length; i++)
    test.assertEqual(keysOnlyItr[i], keysVals[i][0], "the key is correct");
};

function assertObjsEqual(test, obj1, obj2) {
  var items = 0;
  for (let [key, val] in Iterator(obj1)) {
    items++;
    test.assert(key in obj2, "obj1 key should be present in obj2");
    test.assertEqual(obj2[key], val, "obj1 value should match obj2 value");
  }
  for (let [key, val] in Iterator(obj2)) {
    items++;
    test.assert(key in obj1, "obj2 key should be present in obj1");
    test.assertEqual(obj1[key], val, "obj2 value should match obj1 value");
  }
  if (!items)
    test.assertEqual(JSON.stringify(obj1), JSON.stringify(obj2),
                     "obj1 should have same JSON representation as obj2");
}
