import { file, gc, serve, ServeOptions } from "bun";
import { afterEach, describe, expect, it, test } from "bun:test";
import { readFileSync } from "fs";

// afterEach(() => Bun.gc(true));

var port = 4020;

{
  const BodyMixin = [
    Request.prototype.arrayBuffer,
    Request.prototype.blob,
    Request.prototype.text,
    Request.prototype.json,
  ];
  const useRequestObjectValues = [true, false];

  for (let RequestPrototyeMixin of BodyMixin) {
    for (let useRequestObject of useRequestObjectValues) {
      describe(`Request.prototoype.${RequestPrototyeMixin.name}() ${
        useRequestObject ? "fetch(req)" : "fetch(url)"
      }`, () => {
        const inputFixture = [
          [JSON.stringify("Hello World"), JSON.stringify("Hello World")],
          [
            JSON.stringify("Hello World 123"),
            Buffer.from(JSON.stringify("Hello World 123")).buffer,
          ],
          [
            JSON.stringify("Hello World 456"),
            Buffer.from(JSON.stringify("Hello World 456")),
          ],
          [
            JSON.stringify(
              "EXTREMELY LONG VERY LONG STRING WOW SO LONG YOU WONT BELIEVE IT! ".repeat(
                100
              )
            ),
            Buffer.from(
              JSON.stringify(
                "EXTREMELY LONG VERY LONG STRING WOW SO LONG YOU WONT BELIEVE IT! ".repeat(
                  100
                )
              )
            ),
          ],
          [
            JSON.stringify(
              "EXTREMELY LONG 🔥 UTF16 🔥 VERY LONG STRING WOW SO LONG YOU WONT BELIEVE IT! ".repeat(
                100
              )
            ),
            Buffer.from(
              JSON.stringify(
                "EXTREMELY LONG 🔥 UTF16 🔥 VERY LONG STRING WOW SO LONG YOU WONT BELIEVE IT! ".repeat(
                  100
                )
              )
            ),
          ],
        ];
        for (const [name, input] of inputFixture) {
          test(`${name.slice(
            0,
            Math.min(name.length ?? name.byteLength, 64)
          )}`, async () => {
            await runInServer(
              {
                async fetch(req) {
                  var result = await RequestPrototyeMixin.call(req);
                  if (RequestPrototyeMixin === Request.prototype.json) {
                    result = JSON.stringify(result);
                  }
                  if (typeof result === "string") {
                    expect(result.length).toBe(name.length);
                    expect(result).toBe(name);
                  } else if (result && result instanceof Blob) {
                    expect(result.size).toBe(
                      new TextEncoder().encode(name).byteLength
                    );
                    expect(await result.text()).toBe(name);
                  } else {
                    expect(result.byteLength).toBe(
                      Buffer.from(input).byteLength
                    );
                    expect(Bun.SHA1.hash(result, "base64")).toBe(
                      Bun.SHA1.hash(input, "base64")
                    );
                  }
                  return new Response(result, {
                    headers: req.headers,
                  });
                },
              },
              async (url) => {
                var response;

                // once, then batch of 5

                if (useRequestObject) {
                  response = await fetch(
                    new Request({
                      body: input,
                      method: "POST",
                      url: url,
                      headers: {
                        "content-type": "text/plain",
                      },
                    })
                  );
                } else {
                  response = await fetch(url, {
                    body: input,
                    method: "POST",
                    headers: {
                      "content-type": "text/plain",
                    },
                  });
                }

                expect(response.status).toBe(200);
                expect(response.headers.get("content-length")).toBe(
                  String(Buffer.from(input).byteLength)
                );
                expect(response.headers.get("content-type")).toBe("text/plain");
                expect(await response.text()).toBe(name);

                var promises = new Array(5);
                for (let i = 0; i < 5; i++) {
                  if (useRequestObject) {
                    promises[i] = await fetch(
                      new Request({
                        body: input,
                        method: "POST",
                        url: url,
                        headers: {
                          "content-type": "text/plain",
                          "x-counter": i,
                        },
                      })
                    );
                  } else {
                    promises[i] = await fetch(url, {
                      body: input,
                      method: "POST",
                      headers: {
                        "content-type": "text/plain",
                        "x-counter": i,
                      },
                    });
                  }
                }

                const results = await Promise.all(promises);
                for (let i = 0; i < 5; i++) {
                  const response = results[i];
                  expect(response.status).toBe(200);
                  expect(response.headers.get("content-length")).toBe(
                    String(Buffer.from(input).byteLength)
                  );
                  expect(response.headers.get("content-type")).toBe(
                    "text/plain"
                  );
                  expect(response.headers.get("x-counter")).toBe(String(i));
                  expect(await response.text()).toBe(name);
                }
              }
            );
          });
        }
      });
    }
  }
}

