import { Alepha, z } from "alepha";
import { $action, AlephaServer, ServerProvider } from "alepha/server";
import { describe, test } from "vitest";
import { multipartOptions } from "../providers/ServerMultipartProvider.ts";

class App {
  upload = $action({
    schema: {
      body: z.object({
        file: z.file(),
      }),
      response: z.text(),
    },
    handler: ({ body }) => {
      return `${body.file.name} (${body.file.size}b, ${body.file.type})`;
    },
  });
}

describe("ServerMultipartProvider", () => {
  test("should upload file via HTTP", async ({ expect }) => {
    const alepha = Alepha.create().with(AlephaServer).with(App);
    await alepha.start();

    const file = new File(["test content"], "test.txt", { type: "text/plain" });
    const body = new FormData();
    body.append("file", file);

    const resp = await fetch(
      `${alepha.inject(ServerProvider).hostname}/api/upload`,
      { method: "POST", body },
    );

    expect(resp.status).toBe(200);
    expect(await resp.text()).toBe("test.txt (12b, text/plain)");
  });

  test("should upload file locally via run()", async ({ expect }) => {
    const alepha = Alepha.create().with(AlephaServer).with(App);
    await alepha.start();

    const file = new File(["test content"], "test.txt", { type: "text/plain" });

    const result = await alepha.inject(App).upload.run({
      body: { file },
    });

    expect(result).toBe("test.txt (12b, text/plain)");
  });

  test("should read file content via text()", async ({ expect }) => {
    class TextApp {
      upload = $action({
        schema: {
          body: z.object({ file: z.file() }),
          response: z.text(),
        },
        handler: async ({ body }) => body.file.text(),
      });
    }

    const alepha = Alepha.create().with(AlephaServer).with(TextApp);
    await alepha.start();

    const file = new File(["hello world"], "hi.txt", { type: "text/plain" });
    const formData = new FormData();
    formData.append("file", file);

    const resp = await fetch(
      `${alepha.inject(ServerProvider).hostname}/api/upload`,
      { method: "POST", body: formData },
    );

    expect(await resp.text()).toBe("hello world");
  });

  test("should read file content via arrayBuffer()", async ({ expect }) => {
    class BufferApp {
      upload = $action({
        schema: {
          body: z.object({ file: z.file() }),
          response: z.text(),
        },
        handler: async ({ body }) => {
          const ab = await body.file.arrayBuffer();
          return new TextDecoder().decode(ab);
        },
      });
    }

    const alepha = Alepha.create().with(AlephaServer).with(BufferApp);
    await alepha.start();

    const file = new File(["buffer test"], "buf.txt", { type: "text/plain" });

    const result = await alepha.inject(BufferApp).upload.run({
      body: { file },
    });

    expect(result).toBe("buffer test");
  });

  test("should handle binary file uploads", async ({ expect }) => {
    class BinaryApp {
      upload = $action({
        schema: {
          body: z.object({ file: z.file() }),
          response: z.text(),
        },
        handler: async ({ body }) => {
          const ab = await body.file.arrayBuffer();
          const bytes = new Uint8Array(ab);
          return `${body.file.name}: ${bytes.length}b, ${body.file.type}`;
        },
      });
    }

    const alepha = Alepha.create().with(AlephaServer).with(BinaryApp);
    await alepha.start();

    const binary = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a]);
    const file = new File([binary], "image.png", { type: "image/png" });

    const result = await alepha.inject(BinaryApp).upload.run({
      body: { file },
    });

    expect(result).toBe("image.png: 6b, image/png");
  });

  test("should handle empty file", async ({ expect }) => {
    const alepha = Alepha.create().with(AlephaServer).with(App);
    await alepha.start();

    const file = new File([], "empty.txt", { type: "text/plain" });

    const result = await alepha.inject(App).upload.run({
      body: { file },
    });

    expect(result).toBe("empty.txt (0b, text/plain)");
  });

  test("should handle mixed file and text fields", async ({ expect }) => {
    class MixedApp {
      upload = $action({
        schema: {
          body: z.object({
            name: z.text(),
            description: z.text(),
            file: z.file(),
          }),
          response: z.text(),
        },
        handler: ({ body }) =>
          `${body.name}: ${body.description} (${body.file.size}b)`,
      });
    }

    const alepha = Alepha.create().with(AlephaServer).with(MixedApp);
    await alepha.start();

    const file = new File(["content"], "doc.txt", { type: "text/plain" });

    const result = await alepha.inject(MixedApp).upload.run({
      body: {
        name: "Test Document",
        description: "A test file upload",
        file,
      },
    });

    expect(result).toBe("Test Document: A test file upload (7b)");
  });

  test("should not have filepath (no filesystem)", async ({ expect }) => {
    class PathApp {
      upload = $action({
        schema: {
          body: z.object({ file: z.file() }),
          response: z.text(),
        },
        handler: ({ body }) => String(body.file.filepath ?? "none"),
      });
    }

    const alepha = Alepha.create().with(AlephaServer).with(PathApp);
    await alepha.start();

    const file = new File(["data"], "f.txt", { type: "text/plain" });

    const result = await alepha.inject(PathApp).upload.run({
      body: { file },
    });

    expect(result).toBe("none");
  });
});

