// Copyright 2018-2026 the Deno authors. MIT license.

import { assertEquals, assertRejects, assertThrows } from "@std/assert";
import { toTransformStream } from "./to_transform_stream.ts";

Deno.test({
  name: "toTransformStream()",
  async fn() {
    const readable = ReadableStream.from([0, 1, 2])
      .pipeThrough(toTransformStream(async function* (src) {
        for await (const i of src) {
          yield i * 100;
        }
      }));

    const res = await Array.fromAsync(readable);
    assertEquals(res, [0, 100, 200]);
  },
});

Deno.test({
  name: "toTransformStream() handles iterable instead of asyncIterable",
  async fn() {
    const readable = ReadableStream.from([0, 1, 2])
      .pipeThrough(toTransformStream(function* (_src) {
        yield 0;
        yield 100;
        yield 200;
      }));

    const res = await Array.fromAsync(readable);
    assertEquals(res, [0, 100, 200]);
  },
});

Deno.test({
  name: "toTransformStream() throws if transformer returns non-iterable",
  fn() {
    assertThrows(
      () => {
        // @ts-expect-error Testing invalid input
        toTransformStream(() => null);
      },
      TypeError,
      "Transformer must return an iterable or async iterable",
    );

    assertThrows(
      () => {
        // @ts-expect-error Testing invalid input
        toTransformStream(() => 123);
      },
      TypeError,
      "Transformer must return an iterable or async iterable",
    );

    assertThrows(
      () => {
        // @ts-expect-error Testing invalid input
        toTransformStream(() => ({ notAnIterator: true }));
      },
      TypeError,
      "Transformer must return an iterable or async iterable",
    );
  },
});

Deno.test({
  name: "toTransformStream() propagates the error from readable 1",
  async fn(t) {
    // When data is pipelined in the order of readable1 → generator → readable2,
    // Propagate the error that occurred in readable1 to generator and readable2.
    const expectedError = new Error("Error from readable1");
    await t.step({
      name: "to readable 2",
      async fn() {
        // Propagate the error that occurred in readable1 to readable2.
        let actualError = null;

        const readable1 = new ReadableStream({
          start(controller) {
            controller.error(expectedError); // error from readable1
          },
        });
        const readable2 = readable1.pipeThrough(
          toTransformStream(async function* (src) {
            for await (const i of src) {
              yield i;
            }
          }),
        );

        try {
          await readable2.getReader().read();
        } catch (error) {
          actualError = error; // catch error in readable2
        }

        assertEquals(actualError, expectedError);
      },
    });
    await t.step({
      name: "to generator",
      async fn() {
        // Propagate the error that occurred in readable1 to generator.
        let actualError = null;

        const readable1 = new ReadableStream({
          start(controller) {
            controller.error(expectedError); // error from readable1
          },
        });
        const readable2 = readable1.pipeThrough(
          toTransformStream(async function* (src) {
            try {
              await src.getReader().read();
            } catch (error) {
              actualError = error; // catch error in generator
            }
            yield 0;
          }),
        );

        await readable2.getReader().read();
        assertEquals(actualError, expectedError);
      },
    });
  },
});

Deno.test({
  name: "toTransformStream() propagates the error from generator",
  async fn(t) {
    // When data is pipelined in the order of readable1 → generator → readable2,
    // Propagate the error that occurred in generator to readable2 and readable1.
    const expectedError = new Error("Error from generator");
    let actualError1: unknown = null;
    let actualError2: unknown = null;

    const readable1 = new ReadableStream({
      cancel(reason) {
        actualError1 = reason; // catch error in readable1
      },
    });
    const readable2 = readable1.pipeThrough(
      // deno-lint-ignore require-yield
      toTransformStream(function* () {
        throw expectedError; // error from generator
      }),
    );

    try {
      await readable2.getReader().read();
    } catch (error) {
      actualError2 = error; // catch error in readable2
    }

    await t.step({
      name: "to readable 1",
      fn() {
        assertEquals(actualError1, expectedError);
      },
    });
    await t.step({
      name: "to readable 2",
      fn() {
        assertEquals(actualError2, expectedError);
      },
    });
  },
});

Deno.test({
  name: "toTransformStream() propagates cancellation from readable 2",
  async fn(t) {
    // When data is pipelined in the order of readable1 → generator → readable2,
    // Propagate the cancellation that occurred in readable2 to readable1 and generator.
    const expectedError = new Error("Error from readable2");
    await t.step({
      name: "to readable 1",
      async fn() {
        let actualError = null;

        const readable1 = new ReadableStream({
          cancel(reason) {
            actualError = reason; // catch error in readable1
          },
        });
        const readable2 = readable1.pipeThrough(
          toTransformStream(function* () {
            yield 0;
          }),
        );

        await readable2.cancel(expectedError); // cancellation from readable2
        assertEquals(actualError, expectedError);
      },
    });
    await t.step({
      name: "to readable 2",
      async fn() {
        let actualError = null;

        const readable1 = new ReadableStream();
        const readable2 = readable1.pipeThrough(
          toTransformStream(function* () {
            try {
              yield 0;
            } catch (error) {
              actualError = error; // catch error in generator
            }
          }),
        );

        const reader = readable2.getReader();
        await reader.read();
        await reader.cancel(expectedError); // cancellation from readable2
        assertEquals(actualError, expectedError);
      },
    });
  },
});

Deno.test({
  name:
    "toTransformStream() handles streams cancel with the correct error message",
  async fn() {
    const src = ReadableStream.from([0, 1, 2]);
    // deno-lint-ignore require-yield
    const transform = toTransformStream(function* (src) {
      src.getReader(); // lock the source stream to cause error at cancel
      throw new Error("foo");
    });

    await assertRejects(
      async () => await Array.fromAsync(src.pipeThrough(transform)),
      Error,
      "foo",
    );
  },
});

Deno.test({
  name: "toTransformStream() executes finally blocks on cancellation",
  async fn() {
    let finallyCalled = false;

    const readable = ReadableStream.from([1, 2, 3])
      .pipeThrough(toTransformStream(async function* (src) {
        try {
          for await (const chunk of src) {
            yield chunk;
          }
        } finally {
          finallyCalled = true;
        }
      }));

    const reader = readable.getReader();
    await reader.read(); // Read first chunk
    await reader.cancel("test cancellation");

    assertEquals(finallyCalled, true);
  },
});
