// Copyright 2018-2026 the Deno authors. MIT license.
import {
  assert,
  assertEquals,
  assertRejects,
  assertThrows,
} from "./test_util.ts";

async function readTextStream(stream: ReadableStream<string>): Promise<string> {
  const reader = stream.getReader();
  let result = "";
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    result += value;
  }
  return result;
}

// just a hack to get a body object
// deno-lint-ignore no-explicit-any
function buildBody(body: any, headers?: Headers): Body {
  const stub = new Request("http://foo/", {
    body: body,
    headers,
    method: "POST",
  });
  return stub as Body;
}

const intArrays = [
  Int8Array,
  Int16Array,
  Int32Array,
  Uint8Array,
  Uint16Array,
  Uint32Array,
  Uint8ClampedArray,
  Float32Array,
  Float64Array,
];
Deno.test(async function arrayBufferFromByteArrays() {
  const buffer = new TextEncoder().encode("ahoyhoy8").buffer;

  for (const type of intArrays) {
    const body = buildBody(new type(buffer as ArrayBuffer));
    const text = new TextDecoder("utf-8").decode(await body.arrayBuffer());
    assertEquals(text, "ahoyhoy8");
  }
});

// Regression test: set/delete on a FormData with many entries sharing a name
// must run in linear time. A quadratic implementation would take many seconds
// (or minutes) at this size; the linear implementation finishes in
// milliseconds. The 5s bound is intentionally loose to avoid CI flakes.
Deno.test(function formDataSetDeleteManyDuplicatesIsLinear() {
  const N = 20_000;

  const a = new FormData();
  for (let i = 0; i < N; i++) a.append("dup", String(i));
  const t1 = performance.now();
  a.delete("dup");
  assert(performance.now() - t1 < 5_000);
  assertEquals(a.get("dup"), null);

  const b = new FormData();
  for (let i = 0; i < N; i++) b.append("dup", String(i));
  const t2 = performance.now();
  b.set("dup", "final");
  assert(performance.now() - t2 < 5_000);
  assertEquals(b.get("dup"), "final");
  assertEquals(b.getAll("dup").length, 1);
});

//FormData
Deno.test(
  { permissions: { net: true } },
  async function bodyMultipartFormData() {
    const response = await fetch(
      "http://localhost:4545/multipart_form_data.txt",
    );
    assert(response.body instanceof ReadableStream);

    const text = await response.text();

    const body = buildBody(text, response.headers);

    const formData = await body.formData();
    assert(formData.has("field_1"));
    assertEquals(formData.get("field_1")!.toString(), "value_1 \r\n");
    assert(formData.has("field_2"));
  },
);

// FormData: non-ASCII names and filenames
Deno.test(
  { permissions: { net: true } },
  async function bodyMultipartFormDataNonAsciiNames() {
    const boundary = "----01230123";
    const payload = [
      `--${boundary}`,
      `Content-Disposition: form-data; name="文字"`,
      "",
      "文字",
      `--${boundary}`,
      `Content-Disposition: form-data; name="file"; filename="文字"`,
      "Content-Type: application/octet-stream",
      "",
      "",
      `--${boundary}--`,
    ].join("\r\n");

    const body = buildBody(
      new TextEncoder().encode(payload),
      new Headers({
        "Content-Type": `multipart/form-data; boundary=${boundary}`,
      }),
    );

    const formData = await body.formData();
    assert(formData.has("文字"));
    assertEquals(formData.get("文字"), "文字");
    assert(formData.has("file"));
    assert(formData.get("file") instanceof File);
    assertEquals((formData.get("file") as File).name, "文字");
  },
);

// FormData: non-ASCII names and filenames roundtrip
Deno.test(
  { permissions: { net: true } },
  async function bodyMultipartFormDataNonAsciiRoundtrip() {
    const inFormData = new FormData();
    inFormData.append("文字", "文字");
    inFormData.append("file", new File([], "文字"));

    const body = buildBody(inFormData);

    const formData = await body.formData();
    assert(formData.has("文字"));
    assertEquals(formData.get("文字"), "文字");
    assert(formData.has("file"));
    assert(formData.get("file") instanceof File);
    assertEquals((formData.get("file") as File).name, "文字");
  },
);

