var assert = require('assert');
var jsan = require('../');

describe('jsan', function() {
  describe('has a stringify method', function() {
    it('respects the replacer array argument', function() {
      var obj = {a: 1, b: 2};
      var JSONed = JSON.stringify(obj, ['a']);
      var jsaned = jsan.stringify(obj, ['a']);
      assert.equal(JSONed, jsaned);
    });

    it('respects the replacer function argument', function() {
      var obj = {a: 1, b: 2, c: {r: /foo/}};
      var replacer = function(index, value) {
        if (value.test) {
          return value.toString();
        }
      }
      var JSONed = JSON.stringify(obj, replacer);
      var jsaned = jsan.stringify(obj, replacer);
      assert.equal(JSONed, jsaned);
    });

    it('respects the space argument', function() {
      var obj = {a: 1, b: 2, c: {foo: 'bar'}};
      var JSONed = JSON.stringify(obj, null, 2);
      var jsaned = jsan.stringify(obj, null, 2);
      assert.equal(JSONed, jsaned);
    });

    it('behaves the same as JSON.stringify for simple jsonable objects', function() {
      var obj = {
        a: 1,
        b: 'string',
        c: [2,3],
        d: null
      };
      assert.equal(JSON.stringify(obj), jsan.stringify(obj));
    });

    it('uses the toJSON() method when possible', function() {
      var obj = { a: { b: 1, toJSON: function(key) { return key } } };
      assert.equal(jsan.stringify(obj, null, null, false), '{"a":"a"}');
    });

    it('can handle dates', function() {
      var obj = {
        now: new Date()
      }
      var str = jsan.stringify(obj, null, null, true);
      assert(/^\{"now":\{"\$jsan":"d[^"]*"\}\}$/.test(str));
    });

    it('can handle regexes', function() {
      var obj = {
        r: /test/
      }
      var str = jsan.stringify(obj, null, null, true);
      assert.deepEqual(str, '{"r":{"$jsan":"r,test"}}');
    });

    it('can handle functions', function() {
      var obj = {
        f: function () {}
      }
      var str = jsan.stringify(obj, null, null, true);
      assert.deepEqual(str, '{"f":{"$jsan":"ffunction () { /* ... */ }"}}');
    });

    it('can handle undefined', function() {
      var obj = undefined;
      var str = jsan.stringify(obj, null, null, true);
      assert.deepEqual(str, '{"$jsan":"u"}');
    });

    it('can handle NaN', function() {
      var obj = NaN;
      var str = jsan.stringify(obj, null, null, true);
      assert.deepEqual(str, '{"$jsan":"n"}');
    });

    it('can handle Infinity', function() {
      var obj = Infinity;
      var str = jsan.stringify(obj, null, null, true);
      assert.deepEqual(str, '{"$jsan":"i"}');
    });

    it('can handle -Infinity', function() {
      var obj = -Infinity;
      var str = jsan.stringify(obj, null, null, true);
      assert.deepEqual(str, '{"$jsan":"y"}');
    });

    it('can handle nested undefined', function() {
      var obj = {
        u: undefined
      }
      var str = jsan.stringify(obj, null, null, true);
      assert.deepEqual(str, '{"u":{"$jsan":"u"}}');
    });

    it('can handle nested NaN', function() {
      var obj = {
        u: NaN
      }
      var str = jsan.stringify(obj, null, null, true);
      assert.deepEqual(str, '{"u":{"$jsan":"n"}}');
    });

    it('can handle nested Infinity', function() {
      var obj = {
        u: Infinity
      }
      var str = jsan.stringify(obj, null, null, true);
      assert.deepEqual(str, '{"u":{"$jsan":"i"}}');
    });

    it('can handle nested -Infinity', function() {
      var obj = {
        u: -Infinity
      }
      var str = jsan.stringify(obj, null, null, true);
      assert.deepEqual(str, '{"u":{"$jsan":"y"}}');
    });

    it('can handle errors', function() {
      var obj = {
        e: new Error(':(')
      }
      var str = jsan.stringify(obj, null, null, true);
      assert.deepEqual(str, '{"e":{"$jsan":"e:("}}');
    });

    if (typeof Symbol !== 'undefined') {
      it('can handle ES symbols', function() {
        var obj = {
          s: Symbol('a')
        }
        var str = jsan.stringify(obj, null, null, true);
        assert.deepEqual(str, '{"s":{"$jsan":"sa"}}');
      });

      it('can handle global ES symbols', function() {
        var obj = {
          g: Symbol.for('a')
        }
        var str = jsan.stringify(obj, null, null, true);
        assert.deepEqual(str, '{"g":{"$jsan":"ga"}}');
      });
    }

    if (typeof Map !== 'undefined' && typeof Array.from !== 'undefined') {
      it('can handle ES Map', function() {
        var obj = {
          map: new Map([
            ['a', 1],
            [{toString: function (){ return 'a' }}, 2],
            [{}, 3]
          ])
        }
        var str = jsan.stringify(obj, null, null, true);
        assert.deepEqual(str, '{"map":{"$jsan":"m[[\\"a\\",1],[{\\"toString\\":{\\"$jsan\\":\\"ffunction (){ /* ... */ }\\"}},2],[{},3]]"}}');
      });
    }

    if (typeof Set !== 'undefined' && typeof Array.from !== 'undefined') {
      it('can handle ES Set', function() {
        var obj = {
          set: new Set(['a', {toString: function (){ return 'a' }}, {}])
        }
        var str = jsan.stringify(obj, null, null, true);
        assert.deepEqual(str, '{"set":{"$jsan":"l[\\"a\\",{\\"toString\\":{\\"$jsan\\":\\"ffunction (){ /* ... */ }\\"}},{}]"}}');
      });
    }

    it('works on objects with circular references', function() {
      var obj = {};
      obj['self'] = obj;
      assert.equal(jsan.stringify(obj), '{"self":{"$jsan":"$"}}');
    });

    it('can use the circular option', function() {
      var obj = {};
      obj.self = obj;
      obj.a = 1;
      obj.b = {};
      obj.c = obj.b;
      assert.equal(jsan.stringify(obj, null, null, {circular: '∞'}), '{"self":"∞","a":1,"b":{},"c":{"$jsan":"$.b"}}');
      assert.equal(jsan.stringify(obj, null, null, {circular: function() { return '∞!' }}), '{"self":"∞!","a":1,"b":{},"c":{"$jsan":"$.b"}}');
    });

    it('can use the refs option', function() {
      var obj1 = { a: 1 };
      var obj = { "prop1": obj1, "prop2": { "prop3": obj1 } };
      assert.equal(jsan.stringify(obj, null, null, {refs: true}), '{"prop1":{"a":1},"prop2":{"prop3":{"$jsan":"$.prop1"}}}');
      assert.equal(jsan.stringify(obj, null, null, true), '{"prop1":{"a":1},"prop2":{"prop3":{"$jsan":"$.prop1"}}}');
      assert.equal(jsan.stringify(obj, null, null, false), '{"prop1":{"a":1},"prop2":{"prop3":{"$jsan":"$.prop1"}}}');
      assert.equal(jsan.stringify(obj, null, null, {refs: false}), '{"prop1":{"a":1},"prop2":{"prop3":{"a":1}}}');
    });

    it('works with refs option with circular references', function() {
      var obj = {};
      obj.self = obj;
      obj.a = 1;
      obj.b = {t: 1};
      obj.c = obj.b;
      assert.equal(jsan.stringify(obj, null, null, {refs: false}), '{"self":{"$jsan":"$"},"a":1,"b":{"t":1},"c":{"t":1}}');
      assert.equal(jsan.stringify(obj, null, null, {refs: false, circular: 'Circular'}), '{"self":"Circular","a":1,"b":{"t":1},"c":{"t":1}}');
    });

    it('works on objects with "[", "\'", and "]" in the keys', function() {
      var obj = {};
      obj['["key"]'] = {};
      obj['["key"]']['["key"]'] = obj['["key"]'];
      assert.equal(jsan.stringify(obj), '{"[\\"key\\"]":{"[\\"key\\"]":{"$jsan":"$[\\"[\\\\\\"key\\\\\\"]\\"]"}}}');
    });

    it('works on objects that will get encoded with \\uXXXX', function() {
      var obj = {"\u017d\u010d":{},"kraj":"\u017du\u017e"};
      obj["\u017d\u010d"]["\u017d\u010d"] = obj["\u017d\u010d"];
      assert.equal(jsan.stringify(obj), '{"\u017d\u010d":{"\u017d\u010d":{"$jsan":"$[\\\"\u017d\u010d\\\"]"}},"kraj":"Žuž"}');
    });

    it('works on circular arrays', function() {
      var obj = [];
      obj[0] = [];
      obj[0][0] = obj[0];
      assert.equal(jsan.stringify(obj), '[[{"$jsan":"$[0]"}]]');
    });

    it('works correctly for mutiple calls with the same object', function() {
      var obj = {};
      obj.self = obj;
      obj.a = {};
      obj.b = obj.a;
      assert.equal(jsan.stringify(obj), '{"self":{"$jsan":"$"},"a":{},"b":{"$jsan":"$.a"}}');
      assert.equal(jsan.stringify(obj), '{"self":{"$jsan":"$"},"a":{},"b":{"$jsan":"$.a"}}');
    });

    it('does not report false positives for circular references', function() {
      /**
       * This is a test for an edge case in which jsan.stringify falsely reported circular references
       * The minimal conditions are
       * 1) The object has an error preventing serialization by json.stringify
       * 2) The object contains a repeated reference
       * 3) the second path of the reference contains the first path
       */
      var circular = {};
      circular.self = circular;
      
      var ref = {};
      
      var obj = {
        circularRef: circular,
        ref: ref,
        refAgain: ref,
      };
      
      var result = jsan.stringify(obj, null, null, { circular: "[CIRCULAR]" });
      assert.equal('{"circularRef":{"self":"[CIRCULAR]"},"ref":{},"refAgain":{"$jsan":"$.ref"}}', result)
    });
  });



  describe('has a parse method', function() {
    it('behaves the same as JSON.parse for valid json strings', function() {
      var str = '{"a":1,"b":"string","c":[2,3],"d":null}';
      assert.deepEqual(JSON.parse(str), jsan.parse(str));
    });
   
    it('uses reviver function', function() {
      var str = '{"a":1,"b":null}';
      assert.deepEqual({"a":1,"b":2},
        jsan.parse(str, function (key, value) { return key === "b" ? 2 : value; })
      );
    });

    it('can decode dates', function() {
      var str = '{"$jsan":"d1400000000000"}';
      var obj = jsan.parse(str);
      assert(obj instanceof Date);
    });

    it('can decode dates while using reviver', function() {
      var str = '{"$jsan":"d1400000000000"}';
      var obj = jsan.parse(str, function (key, value) { return value; });
      assert(obj instanceof Date);
    });

    it('can decode regexes', function() {
      str = '{"$jsan":"r,test"}';
      var obj = jsan.parse(str);
      assert(obj instanceof RegExp )
    });

    it('can decode functions', function() {
      str = '{"$jsan":"ffunction () { /* ... */ }"}';
      var obj = jsan.parse(str);
      assert(obj instanceof Function);
    });

    it('can decode undefined', function() {
      str = '{"$jsan":"u"}';
      var obj = jsan.parse(str);
      assert(obj === undefined);
    });

    it('can decode NaN', function() {
      str = '{"$jsan":"n"}';
      var obj = jsan.parse(str);
      assert(isNaN(obj) && typeof obj === 'number');
    });

    it('can decode Infinity', function() {
      str = '{"$jsan":"i"}';
      var obj = jsan.parse(str);
      assert(obj === Number.POSITIVE_INFINITY);
    });

    it('can decode -Infinity', function() {
      str = '{"$jsan":"y"}';
      var obj = jsan.parse(str);
      assert(obj === Number.NEGATIVE_INFINITY);
    });

    it('can decode errors', function() {
      str = '{"$jsan":"e:("}';
      var obj = jsan.parse(str);
      assert(obj instanceof Error && obj.message === ':(');
    });

    it('can decode nested dates', function() {
      var str = '{"now":{"$jsan":"d1400000000000"}}';
      var obj = jsan.parse(str);
      assert(obj.now instanceof Date);
    });

    it('can decode nested regexes', function() {
      str = '{"r":{"$jsan":"r,test"}}';
      var obj = jsan.parse(str);
      assert(obj.r instanceof RegExp )
    });

    it('can decode nested functions', function() {
      str = '{"f":{"$jsan":"ffunction () { /* ... */ }"}}';
      var obj = jsan.parse(str);
      assert(obj.f instanceof Function);
    });

    it('can decode nested undefined', function() {
      str = '{"u":{"$jsan":"u"}}';
      var obj = jsan.parse(str);
      assert('u' in obj && obj.u === undefined);
    });

    it('can decode nested NaN', function() {
      str = '{"u":{"$jsan":"n"}}';
      var obj = jsan.parse(str);
      assert('u' in obj && isNaN(obj.u) && typeof obj.u === 'number');
    });

    it('can decode nested Infinity', function() {
      str = '{"u":{"$jsan":"i"}}';
      var obj = jsan.parse(str);
      assert('u' in obj && obj.u === Number.POSITIVE_INFINITY);
    });

    it('can decode nested -Infinity', function() {
      str = '{"u":{"$jsan":"y"}}';
      var obj = jsan.parse(str);
      assert('u' in obj && obj.u === Number.NEGATIVE_INFINITY);
    });

    it('can decode nested errors', function() {
      str = '{"e":{"$jsan":"e:("}}';
      var obj = jsan.parse(str);
      assert(obj.e instanceof Error && obj.e.message === ':(');
    });

    if (typeof Symbol !== 'undefined') {
      it('can decode ES symbols', function() {
        str = '{"s1":{"$jsan":"sfoo"}, "s2":{"$jsan":"s"}}';
        var obj = jsan.parse(str);
        assert(typeof obj.s1 === 'symbol' && obj.s1.toString() === 'Symbol(foo)');
        assert(typeof obj.s2 === 'symbol' && obj.s2.toString() === 'Symbol()');
      });

      it('can decode global ES symbols', function() {
        str = '{"g1":{"$jsan":"gfoo"}, "g2":{"$jsan":"gundefined"}}';
        var obj = jsan.parse(str);
        assert(typeof obj.g1 === 'symbol' && obj.g1 === Symbol.for('foo'));
        assert(typeof obj.g2 === 'symbol' && obj.g2 === Symbol.for());
      });
    }

    if (typeof Map !== 'undefined' && typeof Array.from !== 'undefined') {
      it('can decode ES Map', function() {
        var str = '{"map":{"$jsan":"m[[\\"a\\",1],[{\\"toString\\":{\\"$jsan\\":\\"ffunction (){ /* ... */ }\\"}},2],[{},3]]"}}';
        var obj = jsan.parse(str);
        var keys = obj.map.keys();
        var values = obj.map.values();
        assert.equal(keys.next().value, 'a');
        assert.equal(typeof keys.next().value.toString, 'function');
        assert.equal(typeof keys.next().value, 'object');
        assert.equal(values.next().value, 1);
        assert.equal(values.next().value, 2);
        assert.equal(values.next().value, 3);
      });
    }

    if (typeof Set !== 'undefined' && typeof Array.from !== 'undefined') {
      it('can decode ES Set', function() {
        var str = '{"set":{"$jsan":"l[\\"a\\",{\\"toString\\":{\\"$jsan\\":\\"ffunction (){ /* ... */ }\\"}},{}]"}}';
        var obj = jsan.parse(str);
        var values = obj.set.values();
        assert.equal(values.next().value, 'a');
        assert.equal(typeof values.next().value.toString, 'function');
        assert.equal(typeof values.next().value, 'object');
      });
    }

    it('works on object strings with a circular dereferences', function() {
      var str = '{"a":1,"b":"string","c":[2,3],"d":null,"self":{"$jsan":"$"}}';
      var obj = jsan.parse(str);
      assert.deepEqual(obj['self'], obj);
    });

    it('works on object strings with "[", "\'", and "]" in the keys', function() {
      var str = '{"[\\"key\\"]":{"[\\"key\\"]":{"$jsan":"$[\\"[\\\\\\"key\\\\\\"]\\"]"}}}';
      var obj = jsan.parse(str);
      assert.deepEqual(obj['["key"]']['["key"]'], obj['["key"]']);
    });

    it('works on objects encoded with \\uXXXX', function() {
      var str = '{"\u017d\u010d":{"\u017d\u010d":{"$jsan":"$[\\\"\\u017d\\u010d\\\"]"}},"kraj":"Žuž"}';
      var obj = jsan.parse(str);
      assert.deepEqual(obj["\u017d\u010d"]["\u017d\u010d"], obj["\u017d\u010d"]);
    });

    it('works on array strings with circular dereferences', function() {
      var str = '[[{"$jsan":"$[0]"}]]';
      var arr = jsan.parse(str);
      assert.deepEqual(arr[0][0], arr[0]);
    });
  });

});
