/* @vitest-environment node */

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import fsp from "node:fs/promises";

vi.mock("@/lib/auth/session", () => ({
	getSession: vi.fn(),
}));

import { getSession } from "@/lib/auth/session";
import { GET, dynamic, runtime } from "./route.js";

describe("GET /api/files/[branch]/[year]/[month]/[day]/[filename]", () => {
	let tmpRoot;
	const originalNasRoot = process.env.NAS_ROOT_PATH;

	const paramsOk = {
		branch: "NL01",
		year: "2024",
		month: "10",
		day: "23",
		filename: "test.pdf",
	};

	beforeEach(async () => {
		vi.clearAllMocks();

		tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), "api-pdf-"));
		process.env.NAS_ROOT_PATH = tmpRoot;

		const dir = path.join(tmpRoot, "NL01", "2024", "10", "23");
		await fs.mkdir(dir, { recursive: true });

		await fs.writeFile(path.join(dir, "test.pdf"), "dummy-pdf-content");
	});

	afterEach(async () => {
		process.env.NAS_ROOT_PATH = originalNasRoot;
		if (tmpRoot) await fs.rm(tmpRoot, { recursive: true, force: true });
		vi.restoreAllMocks();
	});

	it('exports dynamic="force-dynamic" (RHL-006)', () => {
		expect(dynamic).toBe("force-dynamic");
	});

	it('exports runtime="nodejs" (required for streaming)', () => {
		expect(runtime).toBe("nodejs");
	});

	it("returns 401 when unauthenticated", async () => {
		getSession.mockResolvedValue(null);

		const res = await GET(
			new Request("http://localhost/api/files/NL01/2024/10/23/test.pdf"),
			{ params: Promise.resolve(paramsOk) }
		);

		expect(res.status).toBe(401);
		expect(await res.json()).toEqual({
			error: { message: "Unauthorized", code: "AUTH_UNAUTHENTICATED" },
		});
	});

	it("returns 403 when branch access is forbidden", async () => {
		getSession.mockResolvedValue({
			role: "branch",
			branchId: "NL01",
			userId: "u1",
		});

		const res = await GET(
			new Request("http://localhost/api/files/NL02/2024/10/23/test.pdf"),
			{
				params: Promise.resolve({
					...paramsOk,
					branch: "NL02",
				}),
			}
		);

		expect(res.status).toBe(403);
		expect(await res.json()).toEqual({
			error: { message: "Forbidden", code: "AUTH_FORBIDDEN_BRANCH" },
		});
	});

	it("returns 400 for non-pdf filename", async () => {
		getSession.mockResolvedValue({
			role: "admin",
			branchId: null,
			userId: "u2",
		});

		const res = await GET(
			new Request("http://localhost/api/files/NL01/2024/10/23/test.txt"),
			{
				params: Promise.resolve({
					...paramsOk,
					filename: "test.txt",
				}),
			}
		);

		expect(res.status).toBe(400);
		expect(await res.json()).toEqual({
			error: {
				message: "Only PDF files are allowed",
				code: "VALIDATION_FILE_EXTENSION",
				details: { filename: "test.txt" },
			},
		});
	});

	it("returns 400 for unsafe filename", async () => {
		getSession.mockResolvedValue({
			role: "admin",
			branchId: null,
			userId: "u2",
		});

		const res = await GET(
			new Request("http://localhost/api/files/NL01/2024/10/23/foo/bar.pdf"),
			{
				params: Promise.resolve({
					...paramsOk,
					filename: "foo/bar.pdf",
				}),
			}
		);

		expect(res.status).toBe(400);
		expect(await res.json()).toEqual({
			error: {
				message: "Invalid filename parameter",
				code: "VALIDATION_FILENAME",
				details: { filename: "foo/bar.pdf" },
			},
		});
	});

	it("returns 404 when the PDF does not exist (authorized)", async () => {
		getSession.mockResolvedValue({
			role: "admin",
			branchId: null,
			userId: "u2",
		});

		const res = await GET(
			new Request("http://localhost/api/files/NL01/2024/10/23/missing.pdf"),
			{
				params: Promise.resolve({
					...paramsOk,
					filename: "missing.pdf",
				}),
			}
		);

		expect(res.status).toBe(404);
		expect(await res.json()).toEqual({
			error: {
				message: "Not found",
				code: "FS_NOT_FOUND",
				details: {
					branch: "NL01",
					year: "2024",
					month: "10",
					day: "23",
					filename: "missing.pdf",
				},
			},
		});
	});

	it("returns 500 for other filesystem errors (mocked)", async () => {
		getSession.mockResolvedValue({
			role: "admin",
			branchId: null,
			userId: "u2",
		});

		const spy = vi
			.spyOn(fsp, "stat")
			.mockRejectedValue(
				Object.assign(new Error("EACCES"), { code: "EACCES" })
			);

		const res = await GET(
			new Request("http://localhost/api/files/NL01/2024/10/23/test.pdf"),
			{ params: Promise.resolve(paramsOk) }
		);

		expect(res.status).toBe(500);
		expect(await res.json()).toEqual({
			error: { message: "Internal server error", code: "FS_STORAGE_ERROR" },
		});

		spy.mockRestore();
	});

	it("streams the PDF with inline Content-Disposition by default", async () => {
		getSession.mockResolvedValue({
			role: "admin",
			branchId: null,
			userId: "u2",
		});

		const res = await GET(
			new Request("http://localhost/api/files/NL01/2024/10/23/test.pdf"),
			{ params: Promise.resolve(paramsOk) }
		);

		expect(res.status).toBe(200);
		expect(res.headers.get("Content-Type")).toBe("application/pdf");
		expect(res.headers.get("Cache-Control")).toBe("no-store");

		const cd = res.headers.get("Content-Disposition") || "";
		expect(cd).toContain("inline;");
		expect(cd).toContain('filename="test.pdf"');
		expect(cd).toContain("filename*=UTF-8''test.pdf");

		const buf = await res.arrayBuffer();
		expect(Buffer.from(buf).toString("utf8")).toBe("dummy-pdf-content");
	});

	it("uses attachment disposition when download=1", async () => {
		getSession.mockResolvedValue({
			role: "admin",
			branchId: null,
			userId: "u2",
		});

		const res = await GET(
			new Request(
				"http://localhost/api/files/NL01/2024/10/23/test.pdf?download=1"
			),
			{ params: Promise.resolve(paramsOk) }
		);

		expect(res.status).toBe(200);

		const cd = res.headers.get("Content-Disposition") || "";
		expect(cd).toContain("attachment;");
		expect(cd).toContain('filename="test.pdf"');
		expect(cd).toContain("filename*=UTF-8''test.pdf");
	});

	it("supports unicode filenames in Content-Disposition (no ByteString crash)", async () => {
		getSession.mockResolvedValue({
			role: "admin",
			branchId: null,
			userId: "u2",
		});

		const unicodeFilename = "Euro €.pdf";
		const encoded = encodeURIComponent(unicodeFilename);

		const dir = path.join(tmpRoot, "NL01", "2024", "10", "23");
		await fs.writeFile(path.join(dir, unicodeFilename), "dummy-euro");

		const res = await GET(
			new Request(`http://localhost/api/files/NL01/2024/10/23/${encoded}`),
			{
				params: Promise.resolve({
					...paramsOk,
					filename: unicodeFilename,
				}),
			}
		);

		expect(res.status).toBe(200);

		const cd = res.headers.get("Content-Disposition") || "";
		expect(cd).toContain("inline;");
		// ASCII fallback replaces the Euro sign with underscore.
		expect(cd).toContain('filename="Euro _.pdf"');
		// RFC 5987 preserves the real name via percent-encoded UTF-8.
		expect(cd).toContain("filename*=UTF-8''Euro%20%E2%82%AC.pdf");

		const buf = await res.arrayBuffer();
		expect(Buffer.from(buf).toString("utf8")).toBe("dummy-euro");
	});
});
