/* global describe, xdescribe, it, xit, expect, require, Reflect */

var arePropertyDescriptorsSupported = function () {
  try {
    Object.defineProperty({}, 'x', {});
    return true;
  } catch (e) { /* this is IE 8. */
    return false;
  }
};
var supportsDescriptors = !!Object.defineProperty && arePropertyDescriptorsSupported();
var functionsHaveNames = function f() {}.name === 'f';

var hasSymbols = typeof Symbol === 'function' && typeof Symbol() === 'symbol';
var ifSymbolsIt = hasSymbols ? it : xit;
var describeIfGetProto = Object.getPrototypeOf ? describe : xdescribe;
var describeIfSetProto = Object.setPrototypeOf ? describe : xdescribe;
var describeIfES5 = supportsDescriptors ? describe : xdescribe;
var describeIfExtensionsPreventible = Object.preventExtensions ? describe : xdescribe;
var describeIfGetOwnPropertyNames = Object.getOwnPropertyNames ? describe : xdescribe;
var ifExtensionsPreventibleIt = Object.preventExtensions ? it : xit;
var ifES5It = supportsDescriptors ? it : xit;
var ifFreezeIt = typeof Object.freeze === 'function' ? it : xit;
var ifFunctionsHaveNamesIt = functionsHaveNames ? it : xit;
var ifShimIt = (typeof process !== 'undefined' && process.env.NO_ES6_SHIM) ? it.skip : it;

