/**
 * Copyright 2009 The Closure Library Authors. All Rights Reserved.
 * Author: brenneman@google.com (Shawn Brenneman)
 */

goog.setTestOnly();
goog.require('goog.array');
goog.require('goog.async.Deferred');
goog.require('goog.async.DeferredList');
goog.require('goog.testing.jsunit');

var Deferred = goog.async.Deferred;
var DeferredList = goog.async.DeferredList;


// Re-throw (after a timeout) any errors not handled in an errback.
Deferred.STRICT_ERRORS = true;


/**
 * A list of unhandled errors.
 * @type {Array.<Error>}
 */
var storedErrors = [];


/**
 * Adds a catch-all error handler to deferred objects. Unhandled errors that
 * reach the catch-all will be rethrown during tearDown.
 *
 * @param {...Deferred} var_args A list of deferred objects.
 */
function addCatchAll(var_args) {
  for (var i = 0, d; d = arguments[i]; i++) {
    d.addErrback(function(res) {
      storedErrors.push(res);
    });
  }
}


/**
 * Checks storedErrors for unhandled errors. If found, the error is rethrown.
 */
function checkCatchAll() {
  var err = storedErrors.shift();
  goog.array.clear(storedErrors);

  if (err) {
    throw err;
  }
}


function tearDown() {
  checkCatchAll();
}

/**
 * @param {*} res
 */
function neverHappen(res) {
  fail('This should not happen');
}


function testNoInputs() {
  var count = 0;
  var d = new DeferredList([]);

  d.addCallback(function(res) {
    assertArrayEquals([], res);
    count++;
  });
  addCatchAll(d);

  assertEquals('An empty DeferredList should fire immediately with an empty ' +
               'list of results.',
               1, count);
}


function testNoInputsAndFireOnOneCallback() {
  var count = 0;
  var d = new DeferredList([], true);

  d.addCallback(function(res) {
    assertArrayEquals([], res);
    count++;
  });
  addCatchAll(d);

  assertEquals('An empty DeferredList with opt_fireOnOneCallback set should ' +
               'not fire unless callback is invoked explicitly.',
               0, count);

  d.callback([]);
  assertEquals('Calling callback explicitly should still fire.', 1, count);
}


function testDeferredList() {
  var count = 0;

  var a = new Deferred();
  var b = new Deferred();
  var c = new Deferred();

  var dl = new DeferredList([a, b, c]);

  dl.addCallback(function(res) {
    assertEquals('Expected 3 Deferred results.', 3, res.length);

    assertTrue('Deferred a should return success.', res[0][0]);
    assertFalse('Deferred b should return failure.', res[1][0]);
    assertTrue('Deferred c should return success.', res[2][0]);

    assertEquals('Unexpected return value for a.', 'A', res[0][1]);
    assertEquals('Unexpected return value for c.', 'C', res[2][1]);

    assertEquals('B', res[1][1]);

    count++;
  });

  addCatchAll(dl);

  c.callback('C');
  assertEquals(0, count);

  b.errback('B');
  assertEquals(0, count);

  a.callback('A');

  checkCatchAll();
  assertEquals('DeferredList should fire on last call or errback.', 1, count);
}


function testFireOnFirstCallback() {
  var a = new Deferred();
  var b = new Deferred();
  var c = new Deferred();

  var dl = new DeferredList([a, b, c], true);

  dl.addCallback(function(res) {
    assertEquals('Should be the deferred index in this mode.', 1, res[0]);
    assertEquals('B', res[1]);
  });
  dl.addErrback(neverHappen);

  addCatchAll(dl);

  a.errback('A');
  b.callback('B');

  // Shouldn't cause any more callbacks on the DeferredList.
  c.callback('C');
}


function testFireOnFirstErrback() {
  var a = new Deferred();
  var b = new Deferred();
  var c = new Deferred();

  var dl = new DeferredList([a, b, c], false, true);

  dl.addCallback(neverHappen);
  dl.addErrback(function(res) {
    assertEquals('A', res);

    // Return a non-error value to break out of the errback path.
    return null;
  });
  addCatchAll(dl);

  b.callback('B');
  a.errback('A');

  assertTrue(c.hasFired());
  c.addErrback(function(res) {
    assertTrue(
        'The DeferredList errback should have canceled all pending inputs.',
        res instanceof Deferred.CanceledError);
    return null;
  });
  addCatchAll(c);

  // Shouldn't cause any more callbacks on the DeferredList.
  c.callback('C');
}


function testNoConsumeErrors() {
  var count = 0;

  var a = new Deferred();
  var dl = new DeferredList([a]);

  a.addErrback(function(res) {
    count++;
    return null;
  });

  addCatchAll(a, dl);

  a.errback('oh noes');
  assertEquals(1, count);
}


function testConsumeErrors() {
  var count = 0;

  var a = new Deferred();
  var dl = new DeferredList([a], false, false, true);

  a.addErrback(neverHappen);

  addCatchAll(a, dl);

  a.errback('oh noes');
  assertEquals(0, count);
}


function testNesting() {

  function upperCase(res) {
    return res.toUpperCase();
  }

  // Concatenates a list of callback or errback results into a single string.
  function combine(res) {
    return goog.array.map(res, function(result) {
      return result[1];
    }).join('');
  }

  var a = new Deferred();
  var b = new Deferred();
  var c = new Deferred();
  var d = new Deferred();

  a.addCallback(upperCase);
  b.addCallback(upperCase);
  c.addCallback(upperCase);
  d.addCallback(upperCase);

  var dl1 = new DeferredList([a, b]);
  var dl2 = new DeferredList([c, d]);

  dl1.addCallback(combine);
  dl2.addCallback(combine);

  var dl3 = new DeferredList([dl1, dl2]);
  dl3.addCallback(combine);
  dl3.addCallback(function(res) {
    assertEquals('AbCd', res);
  });

  addCatchAll(dl1, dl2, dl3);

  a.callback('a');
  c.callback('c');
  b.errback('b');
  d.errback('d');
}


