import assert from "node:assert/strict";
import { describe, it } from "node:test";

import type { ParsedFile } from "../diff-parser.ts";
import { fetchFileContents } from "../git.ts";

function makeFile(overrides: Partial<ParsedFile> & Pick<ParsedFile, "status">): ParsedFile {
	const path = overrides.path ?? "src/foo.ts";
	return {
		path,
		oldPath: overrides.oldPath ?? path,
		newPath: overrides.newPath ?? path,
		additions: overrides.additions ?? 1,
		deletions: overrides.deletions ?? 1,
		status: overrides.status,
		binary: overrides.binary ?? false,
	};
}

function makePi(responses: Record<string, { code: number; stdout: string }>) {
	return {
		exec: async (_cmd: string, args: string[]) => {
			const ref = args[1] ?? "";
			const response = responses[ref] ?? { code: 0, stdout: "" };
			return { code: response.code, stdout: response.stdout, stderr: "" };
		},
	};
}

describe("fetchFileContents", () => {
	it("modified file: fetches BASE:path and HEAD:path", async () => {
		const pi = makePi({
			"main:src/foo.ts": { code: 0, stdout: "original content" },
			"HEAD:src/foo.ts": { code: 0, stdout: "modified content" },
		});

		const files = [makeFile({ status: "modified" })];
		const result = await fetchFileContents(pi as never, "/repo", "main", files);

		assert.equal(result.length, 1);
		assert.equal(result[0].original, "original content");
		assert.equal(result[0].modified, "modified content");
		assert.equal(result[0].status, "modified");
	});

	it("added file: empty original, fetches HEAD:newPath", async () => {
		const pi = makePi({
			"HEAD:src/new.ts": { code: 0, stdout: "new file content" },
		});

		const files = [makeFile({ status: "added", path: "src/new.ts", newPath: "src/new.ts" })];
		const result = await fetchFileContents(pi as never, "/repo", "main", files);

		assert.equal(result[0].original, "");
		assert.equal(result[0].modified, "new file content");
		assert.equal(result[0].status, "added");
	});

	it("deleted file: fetches BASE:oldPath, empty modified", async () => {
		const pi = makePi({
			"main:src/old.ts": { code: 0, stdout: "deleted content" },
		});

		const files = [makeFile({ status: "deleted", path: "src/old.ts", oldPath: "src/old.ts" })];
		const result = await fetchFileContents(pi as never, "/repo", "main", files);

		assert.equal(result[0].original, "deleted content");
		assert.equal(result[0].modified, "");
		assert.equal(result[0].status, "deleted");
	});

	it("renamed file: fetches BASE:oldPath and HEAD:newPath", async () => {
		const pi = makePi({
			"main:src/old-name.ts": { code: 0, stdout: "old name content" },
			"HEAD:src/new-name.ts": { code: 0, stdout: "new name content" },
		});

		const files = [
			makeFile({
				status: "renamed",
				path: "src/new-name.ts",
				oldPath: "src/old-name.ts",
				newPath: "src/new-name.ts",
			}),
		];
		const result = await fetchFileContents(pi as never, "/repo", "main", files);

		assert.equal(result[0].original, "old name content");
		assert.equal(result[0].modified, "new name content");
		assert.equal(result[0].status, "renamed");
	});

	it("binary file: returns empty strings for both sides", async () => {
		const pi = makePi({
			"main:assets/img.png": { code: 0, stdout: "binary\0data" },
			"HEAD:assets/img.png": { code: 0, stdout: "binary\0data" },
		});

		const files = [makeFile({ status: "modified", path: "assets/img.png" })];
		const result = await fetchFileContents(pi as never, "/repo", "main", files);

		assert.equal(result[0].original, "");
		assert.equal(result[0].modified, "");
	});

	it("git show failure: returns empty string, does not crash", async () => {
		const pi = makePi({
			"main:src/foo.ts": { code: 128, stdout: "" },
			"HEAD:src/foo.ts": { code: 128, stdout: "" },
		});

		const files = [makeFile({ status: "modified" })];
		const result = await fetchFileContents(pi as never, "/repo", "main", files);

		assert.equal(result[0].original, "");
		assert.equal(result[0].modified, "");
	});

	it("binary file with binary flag: gitShow is never called", async () => {
		let gitShowCalled = false;
		const pi = {
			exec: async (_cmd: string, _args: string[]) => {
				gitShowCalled = true;
				return { code: 0, stdout: "", stderr: "" };
			},
		};

		const files = [makeFile({ status: "modified", path: "assets/img.png", binary: true })];
		const result = await fetchFileContents(pi as never, "/repo", "main", files);

		assert.equal(gitShowCalled, false);
		assert.equal(result[0].binary, true);
		assert.equal(result[0].original, "");
		assert.equal(result[0].modified, "");
	});

	it("multiple files are all processed", async () => {
		const pi = makePi({
			"main:src/a.ts": { code: 0, stdout: "a original" },
			"HEAD:src/a.ts": { code: 0, stdout: "a modified" },
			"HEAD:src/b.ts": { code: 0, stdout: "b content" },
			"main:src/c.ts": { code: 0, stdout: "c content" },
		});

		const files = [
			makeFile({ status: "modified", path: "src/a.ts" }),
			makeFile({ status: "added", path: "src/b.ts", newPath: "src/b.ts" }),
			makeFile({ status: "deleted", path: "src/c.ts", oldPath: "src/c.ts" }),
		];
		const result = await fetchFileContents(pi as never, "/repo", "main", files);

		assert.equal(result.length, 3);
		assert.equal(result[0].original, "a original");
		assert.equal(result[0].modified, "a modified");
		assert.equal(result[1].original, "");
		assert.equal(result[1].modified, "b content");
		assert.equal(result[2].original, "c content");
		assert.equal(result[2].modified, "");
	});

	it("limits concurrent git show calls to at most 10", async () => {
		let active = 0;
		let peak = 0;

		const pi = {
			exec: async (_cmd: string, args: string[]) => {
				active++;
				if (active > peak) peak = active;
				await new Promise((r) => setTimeout(r, 5));
				active--;
				const ref = args[1] ?? "";
				return { code: 0, stdout: `content-${ref}`, stderr: "" };
			},
		};

		const files = Array.from({ length: 20 }, (_, i) =>
			makeFile({ status: "modified", path: `src/file${i}.ts` }),
		);

		const result = await fetchFileContents(pi as never, "/repo", "main", files);

		assert.equal(result.length, 20);
		assert.ok(peak <= 10, `peak concurrency was ${peak}, expected <= 10`);
		assert.ok(peak > 1, `peak concurrency was ${peak}, expected > 1 (parallelism should work)`);
	});
});
