// META: global=window,worker,shadowrealm
// META: script=../resources/test-utils.js
'use strict';

const iterableFactories = [
  ['an array of values', () => {
    return ['a', 'b'];
  }],

  ['an array of promises', () => {
    return [
      Promise.resolve('a'),
      Promise.resolve('b')
    ];
  }],

  ['an array iterator', () => {
    return ['a', 'b'][Symbol.iterator]();
  }],

  ['a string', () => {
    // This iterates over the code points of the string.
    return 'ab';
  }],

  ['a Set', () => {
    return new Set(['a', 'b']);
  }],

  ['a Set iterator', () => {
    return new Set(['a', 'b'])[Symbol.iterator]();
  }],

  ['a sync generator', () => {
    function* syncGenerator() {
      yield 'a';
      yield 'b';
    }

    return syncGenerator();
  }],

  ['an async generator', () => {
    async function* asyncGenerator() {
      yield 'a';
      yield 'b';
    }

    return asyncGenerator();
  }],

  ['a sync iterable of values', () => {
    const chunks = ['a', 'b'];
    const iterator = {
      next() {
        return {
          done: chunks.length === 0,
          value: chunks.shift()
        };
      }
    };
    const iterable = {
      [Symbol.iterator]: () => iterator
    };
    return iterable;
  }],

  ['a sync iterable of promises', () => {
    const chunks = ['a', 'b'];
    const iterator = {
      next() {
        return chunks.length === 0 ? { done: true } : {
          done: false,
          value: Promise.resolve(chunks.shift())
        };
      }
    };
    const iterable = {
      [Symbol.iterator]: () => iterator
    };
    return iterable;
  }],

  ['an async iterable', () => {
    const chunks = ['a', 'b'];
    const asyncIterator = {
      next() {
        return Promise.resolve({
          done: chunks.length === 0,
          value: chunks.shift()
        })
      }
    };
    const asyncIterable = {
      [Symbol.asyncIterator]: () => asyncIterator
    };
    return asyncIterable;
  }],

  ['a ReadableStream', () => {
    return new ReadableStream({
      start(c) {
        c.enqueue('a');
        c.enqueue('b');
        c.close();
      }
    });
  }],

  ['a ReadableStream async iterator', () => {
    return new ReadableStream({
      start(c) {
        c.enqueue('a');
        c.enqueue('b');
        c.close();
      }
    })[Symbol.asyncIterator]();
  }]
];

for (const [label, factory] of iterableFactories) {
  promise_test(async () => {

    const iterable = factory();
    const rs = ReadableStream.from(iterable);
    assert_equals(rs.constructor, ReadableStream, 'from() should return a ReadableStream');

    const reader = rs.getReader();
    assert_object_equals(await reader.read(), { value: 'a', done: false }, 'first read should be correct');
    assert_object_equals(await reader.read(), { value: 'b', done: false }, 'second read should be correct');
    assert_object_equals(await reader.read(), { value: undefined, done: true }, 'third read should be done');
    await reader.closed;

  }, `ReadableStream.from accepts ${label}`);
}

const badIterables = [
  ['null', null],
  ['undefined', undefined],
  ['0', 0],
  ['NaN', NaN],
  ['true', true],
  ['{}', {}],
  ['Object.create(null)', Object.create(null)],
  ['a function', () => 42],
  ['a symbol', Symbol()],
  ['an object with a non-callable @@iterator method', {
    [Symbol.iterator]: 42
  }],
  ['an object with a non-callable @@asyncIterator method', {
    [Symbol.asyncIterator]: 42
  }],
  ['an object with an @@iterator method returning a non-object', {
    [Symbol.iterator]: () => 42
  }],
  ['an object with an @@asyncIterator method returning a non-object', {
    [Symbol.asyncIterator]: () => 42
  }],
];

for (const [label, iterable] of badIterables) {
  test(() => {
    assert_throws_js(TypeError, () => ReadableStream.from(iterable), 'from() should throw a TypeError')
  }, `ReadableStream.from throws on invalid iterables; specifically ${label}`);
}

test(() => {
  const theError = new Error('a unique string');
  const iterable = {
    [Symbol.iterator]() {
      throw theError;
    }
  };

  assert_throws_exactly(theError, () => ReadableStream.from(iterable), 'from() should re-throw the error');
}, `ReadableStream.from re-throws errors from calling the @@iterator method`);

