// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The SFC licenses this file
// to you 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.

'use strict';

const assert = require('assert');
const promise = require('../../lib/promise');
const {enablePromiseManager, promiseManagerSuite} = require('../../lib/test/promise');

describe('promise.consume()', function() {
  promiseManagerSuite(() => {
    it('requires inputs to be generator functions', function() {
      assert.throws(function() {
        promise.consume(function() {});
      });
    });

    it('handles a basic generator with no yielded promises', function() {
      var values = [];
      return promise.consume(function* () {
        var i = 0;
        while (i < 4) {
          i = yield i + 1;
          values.push(i);
        }
      }).then(function() {
        assert.deepEqual([1, 2, 3, 4], values);
      });
    });

    it('handles a promise yielding generator', function() {
      var values = [];
      return promise.consume(function* () {
        var i = 0;
        while (i < 4) {
          // Test that things are actually async here.
          setTimeout(function() {
            values.push(i * 2);
          }, 10);

          yield promise.delayed(10).then(function() {
            values.push(i++);
          });
        }
      }).then(function() {
        assert.deepEqual([0, 0, 2, 1, 4, 2, 6, 3], values);
      });
    });

    it('assignments to yielded promises get fulfilled value', function() {
      return promise.consume(function* () {
        let x = yield Promise.resolve(2);
        assert.equal(2, x);
      });
    });

    it('uses final return value as fulfillment value', function() {
      return promise.consume(function* () {
        yield 1;
        yield 2;
        return 3;
      }).then(function(value) {
        assert.equal(3, value);
      });
    });

    it('throws rejected promise errors within the generator', function() {
      var values = [];
      return promise.consume(function* () {
        values.push('a');
        var e = Error('stub error');
        try {
          yield Promise.reject(e);
          values.push('b');
        } catch (ex) {
          assert.equal(e, ex);
          values.push('c');
        }
        values.push('d');
      }).then(function() {
        assert.deepEqual(['a', 'c', 'd'], values);
      });
    });

    it('aborts the generator if there is an unhandled rejection', function() {
      var values = [];
      var e = Error('stub error');
      return promise.consume(function* () {
        values.push(1);
        yield promise.rejected(e);
        values.push(2);
      }).catch(function() {
        assert.deepEqual([1], values);
      });
    });

    it('yield waits for promises', function() {
      let values = [];
      let blocker = promise.delayed(100).then(() => {
        assert.deepEqual([1], values);
        return 2;
      });

      return promise.consume(function* () {
        values.push(1);
        values.push(yield blocker, 3);
      }).then(function() {
        assert.deepEqual([1, 2, 3], values);
      });
    });

    it('accepts custom scopes', function() {
      return promise.consume(function* () {
        return this.name;
      }, {name: 'Bob'}).then(function(value) {
        assert.equal('Bob', value);
      });
    });

    it('accepts initial generator arguments', function() {
      return promise.consume(function* (a, b) {
        assert.equal('red', a);
        assert.equal('apples', b);
      }, null, 'red', 'apples');
    });
  });

  enablePromiseManager(() =>  {
    it('is possible to cancel promise generators', function() {
      var values = [];
      var p = promise.consume(function* () {
        var i = 0;
        while (i < 3) {
          yield promise.delayed(100).then(function() {
            values.push(i++);
          });
        }
      });
      return promise.delayed(75).then(function() {
        p.cancel();
        return p.catch(function() {
          return promise.delayed(300);
        });
      }).then(function() {
        assert.deepEqual([0], values);
      });
    });

    it('executes generator within the control flow', function() {
      var promises = [
          promise.defer(),
          promise.defer()
      ];
      var values = [];

      setTimeout(function() {
        assert.deepEqual([], values);
        promises[0].fulfill(1);
      }, 100);

      setTimeout(function() {
        assert.deepEqual([1], values);
        promises[1].fulfill(2);
      }, 200);

      return promise.controlFlow().execute(function* () {
        values.push(yield promises[0].promise);
        values.push(yield promises[1].promise);
        values.push('fin');
      }).then(function() {
        assert.deepEqual([1, 2, 'fin'], values);
      });
    });

    it('handles tasks scheduled in generator', function() {
      var flow = promise.controlFlow();
      return flow.execute(function* () {
        var x = yield flow.execute(function() {
          return promise.delayed(10).then(function() {
            return 1;
          });
        });

        var y = yield flow.execute(function() {
          return 2;
        });

        return x + y;
      }).then(function(value) {
        assert.equal(3, value);
      });
    });

    it('blocks the control flow while processing generator', function() {
      var values = [];
      return promise.controlFlow().wait(function* () {
        yield values.push(1);
        values.push(yield promise.delayed(10).then(function() {
          return 2;
        }));
        yield values.push(3);
        return values.length === 6;
      }, 250).then(function() {
        assert.deepEqual([1, 2, 3, 1, 2, 3], values);
      });
    });

    it('ControlFlow.wait() will timeout on long generator', function() {
      var values = [];
      return promise.controlFlow().wait(function* () {
        var i = 0;
        while (i < 3) {
          yield promise.delayed(100).then(function() {
            values.push(i++);
          });
        }
      }, 75).catch(function() {
        assert.deepEqual(
            [0, 1, 2], values, 'Should complete one loop of wait condition');
      });
    });

    describe('generators in promise callbacks', function() {
      it('works with no initial value', function() {
        var promises = [
          promise.defer(),
          promise.defer()
        ];
        var values = [];

        setTimeout(function() {
          promises[0].fulfill(1);
        }, 50);

        setTimeout(function() {
          promises[1].fulfill(2);
        }, 100);

        return promise.fulfilled().then(function*() {
          values.push(yield promises[0].promise);
          values.push(yield promises[1].promise);
          values.push('fin');
        }).then(function() {
          assert.deepEqual([1, 2, 'fin'], values);
        });
      });

      it('starts the generator with promised value', function() {
        var promises = [
          promise.defer(),
          promise.defer()
        ];
        var values = [];

        setTimeout(function() {
          promises[0].fulfill(1);
        }, 50);

        setTimeout(function() {
          promises[1].fulfill(2);
        }, 100);

        return promise.fulfilled(3).then(function*(value) {
          var p1 = yield promises[0].promise;
          var p2 = yield promises[1].promise;
          values.push(value + p1);
          values.push(value + p2);
          values.push('fin');
        }).then(function() {
          assert.deepEqual([4, 5, 'fin'], values);
        });
      });

      it('throws yielded rejections within the generator callback', function() {
        var d = promise.defer();
        var e = Error('stub');

        setTimeout(function() {
          d.reject(e);
        }, 50);

        return promise.fulfilled().then(function*() {
          var threw = false;
          try {
            yield d.promise;
          } catch (ex) {
            threw = true;
            assert.equal(e, ex);
          }
          assert.ok(threw);
        });
      });
    });
  });
});