/**
 * @license
 * Copyright The Closure Library Authors.
 * SPDX-License-Identifier: Apache-2.0
 */

goog.module('goog.streams.liteTestCases');
goog.setTestOnly();

const {ReadableStream, ReadableStreamDefaultController, ReadableStreamUnderlyingSource} = goog.require('goog.streams.liteTypes');
/** @suppress {extraRequire} */
goog.require('goog.testing.jsunit');

class TestCases {
  /**
   * @param {function(!ReadableStreamUnderlyingSource): !ReadableStream}
   *     newReadableStream
   */
  constructor(newReadableStream) {
    /** @const */
    this.newReadableStream = newReadableStream;
  }

  /**
   * @param {!ReadableStreamUnderlyingSource=} underlyingSource
   * @return {{stream: !ReadableStream<string>, controller:
   *     !ReadableStreamDefaultController<string>}}
   */
  newReadableStreamWithController(
      underlyingSource = /** @type {!ReadableStreamUnderlyingSource} */ ({})) {
    let controller;
    const start = underlyingSource.start;
    underlyingSource = Object.assign({}, underlyingSource, {
      start(ctlr) {
        controller = ctlr;
        return start && start(ctlr);
      },
    });
    const stream = this.newReadableStream(underlyingSource);
    return {stream, controller};
  }

  async testEnqueue_ThenRead() {
    const {stream, controller} = this.newReadableStreamWithController();
    const chunk = 'foo';
    controller.enqueue(chunk);
    const reader = stream.getReader();
    const readResult = await reader.read();
    assertFalse(readResult.done);
    assertEquals(chunk, readResult.value);
  }

  testEnqueue_Closed() {
    const {controller} = this.newReadableStreamWithController();
    controller.close();
    assertThrows(() => {
      controller.enqueue('foo');
    });
  }

  testEnqueue_Closing() {
    const {controller} = this.newReadableStreamWithController();
    controller.enqueue('foo');
    controller.close();
    assertThrows(() => {
      controller.enqueue('bar');
    });
  }

  testEnqueue_Errored() {
    const {controller} = this.newReadableStreamWithController();
    controller.error(new Error('error'));
    assertThrows(() => {
      controller.enqueue('foo');
    });
  }

  async testRead_ThenEnqueue() {
    const {stream, controller} = this.newReadableStreamWithController();
    const chunk = 'foo';
    const reader = stream.getReader();
    const read = reader.read();
    controller.enqueue(chunk);
    const readResult = await read;
    assertFalse(readResult.done);
    assertEquals(chunk, readResult.value);
  }

  async testRead_Closed() {
    const {stream, controller} = this.newReadableStreamWithController();
    controller.close();
    const reader = stream.getReader();
    const readResult = await reader.read();
    assertTrue(readResult.done);
    assertUndefined(readResult.value);
  }

  async testRead_Closing() {
    const {stream, controller} = this.newReadableStreamWithController();
    const chunk = 'foo';
    controller.enqueue(chunk);
    controller.close();
    const reader = stream.getReader();
    let readResult = await reader.read();
    assertFalse(readResult.done);
    assertEquals(chunk, readResult.value);
    readResult = await reader.read();
    assertTrue(readResult.done);
    assertUndefined(readResult.value);
  }

  async testRead_Errored() {
    const {stream, controller} = this.newReadableStreamWithController();
    const error = new Error('error');
    controller.error(error);
    const reader = stream.getReader();
    const rejectedError = await assertRejects(reader.read());
    assertEquals(error, rejectedError);
  }

  async testRead_ThenClosed() {
    const {stream, controller} = this.newReadableStreamWithController();
    const reader = stream.getReader();
    const read = reader.read();
    controller.close();
    const readResult = await read;
    assertTrue(readResult.done);
    assertUndefined(readResult.value);
  }

  async testRead_ThenErrored() {
    const {stream, controller} = this.newReadableStreamWithController();
    const error = new Error('error');
    const reader = stream.getReader();
    const read = reader.read();
    controller.error(error);
    const rejectedError = await assertRejects(read);
    assertEquals(error, rejectedError);
  }

  testClose_Closed() {
    const {controller} = this.newReadableStreamWithController();
    controller.close();
    assertThrows(() => {
      controller.close();
    });
  }

  testClose_Closing() {
    const {controller} = this.newReadableStreamWithController();
    controller.enqueue('foo');
    controller.close();
    assertThrows(() => {
      controller.close();
    });
  }

