text: /*
   * Copyright (c) 2012 Mathieu Turcotte
   * Licensed under the MIT license.
   */
  
  var assert = require('assert');
  var events = require('events');
  var sinon = require('sinon');
  var util = require('util');
  
  var FunctionCall = require('../lib/function_call');
  
  function MockBackoff() {
      events.EventEmitter.call(this);
  
      this.reset = sinon.spy();
      this.backoff = sinon.spy();
      this.failAfter = sinon.spy();
  }
  util.inherits(MockBackoff, events.EventEmitter);
  
  exports["FunctionCall"] = {
      setUp: function(callback) {
          this.wrappedFn = sinon.stub();
          this.callback = sinon.stub();
          this.backoff = new MockBackoff();
          this.backoffFactory = sinon.stub();
          this.backoffFactory.returns(this.backoff);
          callback();
      },
  
      tearDown: function(callback) {
          callback();
      },
  
      "constructor's first argument should be a function": function(test) {
          test.throws(function() {
              new FunctionCall(1, [], function() {});
          }, /Expected fn to be a function./);
          test.done();
      },
  
      "constructor's last argument should be a function": function(test) {
          test.throws(function() {
              new FunctionCall(function() {}, [], 3);
          }, /Expected callback to be a function./);
          test.done();
      },
  
      "isPending should return false once the call is started": function(test) {
          this.wrappedFn.
              onFirstCall().yields(new Error()).
              onSecondCall().yields(null, 'Success!');
          var call = new FunctionCall(this.wrappedFn, [], this.callback);
  
          test.ok(call.isPending());
  
          call.start(this.backoffFactory);
          test.ok(!call.isPending());
  
          this.backoff.emit('ready');
          test.ok(!call.isPending());
  
          test.done();
      },
  
      "isRunning should return true when call is in progress": function(test) {
          this.wrappedFn.
              onFirstCall().yields(new Error()).
              onSecondCall().yields(null, 'Success!');
          var call = new FunctionCall(this.wrappedFn, [], this.callback);
  
          test.ok(!call.isRunning());
  
          call.start(this.backoffFactory);
          test.ok(call.isRunning());
  
          this.backoff.emit('ready');
          test.ok(!call.isRunning());
  
          test.done();
      },
  
      "isCompleted should return true once the call completes": function(test) {
          this.wrappedFn.
              onFirstCall().yields(new Error()).
              onSecondCall().yields(null, 'Success!');
          var call = new FunctionCall(this.wrappedFn, [], this.callback);
  
          test.ok(!call.isCompleted());
  
          call.start(this.backoffFactory);
          test.ok(!call.isCompleted());
  
          this.backoff.emit('ready');
          test.ok(call.isCompleted());
  
          test.done();
      },
  
      "isAborted should return true once the call is aborted": function(test) {
          this.wrappedFn.
              onFirstCall().yields(new Error()).
              onSecondCall().yields(null, 'Success!');
          var call = new FunctionCall(this.wrappedFn, [], this.callback);
  
          test.ok(!call.isAborted());
          call.abort();
          test.ok(call.isAborted());
  
          test.done();
      },
  
      "setStrategy should overwrite the default strategy": function(test) {
          var replacementStrategy = {};
          var call = new FunctionCall(this.wrappedFn, [], this.callback);
          call.setStrategy(replacementStrategy);
          call.start(this.backoffFactory);
          test.ok(this.backoffFactory.calledWith(replacementStrategy),
              'User defined strategy should be used to instantiate ' +
              'the backoff instance.');
          test.done();
      },
  
      "setStrategy should throw if the call is in progress": function(test) {
          var call = new FunctionCall(this.wrappedFn, [], this.callback);
          call.start(this.backoffFactory);
          test.throws(function() {
              call.setStrategy({});
          }, /in progress/);
          test.done();
      },
  
      "failAfter should not be set by default": function(test) {
          var call = new FunctionCall(this.wrappedFn, [], this.callback);
              call.start(this.backoffFactory);
              test.equal(0, this.backoff.failAfter.callCount);
          test.done();
      },
  
      "failAfter should be used as the maximum number of backoffs": function(test) {
          var failAfterValue = 99;
          var call = new FunctionCall(this.wrappedFn, [], this.callback);
          call.failAfter(failAfterValue);
          call.start(this.backoffFactory);
          test.ok(this.backoff.failAfter.calledWith(failAfterValue),
              'User defined maximum number of backoffs shoud be ' +
              'used to configure the backoff instance.');
          test.done();
      },
  
      "failAfter should throw if the call is in progress": function(test) {
          var call = new FunctionCall(this.wrappedFn, [], this.callback);
          call.start(this.backoffFactory);
          test.throws(function() {
              call.failAfter(1234);
          }, /in progress/);
          test.done();
      },
  
      "start shouldn't allow overlapping invocation": function(test) {
          var call = new FunctionCall(this.wrappedFn, [], this.callback);
          var backoffFactory = this.backoffFactory;
  
          call.start(backoffFactory);
          test.throws(function() {
              call.start(backoffFactory);
          }, /already started/);
          test.done();
      },
  
      "start shouldn't allow invocation of aborted call": function(test) {
          var call = new FunctionCall(this.wrappedFn, [], this.callback);
          var backoffFactory = this.backoffFactory;
  
          call.abort();
          test.throws(function() {
              call.start(backoffFactory);
          }, /aborted/);
          test.done();
      },
  
      "call should forward its arguments to the wrapped function": function(test) {
          var call = new FunctionCall(this.wrappedFn, [1, 2, 3], this.callback);
          call.start(this.backoffFactory);
          test.ok(this.wrappedFn.calledWith(1, 2, 3));
          test.done();
      },
  
      "call should complete when the wrapped function succeeds": function(test) {
          var call = new FunctionCall(this.wrappedFn, [1, 2, 3], this.callback);
          this.wrappedFn.
              onCall(0).yields(new Error()).
              onCall(1).yields(new Error()).
              onCall(2).yields(new Error()).
              onCall(3).yields(null, 'Success!');
  
          call.start(this.backoffFactory);
  
          for (var i = 0; i < 2; i++) {
              this.backoff.emit('ready');
          }
  
          test.equals(this.callback.callCount, 0);
          this.backoff.emit('ready');
  
          test.ok(this.callback.calledWith(null, 'Success!'));
          test.ok(this.wrappedFn.alwaysCalledWith(1, 2, 3));
          test.done();
      },
  
      "call should fail when the backoff limit is reached": function(test) {
          var call = new FunctionCall(this.wrappedFn, [1, 2, 3], this.callback);
          var error = new Error();
          this.wrappedFn.yields(error);
          call.start(this.backoffFactory);
  
          for (var i = 0; i < 3; i++) {
              this.backoff.emit('ready');
          }
  
          test.equals(this.callback.callCount, 0);
  
          this.backoff.emit('fail');
  
          test.ok(this.callback.calledWith(error));
          test.ok(this.wrappedFn.alwaysCalledWith(1, 2, 3));
          test.done();
      },
  
      "call should fail when the retry predicate returns false": function(test) {
          var call = new FunctionCall(this.wrappedFn, [1, 2, 3], this.callback);
  		call.retryIf(function(err) { return err.retriable; });
  
  		var retriableError = new Error();
  		retriableError.retriable = true;
  
  		var fatalError = new Error();
  		fatalError.retriable = false;
  
          this.wrappedFn.
              onCall(0).yields(retriableError).
              onCall(1).yields(retriableError).
              onCall(2).yields(fatalError);
  
          call.start(this.backoffFactory);
  
          for (var i = 0; i < 2; i++) {
              this.backoff.emit('ready');
          }
  
          test.equals(this.callback.callCount, 1);
          test.ok(this.callback.calledWith(fatalError));
          test.ok(this.wrappedFn.alwaysCalledWith(1, 2, 3));
          test.done();
  	},
  
      "wrapped function's callback shouldn't be called after abort": function(test) {
          var call = new FunctionCall(function(callback) {
              call.abort(); // Abort in middle of wrapped function's execution.
              callback(null, 'ok');
          }, [], this.callback);
  
          call.start(this.backoffFactory);
  
          test.equals(this.callback.callCount, 1,
              'Wrapped function\'s callback shouldn\'t be called after abort.');
          test.ok(this.callback.calledWithMatch(sinon.match(function (err) {
              return !!err.message.match(/Backoff aborted/);
          }, "abort error")));
          test.done();
      },
  
      "abort event is emitted once when abort is called": function(test) {
          var call = new FunctionCall(this.wrappedFn, [], this.callback);
          this.wrappedFn.yields(new Error());
          var callEventSpy = sinon.spy();
  
          call.on('abort', callEventSpy);
          call.start(this.backoffFactory);
  
          call.abort();
          call.abort();
          call.abort();
  
          test.equals(callEventSpy.callCount, 1);
          test.done();
      },
  
      "getLastResult should return the last intermediary result": function(test) {
          var call = new FunctionCall(this.wrappedFn, [], this.callback);
          this.wrappedFn.yields(1);
          call.start(this.backoffFactory);
  
          for (var i = 2; i < 5; i++) {
              this.wrappedFn.yields(i);
              this.backoff.emit('ready');
              test.deepEqual([i], call.getLastResult());
          }
  
          this.wrappedFn.yields(null);
          this.backoff.emit('ready');
          test.deepEqual([null], call.getLastResult());
  
          test.done();
      },
  
      "getNumRetries should return the number of retries": function(test) {
          var call = new FunctionCall(this.wrappedFn, [], this.callback);
  
          this.wrappedFn.yields(1);
          call.start(this.backoffFactory);
          // The inital call doesn't count as a retry.
          test.equals(0, call.getNumRetries());
  
          for (var i = 2; i < 5; i++) {
              this.wrappedFn.yields(i);
              this.backoff.emit('ready');
              test.equals(i - 1, call.getNumRetries());
          }
  
          this.wrappedFn.yields(null);
          this.backoff.emit('ready');
          test.equals(4, call.getNumRetries());
  
          test.done();
      },
  
      "wrapped function's errors should be propagated": function(test) {
          var call = new FunctionCall(this.wrappedFn, [1, 2, 3], this.callback);
          this.wrappedFn.throws(new Error());
          test.throws(function() {
              call.start(this.backoffFactory);
          }, Error);
          test.done();
      },
  
      "wrapped callback's errors should be propagated": function(test) {
          var call = new FunctionCall(this.wrappedFn, [1, 2, 3], this.callback);
          this.wrappedFn.yields(null, 'Success!');
          this.callback.throws(new Error());
          test.throws(function() {
              call.start(this.backoffFactory);
          }, Error);
          test.done();
      },
  
      "call event should be emitted when wrapped function gets called": function(test) {
          this.wrappedFn.yields(1);
          var callEventSpy = sinon.spy();
  
          var call = new FunctionCall(this.wrappedFn, [1, 'two'], this.callback);
          call.on('call', callEventSpy);
          call.start(this.backoffFactory);
  
          for (var i = 1; i < 5; i++) {
              this.backoff.emit('ready');
          }
  
          test.equal(5, callEventSpy.callCount,
              'The call event should have been emitted 5 times.');
          test.deepEqual([1, 'two'], callEventSpy.getCall(0).args,
              'The call event should carry function\'s args.');
          test.done();
      },
  
      "callback event should be emitted when callback is called": function(test) {
          var call = new FunctionCall(this.wrappedFn, [1, 'two'], this.callback);
          var callbackSpy = sinon.spy();
          call.on('callback', callbackSpy);
  
          this.wrappedFn.yields('error');
          call.start(this.backoffFactory);
  
          this.wrappedFn.yields(null, 'done');
          this.backoff.emit('ready');
  
          test.equal(2, callbackSpy.callCount,
              'Callback event should have been emitted 2 times.');
          test.deepEqual(['error'], callbackSpy.firstCall.args,
              'First callback event should carry first call\'s results.');
          test.deepEqual([null, 'done'], callbackSpy.secondCall.args,
              'Second callback event should carry second call\'s results.');
          test.done();
      },
  
      "backoff event should be emitted on backoff start": function(test) {
          var err = new Error('backoff event error');
          var call = new FunctionCall(this.wrappedFn, [1, 'two'], this.callback);
          var backoffSpy = sinon.spy();
  
          call.on('backoff', backoffSpy);
  
          this.wrappedFn.yields(err);
          call.start(this.backoffFactory);
          this.backoff.emit('backoff', 3, 1234, err);
  
          test.ok(this.backoff.backoff.calledWith(err),
              'The backoff instance should have been called with the error.');
          test.equal(1, backoffSpy.callCount,
              'Backoff event should have been emitted 1 time.');
          test.deepEqual([3, 1234, err], backoffSpy.firstCall.args,
              'Backoff event should carry the backoff number, delay and error.');
          test.done();
      }
  };
  