async function runInServer(
  opts: ServeOptions,
  cb: (url: string) => void | Promise<void>
) {
  var server;
  server = Bun.serve({
    ...opts,
    port: port++,
    fetch(req) {
      try {
        return opts.fetch(req);
      } catch (e) {
        console.error(e.message);
        console.log(e.stack);
        throw e;
      }
    },
    error(err) {
      console.log(err.message);
      console.log(err.stack);
      throw err;
    },
  });
  try {
    await cb(`http://${server.hostname}:${server.port}`);
  } catch (e) {
    throw e;
  } finally {
    server && server.stop();
    server = undefined;
    if (port > 4200) {
      port = 4120;
    }
  }
}

function fillRepeating(dstBuffer, start, end) {
  let len = dstBuffer.length,
    sLen = end - start,
    p = sLen;
  while (p < len) {
    if (p + sLen > len) sLen = len - p;
    dstBuffer.copyWithin(p, start, sLen);
    p += sLen;
    sLen <<= 1;
  }
}

function gc() {
  Bun.gc(true);
}

describe("reader", function () {
  try {
    // - empty
    // - 1 byte
    // - less than the InlineBlob limit
    // - multiple chunks
    // - backpressure
    for (let inputLength of [
      0,
      1,
      2,
      12,
      95,
      1024,
      1024 * 1024,
      1024 * 1024 * 2,
    ]) {
      var bytes = new Uint8Array(inputLength);
      {
        const chunk = Math.min(bytes.length, 256);
        for (var i = 0; i < chunk; i++) {
          bytes[i] = 255 - i;
        }
      }

      if (bytes.length > 255) fillRepeating(bytes, 0, bytes.length);

      for (const huge_ of [
        bytes,
        bytes.buffer,
        new DataView(bytes.buffer),
        new Int8Array(bytes),
        new Blob([bytes]),

        new Uint16Array(bytes),
        new Uint32Array(bytes),
        new Float64Array(bytes),

        new Int16Array(bytes),
        new Int32Array(bytes),
        new Float32Array(bytes),

        // make sure we handle subarray() as expected when reading
        // typed arrays from native code
        new Int16Array(bytes).subarray(1),
        new Int16Array(bytes).subarray(0, new Int16Array(bytes).byteLength - 1),
        new Int32Array(bytes).subarray(1),
        new Int32Array(bytes).subarray(0, new Int32Array(bytes).byteLength - 1),
        new Float32Array(bytes).subarray(1),
        new Float32Array(bytes).subarray(
          0,
          new Float32Array(bytes).byteLength - 1
        ),
        new Int16Array(bytes).subarray(0, 1),
        new Int32Array(bytes).subarray(0, 1),
        new Float32Array(bytes).subarray(0, 1),
      ]) {
        gc();
        const thisArray = huge_;
        it(`works with ${thisArray.constructor.name}(${
          thisArray.byteLength ?? thisArray.size
        }:${inputLength}) via req.body.getReader() in chunks`, async () => {
          var huge = thisArray;
          var called = false;
          gc();

          const expectedHash =
            huge instanceof Blob
              ? Bun.SHA1.hash(
                  new Uint8Array(await huge.arrayBuffer()),
                  "base64"
                )
              : Bun.SHA1.hash(huge, "base64");
          const expectedSize =
            huge instanceof Blob ? huge.size : huge.byteLength;

          const out = await runInServer(
            {
              async fetch(req) {
                try {
                  expect(req.headers.get("x-custom")).toBe("hello");
                  expect(req.headers.get("content-type")).toBe("text/plain");
                  expect(req.headers.get("user-agent")).toBe(
                    navigator.userAgent
                  );

                  gc();
                  expect(req.headers.get("x-custom")).toBe("hello");
                  expect(req.headers.get("content-type")).toBe("text/plain");
                  expect(req.headers.get("user-agent")).toBe(
                    navigator.userAgent
                  );

                  var reader = req.body.getReader();
                  called = true;
                  var buffers = [];
                  while (true) {
                    var { done, value } = await reader.read();
                    if (done) break;
                    buffers.push(value);
                  }
                  const out = new Blob(buffers);
                  gc();
                  expect(out.size).toBe(expectedSize);
                  expect(Bun.SHA1.hash(await out.arrayBuffer(), "base64")).toBe(
                    expectedHash
                  );
                  expect(req.headers.get("x-custom")).toBe("hello");
                  expect(req.headers.get("content-type")).toBe("text/plain");
                  expect(req.headers.get("user-agent")).toBe(
                    navigator.userAgent
                  );
                  gc();
                  return new Response(out, {
                    headers: req.headers,
                  });
                } catch (e) {
                  console.error(e);
                  throw e;
                }
              },
            },
            async (url) => {
              gc();
              const response = await fetch(url, {
                body: huge,
                method: "POST",
                headers: {
                  "content-type": "text/plain",
                  "x-custom": "hello",
                  "x-typed-array": thisArray.constructor.name,
                },
              });
              huge = undefined;
              expect(response.status).toBe(200);
              const response_body = new Uint8Array(
                await response.arrayBuffer()
              );

              expect(response_body.byteLength).toBe(expectedSize);
              expect(Bun.SHA1.hash(response_body, "base64")).toBe(expectedHash);

              gc();
              expect(response.headers.get("content-type")).toBe("text/plain");
              gc();
            }
          );
          expect(called).toBe(true);
          gc();
          return out;
        });

        for (let isDirectStream of [true, false]) {
          const positions = ["begin", "end"];
          const inner = (thisArray) => {
            for (let position of positions) {
              it(`streaming back ${thisArray.constructor.name}(${
                thisArray.byteLength ?? thisArray.size
              }:${inputLength}) starting request.body.getReader() at ${position}`, async () => {
                var huge = thisArray;
                var called = false;
                gc();

                const expectedHash =
                  huge instanceof Blob
                    ? Bun.SHA1.hash(
                        new Uint8Array(await huge.arrayBuffer()),
                        "base64"
                      )
                    : Bun.SHA1.hash(huge, "base64");
                const expectedSize =
                  huge instanceof Blob ? huge.size : huge.byteLength;

                const out = await runInServer(
                  {
                    async fetch(req) {
                      try {
                        var reader;

                        if (position === "begin") {
                          reader = req.body.getReader();
                        }

                        if (position === "end") {
                          await 1;
                          reader = req.body.getReader();
                        }

                        expect(req.headers.get("x-custom")).toBe("hello");
                        expect(req.headers.get("content-type")).toBe(
                          "text/plain"
                        );
                        expect(req.headers.get("user-agent")).toBe(
                          navigator.userAgent
                        );

                        gc();
                        expect(req.headers.get("x-custom")).toBe("hello");
                        expect(req.headers.get("content-type")).toBe(
                          "text/plain"
                        );
                        expect(req.headers.get("user-agent")).toBe(
                          navigator.userAgent
                        );

                        const direct = {
                          type: "direct",
                          async pull(controller) {
                            while (true) {
                              const { done, value } = await reader.read();
                              if (done) {
                                called = true;
                                controller.end();

                                return;
                              }
                              controller.write(value);
                            }
                          },
                        };

                        const web = {
                          async pull(controller) {
                            while (true) {
                              const { done, value } = await reader.read();
                              if (done) {
                                called = true;
                                controller.close();
                                return;
                              }
                              controller.enqueue(value);
                            }
                          },
                        };

                        return new Response(
                          new ReadableStream(isDirectStream ? direct : web),
                          {
                            headers: req.headers,
                          }
                        );
                      } catch (e) {
                        console.error(e);
                        throw e;
                      }
                    },
                  },
                  async (url) => {
                    gc();
                    const response = await fetch(url, {
                      body: huge,
                      method: "POST",
                      headers: {
                        "content-type": "text/plain",
                        "x-custom": "hello",
                        "x-typed-array": thisArray.constructor.name,
                      },
                    });
                    huge = undefined;
                    expect(response.status).toBe(200);
                    const response_body = new Uint8Array(
                      await response.arrayBuffer()
                    );

                    expect(response_body.byteLength).toBe(expectedSize);
                    expect(Bun.SHA1.hash(response_body, "base64")).toBe(
                      expectedHash
                    );

                    gc();
                    if (!response.headers.has("content-type")) {
                      console.error(
                        Object.fromEntries(response.headers.entries())
                      );
                    }

                    expect(response.headers.get("content-type")).toBe(
                      "text/plain"
                    );
                    gc();
                  }
                );
                expect(called).toBe(true);
                gc();
                return out;
              });
            }
          };

          if (isDirectStream) {
            describe(" direct stream", () => inner(thisArray));
          } else {
            describe("default stream", () => inner(thisArray));
          }
        }
      }
    }
  } catch (e) {
    console.error(e);
    throw e;
  }
});
