// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
import { unitTest, assert, assertEquals, assertThrows } from "./test_util.ts";

unitTest(function writableStreamDesiredSizeOnReleasedWriter() {
  const ws = new WritableStream();
  const writer = ws.getWriter();
  writer.releaseLock();
  assertThrows(() => {
    writer.desiredSize;
  }, TypeError);
});

unitTest(function writableStreamDesiredSizeInitialValue() {
  const ws = new WritableStream();
  const writer = ws.getWriter();
  assertEquals(writer.desiredSize, 1);
});

unitTest(async function writableStreamDesiredSizeClosed() {
  const ws = new WritableStream();
  const writer = ws.getWriter();
  await writer.close();
  assertEquals(writer.desiredSize, 0);
});

unitTest(function writableStreamStartThrowsDesiredSizeNull() {
  const ws = new WritableStream({
    start(c): void {
      c.error();
    },
  });

  const writer = ws.getWriter();
  assertEquals(writer.desiredSize, null, "desiredSize should be null");
});

unitTest(function getWriterOnClosingStream() {
  const ws = new WritableStream({});

  const writer = ws.getWriter();
  writer.close();
  writer.releaseLock();

  ws.getWriter();
});

unitTest(async function getWriterOnClosedStream() {
  const ws = new WritableStream({});

  const writer = ws.getWriter();
  await writer.close();
  writer.releaseLock();

  ws.getWriter();
});

unitTest(function getWriterOnAbortedStream() {
  const ws = new WritableStream({});

  const writer = ws.getWriter();
  writer.abort();
  writer.releaseLock();

  ws.getWriter();
});

unitTest(function getWriterOnErroredStream() {
  const ws = new WritableStream({
    start(c): void {
      c.error();
    },
  });

  const writer = ws.getWriter();
  return writer.closed.then(
    (v) => {
      throw new Error(`writer.closed fulfilled unexpectedly with: ${v}`);
    },
    () => {
      writer.releaseLock();
      ws.getWriter();
    },
  );
});

unitTest(function closedAndReadyOnReleasedWriter() {
  const ws = new WritableStream({});

  const writer = ws.getWriter();
  writer.releaseLock();

  return writer.closed.then(
    (v) => {
      throw new Error("writer.closed fulfilled unexpectedly with: " + v);
    },
    (closedRejection) => {
      assertEquals(
        closedRejection.name,
        "TypeError",
        "closed promise should reject with a TypeError",
      );
      return writer.ready.then(
        (v) => {
          throw new Error("writer.ready fulfilled unexpectedly with: " + v);
        },
        (readyRejection) =>
          assertEquals(
            readyRejection,
            closedRejection,
            "ready promise should reject with the same error",
          ),
      );
    },
  );
});

unitTest(function sinkMethodsCalledAsMethods() {
  let thisObject: Sink | null = null;
  // Calls to Sink methods after the first are implicitly ignored. Only the
  // first value that is passed to the resolver is used.
  class Sink {
    start(): void {
      assertEquals(this, thisObject, "start should be called as a method");
    }

    write(): void {
      assertEquals(this, thisObject, "write should be called as a method");
    }

    close(): void {
      assertEquals(this, thisObject, "close should be called as a method");
    }

    abort(): void {
      assertEquals(this, thisObject, "abort should be called as a method");
    }
  }

  const theSink = new Sink();
  thisObject = theSink;
  const ws = new WritableStream(theSink);

  const writer = ws.getWriter();

  writer.write("a");
  const closePromise = writer.close();

  const ws2 = new WritableStream(theSink);
  const writer2 = ws2.getWriter();
  const abortPromise = writer2.abort();

  return Promise.all([closePromise, abortPromise]).then(undefined);
});

unitTest(function sizeShouldNotBeCalledAsMethod() {
  const strategy = {
    size(): number {
      if (this !== undefined) {
        throw new Error("size called as a method");
      }
      return 1;
    },
  };

  const ws = new WritableStream({}, strategy);
  const writer = ws.getWriter();
  return writer.write("a");
});

unitTest(function redundantReleaseLockIsNoOp() {
  const ws = new WritableStream();
  const writer1 = ws.getWriter();
  assertEquals(
    undefined,
    writer1.releaseLock(),
    "releaseLock() should return undefined",
  );
  const writer2 = ws.getWriter();
  assertEquals(
    undefined,
    writer1.releaseLock(),
    "no-op releaseLock() should return undefined",
  );
  // Calling releaseLock() on writer1 should not interfere with writer2. If it did, then the ready promise would be
  // rejected.
  return writer2.ready;
});

unitTest(function readyPromiseShouldFireBeforeReleaseLock() {
  const events: string[] = [];
  const ws = new WritableStream();
  const writer = ws.getWriter();
  return writer.ready.then(() => {
    // Force the ready promise back to a pending state.
    const writerPromise = writer.write("dummy");
    const readyPromise = writer.ready.catch(() => events.push("ready"));
    const closedPromise = writer.closed.catch(() => events.push("closed"));
    writer.releaseLock();
    return Promise.all([readyPromise, closedPromise]).then(() => {
      assertEquals(
        events,
        ["ready", "closed"],
        "ready promise should fire before closed promise",
      );
      // Stop the writer promise hanging around after the test has finished.
      return Promise.all([writerPromise, ws.abort()]).then(undefined);
    });
  });
});

unitTest(function subclassingWritableStream() {
  class Subclass extends WritableStream {
    extraFunction(): boolean {
      return true;
    }
  }
  assert(
    Object.getPrototypeOf(Subclass.prototype) === WritableStream.prototype,
    "Subclass.prototype's prototype should be WritableStream.prototype",
  );
  assert(
    Object.getPrototypeOf(Subclass) === WritableStream,
    "Subclass's prototype should be WritableStream",
  );
  const sub = new Subclass();
  assert(
    sub instanceof WritableStream,
    "Subclass object should be an instance of WritableStream",
  );
  assert(
    sub instanceof Subclass,
    "Subclass object should be an instance of Subclass",
  );
  const lockedGetter = Object.getOwnPropertyDescriptor(
    WritableStream.prototype,
    "locked",
  )!.get!;
  assert(
    lockedGetter.call(sub) === sub.locked,
    "Subclass object should pass brand check",
  );
  assert(
    sub.extraFunction(),
    "extraFunction() should be present on Subclass object",
  );
});

unitTest(function lockedGetterShouldReturnTrue() {
  const ws = new WritableStream();
  assert(!ws.locked, "stream should not be locked");
  ws.getWriter();
  assert(ws.locked, "stream should be locked");
});