Deno.test(async function bodyMultipartFormDataEmptyFilenameIsFile() {
  const boundary = "AaB03x";
  const payload = [
    `--${boundary}`,
    'Content-Disposition: form-data; name="files"; filename=""',
    "Content-Type: application/octet-stream",
    "",
    "",
    `--${boundary}`,
    'Content-Disposition: form-data; name="files"; filename=""',
    "Content-Type: text/plain",
    "",
    "file contents",
    `--${boundary}--`,
  ].join("\r\n");

  const body = buildBody(
    new TextEncoder().encode(payload),
    new Headers({
      "Content-Type": `multipart/form-data; boundary=${boundary}`,
    }),
  );

  const formData = await body.formData();
  const files = formData.getAll("files");
  assertEquals(files.length, 2);

  const emptyFile = files[0];
  if (!(emptyFile instanceof File)) {
    throw new Error("Expected empty filename part to parse as a File");
  }
  assertEquals(emptyFile.name, "");
  assertEquals(emptyFile.size, 0);
  assertEquals(await emptyFile.text(), "");

  const fileWithContents = files[1];
  if (!(fileWithContents instanceof File)) {
    throw new Error(
      "Expected empty filename part with body to parse as a File",
    );
  }
  assertEquals(fileWithContents.name, "");
  assertEquals(fileWithContents.size, 13);
  assertEquals(await fileWithContents.text(), "file contents");
});

Deno.test(
  { permissions: { net: true } },
  async function bodyURLEncodedFormData() {
    const response = await fetch(
      "http://localhost:4545/subdir/form_urlencoded.txt",
    );
    assert(response.body instanceof ReadableStream);

    const text = await response.text();

    const body = buildBody(text, response.headers);

    const formData = await body.formData();
    assert(formData.has("field_1"));
    assertEquals(formData.get("field_1")!.toString(), "Hi");
    assert(formData.has("field_2"));
    assertEquals(formData.get("field_2")!.toString(), "<Deno>");
  },
);

Deno.test({ permissions: {} }, async function bodyURLSearchParams() {
  const body = buildBody(new URLSearchParams({ hello: "world" }));

  const text = await body.text();
  assertEquals(text, "hello=world");
});

Deno.test(async function bodyArrayBufferMultipleParts() {
  const parts: Uint8Array[] = [];
  let size = 0;
  for (let i = 0; i <= 150000; i++) {
    const part = new Uint8Array([1]);
    parts.push(part);
    size += part.length;
  }

  let offset = 0;
  const stream = new ReadableStream({
    pull(controller) {
      // parts.shift() takes forever: https://github.com/denoland/deno/issues/5259
      const chunk = parts[offset++];
      if (!chunk) return controller.close();
      controller.enqueue(chunk);
    },
  });

  const body = buildBody(stream);
  assertEquals((await body.arrayBuffer()).byteLength, size);
});

// https://github.com/denoland/deno/issues/20793
Deno.test(
  { permissions: { net: true } },
  async function bodyMultipartFormDataMultipleHeaders() {
    const boundary = "----formdata-polyfill-0.970665446687947";
    const payload = [
      "------formdata-polyfill-0.970665446687947",
      'Content-Disposition: form-data; name="x"; filename="blob"',
      "Content-Length: 1",
      "Content-Type: application/octet-stream",
      "last-modified: Wed, 04 Oct 2023 20:28:45 GMT",
      "",
      "y",
      "------formdata-polyfill-0.970665446687947--",
    ].join("\r\n");

    const body = buildBody(
      new TextEncoder().encode(payload),
      new Headers({
        "Content-Type": `multipart/form-data; boundary=${boundary}`,
      }),
    );

    const formData = await body.formData();
    const file = formData.get("x");
    assert(file instanceof File);
    const text = await file.text();
    assertEquals(text, "y");
    assertEquals(file.size, 1);
  },
);

Deno.test(async function bodyMultipartFormDataRejectsTooManyPartHeaders() {
  const boundary = "AaB03x";
  const partHeaders = [
    'Content-Disposition: form-data; name="field"',
  ];
  for (let i = 0; i < 128; i++) {
    partHeaders.push(`X-Deno-Test-${i}: ${i}`);
  }
  const payload = [
    `--${boundary}`,
    ...partHeaders,
    "",
    "value",
    `--${boundary}--`,
  ].join("\r\n");

  const body = buildBody(
    new TextEncoder().encode(payload),
    new Headers({
      "Content-Type": `multipart/form-data; boundary=${boundary}`,
    }),
  );

  await assertRejects(
    () => body.formData(),
    TypeError,
    "too many headers",
  );
});

