describe(“QueueRunner”, function() {

it("runs all the functions it's passed", function() {
  var calls = [],
    queueableFn1 = { fn: jasmine.createSpy('fn1') },
    queueableFn2 = { fn: jasmine.createSpy('fn2') },
    queueRunner = new j$.QueueRunner({
      queueableFns: [queueableFn1, queueableFn2]
    });
  queueableFn1.fn.and.callFake(function() {
    calls.push('fn1');
  });
  queueableFn2.fn.and.callFake(function() {
    calls.push('fn2');
  });

  queueRunner.execute();

  expect(calls).toEqual(['fn1', 'fn2']);
});

it("calls each function with a consistent 'this'-- an empty object", function() {
  var queueableFn1 = { fn: jasmine.createSpy('fn1') },
      queueableFn2 = { fn: jasmine.createSpy('fn2') },
      queueableFn3 = { fn: function(done) { asyncContext = this; done(); } },
      queueRunner = new j$.QueueRunner({
        queueableFns: [queueableFn1, queueableFn2, queueableFn3]
      }),
      asyncContext;

  queueRunner.execute();

  var context = queueableFn1.fn.calls.first().object;
  expect(context).toEqual({});
  expect(queueableFn2.fn.calls.first().object).toBe(context);
  expect(asyncContext).toBe(context);
});

describe("with an asynchronous function", function() {
  beforeEach(function() {
    jasmine.clock().install();
  });

  afterEach(function() {
    jasmine.clock().uninstall();
  });

  it("supports asynchronous functions, only advancing to next function after a done() callback", function() {
    //TODO: it would be nice if spy arity could match the fake, so we could do something like:
    //createSpy('asyncfn').and.callFake(function(done) {});

    var onComplete = jasmine.createSpy('onComplete'),
      beforeCallback = jasmine.createSpy('beforeCallback'),
      fnCallback = jasmine.createSpy('fnCallback'),
      afterCallback = jasmine.createSpy('afterCallback'),
      queueableFn1 = { fn: function(done) {
        beforeCallback();
        setTimeout(done, 100);
      } },
      queueableFn2 = { fn: function(done) {
        fnCallback();
        setTimeout(done, 100);
      } },
      queueableFn3 = { fn: function(done) {
        afterCallback();
        setTimeout(done, 100);
      } },
      queueRunner = new j$.QueueRunner({
        queueableFns: [queueableFn1, queueableFn2, queueableFn3],
        onComplete: onComplete
      });

    queueRunner.execute();

    expect(beforeCallback).toHaveBeenCalled();
    expect(fnCallback).not.toHaveBeenCalled();
    expect(afterCallback).not.toHaveBeenCalled();
    expect(onComplete).not.toHaveBeenCalled();

    jasmine.clock().tick(100);

    expect(fnCallback).toHaveBeenCalled();
    expect(afterCallback).not.toHaveBeenCalled();
    expect(onComplete).not.toHaveBeenCalled();

    jasmine.clock().tick(100);

    expect(afterCallback).toHaveBeenCalled();
    expect(onComplete).not.toHaveBeenCalled();

    jasmine.clock().tick(100);

    expect(onComplete).toHaveBeenCalled();
  });

  it("explicitly fails an async function with a provided fail function and moves to the next function", function() {
    var queueableFn1 = { fn: function(done) {
        setTimeout(function() { done.fail('foo'); }, 100);
      } },
      queueableFn2 = { fn: jasmine.createSpy('fn2') },
      failFn = jasmine.createSpy('fail'),
      queueRunner = new j$.QueueRunner({
        queueableFns: [queueableFn1, queueableFn2],
        fail: failFn
      });

    queueRunner.execute();

    expect(failFn).not.toHaveBeenCalled();
    expect(queueableFn2.fn).not.toHaveBeenCalled();

    jasmine.clock().tick(100);

    expect(failFn).toHaveBeenCalledWith('foo');
    expect(queueableFn2.fn).toHaveBeenCalled();
  });

  it("sets a timeout if requested for asynchronous functions so they don't go on forever", function() {
    var timeout = 3,
      beforeFn = { fn: function(done) { }, type: 'before', timeout: function() { return timeout; } },
      queueableFn = { fn: jasmine.createSpy('fn'), type: 'queueable' },
      onComplete = jasmine.createSpy('onComplete'),
      onException = jasmine.createSpy('onException'),
      queueRunner = new j$.QueueRunner({
        queueableFns: [beforeFn, queueableFn],
        onComplete: onComplete,
        onException: onException
      });

    queueRunner.execute();
    expect(queueableFn.fn).not.toHaveBeenCalled();

    jasmine.clock().tick(timeout);

    expect(onException).toHaveBeenCalledWith(jasmine.any(Error));
    expect(queueableFn.fn).toHaveBeenCalled();
    expect(onComplete).toHaveBeenCalled();
  });

  it("by default does not set a timeout for asynchronous functions", function() {
    var beforeFn = { fn: function(done) { } },
      queueableFn = { fn: jasmine.createSpy('fn') },
      onComplete = jasmine.createSpy('onComplete'),
      onException = jasmine.createSpy('onException'),
      queueRunner = new j$.QueueRunner({
        queueableFns: [beforeFn, queueableFn],
        onComplete: onComplete,
        onException: onException,
      });

    queueRunner.execute();
    expect(queueableFn.fn).not.toHaveBeenCalled();

    jasmine.clock().tick(j$.DEFAULT_TIMEOUT_INTERVAL);

    expect(onException).not.toHaveBeenCalled();
    expect(queueableFn.fn).not.toHaveBeenCalled();
    expect(onComplete).not.toHaveBeenCalled();
  });

  it("clears the timeout when an async function throws an exception, to prevent additional exception reporting", function() {
     var queueableFn = { fn: function(done) { throw new Error("error!"); } },
      onComplete = jasmine.createSpy('onComplete'),
      onException = jasmine.createSpy('onException'),
      queueRunner = new j$.QueueRunner({
        queueableFns: [queueableFn],
        onComplete: onComplete,
        onException: onException
      });

    queueRunner.execute();

    expect(onComplete).toHaveBeenCalled();
    expect(onException).toHaveBeenCalled();

    jasmine.clock().tick(j$.DEFAULT_TIMEOUT_INTERVAL);
    expect(onException.calls.count()).toEqual(1);
  });

  it("clears the timeout when the done callback is called", function() {
    var queueableFn = { fn: function(done) { done(); } },
      onComplete = jasmine.createSpy('onComplete'),
      onException = jasmine.createSpy('onException'),
      queueRunner = new j$.QueueRunner({
        queueableFns: [queueableFn],
        onComplete: onComplete,
        onException: onException
      });

    queueRunner.execute();

    expect(onComplete).toHaveBeenCalled();

    jasmine.clock().tick(j$.DEFAULT_TIMEOUT_INTERVAL);
    expect(onException).not.toHaveBeenCalled();
  });

  it("only moves to the next spec the first time you call done", function() {
    var queueableFn = { fn: function(done) {done(); done();} },
      nextQueueableFn = { fn: jasmine.createSpy('nextFn') };
    queueRunner = new j$.QueueRunner({
      queueableFns: [queueableFn, nextQueueableFn]
    });

    queueRunner.execute();
    expect(nextQueueableFn.fn.calls.count()).toEqual(1);
  });

  it("does not move to the next spec if done is called after an exception has ended the spec", function() {
     var queueableFn = { fn: function(done) {
       setTimeout(done, 1);
       throw new Error('error!');
     } },
      nextQueueableFn = { fn: jasmine.createSpy('nextFn') };
    queueRunner = new j$.QueueRunner({
      queueableFns: [queueableFn, nextQueueableFn]
    });

    queueRunner.execute();
    jasmine.clock().tick(1);
    expect(nextQueueableFn.fn.calls.count()).toEqual(1);
  });
});

it("calls exception handlers when an exception is thrown in a fn", function() {
  var queueableFn = { type: 'queueable',
    fn: function() {
      throw new Error('fake error');
    } },
    onExceptionCallback = jasmine.createSpy('on exception callback'),
    queueRunner = new j$.QueueRunner({
      queueableFns: [queueableFn],
      onException: onExceptionCallback
    });

  queueRunner.execute();

  expect(onExceptionCallback).toHaveBeenCalledWith(jasmine.any(Error));
});

it("rethrows an exception if told to", function() {
  var queueableFn = { fn: function() {
      throw new Error('fake error');
    } },
    queueRunner = new j$.QueueRunner({
      queueableFns: [queueableFn],
      catchException: function(e) { return false; }
    });

  expect(function() {
    queueRunner.execute();
  }).toThrowError('fake error');
});

it("continues running the functions even after an exception is thrown in an async spec", function() {
  var queueableFn = { fn: function(done) { throw new Error("error"); } },
    nextQueueableFn = { fn: jasmine.createSpy("nextFunction") },
    queueRunner = new j$.QueueRunner({
      queueableFns: [queueableFn, nextQueueableFn]
    });

  queueRunner.execute();
  expect(nextQueueableFn.fn).toHaveBeenCalled();
});

it("calls a provided complete callback when done", function() {
  var queueableFn = { fn: jasmine.createSpy('fn') },
    completeCallback = jasmine.createSpy('completeCallback'),
    queueRunner = new j$.QueueRunner({
      queueableFns: [queueableFn],
      onComplete: completeCallback
    });

  queueRunner.execute();

  expect(completeCallback).toHaveBeenCalled();
});

it("calls a provided stack clearing function when done", function() {
  var asyncFn = { fn: function(done) { done() } },
      afterFn = { fn: jasmine.createSpy('afterFn') },
      completeCallback = jasmine.createSpy('completeCallback'),
      clearStack = jasmine.createSpy('clearStack'),
      queueRunner = new j$.QueueRunner({
        queueableFns: [asyncFn, afterFn],
        clearStack: clearStack,
        onComplete: completeCallback
      });

  clearStack.and.callFake(function(fn) { fn(); });

  queueRunner.execute();
  expect(afterFn.fn).toHaveBeenCalled();
  expect(clearStack).toHaveBeenCalledWith(completeCallback);
});

});