test(() => {
  const theError = new Error('a unique string');
  const iterable = {
    [Symbol.asyncIterator]() {
      throw theError;
    }
  };

  assert_throws_exactly(theError, () => ReadableStream.from(iterable), 'from() should re-throw the error');
}, `ReadableStream.from re-throws errors from calling the @@asyncIterator method`);

test(t => {
  const theError = new Error('a unique string');
  const iterable = {
    [Symbol.iterator]: t.unreached_func('@@iterator should not be called'),
    [Symbol.asyncIterator]() {
      throw theError;
    }
  };

  assert_throws_exactly(theError, () => ReadableStream.from(iterable), 'from() should re-throw the error');
}, `ReadableStream.from ignores @@iterator if @@asyncIterator exists`);

test(() => {
  const theError = new Error('a unique string');
  const iterable = {
    [Symbol.asyncIterator]: null,
    [Symbol.iterator]() {
      throw theError
    }
  };

  assert_throws_exactly(theError, () => ReadableStream.from(iterable), 'from() should re-throw the error');
}, `ReadableStream.from ignores a null @@asyncIterator`);

promise_test(async () => {

  const iterable = {
    async next() {
      return { value: undefined, done: true };
    },
    [Symbol.asyncIterator]: () => iterable
  };

  const rs = ReadableStream.from(iterable);
  const reader = rs.getReader();

  const read = await reader.read();
  assert_object_equals(read, { value: undefined, done: true }, 'first read should be done');

  await reader.closed;

}, `ReadableStream.from accepts an empty iterable`);

promise_test(async t => {

  const theError = new Error('a unique string');

  const iterable = {
    async next() {
      throw theError;
    },
    [Symbol.asyncIterator]: () => iterable
  };

  const rs = ReadableStream.from(iterable);
  const reader = rs.getReader();

  await Promise.all([
    promise_rejects_exactly(t, theError, reader.read()),
    promise_rejects_exactly(t, theError, reader.closed)
  ]);

}, `ReadableStream.from: stream errors when next() rejects`);

promise_test(async t => {
  const theError = new Error('a unique string');

  const iterable = {
    next() {
      throw theError;
    },
    [Symbol.asyncIterator]: () => iterable
  };

  const rs = ReadableStream.from(iterable);
  const reader = rs.getReader();

  await Promise.all([
    promise_rejects_exactly(t, theError, reader.read()),
    promise_rejects_exactly(t, theError, reader.closed)
  ]);

}, 'ReadableStream.from: stream errors when next() throws synchronously');

promise_test(async t => {

  const iterable = {
    next() {
      return 42; // not a promise or an iterator result
    },
    [Symbol.asyncIterator]: () => iterable
  };

  const rs = ReadableStream.from(iterable);
  const reader = rs.getReader();

  await Promise.all([
    promise_rejects_js(t, TypeError, reader.read()),
    promise_rejects_js(t, TypeError, reader.closed)
  ]);

}, 'ReadableStream.from: stream errors when next() returns a non-object');

promise_test(async t => {

  const iterable = {
    next() {
      return Promise.resolve(42); // not an iterator result
    },
    [Symbol.asyncIterator]: () => iterable
  };

  const rs = ReadableStream.from(iterable);
  const reader = rs.getReader();

  await Promise.all([
    promise_rejects_js(t, TypeError, reader.read()),
    promise_rejects_js(t, TypeError, reader.closed)
  ]);

}, 'ReadableStream.from: stream errors when next() fulfills with a non-object');

promise_test(async t => {

  const iterable = {
    next() {
      return new Promise(() => {});
    },
    [Symbol.asyncIterator]: () => iterable
  };

  const rs = ReadableStream.from(iterable);
  const reader = rs.getReader();

  await Promise.race([
    reader.read().then(t.unreached_func('read() should not resolve'), t.unreached_func('read() should not reject')),
    reader.closed.then(t.unreached_func('closed should not resolve'), t.unreached_func('closed should not reject')),
    flushAsyncEvents()
  ]);

}, 'ReadableStream.from: stream stalls when next() never settles');