function testGatherResults() {
  var a = new Deferred();
  var b = new Deferred();
  var c = new Deferred();

  var dl = DeferredList.gatherResults([a, b, c]);

  dl.addCallback(function(res) {
    assertArrayEquals(['A', 'B', 'C'], res);
  });

  addCatchAll(dl);

  b.callback('B');
  a.callback('A');
  c.callback('C');
}


function testGatherResultsFailure() {
  var a = new Deferred();
  var b = new Deferred();
  var c = new Deferred();

  var dl = DeferredList.gatherResults([a, b, c]);

  var firedErrback = false;
  var firedCallback = false;
  dl.addCallback(function() {
    firedCallback = true;
  });
  dl.addErrback(function() {
    firedErrback = true;
    return null;
  });

  addCatchAll(dl);

  b.callback('B');
  a.callback('A');
  c.errback();

  assertTrue('Errback should be called', firedErrback);
  assertFalse('Callback should not be called', firedCallback);
}


function testGatherResults_cancelCancelsChildren() {
  var canceled = [];
  var a = new Deferred(function() {
    canceled.push('a');
  });
  var b = new Deferred(function() {
    canceled.push('b');
  });
  var c = new Deferred(function() {
    canceled.push('c');
  });

  var dl = new DeferredList([a, b, c]);

  var firedErrback = false;
  var firedCallback = false;
  dl.addCallback(function() {
    firedCallback = true;
  });
  dl.addErrback(function() {
    firedErrback = true;
    return null;
  });

  addCatchAll(dl);

  b.callback('b');
  dl.cancel();

  assertTrue('Errback should be called', firedErrback);
  assertFalse('Callback should not be called', firedCallback);
  assertArrayEquals(['a', 'c'], canceled);
}


function testErrorCancelsPendingChildrenWhenFireOnFirstError() {
  var canceled = [];
  var a = new Deferred(function() {
    canceled.push('a');
  });
  var b = new Deferred(function() {
    canceled.push('b');
  });
  var c = new Deferred(function() {
    canceled.push('c');
  });

  var dl = new DeferredList([a, b, c], false, true);

  var firedErrback = false;
  var firedCallback = false;
  dl.addCallback(function() {
    firedCallback = true;
  });
  dl.addErrback(function() {
    firedErrback = true;
    return null;
  });

  addCatchAll(dl);

  a.callback('a');
  b.errback();

  assertTrue('Errback should be called', firedErrback);
  assertFalse('Callback should not be called', firedCallback);
  assertArrayEquals('Only C should be canceled since A and B fired.',
      ['c'], canceled);
}


function testErrorDoesNotCancelPendingChildrenForVanillaLists() {
  var canceled = [];
  var a = new Deferred(function() {
    canceled.push('a');
  });
  var b = new Deferred(function() {
    canceled.push('b');
  });
  var c = new Deferred(function() {
    canceled.push('c');
  });

  var dl = new DeferredList([a, b, c]);

  var firedErrback = false;
  var firedCallback = false;
  dl.addCallback(function() {
    firedCallback = true;
  });
  dl.addErrback(function() {
    firedErrback = true;
    return null;
  });

  addCatchAll(dl);

  a.callback('a');
  b.errback();
  c.callback('c');

  assertFalse('Errback should not be called', firedErrback);
  assertTrue('Callback should be called', firedCallback);
  assertArrayEquals('No cancellations', [], canceled);
}


function testInputDeferredsStillUsable() {
  var increment = function(res) {
    return res + 1;
  };
  var incrementErrback = function(res) {
    throw res + 1;
  };

  var aComplete = false;
  var bComplete = false;
  var hadListCallback = false;

  var a = new Deferred().addCallback(increment);
  var b = new Deferred().addErrback(incrementErrback);
  var c = new Deferred();

  var dl = new DeferredList([a, b, c]);

  a.callback(0);
  a.addCallback(increment);
  a.addCallback(function(res) {
    aComplete = true;
    assertEquals(
        'The "a" Deferred should have had two increment callbacks.',
        2, res);
  });
  assertTrue('The "a" deferred should complete before the list.', aComplete);

  b.errback(0);
  b.addErrback(incrementErrback);
  b.addErrback(function(res) {
    bComplete = true;
    assertEquals(
        'The "b" Deferred should have had two increment errbacks.',
        2, res);
  });
  assertTrue('The "b" deferred should complete before the list.', bComplete);

  assertFalse('The list should not fire until every input has.', dl.hasFired());
  c.callback();
  assertTrue(dl.hasFired());

  assertFalse(hadListCallback);
  dl.addCallback(function(results) {
    hadListCallback = true;

    var aResult = results[0];
    var bResult = results[1];
    var cResult = results[2];

    assertTrue(aResult[0]);
    assertEquals(
        'Should see the result from before the second callback was added.',
        1, aResult[1]);

    assertFalse(bResult[0]);
    assertEquals(
        'Should see the result from before the second errback was added.',
        1, aResult[1]);

    assertTrue(cResult[0]);
  });
  assertTrue(hadListCallback);

  addCatchAll(dl);
}
