import { EJSON } from './ejson';
import EJSONTest from './custom_models_for_tests';

Tinytest.add('ejson - keyOrderSensitive', test => {
  test.isTrue(EJSON.equals({
    a: {b: 1, c: 2},
    d: {e: 3, f: 4},
  }, {
    d: {f: 4, e: 3},
    a: {c: 2, b: 1},
  }));

  test.isFalse(EJSON.equals({
    a: {b: 1, c: 2},
    d: {e: 3, f: 4},
  }, {
    d: {f: 4, e: 3},
    a: {c: 2, b: 1},
  }, {keyOrderSensitive: true}));

  test.isFalse(EJSON.equals({
    a: {b: 1, c: 2},
    d: {e: 3, f: 4},
  }, {
    a: {c: 2, b: 1},
    d: {f: 4, e: 3},
  }, {keyOrderSensitive: true}));
  test.isFalse(EJSON.equals({a: {}}, {a: {b: 2}}, {keyOrderSensitive: true}));
  test.isFalse(EJSON.equals({a: {b: 2}}, {a: {}}, {keyOrderSensitive: true}));
});

Tinytest.add('ejson - nesting and literal', test => {
  const d = new Date();
  const obj = {$date: d};
  const eObj = EJSON.toJSONValue(obj);
  const roundTrip = EJSON.fromJSONValue(eObj);
  test.equal(obj, roundTrip);
});

Tinytest.add('ejson - some equality tests', test => {
  test.isTrue(EJSON.equals({a: 1, b: 2, c: 3}, {a: 1, c: 3, b: 2}));
  test.isFalse(EJSON.equals({a: 1, b: 2}, {a: 1, c: 3, b: 2}));
  test.isFalse(EJSON.equals({a: 1, b: 2, c: 3}, {a: 1, b: 2}));
  test.isFalse(EJSON.equals({a: 1, b: 2, c: 3}, {a: 1, c: 3, b: 4}));
  test.isFalse(EJSON.equals({a: {}}, {a: {b: 2}}));
  test.isFalse(EJSON.equals({a: {b: 2}}, {a: {}}));
  // XXX: Object and Array were previously mistaken, which is why
  // we add some extra tests for them here
  test.isTrue(EJSON.equals([1, 2, 3, 4, 5], [1, 2, 3, 4, 5]));
  test.isFalse(EJSON.equals([1, 2, 3, 4, 5], [1, 2, 3, 4]));
  test.isFalse(EJSON.equals([1,2,3,4], {0: 1, 1: 2, 2: 3, 3: 4}));
  test.isFalse(EJSON.equals({0: 1, 1: 2, 2: 3, 3: 4}, [1,2,3,4]));
  test.isFalse(EJSON.equals({}, []));
  test.isFalse(EJSON.equals([], {}));
});

Tinytest.add('ejson - equality and falsiness', test => {
  test.isTrue(EJSON.equals(null, null));
  test.isTrue(EJSON.equals(undefined, undefined));
  test.isFalse(EJSON.equals({foo: 'foo'}, null));
  test.isFalse(EJSON.equals(null, {foo: 'foo'}));
  test.isFalse(EJSON.equals(undefined, {foo: 'foo'}));
  test.isFalse(EJSON.equals({foo: 'foo'}, undefined));
});

Tinytest.add('ejson - NaN and Inf', test => {
  test.equal(EJSON.parse('{"$InfNaN": 1}'), Infinity);
  test.equal(EJSON.parse('{"$InfNaN": -1}'), -Infinity);
  test.isTrue(Number.isNaN(EJSON.parse('{"$InfNaN": 0}')));
  test.equal(EJSON.parse(EJSON.stringify(Infinity)), Infinity);
  test.equal(EJSON.parse(EJSON.stringify(-Infinity)), -Infinity);
  test.isTrue(Number.isNaN(EJSON.parse(EJSON.stringify(NaN))));
  test.isTrue(EJSON.equals(NaN, NaN));
  test.isTrue(EJSON.equals(Infinity, Infinity));
  test.isTrue(EJSON.equals(-Infinity, -Infinity));
  test.isFalse(EJSON.equals(Infinity, -Infinity));
  test.isFalse(EJSON.equals(Infinity, NaN));
  test.isFalse(EJSON.equals(Infinity, 0));
  test.isFalse(EJSON.equals(NaN, 0));

  test.isTrue(EJSON.equals(
    EJSON.parse('{"a": {"$InfNaN": 1}}'),
    {a: Infinity}
  ));
  test.isTrue(EJSON.equals(
    EJSON.parse('{"a": {"$InfNaN": 0}}'),
    {a: NaN}
  ));
});

Tinytest.add('ejson - clone', test => {
  const cloneTest = (x, identical) => {
    const y = EJSON.clone(x);
    test.isTrue(EJSON.equals(x, y));
    test.equal(x === y, !!identical);
  };
  cloneTest(null, true);
  cloneTest(undefined, true);
  cloneTest(42, true);
  cloneTest('asdf', true);
  cloneTest([1, 2, 3]);
  cloneTest([1, 'fasdf', {foo: 42}]);
  cloneTest({x: 42, y: 'asdf'});

  function testCloneArgs(/*arguments*/) {
    const clonedArgs = EJSON.clone(arguments);
    test.equal(clonedArgs, [1, 2, 'foo', [4]]);
  };
  testCloneArgs(1, 2, 'foo', [4]);
});

