'use strict';
require('mocha');
require('should');
var assert = require('assert');
var App = require('./fixtures/app');
var cu = require('./');

assert.hasAll = function(a, b) {
  assert(cu.hasAll(a, b));
};
assert.contains = function(a, b) {
  assert(cu.has(a, b));
};


describe('noop', function() {
  it('should return undefined:', function() {
    assert(typeof cu.noop({}) === 'undefined');
    assert(typeof cu.noop('foo') === 'undefined');
  });
});

describe('isObject', function() {
  it('should be true if the value is an object:', function() {
    assert(cu.isObject({}));
  });
  it('should be true if the value is a function:', function() {
    assert(cu.isObject(function() {}));
  });
  it('should be false if the value is an object:', function() {
    assert(!cu.isObject('foo'));
  });
});

describe('identity', function() {
  it('should return the given value:', function() {
    assert.deepEqual(cu.identity({}), {});
    assert.deepEqual(cu.identity('foo'), 'foo');
    assert.deepEqual(cu.identity(['foo']), ['foo']);
    assert.deepEqual(cu.identity([]), []);
  });
});

describe('hasAll', function() {
  describe('object', function() {
    it('should be true if an object has all given value:', function() {
      assert(cu.hasAll({
        a: 'b',
        b: 'c',
        c: 'd'
      }, 'c'));
      assert(cu.hasAll({
        a: 'b',
        b: 'c',
        c: 'd'
      }, ['c', 'b']));
      assert(cu.hasAll({
        a: 'b',
        b: 'c',
        c: 'd'
      }, ['a', 'b', 'c']));
    });

    it('should be false if an object does not have all given value:', function() {
      assert(!cu.hasAll({
        a: 'b',
        b: 'c',
        c: 'd'
      }, 'd'));
      assert(!cu.hasAll({
        a: 'b',
        b: 'c',
        c: 'd'
      }, ['c', 'b', 'z']));
    });
  });

  describe('arrays', function() {
    it('should be true if an array has all given value:', function() {
      assert(cu.hasAll(['a', 'b', 'c'], 'c'));
      assert(cu.hasAll(['a', 'b', 'c'], ['c', 'b']));
      assert(cu.hasAll(['a', 'b', 'c'], ['a', 'b', 'c']));
    });

    it('should be false if an array does not have all given value:', function() {
      assert(!cu.hasAll(['a', 'b', 'c'], 'd'));
      assert(!cu.hasAll(['a', 'b', 'c'], ['c', 'b', 'z']));
    });
  });
});

describe('has', function() {
  describe('objects', function() {
    it('should return true if an array has the given value:', function() {
      assert(cu.has({
        a: 'b',
        b: 'c',
        c: 'd'
      }, 'c'));
      assert(cu.has({
        a: 'b',
        b: 'c',
        c: 'd'
      }, 'b'));
    });
    it('should return false if an array does not have the given value:', function() {
      assert(!cu.has({
        a: 'b',
        b: 'c',
        c: 'd'
      }, 'd'));
      assert(!cu.has({
        a: 'b',
        b: 'c',
        c: 'd'
      }, 'e'));
    });
    it('should return true if an array has any given values:', function() {
      assert(cu.has({
        a: 'b',
        b: 'c',
        c: 'd'
      }, ['c', 'z']));
      assert(cu.has({
        a: 'b',
        b: 'c',
        c: 'd'
      }, ['a', 'z']));
    });
    it('should return false if an array does not have any given values:', function() {
      assert(!cu.has({
        a: 'b',
        b: 'c',
        c: 'd'
      }, ['x', 'z']));
      assert(!cu.has({
        a: 'b',
        b: 'c',
        c: 'd'
      }, ['y', 'z']));
    });
  });

  describe('arrays', function() {
    it('should return true if an array has the given value:', function() {
      assert(cu.has(['a', 'b', 'c'], 'c'));
      assert(cu.has(['a', 'b', 'c'], 'b'));
    });
    it('should return false if an array does not have the given value:', function() {
      assert(!cu.has(['a', 'b', 'c'], 'd'));
      assert(!cu.has(['a', 'b', 'c'], 'e'));
    });
    it('should return true if an array has any given values:', function() {
      assert(cu.has(['a', 'b', 'c'], ['c', 'z']));
      assert(cu.has(['a', 'b', 'c'], ['a', 'z']));
    });
    it('should return false if an array does not have any given values:', function() {
      assert(!cu.has(['a', 'b', 'c'], ['x', 'z']));
      assert(!cu.has(['a', 'b', 'c'], ['y', 'z']));
    });
  });

  it('should throw an error when the value is not an array or object:', function() {
    (function() {
      cu.has('foo');
    }).should.throw('expected an array or object.');
  });
});