describe('Reflect', function () {
  if (typeof Reflect === 'undefined') {
    return it('exists', function () {
      expect(this).to.have.property('Reflect');
    });
  }

  var object = {
    something: 1,
    _value: 0
  };

  if (supportsDescriptors) {
    /* eslint-disable accessor-pairs */
    Object.defineProperties(object, {
      value: {
        get: function () {
          return this._value;
        }
      },

      setter: {
        set: function (val) {
          this._value = val;
        }
      },

      bool: {
        value: true
      }
    });
    /* eslint-enable accessor-pairs */
  }

  var testXThrow = function (values, func) {
    var checker = function checker(item) {
      try {
        func(item);
        return false;
      } catch (e) {
        return e instanceof TypeError;
      }
    };

    values.forEach(function (item) {
      expect(item).to.satisfy(checker);
    });
  };

  var testCallableThrow = testXThrow.bind(null, [null, undefined, 1, 'string', true, [], {}]);

  var testPrimitiveThrow = testXThrow.bind(null, [null, undefined, 1, 'string', true]);

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

  describe('.apply()', function () {
    if (typeof Reflect.apply === 'undefined') {
      return it('exists', function () {
        expect(Reflect).to.have.property('apply');
      });
    }

    it('is a function', function () {
      expect(typeof Reflect.apply).to.equal('function');
    });

    ifFunctionsHaveNamesIt('has the right name', function () {
      expect(Reflect.apply.name).to.equal('apply');
    });

    it('throws if target isn’t callable', function () {
      testCallableThrow(function (item) {
        return Reflect.apply(item, null, []);
      });
    });

    it('works also with redefined apply', function () {
      expect(Reflect.apply(Array.prototype.push, [1, 2], [3, 4, 5])).to.equal(5);

      var F = function F(a, b, c) {
        return a + b + c;
      };

      F.apply = false;

      expect(Reflect.apply(F, null, [1, 2, 3])).to.equal(6);

      var G = function G(last) {
        return this.x + 'lo' + last;
      };

      G.apply = function nop() {};

      expect(Reflect.apply(G, { x: 'yel' }, ['!'])).to.equal('yello!');
    });
  });

  describe('.construct()', function () {
    if (typeof Reflect.construct === 'undefined') {
      return it('exists', function () {
        expect(Reflect).to.have.property('construct');
      });
    }

    it('is a function', function () {
      expect(typeof Reflect.construct).to.equal('function');
    });

    ifFunctionsHaveNamesIt('has the right name', function () {
      expect(Reflect.construct.name).to.equal('construct');
    });

    it('throws if target isn’t callable', function () {
      testCallableThrow(function (item) {
        return Reflect.apply(item, null, []);
      });
    });

    it('works also with redefined apply', function () {
      var C = function C(a, b, c) {
        this.qux = [a, b, c].join('|');
      };

      C.apply = undefined;

      expect(Reflect.construct(C, ['foo', 'bar', 'baz']).qux).to.equal('foo|bar|baz');
    });

    it('correctly handles newTarget param', function () {
      var F = function F() {};
      expect(Reflect.construct(function () {}, [], F) instanceof F).to.equal(true);
    });
  });

  describeIfES5('.defineProperty()', function () {
    if (typeof Reflect.defineProperty === 'undefined') {
      return it('exists', function () {
        expect(Reflect).to.have.property('defineProperty');
      });
    }

    it('is a function', function () {
      expect(typeof Reflect.defineProperty).to.equal('function');
    });

    ifFunctionsHaveNamesIt('has the right name', function () {
      expect(Reflect.defineProperty.name).to.equal('defineProperty');
    });

    it('throws if the target isn’t an object', function () {
      testPrimitiveThrow(function (item) {
        return Reflect.defineProperty(item, 'prop', { value: true });
      });
    });

    ifExtensionsPreventibleIt('returns false for non-extensible objects', function () {
      var o = Object.preventExtensions({});

      expect(Reflect.defineProperty(o, 'prop', {})).to.equal(false);
    });

    it('can return true, even for non-configurable, non-writable properties', function () {
      var o = {};
      var desc = {
        value: 13,
        enumerable: false,
        writable: false,
        configurable: false
      };

      expect(Reflect.defineProperty(o, 'prop', desc)).to.equal(true);

      // Defined as non-configurable, but descriptor is identical.
      expect(Reflect.defineProperty(o, 'prop', desc)).to.equal(true);

      desc.value = 37; // Change

      expect(Reflect.defineProperty(o, 'prop', desc)).to.equal(false);
    });

    it('can change from one property type to another, if configurable', function () {
      var o = {};

      var desc1 = {
        set: function () {},
        configurable: true
      };

      var desc2 = {
        value: 13,
        configurable: false
      };

      var desc3 = {
        get: function () {}
      };

      expect(Reflect.defineProperty(o, 'prop', desc1)).to.equal(true);

      expect(Reflect.defineProperty(o, 'prop', desc2)).to.equal(true);

      expect(Reflect.defineProperty(o, 'prop', desc3)).to.equal(false);
    });
  });

  describe('.deleteProperty()', function () {
    if (typeof Reflect.deleteProperty === 'undefined') {
      return it('exists', function () {
        expect(Reflect).to.have.property('deleteProperty');
      });
    }

    it('is a function', function () {
      expect(typeof Reflect.deleteProperty).to.equal('function');
    });

    ifFunctionsHaveNamesIt('has the right name', function () {
      expect(Reflect.deleteProperty.name).to.equal('deleteProperty');
    });

    it('throws if the target isn’t an object', function () {
      testPrimitiveThrow(function (item) {
        return Reflect.deleteProperty(item, 'prop');
      });
    });

    ifES5It('returns true for success and false for failure', function () {
      var o = { a: 1 };

      Object.defineProperty(o, 'b', { value: 2 });

      expect(o).to.have.property('a');
      expect(o).to.have.property('b');
      expect(o.a).to.equal(1);
      expect(o.b).to.equal(2);

      expect(Reflect.deleteProperty(o, 'a')).to.equal(true);

      expect(o).not.to.have.property('a');
      expect(o.b).to.equal(2);

      expect(Reflect.deleteProperty(o, 'b')).to.equal(false);

      expect(o).to.have.property('b');
      expect(o.b).to.equal(2);

      expect(Reflect.deleteProperty(o, 'a')).to.equal(true);
    });

    it('cannot delete an array’s length property', function () {
      expect(Reflect.deleteProperty([], 'length')).to.equal(false);
    });
  });

  describeIfES5('.get()', function () {
    if (typeof Reflect.get === 'undefined') {
      return it('exists', function () {
        expect(Reflect).to.have.property('get');
      });
    }

    it('is a function', function () {
      expect(typeof Reflect.get).to.equal('function');
    });

    ifFunctionsHaveNamesIt('has the right name', function () {
      expect(Reflect.get.name).to.equal('get');
    });

    it('throws on null and undefined', function () {
      [null, undefined].forEach(function (item) {
        expect(function () {
          return Reflect.get(item, 'property');
        }).to['throw'](TypeError);
      });
    });

    it('can retrieve a simple value, from the target', function () {
      var p = { something: 2, bool: false };

      expect(Reflect.get(object, 'something')).to.equal(1);
      // p has no effect
      expect(Reflect.get(object, 'something', p)).to.equal(1);

      // Value-defined properties take the target's value,
      // and ignore that of the receiver.
      expect(Reflect.get(object, 'bool', p)).to.equal(true);

      // Undefined values
      expect(Reflect.get(object, 'undefined_property')).to.equal(undefined);
    });

    it('will invoke getters on the receiver rather than target', function () {
      var other = { _value: 1337 };

      expect(Reflect.get(object, 'value', other)).to.equal(1337);

      // No getter for setter property
      expect(Reflect.get(object, 'setter', other)).to.equal(undefined);
    });

    it('will search the prototype chain', function () {
      var other = Object.create(object);
      other._value = 17;

      var yetAnother = { _value: 4711 };

      expect(Reflect.get(other, 'value', yetAnother)).to.equal(4711);

      expect(Reflect.get(other, 'bool', yetAnother)).to.equal(true);
    });
  });

  describeIfES5('.set()', function () {
    if (typeof Reflect.set === 'undefined') {
      return it('exists', function () {
        expect(Reflect).to.have.property('set');
      });
    }

    it('is a function', function () {
      expect(typeof Reflect.set).to.equal('function');
    });

    ifFunctionsHaveNamesIt('has the right name', function () {
      expect(Reflect.set.name).to.equal('set');
    });

    it('throws if the target isn’t an object', function () {
      testPrimitiveThrow(function (item) {
        return Reflect.set(item, 'prop', 'value');
      });
    });

    it('sets values on receiver', function () {
      var target = {};
      var receiver = {};

      expect(Reflect.set(target, 'foo', 1, receiver)).to.equal(true);

      expect('foo' in target).to.equal(false);
      expect(receiver.foo).to.equal(1);

      expect(Reflect.defineProperty(receiver, 'bar', {
        value: 0,
        writable: true,
        enumerable: false,
        configurable: true
      })).to.equal(true);

      expect(Reflect.set(target, 'bar', 1, receiver)).to.equal(true);
      expect(receiver.bar).to.equal(1);
      expect(Reflect.getOwnPropertyDescriptor(receiver, 'bar').enumerable).to.equal(false);

      var out;
      /* eslint-disable accessor-pairs */
      target = Object.create({}, {
        o: {
          set: function () { out = this; }
        }
      });
      /* eslint-enable accessor-pairs */

      expect(Reflect.set(target, 'o', 17, receiver)).to.equal(true);
      expect(out).to.equal(receiver);
    });
  });

  describeIfES5('.getOwnPropertyDescriptor()', function () {
    if (typeof Reflect.getOwnPropertyDescriptor === 'undefined') {
      return it('exists', function () {
        expect(Reflect).to.have.property('getOwnPropertyDescriptor');
      });
    }

    it('is a function', function () {
      expect(typeof Reflect.getOwnPropertyDescriptor).to.equal('function');
    });

    ifFunctionsHaveNamesIt('has the right name', function () {
      expect(Reflect.getOwnPropertyDescriptor.name).to.equal('getOwnPropertyDescriptor');
    });

    it('throws if the target isn’t an object', function () {
      testPrimitiveThrow(function (item) {
        return Reflect.getOwnPropertyDescriptor(item, 'prop');
      });
    });

    it('retrieves property descriptors', function () {
      var obj = { a: 4711 };

      var desc = Reflect.getOwnPropertyDescriptor(obj, 'a');

      expect(desc).to.deep.equal({
        value: 4711,
        configurable: true,
        writable: true,
        enumerable: true
      });
    });
  });

  describeIfGetProto('.getPrototypeOf()', function () {
    if (typeof Reflect.getPrototypeOf === 'undefined') {
      return it('exists', function () {
        expect(Reflect).to.have.property('getPrototypeOf');
      });
    }

    it('is a function', function () {
      expect(typeof Reflect.getPrototypeOf).to.equal('function');
    });

    ifFunctionsHaveNamesIt('has the right name', function () {
      expect(Reflect.getPrototypeOf.name).to.equal('getPrototypeOf');
    });

    it('throws if the target isn’t an object', function () {
      testPrimitiveThrow(function (item) {
        return Reflect.getPrototypeOf(item);
      });
    });

    it('retrieves prototypes', function () {
      expect(Reflect.getPrototypeOf(Object.create(null))).to.equal(null);

      expect(Reflect.getPrototypeOf([])).to.equal(Array.prototype);
    });
  });

  describe('.has()', function () {
    if (typeof Reflect.has === 'undefined') {
      return it('exists', function () {
        expect(Reflect).to.have.property('has');
      });
    }

    it('is a function', function () {
      expect(typeof Reflect.has).to.equal('function');
    });

    ifFunctionsHaveNamesIt('has the right name', function () {
      expect(Reflect.has.name).to.equal('has');
    });

    it('throws if the target isn’t an object', function () {
      testPrimitiveThrow(function (item) {
        return Reflect.has(item, 'prop');
      });
    });

    it('will detect own properties', function () {
      var target = Object.create ? Object.create(null) : {};

      expect(Reflect.has(target, 'prop')).to.equal(false);

      target.prop = undefined;
      expect(Reflect.has(target, 'prop')).to.equal(true);

      delete target.prop;
      expect(Reflect.has(target, 'prop')).to.equal(false);

      expect(Reflect.has(Reflect.has, 'length')).to.equal(true);
    });

    ifES5It('will detect an own accessor property', function () {
      var target = Object.create(null);
      /* eslint-disable accessor-pairs */
      Object.defineProperty(target, 'accessor', {
        set: function () {}
      });
      /* eslint-enable accessor-pairs */

      expect(Reflect.has(target, 'accessor')).to.equal(true);
    });

    it('will search the prototype chain', function () {
      var Parent = function () {};
      Parent.prototype.someProperty = undefined;

      var Child = function () {};
      Child.prototype = new Parent();

      var target = new Child();
      target.bool = true;

      expect(Reflect.has(target, 'bool')).to.equal(true);
      expect(Reflect.has(target, 'someProperty')).to.equal(true);
      expect(Reflect.has(target, 'undefinedProperty')).to.equal(false);
    });
  });

  describeIfExtensionsPreventible('.isExtensible()', function () {
    if (typeof Reflect.isExtensible === 'undefined') {
      return it('exists', function () {
        expect(Reflect).to.have.property('isExtensible');
      });
    }

    it('is a function', function () {
      expect(typeof Reflect.isExtensible).to.equal('function');
    });

    ifFunctionsHaveNamesIt('has the right name', function () {
      expect(Reflect.isExtensible.name).to.equal('isExtensible');
    });

    it('returns true for plain objects', function () {
      expect(Reflect.isExtensible({})).to.equal(true);
      expect(Reflect.isExtensible(Object.preventExtensions({}))).to.equal(false);
    });

    it('throws if the target isn’t an object', function () {
      testPrimitiveThrow(function (item) {
        return Reflect.isExtensible(item);
      });
    });
  });

  describeIfGetOwnPropertyNames('.ownKeys()', function () {
    if (typeof Reflect.ownKeys === 'undefined') {
      return it('exists', function () {
        expect(Reflect).to.have.property('ownKeys');
      });
    }

    it('is a function', function () {
      expect(typeof Reflect.ownKeys).to.equal('function');
    });

    ifFunctionsHaveNamesIt('has the right name', function () {
      expect(Reflect.ownKeys.name).to.equal('ownKeys');
    });

    it('throws if the target isn’t an object', function () {
      testPrimitiveThrow(function (item) {
        return Reflect.ownKeys(item);
      });
    });

    it('should return the same result as Object.getOwnPropertyNames if there are no Symbols', function () {
      var obj = { foo: 1, bar: 2 };

      obj[1] = 'first';

      var result = Object.getOwnPropertyNames(obj);

      // Reflect.ownKeys depends on the implementation of
      // Object.getOwnPropertyNames, at least for non-symbol keys.
      expect(Reflect.ownKeys(obj)).to.deep.equal(result);

      // We can only be sure of which keys should exist.
      expect(result.sort()).to.deep.equal(['1', 'bar', 'foo']);
    });

    ifSymbolsIt('symbols come last', function () {
      var s = Symbol();

      var o = {
        'non-symbol': true
      };

      o[s] = true;

      expect(Reflect.ownKeys(o)).to.deep.equal(['non-symbol', s]);
    });
  });

  describeIfExtensionsPreventible('.preventExtensions()', function () {
    if (typeof Reflect.preventExtensions === 'undefined') {
      return it('exists', function () {
        expect(Reflect).to.have.property('preventExtensions');
      });
    }

    it('is a function', function () {
      expect(typeof Reflect.preventExtensions).to.equal('function');
    });

    ifFunctionsHaveNamesIt('has the right name', function () {
      expect(Reflect.preventExtensions.name).to.equal('preventExtensions');
    });

    it('throws if the target isn’t an object', function () {
      testPrimitiveThrow(function (item) {
        return Reflect.preventExtensions(item);
      });
    });

    it('prevents extensions on objects', function () {
      var obj = {};
      Reflect.preventExtensions(obj);
      expect(Object.isExtensible(obj)).to.equal(false);
    });
  });

  describeIfSetProto('.setPrototypeOf()', function () {
    if (typeof Reflect.setPrototypeOf === 'undefined') {
      return it('exists', function () {
        expect(Reflect).to.have.property('setPrototypeOf');
      });
    }

    it('is a function', function () {
      expect(typeof Reflect.setPrototypeOf).to.equal('function');
    });

    ifFunctionsHaveNamesIt('has the right name', function () {
      expect(Reflect.setPrototypeOf.name).to.equal('setPrototypeOf');
    });

    it('throws if the target isn’t an object', function () {
      testPrimitiveThrow(function (item) {
        return Reflect.setPrototypeOf(item, null);
      });
    });

    it('throws if the prototype is neither object nor null', function () {
      var o = {};

      [undefined, 1, 'string', true].forEach(function (item) {
        expect(function () {
          return Reflect.setPrototypeOf(o, item);
        }).to['throw'](TypeError);
      });
    });

    it('can set prototypes, and returns true on success', function () {
      var obj = {};

      expect(Reflect.setPrototypeOf(obj, Array.prototype)).to.equal(true);
      expect(obj).to.be.an.instanceOf(Array);

      expect(obj.toString).not.to.equal(undefined);
      expect(Reflect.setPrototypeOf(obj, null)).to.equal(true);
      expect(obj.toString).to.equal(undefined);
    });

    ifFreezeIt('is returns false on failure', function () {
      var obj = Object.freeze({});

      expect(Reflect.setPrototypeOf(obj, null)).to.equal(false);
    });

    it('fails when attempting to create a circular prototype chain', function () {
      var o = {};

      expect(Reflect.setPrototypeOf(o, o)).to.equal(false);
    });
  });
});