  async testLocked() {
    const {stream, controller} = this.newReadableStreamWithController();
    assertFalse(stream.locked);
    const reader = stream.getReader();
    assertTrue(stream.locked);
    reader.releaseLock();
    assertFalse(stream.locked);
  }

  testLocked_Closed() {
    const {stream, controller} = this.newReadableStreamWithController();
    const reader = stream.getReader();
    assertTrue(stream.locked);
    controller.close();
    assertTrue(stream.locked);
    reader.releaseLock();
    assertFalse(stream.locked);
  }

  testLocked_Closing() {
    const {stream, controller} = this.newReadableStreamWithController();
    const reader = stream.getReader();
    controller.enqueue('foo');
    assertTrue(stream.locked);
    controller.close();
    assertTrue(stream.locked);
    reader.releaseLock();
    assertFalse(stream.locked);
  }

  testLocked_Errored() {
    const {stream, controller} = this.newReadableStreamWithController();
    const reader = stream.getReader();
    assertTrue(stream.locked);
    controller.error(new Error('error'));
    assertTrue(stream.locked);
    reader.releaseLock();
    assertFalse(stream.locked);
  }

  async testClosed_Close() {
    const {stream, controller} = this.newReadableStreamWithController();
    controller.close();
    const reader = stream.getReader();
    const closed = reader.closed;
    const closedResult = await closed;
    assertUndefined(closedResult);
  }

  async testClosed_ThenClosed() {
    const {stream, controller} = this.newReadableStreamWithController();
    const reader = stream.getReader();
    const closed = reader.closed;
    controller.close();
    const closedResult = await closed;
    assertUndefined(closedResult);
  }

  async testClosed_Closing() {
    const {stream, controller} = this.newReadableStreamWithController();
    controller.enqueue('foo');
    controller.close();
    const reader = stream.getReader();
    const closed = reader.closed;
    await reader.read();
    const closedResult = await closed;
    assertUndefined(closedResult);
  }

  async testClosed_ThenClosing() {
    const {stream, controller} = this.newReadableStreamWithController();
    const reader = stream.getReader();
    const closed = reader.closed;
    controller.enqueue('foo');
    controller.close();
    await reader.read();
    const closedResult = await closed;
    assertUndefined(closedResult);
  }

  async testClosed_Errored() {
    const {stream, controller} = this.newReadableStreamWithController();
    const error = new Error('error');
    controller.error(error);
    const reader = stream.getReader();
    const rejectedError = await assertRejects(reader.closed);
    assertEquals(error, rejectedError);
  }

  async testClosed_ThenErrored() {
    const {stream, controller} = this.newReadableStreamWithController();
    const reader = stream.getReader();
    const closed = reader.closed;
    controller.error(new Error('error'));
    await assertRejects(closed);
  }

  async testClosed_ThenReleaseLock() {
    const {stream} = this.newReadableStreamWithController();
    const reader = stream.getReader();
    const closed = reader.closed;
    reader.releaseLock();
    await assertRejects(closed);
  }

  testGetReader_WhileLocked() {
    const {stream} = this.newReadableStreamWithController();
    stream.getReader();
    assertThrows(() => {
      stream.getReader();
    });
  }

  async testReleaseLock_WhileOutstandingReads() {
    const {stream} = this.newReadableStreamWithController();
    const reader = stream.getReader();
    const readResult = reader.read();
    // NOTE: Older browsers (i.e. Chrome < 106, Safari, Firefox) used to have
    // releaseLock() immediately throw if a read is outstanding, but this is
    // contrary to the spec.  The spec requires that instead the releaseLock()
    // call should succeed, but the promise returned from read() should
    // immediately reject.  The following structure checks for either behavior.
    await assertRejects((async () => {
      // Await a meaningless value to wait for a new microtask (ensuring
      // synchronous throws are converted to a rejected promise instead).
      await 0;
      // In older browsers, this will throw synchronously.  Chrome >= 106 does
      // not throw here, but instead causes readResult to immediately reject.
      reader.releaseLock();
      // This will only reject if readResult is already rejected.
      await Promise.race([readResult, Promise.resolve()]);
    })());
  }

  testReleaseLock_Released() {
    const {stream} = this.newReadableStreamWithController();
    const reader = stream.getReader();
    reader.releaseLock();
    reader.releaseLock();
  }

  async testStart_RejectedPromise() {
    const error = new Error('error');
    const stream = this.newReadableStream({
      start() {
        return Promise.reject(error);
      }
    });
    const reader = stream.getReader();
    const rejectedError = await assertRejects(reader.read());
    assertEquals(error, rejectedError);
  }
}

exports = {
  TestCases,
};