describe('hasConstructor', function() {
  it('should return true if a value is an object and has a constructor:', function() {
    assert(cu.hasConstructor({
      a: 'b',
      b: 'c',
      c: 'd'
    }));
    assert(cu.hasConstructor(function() {}));
    assert(cu.hasConstructor(App));
    assert(cu.hasConstructor(new App()));
    assert(cu.hasConstructor(Object.create(new App())));
  });

  it('should return false if a value is not object:', function() {
    assert(!cu.hasConstructor('foo'));
    assert(!cu.hasConstructor(5));
  });

  it('should return false if an object does not have a constructor:', function() {
    assert(!cu.hasConstructor(Object.create(null)));
    assert(!cu.hasConstructor(null));
  });
});

describe('nativeKeys', function() {
  it('should get the native keys of an object:', function() {
    assert.hasAll(cu.nativeKeys({
      a: 'b',
      b: 'c',
      c: 'd'
    }), ['a', 'b', 'c']);
    assert.hasAll(cu.nativeKeys(function() {}), ['length', 'name', 'prototype']);
    assert.hasAll(cu.nativeKeys(App), ['length', 'name', 'prototype']);
    assert.hasAll(cu.nativeKeys(App.prototype), ['constructor', 'set', 'get', 'del']);
    assert.hasAll(cu.nativeKeys(App.constructor), ['length', 'name', 'caller']);
    assert.hasAll(cu.nativeKeys(App.prototype.constructor), ['length', 'caller']);
    assert.hasAll(cu.nativeKeys(new App()), ['options']);
    assert.hasAll(cu.nativeKeys(Object.create(new App())), []);
  });

  it('should return empty array if a value does not have native keys:', function() {
    assert.deepEqual(cu.nativeKeys(Object.create(null)), []);
    assert.deepEqual(cu.nativeKeys(null), []);
  });
});

describe('getDescriptor', function() {
  it('should get the native keys of an object:', function() {
    assert.contains(cu.getDescriptor(App.prototype, 'count'), ['get',
      'set'
    ]);
    assert(typeof cu.getDescriptor(App.prototype, 'foo') ===
      'undefined');
  });

  it('should throw an error when key is not a string:', function() {
    (function() {
      cu.getDescriptor({}, {}, null);
    }).should.throw('expected key to be a string.');
  });

  it('should throw an error when receiver is not an object:', function() {
    (function() {
      cu.getDescriptor('foo');
    }).should.throw('expected an object.');
  });
});

describe('copyDescriptor', function() {
  it('should copy a descriptor from the provider to receiver:', function() {
    var proto = App.prototype;
    var obj = {};
    cu.copyDescriptor(obj, proto, 'count');
    assert.contains(cu.getDescriptor(obj, 'count'), ['get', 'set']);
  });

  it('should do nothing when the descriptor does not exist:', function() {
    var proto = App.prototype;
    var obj = {};
    cu.copyDescriptor(obj, proto, 'foo');
    assert.deepEqual(obj, {});
  });

  it('should throw an error when name is not a string:', function() {
    (function() {
      cu.copyDescriptor({}, {}, null);
    }).should.throw('expected name to be a string.');
  });

  it('should throw an error when receiver is not an object:', function() {
    (function() {
      cu.copyDescriptor('foo');
    }).should.throw('expected receiving object to be an object.');
  });

  it('should throw an error when provider is not an object:', function() {
    (function() {
      cu.copyDescriptor({}, 'foo');
    }).should.throw('expected providing object to be an object.');
  });
});

describe('copy', function() {
  it('should copy descriptors from the provider to receiver:', function() {
    var proto = App.prototype;
    var obj = {};
    cu.copy(obj, proto);
    assert.contains(cu.getDescriptor(obj, 'count'), ['get', 'set']);
  });

  it('should copy properties from the provider to receiver:', function() {
    var proto = App.prototype;
    var obj = {};
    cu.copy(obj, proto);
    obj.set('a', 'b');
    assert(obj.a === 'b');
    assert.contains(obj, ['get', 'set']);
  });

  it('should do nothing when the property does not exist:', function() {
    var proto = App.prototype;
    var obj = {};
    cu.copy(obj, proto, 'foo');
    assert.deepEqual(obj, {});
  });

  it('should throw an error when receiver is not an object:', function() {
    (function() {
      cu.copy('foo');
    }).should.throw('expected receiving object to be an object.');
  });

  it('should throw an error when provider is not an object:', function() {
    (function() {
      cu.copy({}, 'foo');
    }).should.throw('expected providing object to be an object.');
  });
});