promise_test(async () => {

  let nextCalls = 0;
  let nextArgs;
  const iterable = {
    async next(...args) {
      nextCalls += 1;
      nextArgs = args;
      return { value: 'a', done: false };
    },
    [Symbol.asyncIterator]: () => iterable
  };

  const rs = ReadableStream.from(iterable);
  const reader = rs.getReader();

  await flushAsyncEvents();
  assert_equals(nextCalls, 0, 'next() should not be called yet');

  const read = await reader.read();
  assert_object_equals(read, { value: 'a', done: false }, 'first read should be correct');
  assert_equals(nextCalls, 1, 'next() should be called after first read()');
  assert_array_equals(nextArgs, [], 'next() should be called with no arguments');

}, `ReadableStream.from: calls next() after first read()`);

promise_test(async t => {

  const theError = new Error('a unique string');

  let returnCalls = 0;
  let returnArgs;
  let resolveReturn;
  const iterable = {
    next: t.unreached_func('next() should not be called'),
    throw: t.unreached_func('throw() should not be called'),
    async return(...args) {
      returnCalls += 1;
      returnArgs = args;
      await new Promise(r => resolveReturn = r);
      return { done: true };
    },
    [Symbol.asyncIterator]: () => iterable
  };

  const rs = ReadableStream.from(iterable);
  const reader = rs.getReader();
  assert_equals(returnCalls, 0, 'return() should not be called yet');

  let cancelResolved = false;
  const cancelPromise = reader.cancel(theError).then(() => {
    cancelResolved = true;
  });

  await flushAsyncEvents();
  assert_equals(returnCalls, 1, 'return() should be called');
  assert_array_equals(returnArgs, [theError], 'return() should be called with cancel reason');
  assert_false(cancelResolved, 'cancel() should not resolve while promise from return() is pending');

  resolveReturn();
  await Promise.all([
    cancelPromise,
    reader.closed
  ]);

}, `ReadableStream.from: cancelling the returned stream calls and awaits return()`);

promise_test(async t => {

  let nextCalls = 0;
  let returnCalls = 0;

  const iterable = {
    async next() {
      nextCalls += 1;
      return { value: undefined, done: true };
    },
    throw: t.unreached_func('throw() should not be called'),
    async return() {
      returnCalls += 1;
    },
    [Symbol.asyncIterator]: () => iterable
  };

  const rs = ReadableStream.from(iterable);
  const reader = rs.getReader();

  const read = await reader.read();
  assert_object_equals(read, { value: undefined, done: true }, 'first read should be done');
  assert_equals(nextCalls, 1, 'next() should be called once');

  await reader.closed;
  assert_equals(returnCalls, 0, 'return() should not be called');

}, `ReadableStream.from: return() is not called when iterator completes normally`);

promise_test(async t => {

  const theError = new Error('a unique string');

  const iterable = {
    next: t.unreached_func('next() should not be called'),
    throw: t.unreached_func('throw() should not be called'),
    // no return method
    [Symbol.asyncIterator]: () => iterable
  };

  const rs = ReadableStream.from(iterable);
  const reader = rs.getReader();

  await Promise.all([
    reader.cancel(theError),
    reader.closed
  ]);

}, `ReadableStream.from: cancel() resolves when return() method is missing`);

promise_test(async t => {

  const theError = new Error('a unique string');

  const iterable = {
    next: t.unreached_func('next() should not be called'),
    throw: t.unreached_func('throw() should not be called'),
    return: 42,
    [Symbol.asyncIterator]: () => iterable
  };

  const rs = ReadableStream.from(iterable);
  const reader = rs.getReader();

  await promise_rejects_js(t, TypeError, reader.cancel(theError), 'cancel() should reject with a TypeError');

  await reader.closed;

}, `ReadableStream.from: cancel() rejects when return() is not a method`);

promise_test(async t => {

  const cancelReason = new Error('cancel reason');
  const rejectError = new Error('reject error');

  const iterable = {
    next: t.unreached_func('next() should not be called'),
    throw: t.unreached_func('throw() should not be called'),
    async return() {
      throw rejectError;
    },
    [Symbol.asyncIterator]: () => iterable
  };

  const rs = ReadableStream.from(iterable);
  const reader = rs.getReader();

  await promise_rejects_exactly(t, rejectError, reader.cancel(cancelReason), 'cancel() should reject with error from return()');

  await reader.closed;

}, `ReadableStream.from: cancel() rejects when return() rejects`);

