// Copyright 2015 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

goog.module('goog.labs.promiseTest');
goog.setTestOnly('goog.labs.promiseTest');

goog.require('goog.testing.jsunit');

var Promise = goog.require('goog.Promise');
var Timer = goog.require('goog.Timer');
var promise = goog.require('goog.labs.promise');
var MockClock = goog.require('goog.testing.MockClock');
var testSuite = goog.require('goog.testing.testSuite');

// Workaround roughly equivalent to the following ES6 code:
//
//   function*() {
//     while (cond()) {
//       yield body();
//     }
//   }
function whileLoopIterator(cond, body) {
  return function() {
    var done = false;
    return {
      next: function(opt_arg) {
        done = done || !cond();
        if (done) {
          return {done: true};
        } else {
          return {done: false, value: body()};
        }
      },
      'throw': function(error) { throw error; }
    };
  };
}


/**
 * Dummy onfulfilled or onrejected function that should not be called.
 *
 * @param {*} result The result passed into the callback.
 */
function shouldNotCall(result) {
  fail('This should not have been called (result: ' + String(result) + ')');
}

var mockClock;
var sentinel = {};

testSuite({
  setUp: function() { mockClock = new MockClock(); },

  tearDown: function() { mockClock.uninstall(); },

  testWhileLoopIterator: function() {
    var counter = 3;
    var it = whileLoopIterator(
        function() { return counter > 0; }, function() { --counter; })();
    assertEquals(3, counter);
    assertFalse(it.next().done);
    assertEquals(2, counter);
    assertFalse(it.next().done);
    assertEquals(1, counter);
    assertFalse(it.next().done);
    assertEquals(0, counter);
    assertTrue(it.next().done);
    assertEquals(0, counter);
  },

  testRun: function() {
    var counter = 5;
    var resolved = 0;
    return promise
        .run(
            whileLoopIterator(
                function() { return counter > 0; },
                function() {
                  --counter;
                  return Promise.resolve().then(function() { ++resolved; });
                }))
        .then(function(result) {
          assertFalse(goog.isDef(result));
          assertEquals(0, counter);
          assertEquals(5, resolved);
        });
  },

  testRunWithNonPromise: function() {
    var counter = 5;
    return promise
        .run(
            whileLoopIterator(
                function() { return counter > 0; }, function() { --counter; }))
        .then(function(result) {
          assertFalse(goog.isDef(result));
          assertEquals(0, counter);
        });
  },

  testRunWithMockClock: function() {
    mockClock.install();

    var counter = 3;
    var innerResolved = 0;
    var outerResolved = false;
    promise
        .run(
            whileLoopIterator(
                function() { return counter > 0; },
                function() {
                  --counter;
                  return Timer.promise(10).then(function() {
                    ++innerResolved;
                  });
                }))
        .then(function() { outerResolved = true; });

    assertEquals(3, counter);
    assertEquals(0, innerResolved);
    assertFalse(outerResolved);

    mockClock.tick();

    assertEquals(2, counter);
    assertEquals(0, innerResolved);
    assertFalse(outerResolved);

    mockClock.tick(10);

    assertEquals(1, counter);
    assertEquals(1, innerResolved);
    assertFalse(outerResolved);

    mockClock.tick(10);

    assertEquals(0, counter);
    assertEquals(2, innerResolved);
    assertFalse(outerResolved);

    mockClock.tick(10);

    assertEquals(0, counter);
    assertEquals(3, innerResolved);
    assertTrue(outerResolved);
  },

  testRunWithRejection: function() {
    var counter = 5;
    return promise
        .run(
            whileLoopIterator(
                function() { return counter > 0; },
                function() {
                  --counter;
                  if (counter == 2) {
                    return Promise.reject(sentinel);
                  }
                  return Promise.resolve();
                }))
        .then(shouldNotCall, function(error) {
          assertEquals(2, counter);
          assertEquals(sentinel, error);
        });
  },

  testRunWithException: function() {
    var counter = 5;
    return promise
        .run(
            whileLoopIterator(
                function() { return counter > 0; },
                function() {
                  --counter;
                  if (counter == 2) {
                    throw sentinel;
                  }
                  return Promise.resolve();
                }))
        .then(shouldNotCall, function(error) {
          assertEquals(2, counter);
          assertEquals(sentinel, error);
        });
  },

  testRunWithImmediateException: function() {
    return promise
        .run(whileLoopIterator(function() { throw sentinel; }, function() {}))
        .then(
            shouldNotCall, function(error) { assertEquals(sentinel, error); });
  },

  testRunWithImmediateRejection: function() {
    return promise
        .run(
            whileLoopIterator(
                function() { return true; },
                function() { return Promise.reject(sentinel); }))
        .then(
            shouldNotCall, function(error) { assertEquals(sentinel, error); });
  },

  testRunWithoutYield: function() {
    return promise
        .run(whileLoopIterator(function() { return false; }, goog.nullFunction))
        .then(function(result) { assertFalse(goog.isDef(result)); });
  },

  testRunYieldWithValue: function() {
    // ES6 version:
    //   return promise.run(function*() {
    //     var x = yield Promise.resolve(1);
    //     var y = yield Promise.resolve(2);
    //     return x + y;
    //   }).then(result => assertEquals(3, result));

    return promise
        .run(function() {
          var step = 0;
          var x, y;
          return {
            next: function(nextArg) {
              switch (++step) {
                case 1:
                  return {done: false, value: Promise.resolve(1)};
                case 2:
                  x = nextArg;
                  return {done: false, value: Promise.resolve(2)};
                case 3:
                  y = nextArg;
                  return {done: true, value: x + y};
                default:
                  return {done: true};
              }
            }
          };
        })
        .then(function(result) { assertEquals(3, result); });
  },

  testRunYieldWithThrow: function() {
    // ES6 version:
    //   return promise.run(function*() {
    //     var x = 0;
    //     try {
    //       x = yield Promise.reject('error');
    //     catch (e) {
    //       x = yield Promise.resolve(1);
    //     }
    //     var y = yield Promise.resolve(2);
    //     return x + y;
    //   }).then(result => assertEquals(3, result));

    return promise
        .run(function() {
          var step = 0;
          var x, y;
          return {
            next: function(nextArg) {
              switch (++step) {
                case 1:
                  return {done: false, value: Promise.reject('error')};
                case 2:
                  x = nextArg;
                  return {done: false, value: Promise.resolve(2)};
                case 3:
                  y = nextArg;
                  return {done: true, value: x + y};
                default:
                  return {done: true};
              }
            },
            'throw': function(error) {
              return {done: false, value: Promise.resolve(1)};
            }
          };
        })
        .then(function(result) { assertEquals(3, result); });
  }
});