describe('inherit', function() {
  it('should inherit descriptors from provider:', function() {
    var proto = App.prototype;
    var obj = {};
    cu.inherit(obj, proto);
    assert.contains(cu.getDescriptor(obj, 'count'), ['get', 'set']);
  });

  it('should inherit properties from provider:', function() {
    var proto = App.prototype;
    var obj = {};
    cu.inherit(obj, proto);
    obj.set('a', 'b');
    assert(obj.a === 'b');
    assert.contains(obj, ['get', 'set', 'del']);
  });

  it('should do nothing when the property does not exist:', function() {
    var proto = App.prototype;
    var obj = {};
    cu.inherit(obj, proto, 'foo');
    assert.deepEqual(obj, {});
  });

  it('should throw an error when receiver is not an object:', function() {
    (function() {
      cu.inherit('foo');
    }).should.throw('expected receiving object to be an object.');
  });

  it('should throw an error when provider is not an object:', function() {
    (function() {
      cu.inherit({}, 'foo');
    }).should.throw('expected providing object to be an object.');
  });
});

describe('extend', function() {
  var Parent;
  var Ctor;
  var proto;

  beforeEach(function() {
    Parent = function() {}
    Parent.foo = 'bar';
    Parent.prototype.a = function() {};
    Parent.prototype.b = function() {};
    Parent.prototype.c = function() {};
    Object.defineProperty(Parent.prototype, 'count', {
      get: function() {
        return Object.keys(this).length;
      },
      set: function() {}
    });
    Ctor = function() {
      Parent.call(this);
    };
    proto = App.prototype;
  });

  it('should add `Parent.prototype` to `Ctor` instances as `_parent_`', function() {
    var extend = cu.extend(Parent);
    var instance1 = new Ctor();
    assert.equal(typeof instance1._parent_, 'undefined');
    extend(Ctor);
    var instance2 = new Ctor();
    assert.equal(typeof instance2._parent_, 'object');
    assert.deepEqual(instance2._parent_, Parent.prototype);
  });

  it('should access `Parent` methods through `_parent_`', function() {
    Parent.prototype.upper = function(str) {
      return str.toUpperCase();
    };

    var extend = cu.extend(Parent);
    extend(Ctor);

    var instance = new Ctor();
    assert.equal(instance.upper('foo'), 'FOO');

    instance.upper = function(str) {
      return str;
    };
    assert.equal(instance.upper('foo'), 'foo');

    instance.upper = function(str) {
      return this._parent_.upper(str) + ' ' + str;
    };
    assert.equal(instance.upper('foo'), 'FOO foo');
  });

  it('should add static methods to Ctor:', function() {
    var extend = cu.extend(Parent);
    extend(Ctor);
    assert(typeof Ctor.extend === 'function');
    assert(Ctor.foo === 'bar');
  });

  it('should add prototype methods to Ctor:', function() {
    var extend = cu.extend(Parent);
    extend(Ctor);
    assert(typeof Ctor.prototype.a === 'function');
    assert(typeof Ctor.prototype.b === 'function');
    assert(typeof Ctor.prototype.c === 'function');
  });

  it('should add descriptors to Ctor:', function() {
    var extend = cu.extend(Parent);
    extend(Ctor);
  });

  it('should copy prototype properties to Ctor:', function() {
    var extend = cu.extend(Parent);
    extend(Ctor, App.prototype);
    assert(typeof Ctor.prototype.get === 'function');
    assert(typeof Ctor.prototype.set === 'function');
    assert(typeof Ctor.prototype.del === 'function');
  });

  it('should add a mixin method to the prototype of Ctor using `extend` function:', function() {
    var extend = cu.extend(Parent, function(Child) {
      Child.prototype.mixin = function(key, val) {
        Child.prototype[key] = val;
      };
    });
    extend(Ctor, App.prototype);
    assert(typeof Ctor.prototype.mixin === 'function');
    assert(typeof Ctor.prototype.get === 'function');
    assert(typeof Ctor.prototype.set === 'function');
    assert(typeof Ctor.prototype.del === 'function');
  });

  it('should mixin methods to the Ctor.prototype using `extend` function:', function() {
    var extend = cu.extend(Parent, function(Child) {
      Child.prototype.mixin = function(key, val) {
        Child.prototype[key] = val;
      };
    });
    extend(Ctor, App.prototype);
    var app = new Ctor();
    app.mixin('foo', function() {});
    assert.equal(typeof Ctor.prototype.foo, 'function');
  });

  it('should throw an error when Parent is not a function:', function() {
    (function() {
      cu.extend('foo');
    }).should.throw('expected Parent to be a function.');
  });

  it('should throw an error when Ctor is not a function:', function() {
    (function() {
      cu.extend(function Foo() {})('bar')
    }).should.throw('expected Ctor to be a function.');
  });
});