Tinytest.add('ejson - stringify', test => {
  test.equal(EJSON.stringify(null), 'null');
  test.equal(EJSON.stringify(true), 'true');
  test.equal(EJSON.stringify(false), 'false');
  test.equal(EJSON.stringify(123), '123');
  test.equal(EJSON.stringify('abc'), '"abc"');

  test.equal(EJSON.stringify([1, 2, 3]),
     '[1,2,3]'
  );
  test.equal(EJSON.stringify([1, 2, 3], {indent: true}),
    '[\n  1,\n  2,\n  3\n]'
  );
  test.equal(EJSON.stringify([1, 2, 3], {canonical: false}),
    '[1,2,3]'
  );
  test.equal(EJSON.stringify([1, 2, 3], {indent: true, canonical: false}),
    '[\n  1,\n  2,\n  3\n]'
  );

  test.equal(EJSON.stringify([1, 2, 3], {indent: 4}),
    '[\n    1,\n    2,\n    3\n]'
  );
  test.equal(EJSON.stringify([1, 2, 3], {indent: '--'}),
    '[\n--1,\n--2,\n--3\n]'
  );

  test.equal(
    EJSON.stringify(
      {b: [2, {d: 4, c: 3}], a: 1},
      {canonical: true}
    ),
    '{"a":1,"b":[2,{"c":3,"d":4}]}'
  );
  test.equal(
    EJSON.stringify(
      {b: [2, {d: 4, c: 3}], a: 1},
      {
        indent: true,
        canonical: true,
      }
    ),
    '{\n' +
    '  "a": 1,\n' +
    '  "b": [\n' +
    '    2,\n' +
    '    {\n' +
    '      "c": 3,\n' +
    '      "d": 4\n' +
    '    }\n' +
    '  ]\n' +
    '}'
  );
  test.equal(
    EJSON.stringify(
      {b: [2, {d: 4, c: 3}], a: 1},
      {canonical: false}
    ),
    '{"b":[2,{"d":4,"c":3}],"a":1}'
  );
  test.equal(
    EJSON.stringify(
      {b: [2, {d: 4, c: 3}], a: 1},
      {indent: true, canonical: false}
    ),
    '{\n' +
    '  "b": [\n' +
    '    2,\n' +
    '    {\n' +
    '      "d": 4,\n' +
    '      "c": 3\n' +
    '    }\n' +
    '  ],\n' +
    '  "a": 1\n' +
    '}'
  );

  test.throws(
    () => {
      const col = new Mongo.Collection('test');
      EJSON.stringify(col)
    },
    /Converting circular structure to JSON/
  );
});

Tinytest.add('ejson - parse', test => {
  test.equal(EJSON.parse('[1,2,3]'), [1, 2, 3]);
  test.throws(
    () => { EJSON.parse(null); },
    /argument should be a string/
  );
});

Tinytest.add("ejson - regexp", test => {
  test.equal(EJSON.stringify(/foo/gi), "{\"$regexp\":\"foo\",\"$flags\":\"gi\"}");
  var d = new RegExp("foo", "gi");
  var obj = { $regexp: "foo", $flags: "gi" };

  var eObj = EJSON.toJSONValue(obj);
  var roundTrip = EJSON.fromJSONValue(eObj);
  test.equal(obj, roundTrip);
});

Tinytest.add('ejson - custom types', test => {
  const testSameConstructors = (someObj, compareWith) => {
    test.equal(someObj.constructor, compareWith.constructor);
    if (typeof someObj === 'object') {
      Object.keys(someObj).forEach(key => {
        const value = someObj[key];
        testSameConstructors(value, compareWith[key]);
      });
    }
  };

  const testReallyEqual = (someObj, compareWith) => {
    test.equal(someObj, compareWith);
    testSameConstructors(someObj, compareWith);
  };

  const testRoundTrip = (someObj) => {
    const str = EJSON.stringify(someObj);
    const roundTrip = EJSON.parse(str);
    testReallyEqual(someObj, roundTrip);
  };

  const testCustomObject = (someObj) => {
    testRoundTrip(someObj);
    testReallyEqual(someObj, EJSON.clone(someObj));
  };

  const a = new EJSONTest.Address('Montreal', 'Quebec');
  testCustomObject( {address: a} );
  // Test that difference is detected even if they
  // have similar toJSONValue results:
  const nakedA = {city: 'Montreal', state: 'Quebec'};
  test.notEqual(nakedA, a);
  test.notEqual(a, nakedA);
  const holder = new EJSONTest.Holder(nakedA);
  test.equal(holder.toJSONValue(), a.toJSONValue()); // sanity check
  test.notEqual(holder, a);
  test.notEqual(a, holder);

  const d = new Date();
  const obj = new EJSONTest.Person('John Doe', d, a);
  testCustomObject( obj );

  // Test clone is deep:
  const clone = EJSON.clone(obj);
  clone.address.city = 'Sherbrooke';
  test.notEqual( obj, clone );
});

// Verify objects with a property named "length" can be handled by the EJSON
// API properly (see https://github.com/meteor/meteor/issues/5175).
Tinytest.add('ejson - handle objects with properties named "length"', test => {
  class Widget {
    constructor() {
      this.length = 10;
    }
  }
  const widget = new Widget();

  const toJsonWidget = EJSON.toJSONValue(widget);
  test.equal(widget, toJsonWidget);

  const fromJsonWidget = EJSON.fromJSONValue(widget);
  test.equal(widget, fromJsonWidget);

  const stringifiedWidget = EJSON.stringify(widget);
  test.equal(stringifiedWidget, '{"length":10}');

  const parsedWidget = EJSON.parse('{"length":10}');
  test.equal({ length: 10 }, parsedWidget);

  test.isFalse(EJSON.isBinary(widget));

  const widget2 = new Widget();
  test.isTrue(widget, widget2);

  const clonedWidget = EJSON.clone(widget);
  test.equal(widget, clonedWidget);
});