Deno.test(async function bodyMultipartFormDataRejectsLargePartHeaders() {
  const boundary = "AaB03x";
  const payload = [
    `--${boundary}`,
    'Content-Disposition: form-data; name="field"',
    `X-Deno-Large: ${"a".repeat(16 * 1024)}`,
    "",
    "value",
    `--${boundary}--`,
  ].join("\r\n");

  const body = buildBody(
    new TextEncoder().encode(payload),
    new Headers({
      "Content-Type": `multipart/form-data; boundary=${boundary}`,
    }),
  );

  await assertRejects(
    () => body.formData(),
    TypeError,
    "headers are too large",
  );
});

Deno.test(
  async function bodyMultipartFormDataEmbeddedBoundaryTextInFileContent() {
    const boundary = "AaB03x";
    const payload = [
      `--${boundary}`,
      'Content-Disposition: form-data; name="file"; filename="x.txt"',
      "Content-Type: text/plain",
      "",
      `before--${boundary}after`,
      `--${boundary}--`,
    ].join("\r\n");

    const body = buildBody(
      new TextEncoder().encode(payload),
      new Headers({
        "Content-Type": `multipart/form-data; boundary=${boundary}`,
      }),
    );

    const formData = await body.formData();
    const file = formData.get("file");
    assert(file instanceof File);
    assertEquals(await file.text(), `before--${boundary}after`);
  },
);

Deno.test(
  async function bodyMultipartFormDataBoundaryPrefixLineInFileContent() {
    const boundary = "AaB03x";
    const payload = [
      `--${boundary}`,
      'Content-Disposition: form-data; name="file"; filename="x.txt"',
      "Content-Type: text/plain",
      "",
      "before",
      `--${boundary}x`,
      "after",
      `--${boundary}--`,
    ].join("\r\n");

    const body = buildBody(
      new TextEncoder().encode(payload),
      new Headers({
        "Content-Type": `multipart/form-data; boundary=${boundary}`,
      }),
    );

    const formData = await body.formData();
    const file = formData.get("file");
    assert(file instanceof File);
    assertEquals(await file.text(), `before\r\n--${boundary}x\r\nafter`);
  },
);

Deno.test(
  async function bodyMultipartFormDataSkipsNamelessPartAndParsesNextPart() {
    const boundary = "AaB03x";
    const payload = [
      `--${boundary}`,
      'Content-Disposition: form-data; filename="skip.txt"',
      "Content-Type: text/plain",
      "",
      "skip me",
      `--${boundary}`,
      'Content-Disposition: form-data; name="field"',
      "",
      "value",
      `--${boundary}--`,
    ].join("\r\n");

    const body = buildBody(
      new TextEncoder().encode(payload),
      new Headers({
        "Content-Type": `multipart/form-data; boundary=${boundary}`,
      }),
    );

    const formData = await body.formData();
    assertEquals(formData.get("field"), "value");
  },
);

Deno.test(async function responseTextStream() {
  const text = "Hello 👋 wörld";
  const stream = new Response(text).textStream();
  assert(stream instanceof ReadableStream);
  assertEquals(await readTextStream(stream), text);
});

Deno.test(async function requestTextStream() {
  const text = "Hello 👋 wörld";
  const stream = new Request("http://foo/", { method: "POST", body: text })
    .textStream();
  assert(stream instanceof ReadableStream);
  assertEquals(await readTextStream(stream), text);
});

Deno.test(async function textStreamNullBody() {
  const stream = new Response(null).textStream();
  assert(stream instanceof ReadableStream);
  assertEquals(await readTextStream(stream), "");

  const stream2 = new Response().textStream();
  assert(stream2 instanceof ReadableStream);
  assertEquals(await readTextStream(stream2), "");
});

Deno.test(async function textStreamConsumedBodyThrows() {
  const response = new Response("already consumed");
  await response.text();
  assertThrows(
    () => response.textStream(),
    TypeError,
    "Body already consumed.",
  );
});

Deno.test(async function bodyBadResourceError() {
  const file = await Deno.open("README.md");
  file.close();
  const body = buildBody(file.readable);
  await assertRejects(
    () => body.arrayBuffer(),
    Deno.errors.BadResource,
    "Cannot read body as underlying resource unavailable",
  );
});
