var chai = require('chai')
  , expect = chai.expect
  , Promise = require('bluebird')
  , moment = require('moment')
  , sinon = require('sinon')
  , sinonChai = require('sinon-chai')
  , retry = require('../');

chai.use(require('chai-as-promised'));
require('sinon-as-promised')(Promise);

describe('bluebird', function () {
  var count
    , soRejected
    , soResolved;

  beforeEach(function () {
    count = 0;
    soRejected = Math.random().toString();
    soResolved = Math.random().toString();
  });

  it('should reject immediately if max is 1 (using options)', function () {
    var callback = sinon.stub();
    callback.resolves(soResolved);
    callback.onCall(0).rejects(soRejected);
    return expect(retry(callback, {max: 1, backoffBase: 0})).to.eventually.be.rejectedWith(soRejected).then(function () {
      expect(callback.callCount).to.equal(1);
    });
  });

  it('should reject immediately if max is 1 (using integer)', function () {
    var callback = sinon.stub();
    callback.resolves(soResolved);
    callback.onCall(0).rejects(soRejected);
    return expect(retry(callback, 1)).to.eventually.be.rejectedWith(soRejected).then(function () {
      expect(callback.callCount).to.equal(1);
    });
  });

  it('should reject after all tries if still rejected', function () {
    var callback = sinon.stub();
    callback.rejects(soRejected);
    return expect(retry(callback, {max: 3, backoffBase: 0})).to.eventually.be.rejectedWith(soRejected).then(function () {
      expect(callback.callCount).to.equal(3);
    });
  });

  it('should resolve immediately if resolved on first try', function () {
    var callback = sinon.stub();
    callback.resolves(soResolved);
    callback.onCall(0).resolves(soResolved);
    return expect(retry(callback, {max: 10, backoffBase: 0})).to.eventually.equal(soResolved).then(function () {
      expect(callback.callCount).to.equal(1);
    });
  });

  it('should resolve if resolved before hitting max', function () {
    var callback = sinon.stub();
    callback.rejects(soRejected);
    callback.onCall(3).resolves(soResolved);
    return expect(retry(callback, {max: 10, backoffBase: 0})).to.eventually.equal(soResolved).then(function () {
      expect(callback.callCount).to.equal(4);
    });
  });

  describe('timeout', function () {
    it('should throw if reject on first attempt', function () {
      return expect(retry(function () {
        return Promise.delay(2000);
      }, {
        max: 1,
        backoffBase: 0,
        timeout: 1000
      })).to.eventually.be.rejectedWith(Promise.TimeoutError);
    });

    it('should throw if reject on last attempt', function () {
      return expect(retry(function () {
        count++;
        if (count === 3) {
          return Promise.delay(3500);
        }
        return Promise.reject();
      }, {
        max: 3,
        backoffBase: 0,
        timeout: 1500
      })).to.eventually.be.rejectedWith(Promise.TimeoutError).then(function () {
        expect(count).to.equal(3);
      });
    });
  });
  
  describe('match', function () {
    it('should continue retry while error is equal to match string', function () {
      var callback = sinon.stub();
      callback.rejects(soRejected);
      callback.onCall(3).resolves(soResolved);
      return expect(retry(callback, {max: 15, backoffBase: 0, match: 'Error: ' + soRejected})).to.eventually.equal(soResolved).then(function () {
        expect(callback.callCount).to.equal(4);
      });
    });
    
    it('should reject immediately if error is not equal to match string', function () {
      var callback = sinon.stub();
      callback.rejects(soRejected);
      return expect(retry(callback, {max: 15, backoffBase: 0, match: 'A custom error string'})).to.eventually.be.rejectedWith(soRejected).then(function () {
        expect(callback.callCount).to.equal(1);
      });
    });
    
    it('should continue retry while error is instanceof match', function () {
      var callback = sinon.stub();
      callback.rejects(soRejected);
      callback.onCall(4).resolves(soResolved);
      return expect(retry(callback, {max: 15, backoffBase: 0, match: Error})).to.eventually.equal(soResolved).then(function () {
        expect(callback.callCount).to.equal(5);
      });
    });

    it('should reject immediately if error is not instanceof match', function () {
      var callback = sinon.stub();
      callback.rejects(soRejected);
      return expect(retry(callback, {max: 15, backoffBase: 0, match: function foo(){}})).to.eventually.be.rejectedWith(Error).then(function () {
        expect(callback.callCount).to.equal(1);
      });
    });
    
    it('should continue retry while error is equal to match string in array', function () {
      var callback = sinon.stub();
      callback.rejects(soRejected);
      callback.onCall(4).resolves(soResolved);
      return expect(retry(callback, {max: 15, backoffBase: 0, match: ['Error: ' + (soRejected + 1), 'Error: ' + soRejected]})).to.eventually.equal(soResolved).then(function () {
        expect(callback.callCount).to.equal(5);
      });
    });

    it('should reject immediately if error is not equal to match string in array', function () {
      var callback = sinon.stub();
      callback.rejects(soRejected);
      return expect(retry(callback, {max: 15, backoffBase: 0, match: ['Error: ' + (soRejected + 1), 'Error: ' + (soRejected + 2)]})).to.eventually.be.rejectedWith(Error).then(function () {
        expect(callback.callCount).to.equal(1);
      });
    });

    it('should reject immediately if error is not instanceof match in array', function () {
      var callback = sinon.stub();
      callback.rejects(soRejected);
      return expect(retry(callback, {max: 15, backoffBase: 0, match: ['Error: ' + (soRejected + 1), function foo(){}]})).to.eventually.be.rejectedWith(Error).then(function () {
        expect(callback.callCount).to.equal(1);
      });
    });

    it('should continue retry while error is instanceof match in array', function () {
      var callback = sinon.stub();
      callback.rejects(soRejected);
      callback.onCall(4).resolves(soResolved);
      return expect(retry(callback, {max: 15, backoffBase: 0, match: ['Error: ' + (soRejected + 1), Error]})).to.eventually.equal(soResolved).then(function () {
        expect(callback.callCount).to.equal(5);
      });
    });
  });

  describe('backoff', function () {
    it('should resolve after 10 retries and an eventual delay over 3400ms using default backoff', function () {
      var startTime = moment();
      var callback = sinon.stub();
      callback.rejects(soRejected);
      callback.onCall(5).resolves(soResolved);
      return expect(retry(callback, {max: 15})).to.eventually.equal(soResolved).then(function () {
        expect(callback.callCount).to.equal(6);
        expect(moment().diff(startTime)).to.be.above(3400);
      });
    });
    
    it('should throw TimeoutError and cancel backoff delay if timeout is reached', function () {
      return expect(retry(function () {
        return Promise.delay(2000);
      }, {
        max: 15,
        timeout: 1000
      })).to.eventually.be.rejectedWith(Promise.TimeoutError);
    });
  });
});