describe("ServerMultipartProvider - Size Limits", () => {
  test("should reject file exceeding individual file size limit", async ({
    expect,
  }) => {
    const alepha = Alepha.create().with(AlephaServer).with(App);
    alepha.store.mut(multipartOptions, () => ({
      limit: 10_000_000,
      fileLimit: 100,
      fileCount: 10,
    }));
    await alepha.start();

    const file = new File(["x".repeat(200)], "large.txt", {
      type: "text/plain",
    });
    const formData = new FormData();
    formData.append("file", file);

    const resp = await fetch(
      `${alepha.inject(ServerProvider).hostname}/api/upload`,
      { method: "POST", body: formData },
    );

    expect(resp.status).toBe(413);
    const body = await resp.json();
    expect(body.message).toMatch(/exceeds size limit/i);
  });

  test("should reject request exceeding total size limit", async ({
    expect,
  }) => {
    class MultiFileApp {
      upload = $action({
        schema: {
          body: z.object({
            file1: z.file(),
            file2: z.file(),
          }),
          response: z.text(),
        },
        handler: () => "ok",
      });
    }

    const alepha = Alepha.create().with(AlephaServer).with(MultiFileApp);
    alepha.store.mut(multipartOptions, () => ({
      limit: 150,
      fileLimit: 100,
      fileCount: 10,
    }));
    await alepha.start();

    const file1 = new File(["x".repeat(80)], "f1.txt", {
      type: "text/plain",
    });
    const file2 = new File(["y".repeat(80)], "f2.txt", {
      type: "text/plain",
    });
    const formData = new FormData();
    formData.append("file1", file1);
    formData.append("file2", file2);

    const resp = await fetch(
      `${alepha.inject(ServerProvider).hostname}/api/upload`,
      { method: "POST", body: formData },
    );

    expect(resp.status).toBe(413);
    const body = await resp.json();
    expect(body.message).toMatch(/size limit exceeded/i);
  });

  test("should reject request with too many files", async ({ expect }) => {
    class ManyFilesApp {
      upload = $action({
        schema: {
          body: z.object({
            file1: z.file(),
            file2: z.file(),
            file3: z.file(),
          }),
          response: z.text(),
        },
        handler: () => "ok",
      });
    }

    const alepha = Alepha.create().with(AlephaServer).with(ManyFilesApp);
    alepha.store.mut(multipartOptions, () => ({
      limit: 10_000_000,
      fileLimit: 5_000_000,
      fileCount: 2,
    }));
    await alepha.start();

    const formData = new FormData();
    formData.append("file1", new File(["a"], "f1.txt", { type: "text/plain" }));
    formData.append("file2", new File(["b"], "f2.txt", { type: "text/plain" }));
    formData.append("file3", new File(["c"], "f3.txt", { type: "text/plain" }));

    const resp = await fetch(
      `${alepha.inject(ServerProvider).hostname}/api/upload`,
      { method: "POST", body: formData },
    );

    expect(resp.status).toBe(413);
    const body = await resp.json();
    expect(body.message).toMatch(/Too many files/i);
  });

  test("should accept file exactly at the size limit", async ({ expect }) => {
    const alepha = Alepha.create().with(AlephaServer).with(App);
    alepha.store.mut(multipartOptions, () => ({
      limit: 10_000_000,
      fileLimit: 100,
      fileCount: 10,
    }));
    await alepha.start();

    const file = new File(["x".repeat(100)], "exact.txt", {
      type: "text/plain",
    });

    const result = await alepha.inject(App).upload.run({
      body: { file },
    });

    expect(result).toBe("exact.txt (100b, text/plain)");
  });

  test("should reject file just over the size limit", async ({ expect }) => {
    const alepha = Alepha.create().with(AlephaServer).with(App);
    alepha.store.mut(multipartOptions, () => ({
      limit: 10_000_000,
      fileLimit: 100,
      fileCount: 10,
    }));
    await alepha.start();

    const file = new File(["x".repeat(101)], "over.txt", {
      type: "text/plain",
    });
    const formData = new FormData();
    formData.append("file", file);

    const resp = await fetch(
      `${alepha.inject(ServerProvider).hostname}/api/upload`,
      { method: "POST", body: formData },
    );

    expect(resp.status).toBe(413);
    const body = await resp.json();
    expect(body.message).toMatch(/exceeds size limit/i);
  });

  test("should reject via content-length pre-check", async ({ expect }) => {
    const alepha = Alepha.create().with(AlephaServer).with(App);
    alepha.store.mut(multipartOptions, () => ({
      limit: 50,
      fileLimit: 5_000_000,
      fileCount: 10,
    }));
    await alepha.start();

    const file = new File(["x".repeat(1000)], "big.txt", {
      type: "text/plain",
    });
    const formData = new FormData();
    formData.append("file", file);

    const resp = await fetch(
      `${alepha.inject(ServerProvider).hostname}/api/upload`,
      { method: "POST", body: formData },
    );

    expect(resp.status).toBe(413);
    const body = await resp.json();
    expect(body.message).toMatch(/size limit exceeded/i);
  });

  test("should count only file fields toward file count limit", async ({
    expect,
  }) => {
    class TextAndFilesApp {
      upload = $action({
        schema: {
          body: z.object({
            field1: z.text(),
            field2: z.text(),
            field3: z.text(),
            file1: z.file(),
            file2: z.file(),
          }),
          response: z.text(),
        },
        handler: () => "success",
      });
    }

    const alepha = Alepha.create().with(AlephaServer).with(TextAndFilesApp);
    alepha.store.mut(multipartOptions, () => ({
      limit: 10_000_000,
      fileLimit: 5_000_000,
      fileCount: 2,
    }));
    await alepha.start();

    const result = await alepha.inject(TextAndFilesApp).upload.run({
      body: {
        field1: "a",
        field2: "b",
        field3: "c",
        file1: new File(["x"], "f1.txt", { type: "text/plain" }),
        file2: new File(["y"], "f2.txt", { type: "text/plain" }),
      },
    });

    expect(result).toBe("success");
  });

  test("should use default limits", async ({ expect }) => {
    const alepha = Alepha.create().with(AlephaServer).with(App);
    await alepha.start();

    const file = new File(["x".repeat(1000)], "normal.txt", {
      type: "text/plain",
    });

    const result = await alepha.inject(App).upload.run({
      body: { file },
    });

    expect(result).toBe("normal.txt (1000b, text/plain)");
  });
});
