/* global describe, it, expect, require, beforeEach, afterEach */

var runArrayTests = function (it) {
  'use strict';

  var Sym = typeof Symbol === 'undefined' ? {} : Symbol;
  var isSymbol = function (sym) {
    return typeof Sym === 'function' && typeof sym === 'symbol';
  };
  var functionsHaveNames = (function foo() {}).name === 'foo';
  var ifFunctionsHaveNamesIt = functionsHaveNames ? it : it.skip;
  var ifSymbolIteratorIt = isSymbol(Sym.iterator) ? it : it.skip;
  var ifSymbolIteratorAndArrayValuesIt = isSymbol(Sym.iterator) && Array.prototype.values ? it : it.skip;
  var ifSymbolUnscopablesIt = isSymbol(Sym.unscopables) ? it : it.skip;
  var ifShimIt = (typeof process !== 'undefined' && process.env.NO_ES6_SHIM) ? it.skip : it;
  var ifSupportsDescriptorsIt = Object.getOwnPropertyDescriptor ? it : it.skip;

  var isNegativeZero = function (x) {
    return (1 / x) < 0;
  };

  describe('Array', function () {
    var list = [5, 10, 15, 20];

    ifShimIt('is on the exported object', function () {
      var exported = require('../');
      expect(exported.Array).to.equal(Array);
    });

    describe('@@iterator', function () {
      ifSymbolIteratorIt('uses Symbol.iterator if available', function () {
        var b = {};
        var c = {};
        var a = [b, c];
        var iteratorFn = a[Sym.iterator];
        var iterator = iteratorFn.call(a);
        expect(iterator.next()).to.eql({ done: false, value: b });
        expect(iterator.next()).to.eql({ done: false, value: c });
        expect(iterator.next()).to.eql({ done: true, value: undefined });
      });

      ifSymbolIteratorAndArrayValuesIt('has the right default iteration function', function () {
        // fixed in Webkit https://bugs.webkit.org/show_bug.cgi?id=143838
        expect(Array.prototype).to.have.property(Sym.iterator, Array.prototype.values);
      });
    });

    describe('.from()', function () {
      if (!Array.hasOwnProperty('from')) {
        return it('exists', function () {
          expect(Array).to.have.property('from');
        });
      }

      ifFunctionsHaveNamesIt('has the correct name', function () {
        expect(Array.from).to.have.property('name', 'from');
      });

      it('has the right arity', function () {
        expect(Array.from).to.have.property('length', 1);
      });

      it('is not enumerable', function () {
        expect(Array).ownPropertyDescriptor('from').to.have.property('enumerable', false);
      });

      it('works with primitives', function () {
        expect(Array.from(false)).to.eql([]);
        expect(Array.from(true)).to.eql([]);
        expect(Array.from(-Infinity)).to.eql([]);
        expect(Array.from(-0)).to.eql([]);
        expect(Array.from(0)).to.eql([]);
        expect(Array.from(1)).to.eql([]);
        expect(Array.from(Infinity)).to.eql([]);
      });

      it('should create correct array from iterable', function () {
        (function () {
          expect(Array.from(arguments)).to.eql([0, 1, 2]);
        }(0, 1, 2));

        expect(Array.from([null, undefined, 0.1248, -0, 0])).to.eql(
          [null, undefined, 0.1248, -0, 0]
        );

        if (Array.prototype.values) {
          expect(Array.from([null, undefined, 0.1248, -0, 0].values())).to.eql(
            [null, undefined, 0.1248, -0, 0]
          );
        }
      });

      it('works with arraylike objects', function () {
        expect(Array.from({ length: 1 })).to.eql([undefined]);
        expect(Array.from({ 0: 'a', 1: 'b', length: 2 })).to.eql(['a', 'b']);
      });

      it('swallows negative lengths', function () {
        expect(Array.from({ length: -1 })).to.have.property('length', 0);
        expect(Array.from({ length: -Infinity })).to.have.property('length', 0);
        expect(Array.from({ length: -0 })).to.have.property('length', 0);
        expect(Array.from({ length: -42 })).to.have.property('length', 0);
      });

      it('works with strings', function () {
        expect(Array.from('')).to.eql([]);
        expect(Array.from('abc')).to.eql('abc'.split(''));
      });

      it('should handle empty iterables correctly', function () {
        (function () {
          expect(Array.from(arguments)).to.eql([]);
        }());
        expect(Array.from([])).to.eql([]);
        expect(Array.from({})).to.eql([]);
        expect(Array.from({ a: 1 })).to.eql([]);
      });

      it('should work with other constructors', function () {
        var Foo = function FooBar(length, args) {
          this.args = args;
          this.length = length;
        };
        var args = ['a', 'b', 'c'];
        var expected = new Foo(args.length);
        args.forEach(function (arg, index) {
          expected[index] = arg;
        });
        expect(Array.from.call(Foo, args)).to.be.an.instanceOf(Foo);
        expect(Array.from.call(Foo, args)).to.eql(expected);
      });

      describe('map functions', function () {
        it('supports a map function', function () {
          var original = [1, 2, 3];
          var mapper = function (item) {
            return item * 2;
          };
          var mapped = Array.from(original, mapper);
          expect(mapped).to.eql([2, 4, 6]);
        });

        it('passes both (and only) the item and the current index to the map function', function () {
          var original = [1, 2, 3];
          var expectedItems = [1, 2, 3];
          var expectedIndices = [0, 1, 2];

          var actualItems = [];
          var actualIndices = [];
          var mapper = function (item, index) {
            actualItems.push(item);
            actualIndices.push(index);
            expect(arguments).to.have.property('length', 2);
            return item;
          };

          var mapped = Array.from(original, mapper);
          expect(mapped).to.eql(expectedItems);
          expect(actualItems).to.eql(expectedItems);
          expect(actualIndices).to.eql(expectedIndices);
        });

        it('passes both the item and the current index to the map function with a "this" value', function () {
          var original = [1, 2, 3];
          var expectedItems = [1, 2, 3];
          var expectedIndices = [0, 1, 2];
          var expectedContext = {};

          var actualItems = [];
          var actualIndices = [];
          var mapper = function (item, index) {
            actualItems.push(item);
            actualIndices.push(index);
            expect(arguments).to.have.property('length', 2);
            expect(this).to.eql(expectedContext);
            return item;
          };

          var mapped = Array.from(original, mapper, expectedContext);
          expect(mapped).to.eql(expectedItems);
          expect(actualItems).to.eql(expectedItems);
          expect(actualIndices).to.eql(expectedIndices);
        });

        it('accepts an object thisArg', function () {
          var context = {};
          Array.from([1, 2, 3], function () {
            expect(this).to.equal(context);
          }, context);
        });

        it('accepts a primitive thisArg', function () {
          Array.from([1, 2, 3], function () {
            expect(this.valueOf()).to.equal(42);
            expect(Object.prototype.toString.call(this)).to.equal('[object Number]');
          }, 42);
        });

        it('accepts a falsy thisArg', function () {
          Array.from([1, 2, 3], function () {
            expect(this.valueOf()).to.equal(false);
            expect(Object.prototype.toString.call(this)).to.equal('[object Boolean]');
          }, false);
        });
      });

      it('does not throw when provided an undefined second arg', function () {
        expect(Array.from([], undefined)).to.eql([]);
      });

      it('throws when provided a nonfunction second arg', function () {
        expect(function () { Array.from([], null); }).to['throw'](TypeError);
        expect(function () { Array.from([], false); }).to['throw'](TypeError);
        expect(function () { Array.from([], true); }).to['throw'](TypeError);
        expect(function () { Array.from([], /a/g); }).to['throw'](TypeError);
        expect(function () { Array.from([], {}); }).to['throw'](TypeError);
        expect(function () { Array.from([], []); }).to['throw'](TypeError);
        expect(function () { Array.from([], ''); }).to['throw'](TypeError);
        expect(function () { Array.from([], 3); }).to['throw'](TypeError);
      });

      it('supports a this arg', function () {
        var original = [1, 2, 3];
        var context = {};
        var mapper = function (item) {
          expect(this).to.equal(context);
          return item * 2;
        };
        var mapped = Array.from(original, mapper, context);
        expect(mapped).to.eql([2, 4, 6]);
      });

      it('throws when provided null or undefined', function () {
        expect(function () { Array.from(); }).to['throw'](TypeError);
        expect(function () { Array.from(undefined); }).to['throw'](TypeError);
        expect(function () { Array.from(null); }).to['throw'](TypeError);
      });

      it('removes holes', function () {
        /*jshint elision: true */
        /* jscs:disable disallowSpaceBeforeComma */
        /* jscs:disable requireSpaceAfterComma */
        /* eslint-disable no-sparse-arrays */
        var input = [0, , 2];
        var result = Array.from([0, , 2]);
        /* eslint-enable no-sparse-arrays */
        /* jscs:enable requireSpaceAfterComma */
        /* jscs:enable disallowSpaceBeforeComma */
        /*jshint elision: false */
        expect(1 in input).to.equal(false);
        expect(1 in result).to.equal(true);
        expect(result).to.eql([0, undefined, 2]);
      });

      it('works with this flaky example', function () {
        expect(Array.from([1, NaN, false])).to.eql([1, NaN, false]);
      });

      ifSupportsDescriptorsIt('works when Object.prototype has a throwing setter', function () {
        var key = 10;
        /* eslint no-extend-native: 0 */
        Object.defineProperty(Object.prototype, key, {
          configurable: true,
          get: function () {},
          set: function (v) { throw new EvalError('boom'); }
        });
        expect(function () { var arr = []; arr[key] = 42; }).to['throw'](EvalError); // assert thrower

        expect(function () { Array.from({ length: key + 1 }); }).not.to['throw']();

        delete Object.prototype[key];
        expect(key in Object.prototype).to.equal(false); // assert cleanup
      });
    });

    describe('.of()', function () {
      if (!Array.hasOwnProperty('of')) {
        return it('exists', function () {
          expect(Array).to.have.property('of');
        });
      }

      ifFunctionsHaveNamesIt('has the correct name', function () {
        expect(Array.of).to.have.property('name', 'of');
      });

      it('has the right arity', function () {
        expect(Array.of).to.have.property('length', 0);
      });

      it('is not enumerable', function () {
        expect(Array).ownPropertyDescriptor('of').to.have.property('enumerable', false);
      });

      it('should create correct array from arguments', function () {
        expect(Array.of(1, null, undefined)).to.eql([1, null, undefined]);
      });

      it('should work with other constructors', function () {
        var Foo = function FooBar(length) {
          this.args = Array.prototype.slice.call(arguments, 1);
          this.length = length;
        };
        var args = ['a', 'b', 'c'];
        var expected = new Foo(args.length);
        args.forEach(function (arg, index) {
          expected[index] = arg;
        });
        expect(Array.of.apply(Foo, args)).to.be.an.instanceOf(Foo);
        expect(Array.of.apply(Foo, args)).to.eql(expected);
      });

      describe('without Array.from', function () {
        var originalFrom = Array.from;
        beforeEach(function () {
          Array.from = 42;
        });

        afterEach(function () {
          Array.from = originalFrom;
        });

        it('still works', function () {
          expect(Array.of(1, 2, 3)).to.eql([1, 2, 3]);
        });
      });
    });

    describe('#copyWithin()', function () {
      if (!Array.prototype.hasOwnProperty('copyWithin')) {
        return it('exists', function () {
          expect(Array.prototype).to.have.property('copyWithin');
        });
      }

      ifFunctionsHaveNamesIt('has the correct name', function () {
        expect(Array.prototype.copyWithin).to.have.property('name', 'copyWithin');
      });

      it('has the right arity', function () {
        expect(Array.prototype.copyWithin).to.have.property('length', 2);
      });

      it('is not enumerable', function () {
        expect(Array.prototype).ownPropertyDescriptor('copyWithin').to.have.property('enumerable', false);
      });

      it('modifies the object in-place', function () {
        var arr = [1, 2, 3, 4, 5];
        expect(arr.copyWithin(0, 3)).to.eql([4, 5, 3, 4, 5]);
        expect(arr).to.eql([4, 5, 3, 4, 5]);
      });

      it('works with 2 args', function () {
        expect([1, 2, 3, 4, 5].copyWithin(0, 3)).to.eql([4, 5, 3, 4, 5]);
        expect([1, 2, 3, 4, 5].copyWithin(1, 3)).to.eql([1, 4, 5, 4, 5]);
        expect([1, 2, 3, 4, 5].copyWithin(1, 2)).to.eql([1, 3, 4, 5, 5]);
        expect([1, 2, 3, 4, 5].copyWithin(2, 2)).to.eql([1, 2, 3, 4, 5]);
      });

      it('works with 3 args', function () {
        expect([1, 2, 3, 4, 5].copyWithin(0, 3, 4)).to.eql([4, 2, 3, 4, 5]);
        expect([1, 2, 3, 4, 5].copyWithin(1, 3, 4)).to.eql([1, 4, 3, 4, 5]);
        expect([1, 2, 3, 4, 5].copyWithin(1, 2, 4)).to.eql([1, 3, 4, 4, 5]);
      });

      it('works with negative args', function () {
        expect([1, 2, 3, 4, 5].copyWithin(0, -2)).to.eql([4, 5, 3, 4, 5]);
        expect([1, 2, 3, 4, 5].copyWithin(0, -2, -1)).to.eql([4, 2, 3, 4, 5]);
        expect([1, 2, 3, 4, 5].copyWithin(-4, -3, -2)).to.eql([1, 3, 3, 4, 5]);
        expect([1, 2, 3, 4, 5].copyWithin(-4, -3, -1)).to.eql([1, 3, 4, 4, 5]);
        expect([1, 2, 3, 4, 5].copyWithin(-4, -3)).to.eql([1, 3, 4, 5, 5]);
      });

      it('works with arraylike objects', function () {
        var args = (function () { return arguments; }(1, 2, 3));
        expect(Array.isArray(args)).to.equal(false);
        var argsClass = Object.prototype.toString.call(args);
        expect(Array.prototype.slice.call(args)).to.eql([1, 2, 3]);
        Array.prototype.copyWithin.call(args, -2, 0);
        expect(Array.prototype.slice.call(args)).to.eql([1, 1, 2]);
        expect(Object.prototype.toString.call(args)).to.equal(argsClass);
      });

      ifSymbolUnscopablesIt('should be unscopable if Symbols exist', function () {
        var unscopables = Array.prototype[Sym.unscopables];
        expect(!!unscopables).to.equal(true);
        expect(unscopables.copyWithin).to.equal(true);
      });

      it('should delete the target key if the source key is not present', function () {
        /* jshint elision: true */
        /* jscs:disable disallowSpaceBeforeComma */
        /* jscs:disable requireSpaceAfterComma */
        /* eslint-disable no-sparse-arrays */
        expect([, 1, 2].copyWithin(1, 0)).to.eql([, , 1]);
        /* jshint elision: false */
        /* jscs:enable requireSpaceAfterComma */
        /* jscs:enable disallowSpaceBeforeComma */
        /* eslint-enable no-sparse-arrays */
      });

      it('should check inherited properties as well', function () {
        var Parent = function Parent() {};
        Parent.prototype[0] = 'foo';
        var sparse = new Parent();
        sparse[1] = 1;
        sparse[2] = 2;
        sparse.length = 3;
        var result = Array.prototype.copyWithin.call(sparse, 1, 0);
        expect(result).to.have.property('0');
        expect(result).not.to.have.ownProperty('0');
        expect(result).to.have.ownProperty('1');
        expect(result).to.eql({ 0: 'foo', 1: 'foo', 2: 1, length: 3 });
      });
    });

    describe('#find()', function () {
      if (!Array.prototype.hasOwnProperty('find')) {
        return it('exists', function () {
          expect(Array.prototype).to.have.property('find');
        });
      }

      ifFunctionsHaveNamesIt('has the correct name', function () {
        expect(Array.prototype.find).to.have.property('name', 'find');
      });

      it('should have the right arity', function () {
        expect(Array.prototype.find).to.have.property('length', 1);
      });

      it('is not enumerable', function () {
        expect(Array.prototype).ownPropertyDescriptor('find').to.have.property('enumerable', false);
      });

      it('should find item by predicate', function () {
        var result = list.find(function (item) { return item === 15; });
        expect(result).to.equal(15);
      });

      it('should return undefined when nothing matched', function () {
        var result = list.find(function (item) { return item === 'a'; });
        expect(result).to.equal(undefined);
      });

      it('should throw TypeError when function was not passed', function () {
        expect(function () { list.find(); }).to['throw'](TypeError);
      });

      it('should receive all three parameters', function () {
        var foundIndex = list.find(function (value, index, arr) {
          expect(list).to.have.property(index, value);
          expect(list).to.eql(arr);
          return false;
        });
        expect(foundIndex).to.equal(undefined);
      });

      it('should work with the context argument', function () {
        var context = {};
        [1].find(function () { expect(this).to.equal(context); }, context);
      });

      it('should work with an array-like object', function () {
        var obj = { 0: 1, 1: 2, 2: 3, length: 3 };
        var found = Array.prototype.find.call(obj, function (item) { return item === 2; });
        expect(found).to.equal(2);
      });

      it('should work with an array-like object with negative length', function () {
        var obj = { 0: 1, 1: 2, 2: 3, length: -3 };
        var found = Array.prototype.find.call(obj, function () {
          throw new Error('should not reach here');
        });
        expect(found).to.equal(undefined);
      });

      it('should work with a sparse array', function () {
        /*jshint elision: true */
        /* jscs:disable disallowSpaceBeforeComma */
        /* jscs:disable requireSpaceAfterComma */
        /* eslint-disable no-sparse-arrays */
        var obj = [1, , undefined];
        /* eslint-enable no-sparse-arrays */
        /* jscs:enable requireSpaceAfterComma */
        /* jscs:enable disallowSpaceBeforeComma */
        /*jshint elision: false */
        expect(1 in obj).to.equal(false);
        var seen = [];
        var found = obj.find(function (item, idx) {
          seen.push([idx, item]);
          return false;
        });
        expect(found).to.equal(undefined);
        expect(seen).to.eql([[0, 1], [1, undefined], [2, undefined]]);
      });

      it('should work with a sparse array-like object', function () {
        var obj = { 0: 1, 2: undefined, length: 3.2 };
        var seen = [];
        var found = Array.prototype.find.call(obj, function (item, idx) {
          seen.push([idx, item]);
          return false;
        });
        expect(found).to.equal(undefined);
        expect(seen).to.eql([[0, 1], [1, undefined], [2, undefined]]);
      });

      ifSymbolUnscopablesIt('should be unscopable if Symbols exist', function () {
        var unscopables = Array.prototype[Sym.unscopables];
        expect(!!unscopables).to.equal(true);
        expect(unscopables.find).to.equal(true);
      });
    });

    describe('#findIndex()', function () {
      if (!Array.prototype.hasOwnProperty('findIndex')) {
        return it('exists', function () {
          expect(Array.prototype).to.have.property('findIndex');
        });
      }

      ifFunctionsHaveNamesIt('has the correct name', function () {
        expect(Array.prototype.findIndex).to.have.property('name', 'findIndex');
      });

      it('should have the right arity', function () {
        expect(Array.prototype.findIndex).to.have.property('length', 1);
      });

      it('is not enumerable', function () {
        expect(Array.prototype).ownPropertyDescriptor('findIndex').to.have.property('enumerable', false);
      });

      it('should find item key by predicate', function () {
        var result = list.findIndex(function (item) { return item === 15; });
        expect(result).to.equal(2);
      });

      it('should return -1 when nothing matched', function () {
        var result = list.findIndex(function (item) { return item === 'a'; });
        expect(result).to.equal(-1);
      });

      it('should throw TypeError when function was not passed', function () {
        expect(function () { list.findIndex(); }).to['throw'](TypeError);
      });

      it('should receive all three parameters', function () {
        var foundIndex = list.findIndex(function (value, index, arr) {
          expect(list[index]).to.equal(value);
          expect(list).to.eql(arr);
          return false;
        });
        expect(foundIndex).to.equal(-1);
      });

      it('should work with the context argument', function () {
        var context = {};
        [1].findIndex(function () { expect(this).to.equal(context); }, context);
      });

      it('should work with an array-like object', function () {
        var obj = { 0: 1, 1: 2, 2: 3, length: 3 };
        var foundIndex = Array.prototype.findIndex.call(obj, function (item) { return item === 2; });
        expect(foundIndex).to.equal(1);
      });

      it('should work with an array-like object with negative length', function () {
        var obj = { 0: 1, 1: 2, 2: 3, length: -3 };
        var foundIndex = Array.prototype.findIndex.call(obj, function () {
          throw new Error('should not reach here');
        });
        expect(foundIndex).to.equal(-1);
      });

      it('should work with a sparse array', function () {
        /*jshint elision: true */
        /* jscs:disable disallowSpaceBeforeComma */
        /* jscs:disable requireSpaceAfterComma */
        /* eslint-disable no-sparse-arrays */
        var obj = [1, , undefined];
        /* eslint-enable no-sparse-arrays */
        /* jscs:enable requireSpaceAfterComma */
        /* jscs:enable disallowSpaceBeforeComma */
        /*jshint elision: false */
        expect(1 in obj).to.equal(false);
        var seen = [];
        var foundIndex = obj.findIndex(function (item, idx) {
          seen.push([idx, item]);
          return item === undefined && idx === 2;
        });
        expect(foundIndex).to.equal(2);
        expect(seen).to.eql([[0, 1], [1, undefined], [2, undefined]]);
      });

      it('should work with a sparse array-like object', function () {
        var obj = { 0: 1, 2: undefined, length: 3.2 };
        var seen = [];
        var foundIndex = Array.prototype.findIndex.call(obj, function (item, idx) {
          seen.push([idx, item]);
          return false;
        });
        expect(foundIndex).to.equal(-1);
        expect(seen).to.eql([[0, 1], [1, undefined], [2, undefined]]);
      });

      ifSymbolUnscopablesIt('should be unscopable if Symbols exist', function () {
        var unscopables = Array.prototype[Sym.unscopables];
        expect(!!unscopables).to.equal(true);
        expect(unscopables.findIndex).to.equal(true);
      });
    });

    describe('ArrayIterator', function () {
      if (!Array.prototype.hasOwnProperty('keys')) {
        return it('can be tested', function () {
          expect(Array.prototype).to.have.property('keys');
        });
      }

      var arrayIterator;
      beforeEach(function () {
        arrayIterator = [1, 2, 3].keys();
      });

      describe('#next()', function () {
        it('should work when applied to an ArrayIterator', function () {
          expect(arrayIterator.next.apply(arrayIterator)).to.eql({ value: 0, done: false });
          expect(arrayIterator.next.apply(arrayIterator)).to.eql({ value: 1, done: false });
          expect(arrayIterator.next.apply(arrayIterator)).to.eql({ value: 2, done: false });
          expect(arrayIterator.next.apply(arrayIterator)).to.eql({ value: undefined, done: true });
        });

        it('throws when not applied to an ArrayIterator', function () {
          expect(function () { arrayIterator.next.apply({}); }).to['throw'](TypeError);
        });
      });
    });

    describe('#keys()', function () {
      if (!Array.prototype.hasOwnProperty('keys')) {
        return it('exists', function () {
          expect(Array.prototype).to.have.property('keys');
        });
      }

      ifFunctionsHaveNamesIt('has the correct name', function () {
        expect(Array.prototype.keys).to.have.property('name', 'keys');
      });

      it('should have the right arity ', function () {
        expect(Array.prototype.keys.length).to.equal(0);
      });

      it('is not enumerable', function () {
        expect(Array.prototype).ownPropertyDescriptor('keys').to.have.property('enumerable', false);
      });

      describe('basic keys iteration', function () {
        var mylist = [5, 10, 15, 20];
        var keys;
        beforeEach(function () {
          if (!keys) {
            keys = mylist.keys();
          }
        });

        it('should return 0 on first object', function () {
          expect(keys.next()).to.eql({ value: 0, done: false });
        });

        it('should return 1 on second object', function () {
          expect(keys.next()).to.eql({ value: 1, done: false });
        });

        it('should return 2 on third object', function () {
          expect(keys.next()).to.eql({ value: 2, done: false });
        });

        it('should return 3 on fourth object', function () {
          expect(keys.next()).to.eql({ value: 3, done: false });
        });

        it('should set done on completing iteration', function () {
          expect(keys.next()).to.eql({ value: undefined, done: true });
        });

        it('once done it should stay done', function () {
          mylist.push(4);
          expect(keys.next()).to.eql({ value: undefined, done: true });
        });
      });

      it('should not skip sparse keys', function () {
        var sparse = [1];
        sparse[2] = 3;
        var keys = sparse.keys();
        expect(keys.next()).to.eql({ value: 0, done: false });
        expect(keys.next()).to.eql({ value: 1, done: false });
        expect(keys.next()).to.eql({ value: 2, done: false });
        expect(keys.next()).to.eql({ value: undefined, done: true });
      });

      ifSymbolUnscopablesIt('should be unscopable if Symbols exist', function () {
        var unscopables = Array.prototype[Sym.unscopables];
        expect(!!unscopables).to.equal(true);
        expect(unscopables.keys).to.equal(true);
      });
    });

    describe('#values()', function () {
      if (!Array.prototype.hasOwnProperty('values')) {
        return it('exists', function () {
          expect(Array.prototype).to.have.property('values');
        });
      }

      ifFunctionsHaveNamesIt('has the correct name', function () {
        expect(Array.prototype.values).to.have.property('name', 'values');
      });

      it('has the right arity', function () {
        expect(Array.prototype.values).to.have.property('length', 0);
      });

      it('is not enumerable', function () {
        expect(Array.prototype).ownPropertyDescriptor('values').to.have.property('enumerable', false);
      });

      describe('basic list iteration', function () {
        var mylist = [5, 10, 15, 20];
        var values;
        beforeEach(function () {
          if (!values) {
            values = mylist.values();
          }
        });

        it('should return 5 on first object', function () {
          expect(values.next()).to.eql({ value: 5, done: false });
        });

        it('should return 10 on second object', function () {
          expect(values.next()).to.eql({ value: 10, done: false });
        });

        it('should return 15 on third object', function () {
          expect(values.next()).to.eql({ value: 15, done: false });
        });

        it('should return 20 on fourth object', function () {
          expect(values.next()).to.eql({ value: 20, done: false });
        });

        it('should set done on completing iteration', function () {
          expect(values.next()).to.eql({ value: undefined, done: true });
        });

        it('once done it should stay done', function () {
          mylist.push(4);
          expect(values.next()).to.eql({ value: undefined, done: true });
        });
      });

      it('should not skip sparse values', function () {
        var sparse = [1];
        sparse[2] = 3;
        var values = sparse.values();
        expect(values.next()).to.eql({ value: 1, done: false });
        expect(values.next()).to.eql({ value: undefined, done: false });
        expect(values.next()).to.eql({ value: 3, done: false });
        expect(values.next()).to.eql({ value: undefined, done: true });
      });

      ifSymbolUnscopablesIt('should be unscopable if Symbols exist', function () {
        var unscopables = Array.prototype[Sym.unscopables];
        expect(!!unscopables).to.equal(true);
        expect(unscopables.values).to.equal(true);
      });
    });

    describe('#entries()', function () {
      if (!Array.prototype.hasOwnProperty('entries')) {
        return it('exists', function () {
          expect(Array.prototype).to.have.property('entries');
        });
      }

      ifFunctionsHaveNamesIt('has the correct name', function () {
        expect(Array.prototype.entries).to.have.property('name', 'entries');
      });

      it('has the right arity', function () {
        expect(Array.prototype.entries).to.have.property('length', 0);
      });

      it('is not enumerable', function () {
        expect(Array.prototype).ownPropertyDescriptor('entries').to.have.property('enumerable', false);
      });

      describe('basic list iteration', function () {
        var mylist = [5, 10, 15, 20];
        var entries;
        beforeEach(function () {
          if (!entries) {
            entries = mylist.entries();
          }
        });

        it('should return [0, 5] on first object', function () {
          var val = entries.next();
          expect(val).to.eql({ value: [0, 5], done: false });
        });

        it('should return [1, 10] on second object', function () {
          var val = entries.next();
          expect(val).to.eql({ value: [1, 10], done: false });
        });

        it('should return [2, 15] on third object', function () {
          var val = entries.next();
          expect(val).to.eql({ value: [2, 15], done: false });
        });

        it('should return [3, 20] on fourth object', function () {
          var val = entries.next();
          expect(val).to.eql({ value: [3, 20], done: false });
        });

        it('should set done on completing iteration', function () {
          var val = entries.next();
          expect(val).to.eql({ value: undefined, done: true });
        });

        it('once done it should stay done', function () {
          mylist.push(4);
          var val = entries.next();
          expect(val).to.eql({ value: undefined, done: true });
        });
      });

      it('should not skip sparse entries', function () {
        var sparse = [1];
        sparse[2] = 3;
        var entries = sparse.entries();
        expect(entries.next()).to.eql({ value: [0, 1], done: false });
        expect(entries.next()).to.eql({ value: [1, undefined], done: false });
        expect(entries.next()).to.eql({ value: [2, 3], done: false });
        expect(entries.next()).to.eql({ value: undefined, done: true });
      });

      ifSymbolUnscopablesIt('should be unscopable if Symbols exist', function () {
        var unscopables = Array.prototype[Sym.unscopables];
        expect(!!unscopables).to.equal(true);
        expect(unscopables.entries).to.equal(true);
      });
    });

    describe('#fill()', function () {
      if (!Array.prototype.hasOwnProperty('fill')) {
        return it('exists', function () {
          expect(Array.prototype).to.have.property('fill');
        });
      }

      ifFunctionsHaveNamesIt('has the correct name', function () {
        expect(Array.prototype.fill).to.have.property('name', 'fill');
      });

      it('has the right arity', function () {
        expect(Array.prototype.fill).to.have.property('length', 1);
      });

      it('is not enumerable', function () {
        expect(Array.prototype).ownPropertyDescriptor('fill').to.have.property('enumerable', false);
      });

      it('works with just a value', function () {
        var original = [1, 2, 3, 4, 5, 6];
        var filled = [-1, -1, -1, -1, -1, -1];

        expect(original.fill(-1)).to.eql(filled);
      });

      it('accepts a positive start index', function () {
        var original = [1, 2, 3, 4, 5, 6];
        var filled = [1, 2, 3, -1, -1, -1];

        expect(original.fill(-1, 3)).to.eql(filled);
      });

      it('accepts a negative start index', function () {
        var original = [1, 2, 3, 4, 5, 6];
        var filled = [1, 2, 3, -1, -1, -1];

        expect(original.fill(-1, -3)).to.eql(filled);
      });

      it('accepts a negative end index', function () {
        var original = [1, 2, 3];
        var filled = [4, 2, 3];

        expect(original.fill(4, -3, -2)).to.eql(filled);
      });

      it('accepts a large start index', function () {
        var original = [1, 2, 3, 4, 5, 6];
        var filled = [1, 2, 3, 4, 5, 6];

        expect(original.fill(-1, 9)).to.eql(filled);
      });

      ifSymbolUnscopablesIt('should be unscopable if Symbols exist', function () {
        var unscopables = Array.prototype[Sym.unscopables];
        expect(!!unscopables).to.equal(true);
        expect(unscopables.fill).to.equal(true);
      });
    });

    // this length converts to "1", preventing a crazy crash in older FF
    var negativeLength = { 0: 1, length: -Math.pow(2, 32) + 1 };
    var throwRangeError = function (v, i) {
      if (i === negativeLength.length >>> 0) {
        return v;
      }
      // note: in nonconforming browsers, this will be called
      // -1 >>> 0 times, which is 4294967295, so the throw matters.
      throw new RangeError('should not reach here: length of -1 should clamp to length of 0');
    };
    var throwRangeErrorReduce = function throwRangeErrorReduce(acc, v, i) {
      return throwRangeErrorReduce(v, i);
    };

    describe('#forEach()', function () {
      it('uses ToLength to clamp negative values to zero', function () {
        expect(function () {
          Array.prototype.forEach.call(negativeLength, throwRangeError);
        }).not.to['throw']();
      });
    });

    describe('#map()', function () {
      it('uses ToLength to clamp negative values to zero', function () {
        var mapped;
        expect(function () {
          mapped = Array.prototype.map.call(negativeLength, throwRangeError);
        }).not.to['throw']();
        expect(mapped).to.eql([]);
      });
    });

    describe('#filter()', function () {
      it('uses ToLength to clamp negative values to zero', function () {
        var filtered;
        expect(function () {
          filtered = Array.prototype.filter.call(negativeLength, throwRangeError);
        }).not.to['throw']();
        expect(filtered).to.eql([]);
      });
    });

    describe('#some()', function () {
      it('uses ToLength to clamp negative values to zero', function () {
        var result;
        expect(function () {
          result = Array.prototype.some.call(negativeLength, throwRangeError);
        }).not.to['throw']();
        expect(result).to.equal([].some(Object));
      });
    });

    describe('#every()', function () {
      it('uses ToLength to clamp negative values to zero', function () {
        var result;
        expect(function () {
          result = Array.prototype.every.call(negativeLength, throwRangeError);
        }).not.to['throw']();
        expect(result).to.equal([].every(Object));
      });
    });

    describe('#reduce()', function () {
      it('uses ToLength to clamp negative values to zero', function () {
        var accumulator = {};
        var reduced;
        expect(function () {
          reduced = Array.prototype.reduce.call(negativeLength, throwRangeErrorReduce, accumulator);
        }).not.to['throw']();
        expect(reduced).to.equal(accumulator);
      });
    });

    describe('#reduceRight()', function () {
      it('uses ToLength to clamp negative values to zero', function () {
        var negativeOneToUint32minusOne = (-1 >>> 0) - 1;
        var obj = { length: -1 };
        obj[negativeOneToUint32minusOne] = true;
        var accumulator = {};
        var reduced;
        expect(function () {
          reduced = Array.prototype.reduceRight.call(obj, throwRangeErrorReduce, accumulator);
        }).not.to['throw']();
        expect(reduced).to.equal(accumulator);
      });
    });

    describe('#indexOf()', function () {
      it('converts second argument from -0 to +0', function () {
        expect(isNegativeZero([true].indexOf(true, -0))).to.equal(false);
      });
    });
  });
};

describe('clean Object.prototype', function () {
  'use strict';

  runArrayTests.call(this, it);
});

describe('polluted Object.prototype', function () {
  'use strict';

  var shimmedIt = function () {
    /* eslint-disable no-extend-native */
    Object.prototype[1] = 42;
    /* eslint-enable no-extend-native */
    it.apply(this, arguments);
    delete Object.prototype[1];
  };
  shimmedIt.skip = it.skip;
  return runArrayTests.call(this, shimmedIt);
});