promise_test(async t => {

  const cancelReason = new Error('cancel reason');
  const rejectError = new Error('reject error');

  const iterable = {
    next: t.unreached_func('next() should not be called'),
    throw: t.unreached_func('throw() should not be called'),
    return() {
      throw rejectError;
    },
    [Symbol.asyncIterator]: () => iterable
  };

  const rs = ReadableStream.from(iterable);
  const reader = rs.getReader();

  await promise_rejects_exactly(t, rejectError, reader.cancel(cancelReason), 'cancel() should reject with error from return()');

  await reader.closed;

}, `ReadableStream.from: cancel() rejects when return() throws synchronously`);

promise_test(async t => {

  const theError = new Error('a unique string');

  const iterable = {
    next: t.unreached_func('next() should not be called'),
    throw: t.unreached_func('throw() should not be called'),
    async return() {
      return 42;
    },
    [Symbol.asyncIterator]: () => iterable
  };

  const rs = ReadableStream.from(iterable);
  const reader = rs.getReader();

  await promise_rejects_js(t, TypeError, reader.cancel(theError), 'cancel() should reject with a TypeError');

  await reader.closed;

}, `ReadableStream.from: cancel() rejects when return() fulfills with a non-object`);

promise_test(async () => {

  let nextCalls = 0;
  let reader;
  let values = ['a', 'b', 'c'];

  const iterable = {
    async next() {
      nextCalls += 1;
      if (nextCalls === 1) {
        reader.read();
      }
      return { value: values.shift(), done: false };
    },
    [Symbol.asyncIterator]: () => iterable
  };

  const rs = ReadableStream.from(iterable);
  reader = rs.getReader();

  const read1 = await reader.read();
  assert_object_equals(read1, { value: 'a', done: false }, 'first read should be correct');
  await flushAsyncEvents();
  assert_equals(nextCalls, 2, 'next() should be called two times');

  const read2 = await reader.read();
  assert_object_equals(read2, { value: 'c', done: false }, 'second read should be correct');
  assert_equals(nextCalls, 3, 'next() should be called three times');

}, `ReadableStream.from: reader.read() inside next()`);

promise_test(async () => {

  let nextCalls = 0;
  let returnCalls = 0;
  let reader;

  const iterable = {
    async next() {
      nextCalls++;
      await reader.cancel();
      assert_equals(returnCalls, 1, 'return() should be called once');
      return { value: 'something else', done: false };
    },
    async return() {
      returnCalls++;
    },
    [Symbol.asyncIterator]: () => iterable
  };

  const rs = ReadableStream.from(iterable);
  reader = rs.getReader();

  const read = await reader.read();
  assert_object_equals(read, { value: undefined, done: true }, 'first read should be done');
  assert_equals(nextCalls, 1, 'next() should be called once');

  await reader.closed;

}, `ReadableStream.from: reader.cancel() inside next()`);

promise_test(async t => {

  let returnCalls = 0;
  let reader;

  const iterable = {
    next: t.unreached_func('next() should not be called'),
    async return() {
      returnCalls++;
      await reader.cancel();
      return { done: true };
    },
    [Symbol.asyncIterator]: () => iterable
  };

  const rs = ReadableStream.from(iterable);
  reader = rs.getReader();

  await reader.cancel();
  assert_equals(returnCalls, 1, 'return() should be called once');

  await reader.closed;

}, `ReadableStream.from: reader.cancel() inside return()`);

promise_test(async t => {

  let array = ['a', 'b'];

  const rs = ReadableStream.from(array);
  const reader = rs.getReader();

  const read1 = await reader.read();
  assert_object_equals(read1, { value: 'a', done: false }, 'first read should be correct');
  const read2 = await reader.read();
  assert_object_equals(read2, { value: 'b', done: false }, 'second read should be correct');

  array.push('c');

  const read3 = await reader.read();
  assert_object_equals(read3, { value: 'c', done: false }, 'third read after push() should be correct');
  const read4 = await reader.read();
  assert_object_equals(read4, { value: undefined, done: true }, 'fourth read should be done');

  await reader.closed;

}, `ReadableStream.from(array), push() to array while reading`);
