/* global BigInt */
'use strict';

var utils = require('../../lib/utils');
var sinon = require('sinon');

describe('lib/utils', function () {
  afterEach(function () {
    sinon.restore();
  });

  describe('clean()', function () {
    it('should remove the wrapping function declaration', function () {
      expect(
        utils.clean('function  (one, two, three)  {\n//code\n}'),
        'to be',
        '//code'
      );
    });

    it('should handle newlines in the function declaration', function () {
      expect(
        utils.clean('function  (one, two, three)\n  {\n//code\n}'),
        'to be',
        '//code'
      );
    });

    it('should remove space character indentation from the function body', function () {
      expect(
        utils.clean('  //line1\n    //line2'),
        'to be',
        '//line1\n  //line2'
      );
    });

    it('should remove tab character indentation from the function body', function () {
      expect(
        utils.clean('\t//line1\n\t\t//line2'),
        'to be',
        '//line1\n\t//line2'
      );
    });

    it('should handle functions with tabs in their declarations', function () {
      expect(utils.clean('function\t(\t)\t{\n//code\n}'), 'to be', '//code');
    });

    it('should handle named functions without space after name', function () {
      expect(
        utils.clean('function withName() {\n//code\n}'),
        'to be',
        '//code'
      );
    });

    it('should handle named functions with space after name', function () {
      expect(
        utils.clean('function withName () {\n//code\n}'),
        'to be',
        '//code'
      );
    });

    it('should handle functions with no space between the end and the closing brace', function () {
      expect(utils.clean('function() {/*code*/}'), 'to be', '/*code*/');
    });

    it('should handle functions with parentheses in the same line', function () {
      expect(
        utils.clean('function() { if (true) { /* code */ } }'),
        'to be',
        'if (true) { /* code */ }'
      );
    });

    it('should handle empty functions', function () {
      expect(utils.clean('function() {}'), 'to be', '');
    });

    it('should format a single line test function', function () {
      var fn = ['function () {', '  var a = 1;', '}'].join('\n');
      expect(utils.clean(fn), 'to be', 'var a = 1;');
    });

    it('should format a multi line test indented with spaces', function () {
      // and no new lines after curly braces, shouldn't matter
      var fn = [
        'function(){  var a = 1;',
        // this one has more spaces
        '    var b = 2;',
        '  var c = 3;  }'
      ].join('\n');
      expect(utils.clean(fn), 'to be', 'var a = 1;\n  var b = 2;\nvar c = 3;');
    });

    it('should format a multi line test indented with tabs', function () {
      var fn = [
        'function (arg1, arg2)   {',
        '\tif (true) {',
        '\t\tvar a = 1;',
        '\t}',
        '}'
      ].join('\n');
      expect(utils.clean(fn), 'to be', 'if (true) {\n\tvar a = 1;\n}');
    });

    it('should format functions saved in windows style - spaces', function () {
      var fn = [
        'function (one) {',
        '   do {',
        '    "nothing";',
        '   } while (false);',
        ' }'
      ].join('\r\n');
      expect(utils.clean(fn), 'to be', 'do {\n "nothing";\n} while (false);');
    });

    it('should format functions saved in windows style - tabs', function () {
      var fn = [
        'function ( )   {',
        '\tif (false) {',
        '\t\tvar json = {',
        '\t\t\tone : 1',
        '\t\t};',
        '\t}',
        '}'
      ].join('\r\n');
      expect(
        utils.clean(fn),
        'to be',
        'if (false) {\n\tvar json = {\n\t\tone : 1\n\t};\n}'
      );
    });

    it('should format es6 arrow functions', function () {
      var fn = ['() => {', '  var a = 1;', '}'].join('\n');
      expect(utils.clean(fn), 'to be', 'var a = 1;');
    });

    it('should format es6 arrow functions with implicit return', function () {
      var fn = '() => foo()';
      expect(utils.clean(fn), 'to be', 'foo()');
    });
  });

  describe('stringify()', function () {
    var stringify = utils.stringify;

    it('should return an object representation of a string created with a String constructor', function () {
      /* eslint no-new-wrappers: off */
      expect(
        stringify(new String('foo')),
        'to be',
        '{\n  "0": "f"\n  "1": "o"\n  "2": "o"\n}'
      );
    });

    it('should return Buffer with .toJSON representation', function () {
      expect(stringify(Buffer.from([0x01])), 'to be', '[\n  1\n]');
      expect(stringify(Buffer.from([0x01, 0x02])), 'to be', '[\n  1\n  2\n]');

      expect(
        stringify(Buffer.from('ABCD')),
        'to be',
        '[\n  65\n  66\n  67\n  68\n]'
      );
    });

    it('should return Date object with .toISOString() + string prefix', function () {
      expect(
        stringify(new Date(0)),
        'to be',
        '[Date: ' + new Date(0).toISOString() + ']'
      );

      var date = new Date(); // now
      expect(stringify(date), 'to be', '[Date: ' + date.toISOString() + ']');
    });

    it('should return invalid Date object with .toString() + string prefix', function () {
      expect(
        stringify(new Date('')),
        'to be',
        '[Date: ' + new Date('').toString() + ']'
      );
    });

    describe('#Number', function () {
      it('should show the handle -0 situations', function () {
        expect(stringify(-0), 'to be', '-0');
        expect(stringify(0), 'to be', '0');
        expect(stringify('-0'), 'to be', '"-0"');
      });

      it('should work well with `NaN` and `Infinity`', function () {
        expect(stringify(NaN), 'to be', 'NaN');
        expect(stringify(Infinity), 'to be', 'Infinity');
        expect(stringify(-Infinity), 'to be', '-Infinity');
      });

      it('floats and ints', function () {
        expect(stringify(1), 'to be', '1');
        expect(stringify(1.2), 'to be', '1.2');
        expect(stringify(1e9), 'to be', '1000000000');
      });

      if (typeof BigInt === 'function') {
        it('should work with bigints when possible', function () {
          expect(stringify(BigInt(1)), 'to be', '1n');
          expect(stringify(BigInt(2)), 'to be', '2n');
        });
      }
    });

    describe('canonicalize example', function () {
      it('should represent the actual full result', function () {
        var expected = {
          str: 'string',
          int: 90,
          float: 9.99,
          boolean: false,
          nil: null,
          undef: undefined,
          regex: /^[a-z|A-Z]/,
          date: new Date(0),
          func: function () {},
          infi: Infinity,
          nan: NaN,
          zero: -0,
          buffer: Buffer.from([0x01, 0x02]),
          array: [1, 2, 3],
          empArr: [],
          matrix: [[1], [2, 3, 4]],
          object: {a: 1, b: 2},
          canObj: {a: {b: 1, c: 2}, b: {}},
          empObj: {}
        };
        expected.circular = expected; // Make `Circular` situation
        var actual = [
          '{',
          '  "array": [',
          '    1',
          '    2',
          '    3',
          '  ]',
          '  "boolean": false',
          '  "buffer": [Buffer: [',
          '    1',
          '    2',
          '  ]]',
          '  "canObj": {',
          '    "a": {',
          '      "b": 1',
          '      "c": 2',
          '    }',
          '    "b": {}',
          '  }',
          '  "circular": [Circular]',
          '  "date": [Date: 1970-01-01T00:00:00.000Z]',
          '  "empArr": []',
          '  "empObj": {}',
          '  "float": 9.99',
          '  "func": [Function]',
          '  "infi": Infinity',
          '  "int": 90',
          '  "matrix": [',
          '    [',
          '      1',
          '    ]',
          '    [',
          '      2',
          '      3',
          '      4',
          '    ]',
          '  ]',
          '  "nan": NaN',
          '  "nil": [null]',
          '  "object": {',
          '    "a": 1',
          '    "b": 2',
          '  }',
          '  "regex": /^[a-z|A-Z]/',
          '  "str": "string"',
          '  "undef": [undefined]',
          '  "zero": -0',
          '}'
        ].join('\n');
        expect(stringify(expected), 'to be', actual);
      });

      describe('should represent null prototypes', function () {
        it('With explicit names', function () {
          const foo = Object.create(null, {
            [Symbol.toStringTag]: {value: 'Foo'},
            bing: {get: () => 'bong', enumerable: true}
          });
          const expected = [
            '{',
            '  "[Symbol.toStringTag]": "Foo"',
            '  "bing": "bong"',
            '}'
          ].join('\n');

          expect(stringify(foo), 'to be', expected);
        });

        it('Without names', function () {
          const unnamed = {
            bing: 'bong',
            abc: 123
          };
          unnamed.self = unnamed;
          const expected = [
            '{',
            '  "abc": 123',
            '  "bing": "bong"',
            '  "self": [Circular]',
            '}'
          ].join('\n');

          expect(
            stringify(Object.setPrototypeOf(unnamed, null)),
            'to be',
            expected
          );
        });
      });
    });

    it('should canonicalize the object', function () {
      var travis = {name: 'travis', age: 24};
      var travis2 = {age: 24, name: 'travis'};

      expect(stringify(travis), 'to be', stringify(travis2));
    });

    it('should handle circular structures in objects', function () {
      var travis = {name: 'travis'};
      travis.whoami = travis;

      expect(
        stringify(travis),
        'to be',
        '{\n  "name": "travis"\n  "whoami": [Circular]\n}'
      );
    });

    it('should handle circular structures in arrays', function () {
      var travis = ['travis'];
      travis.push(travis);

      expect(stringify(travis), 'to be', '[\n  "travis"\n  [Circular]\n]');
    });

    it('should handle circular structures in functions', function () {
      var travis = function () {};
      travis.fn = travis;

      expect(stringify(travis), 'to be', '{\n  "fn": [Circular]\n}');
    });

    it('should handle various non-undefined, non-null, non-object, non-array, non-date, and non-function values', function () {
      var regexp = /(?:)/;
      var regExpObj = {regexp};
      var regexpString = '/(?:)/';

      expect(
        stringify(regExpObj),
        'to be',
        '{\n  "regexp": ' + regexpString + '\n}'
      );
      expect(stringify(regexp), 'to be', regexpString);

      var number = 1;
      var numberObj = {number};
      var numberString = '1';

      expect(stringify(numberObj), 'to be', '{\n  "number": ' + number + '\n}');
      expect(stringify(number), 'to be', numberString);

      var boolean = false;
      var booleanObj = {boolean};
      var booleanString = 'false';

      expect(
        stringify(booleanObj),
        'to be',
        '{\n  "boolean": ' + boolean + '\n}'
      );
      expect(stringify(boolean), 'to be', booleanString);

      var string = 'sneepy';
      var stringObj = {string};

      expect(
        stringify(stringObj),
        'to be',
        '{\n  "string": "' + string + '"\n}'
      );
      expect(stringify(string), 'to be', JSON.stringify(string));

      var nullValue = null;
      var nullObj = {null: null};
      var nullString = '[null]';

      expect(stringify(nullObj), 'to be', '{\n  "null": [null]\n}');
      expect(stringify(nullValue), 'to be', nullString);
    });

    it('should handle arrays', function () {
      var array = ['dave', 'dave', 'dave', 'dave'];
      var arrayObj = {array};
      var arrayString = '    "dave"\n    "dave"\n    "dave"\n    "dave"';

      expect(
        stringify(arrayObj),
        'to be',
        '{\n  "array": [\n' + arrayString + '\n  ]\n}'
      );
      expect(
        stringify(array),
        'to be',
        '[' + arrayString.replace(/\s+/g, '\n  ') + '\n]'
      );
    });

    it('should handle functions', function () {
      var fn = function () {};
      var fnObj = {fn};
      var fnString = '[Function]';

      expect(stringify(fnObj), 'to be', '{\n  "fn": ' + fnString + '\n}');
      expect(stringify(fn), 'to be', '[Function]');
    });

    it('should handle empty objects', function () {
      expect(stringify({}), 'to be', '{}');
      expect(stringify({foo: {}}), 'to be', '{\n  "foo": {}\n}');
    });

    it('should handle empty arrays', function () {
      expect(stringify([]), 'to be', '[]');
      expect(stringify({foo: []}), 'to be', '{\n  "foo": []\n}');
    });

    it('should handle non-empty arrays', function () {
      expect(stringify(['a', 'b', 'c']), 'to be', '[\n  "a"\n  "b"\n  "c"\n]');
    });

    it('should handle empty functions (with no properties)', function () {
      expect(
        stringify(function () {}),
        'to be',
        '[Function]'
      );
      expect(
        stringify({foo: function () {}}),
        'to be',
        '{\n  "foo": [Function]\n}'
      );
      expect(
        stringify({foo: function () {}, bar: 'baz'}),
        'to be',
        '{\n  "bar": "baz"\n  "foo": [Function]\n}'
      );
    });

    it('should handle functions w/ properties', function () {
      var fn = function () {};
      fn.bar = 'baz';
      expect(stringify(fn), 'to be', '{\n  "bar": "baz"\n}');
      expect(
        stringify({foo: fn}),
        'to be',
        '{\n  "foo": {\n    "bar": "baz"\n  }\n}'
      );
    });

    it('should handle undefined values', function () {
      expect(
        stringify({foo: undefined}),
        'to be',
        '{\n  "foo": [undefined]\n}'
      );
      expect(
        stringify({foo: 'bar', baz: undefined}),
        'to be',
        '{\n  "baz": [undefined]\n  "foo": "bar"\n}'
      );
      expect(stringify(), 'to be', '[undefined]');
    });

    it('should recurse', function () {
      expect(
        stringify({foo: {bar: {baz: {quux: {herp: 'derp'}}}}}),
        'to be',
        '{\n  "foo": {\n    "bar": {\n      "baz": {\n        "quux": {\n          "herp": "derp"\n        }\n      }\n    }\n  }\n}'
      );
    });

    it('might get confusing', function () {
      expect(stringify(null), 'to be', '[null]');
    });

    it('should not freak out if it sees a primitive twice', function () {
      expect(
        stringify({foo: null, bar: null}),
        'to be',
        '{\n  "bar": [null]\n  "foo": [null]\n}'
      );
      expect(
        stringify({foo: 1, bar: 1}),
        'to be',
        '{\n  "bar": 1\n  "foo": 1\n}'
      );
    });

    it('should stringify dates', function () {
      var date = new Date(0);
      expect(stringify(date), 'to be', '[Date: 1970-01-01T00:00:00.000Z]');
      expect(
        stringify({date}),
        'to be',
        '{\n  "date": [Date: 1970-01-01T00:00:00.000Z]\n}'
      );
    });

    it('should handle object without an Object prototype', function () {
      var a;
      if (Object.create) {
        a = Object.create(null);
      } else {
        a = {};
      }
      a.foo = 1;

      expect(stringify(a), 'to be', '{\n  "foo": 1\n}');
    });

    // In old version node.js, Symbol is not available by default.
    if (typeof global.Symbol === 'function') {
      it('should handle Symbol', function () {
        var symbol = Symbol('value');
        expect(stringify(symbol), 'to match', /^Symbol\(value\)/);
        expect(stringify({symbol}), 'to match', /"symbol": Symbol\(value\)/);
      });
    }

    it('should handle length properties that cannot be coerced to a number', function () {
      expect(
        stringify({length: {nonBuiltinProperty: 0}}),
        'to be',
        '{\n  "length": {\n    "nonBuiltinProperty": 0\n  }\n}'
      );
      expect(
        stringify({length: 'a string where length should be'}),
        'to be',
        '{\n  "length": "a string where length should be"\n}'
      );
    });
  });

  describe('type()', function () {
    /* eslint no-extend-native: off */

    var type = utils.type;
    var toString = Object.prototype.toString;

    beforeEach(function () {
      // some JS engines such as PhantomJS 1.x exhibit this behavior
      Object.prototype.toString = function () {
        if (this === global) {
          return '[object DOMWindow]';
        }
        return toString.call(this);
      };
    });

    it('should recognize various types', function () {
      expect(type({}), 'to be', 'object');
      expect(type([]), 'to be', 'array');
      expect(type(1), 'to be', 'number');
      expect(type(Infinity), 'to be', 'number');
      expect(type(null), 'to be', 'null');
      expect(type(undefined), 'to be', 'undefined');
      expect(type(new Date()), 'to be', 'object');
      expect(type(/foo/), 'to be', 'object');
      expect(type('type'), 'to be', 'string');
      expect(type(new Error()), 'to be', 'error');
      expect(type(global), 'to be', 'object');
      expect(type(true), 'to be', 'boolean');
      expect(type(Buffer.from('ff', 'hex')), 'to be', 'object');
      expect(type(Symbol.iterator), 'to be', 'symbol');
      expect(type(new Map()), 'to be', 'object');
      expect(type(new WeakMap()), 'to be', 'object');
      expect(type(new Set()), 'to be', 'object');
      expect(type(new WeakSet()), 'to be', 'object');
      expect(
        type(async () => {}),
        'to be',
        'function'
      );
    });

    describe('when toString on null or undefined stringifies window', function () {
      it('should recognize null and undefined', function () {
        expect(type(null), 'to be', 'null');
        expect(type(undefined), 'to be', 'undefined');
      });
    });

    afterEach(function () {
      Object.prototype.toString = toString;
    });
  });

  describe('canonicalType()', function () {
    /* eslint no-extend-native: off */

    var type = utils.canonicalType;
    var toString = Object.prototype.toString;

    beforeEach(function () {
      // some JS engines such as PhantomJS 1.x exhibit this behavior
      Object.prototype.toString = function () {
        if (this === global) {
          return '[object DOMWindow]';
        }
        return toString.call(this);
      };
    });

    it('should recognize various types', function () {
      expect(type({}), 'to be', 'object');
      expect(type([]), 'to be', 'array');
      expect(type(1), 'to be', 'number');
      expect(type(Infinity), 'to be', 'number');
      expect(type(null), 'to be', 'null');
      expect(type(undefined), 'to be', 'undefined');
      expect(type(new Date()), 'to be', 'date');
      expect(type(/foo/), 'to be', 'regexp');
      expect(type('type'), 'to be', 'string');
      expect(type(global), 'to be', 'domwindow');
      expect(type(true), 'to be', 'boolean');
    });

    describe('when toString on null or undefined stringifies window', function () {
      it('should recognize null and undefined', function () {
        expect(type(null), 'to be', 'null');
        expect(type(undefined), 'to be', 'undefined');
      });
    });

    afterEach(function () {
      Object.prototype.toString = toString;
    });
  });

  describe('isPromise()', function () {
    it('should return true if the value is Promise-ish', function () {
      expect(
        utils.isPromise({
          then: function () {}
        }),
        'to be',
        true
      );
    });

    it('should return false if the value is not an object', function () {
      expect(utils.isPromise(1), 'to be', false);
    });

    it('should return false if the value is an object w/o a "then" function', function () {
      expect(utils.isPromise({}), 'to be', false);
    });

    it('should return false if the object is null', function () {
      expect(utils.isPromise(null), 'to be', false);
    });
  });

  describe('escape()', function () {
    it('replaces the usual xml suspects', function () {
      expect(utils.escape('<a<bc<d<'), 'to be', '&#x3C;a&#x3C;bc&#x3C;d&#x3C;');
      expect(utils.escape('>a>bc>d>'), 'to be', '&#x3E;a&#x3E;bc&#x3E;d&#x3E;');
      expect(utils.escape('"a"bc"d"'), 'to be', '&#x22;a&#x22;bc&#x22;d&#x22;');
      expect(utils.escape('<>"&'), 'to be', '&#x3C;&#x3E;&#x22;&#x26;');

      expect(utils.escape('&a&bc&d&'), 'to be', '&#x26;a&#x26;bc&#x26;d&#x26;');
      expect(utils.escape('&amp;&lt;'), 'to be', '&#x26;amp;&#x26;lt;');
    });

    it('replaces invalid xml characters', function () {
      expect(
        utils.escape('\x1B[32mfoo\x1B[0m'),
        'to be',
        '&#x1B;[32mfoo&#x1B;[0m'
      );
      // Ensure we can handle non-trivial unicode characters as well
      expect(utils.escape('💩'), 'to be', '&#x1F4A9;');
    });
  });

  describe('createMap()', function () {
    it('should return an object with a null prototype', function () {
      expect(Object.getPrototypeOf(utils.createMap()), 'to be', null);
    });

    it('should add props to the object', function () {
      expect(utils.createMap({foo: 'bar'}), 'to exhaustively satisfy', {
        foo: 'bar'
      });
    });

    it('should add props from all object parameters to the object', function () {
      expect(
        utils.createMap({foo: 'bar'}, {bar: 'baz'}),
        'to exhaustively satisfy',
        {foo: 'bar', bar: 'baz'}
      );
    });
  });

  describe('slug()', function () {
    it('should convert the string to lowercase', function () {
      expect(utils.slug('FOO'), 'to be', 'foo');
    });

    it('should convert whitespace to dashes', function () {
      expect(
        utils.slug('peanut butter\nand\tjelly'),
        'to be',
        'peanut-butter-and-jelly'
      );
    });

    it('should strip non-alphanumeric and non-dash characters', function () {
      expect(utils.slug('murder-hornets!!'), 'to be', 'murder-hornets');
    });

    it('should disallow consecutive dashes', function () {
      expect(utils.slug('poppies & fritz'), 'to be', 'poppies-fritz');
    });
  });

  describe('castArray()', function () {
    describe('when provided an array value', function () {
      it('should return a copy of the array', function () {
        const v = ['foo', 'bar', 'baz'];
        expect(utils.castArray(v), 'to equal', ['foo', 'bar', 'baz']).and(
          'not to be',
          v
        );
      });
    });

    describe('when provided an "arguments" value', function () {
      it('should return an array containing the arguments', function () {
        (function () {
          expect(utils.castArray(arguments), 'to equal', [
            'foo',
            'bar',
            'baz'
          ]).and('not to be', arguments);
        })('foo', 'bar', 'baz');
      });
    });

    describe('when provided an object', function () {
      it('should return an array containing the object only', function () {
        const v = {foo: 'bar'};
        expect(utils.castArray(v), 'to equal', [v]);
      });
    });

    describe('when provided no parameters', function () {
      it('should return an empty array', function () {
        expect(utils.castArray(), 'to equal', []);
      });
    });

    describe('when provided a primitive value', function () {
      it('should return an array containing the primitive value only', function () {
        expect(utils.castArray('butts'), 'to equal', ['butts']);
      });
    });

    describe('when provided null', function () {
      it('should return an array containing a null value only', function () {
        expect(utils.castArray(null), 'to equal', [null]);
      });
    });
  });

  describe('uniqueID()', function () {
    it('should return a non-empty string', function () {
      expect(utils.uniqueID(), 'to be a string').and('not to be empty');
    });
    it('should have length of 21', function () {
      expect(utils.uniqueID().length, 'to equal', 21);
    });
  });
});
