import { describe, expect, it } from "vitest";
import { LineOp, type NiceDiff } from "./diff";
import { renderFormatPatch, renderUnifiedDiff } from "./rawdiff";

const modifiedFile: NiceDiff = {
	diff: [
		{
			name: { old: "foo.go", new: "foo.go" },
			text_fragments: [
				{
					Comment: "func main()",
					OldPosition: 1,
					OldLines: 3,
					NewPosition: 1,
					NewLines: 3,
					Lines: [
						{ Op: LineOp.Context, Line: "package main\n" },
						{ Op: LineOp.Delete, Line: "// old comment\n" },
						{ Op: LineOp.Add, Line: "// new comment\n" },
						{ Op: LineOp.Context, Line: "func main() {}\n" }
					]
				}
			]
		}
	]
};

describe("renderUnifiedDiff", () => {
	it("renders nothing for a missing diff", () => {
		expect(renderUnifiedDiff(null)).toBe("");
		expect(renderUnifiedDiff(undefined)).toBe("");
	});

	it("renders a modified file with the hunk comment", () => {
		expect(renderUnifiedDiff(modifiedFile)).toBe(
			"diff --git a/foo.go b/foo.go\n" +
				"--- a/foo.go\n" +
				"+++ b/foo.go\n" +
				"@@ -1,3 +1,3 @@ func main()\n" +
				" package main\n" +
				"-// old comment\n" +
				"+// new comment\n" +
				" func main() {}\n"
		);
	});

	it("renders a new file", () => {
		const got = renderUnifiedDiff({
			diff: [
				{
					name: { old: "", new: "new.go" },
					is_new: true,
					text_fragments: [
						{
							OldPosition: 0,
							OldLines: 0,
							NewPosition: 1,
							NewLines: 2,
							Lines: [
								{ Op: LineOp.Add, Line: "package main\n" },
								{ Op: LineOp.Add, Line: "func main() {}\n" }
							]
						}
					]
				}
			]
		});
		expect(got).toBe(
			"diff --git a/new.go b/new.go\n" +
				"new file mode 100644\n" +
				"--- /dev/null\n" +
				"+++ b/new.go\n" +
				"@@ -0,0 +1,2 @@\n" +
				"+package main\n" +
				"+func main() {}\n"
		);
	});

	it("renders a deleted file", () => {
		const got = renderUnifiedDiff({
			diff: [
				{
					name: { old: "old.go", new: "" },
					is_delete: true,
					text_fragments: [
						{
							OldPosition: 1,
							OldLines: 2,
							NewPosition: 0,
							NewLines: 0,
							Lines: [
								{ Op: LineOp.Delete, Line: "package main\n" },
								{ Op: LineOp.Delete, Line: "func main() {}\n" }
							]
						}
					]
				}
			]
		});
		expect(got).toBe(
			"diff --git a/old.go b/old.go\n" +
				"deleted file mode 100644\n" +
				"--- a/old.go\n" +
				"+++ /dev/null\n" +
				"@@ -1,2 +0,0 @@\n" +
				"-package main\n" +
				"-func main() {}\n"
		);
	});

	it("renders a renamed file with multiple fragments", () => {
		const got = renderUnifiedDiff({
			diff: [
				{
					name: { old: "old.go", new: "renamed.go" },
					is_rename: true,
					text_fragments: [
						{
							OldPosition: 1,
							OldLines: 2,
							NewPosition: 1,
							NewLines: 2,
							Lines: [
								{ Op: LineOp.Context, Line: "package main\n" },
								{ Op: LineOp.Delete, Line: "func old() {}\n" },
								{ Op: LineOp.Add, Line: "func renamed() {}\n" }
							]
						},
						{
							Comment: "func init()",
							OldPosition: 10,
							OldLines: 1,
							NewPosition: 10,
							NewLines: 1,
							Lines: [{ Op: LineOp.Context, Line: "var x = 1\n" }]
						}
					]
				}
			]
		});
		expect(got).toBe(
			"diff --git a/old.go b/renamed.go\n" +
				"rename from old.go\n" +
				"rename to renamed.go\n" +
				"--- a/old.go\n" +
				"+++ b/renamed.go\n" +
				"@@ -1,2 +1,2 @@\n" +
				" package main\n" +
				"-func old() {}\n" +
				"+func renamed() {}\n" +
				"@@ -10,1 +10,1 @@ func init()\n" +
				" var x = 1\n"
		);
	});

	it("renders multiple files", () => {
		const file = (name: string, oldLine: string, newLine: string) => ({
			name: { old: name, new: name },
			text_fragments: [
				{
					OldPosition: 1,
					OldLines: 1,
					NewPosition: 1,
					NewLines: 1,
					Lines: [
						{ Op: LineOp.Delete, Line: `${oldLine}\n` },
						{ Op: LineOp.Add, Line: `${newLine}\n` }
					]
				}
			]
		});
		const got = renderUnifiedDiff({
			diff: [file("a.go", "old a", "new a"), file("b.go", "old b", "new b")]
		});
		expect(got).toBe(
			"diff --git a/a.go b/a.go\n" +
				"--- a/a.go\n" +
				"+++ b/a.go\n" +
				"@@ -1,1 +1,1 @@\n" +
				"-old a\n" +
				"+new a\n" +
				"diff --git a/b.go b/b.go\n" +
				"--- a/b.go\n" +
				"+++ b/b.go\n" +
				"@@ -1,1 +1,1 @@\n" +
				"-old b\n" +
				"+new b\n"
		);
	});

	it("marks a missing trailing newline on delete and context lines", () => {
		const got = renderUnifiedDiff({
			diff: [
				{
					name: { old: "foo.go", new: "foo.go" },
					text_fragments: [
						{
							OldPosition: 1,
							OldLines: 2,
							NewPosition: 1,
							NewLines: 2,
							Lines: [
								{ Op: LineOp.Delete, Line: "old" },
								{ Op: LineOp.Add, Line: "new\n" },
								{ Op: LineOp.Context, Line: "tail" }
							]
						}
					]
				}
			]
		});
		expect(got).toBe(
			"diff --git a/foo.go b/foo.go\n" +
				"--- a/foo.go\n" +
				"+++ b/foo.go\n" +
				"@@ -1,2 +1,2 @@\n" +
				"-old\n" +
				"\\ No newline at end of file\n" +
				"+new\n" +
				" tail\n" +
				"\\ No newline at end of file\n"
		);
	});
});

describe("renderFormatPatch", () => {
	it("renders nothing for a missing diff", () => {
		expect(renderFormatPatch(null)).toBe("");
		expect(renderFormatPatch(undefined)).toBe("");
	});

	it("renders the full patch for a rename and a delete", () => {
		const got = renderFormatPatch({
			commit: {
				hash: [0xab, 0xc1, 0x23, 0x45, 0x67, 0x89, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
				message: "Fix the bug\n\nThis patch resolves the long-standing issue.\n\n\n",
				author: { Name: "Alice Dev", Email: "alice@example.com", When: "2024-03-15T10:30:00Z" }
			},
			stat: { files_changed: 2, insertions: 1, deletions: 3 },
			diff: [
				{
					name: { old: "old.go", new: "renamed.go" },
					is_rename: true,
					text_fragments: [
						{
							OldPosition: 1,
							OldLines: 2,
							NewPosition: 1,
							NewLines: 2,
							LinesAdded: 1,
							LinesDeleted: 1,
							Lines: [
								{ Op: LineOp.Context, Line: "package main\n" },
								{ Op: LineOp.Delete, Line: "func old() {}\n" },
								{ Op: LineOp.Add, Line: "func renamed() {}\n" }
							]
						}
					]
				},
				{
					name: { old: "gone.go", new: "" },
					is_delete: true,
					text_fragments: [
						{
							OldPosition: 1,
							OldLines: 2,
							NewPosition: 0,
							NewLines: 0,
							LinesAdded: 0,
							LinesDeleted: 2,
							Lines: [
								{ Op: LineOp.Delete, Line: "package main\n" },
								{ Op: LineOp.Delete, Line: "func main() {}\n" }
							]
						}
					]
				}
			]
		});
		expect(got).toBe(
			"From abc1234567890000000000000000000000000000 Mon Sep 17 00:00:00 2001\n" +
				"From: Alice Dev <alice@example.com>\n" +
				"Date: Fri, 15 Mar 2024 10:30:00 +0000\n" +
				"Subject: [PATCH] Fix the bug\n" +
				"\n" +
				"This patch resolves the long-standing issue.\n" +
				"---\n" +
				" renamed.go | 2 +-\n" +
				" gone.go | 2 --\n" +
				" 2 file(s) changed, 1 insertion(s)(+), 3 deletion(s)(-)\n" +
				"\n" +
				"diff --git a/old.go b/renamed.go\n" +
				"rename from old.go\n" +
				"rename to renamed.go\n" +
				"--- a/old.go\n" +
				"+++ b/renamed.go\n" +
				"@@ -1,2 +1,2 @@\n" +
				" package main\n" +
				"-func old() {}\n" +
				"+func renamed() {}\n" +
				"diff --git a/gone.go b/gone.go\n" +
				"deleted file mode 100644\n" +
				"--- a/gone.go\n" +
				"+++ /dev/null\n" +
				"@@ -1,2 +0,0 @@\n" +
				"-package main\n" +
				"-func main() {}\n" +
				"\n--\ntangled.sh\n"
		);
	});

	it("omits the body for a single-line message and uses the zero time for a missing When", () => {
		const got = renderFormatPatch({
			commit: {
				message: "Single line commit",
				author: { Name: "", Email: "", When: "" }
			}
		});
		expect(got).toBe(
			"From  Mon Sep 17 00:00:00 2001\n" +
				"From:  <>\n" +
				"Date: Mon, 01 Jan 0001 00:00:00 +0000\n" +
				"Subject: [PATCH] Single line commit\n" +
				"\n" +
				"---\n" +
				" 0 file(s) changed, 0 insertion(s)(+), 0 deletion(s)(-)\n" +
				"\n" +
				"\n--\ntangled.sh\n"
		);
	});

	it("uses the zero time for an unparseable When", () => {
		const got = renderFormatPatch({
			commit: {
				this: "abc123",
				message: "x",
				author: { Name: "A", Email: "a@b.c", When: "not a date" }
			}
		});
		expect(got).toContain("Date: Mon, 01 Jan 0001 00:00:00 +0000\n");
	});

	it("pads the year to four digits", () => {
		const got = renderFormatPatch({
			commit: {
				this: "abc123",
				message: "x",
				author: { Name: "A", Email: "a@b.c", When: "0999-06-15T10:30:00Z" }
			}
		});
		expect(got).toContain("Date: Sat, 15 Jun 0999 10:30:00 +0000\n");
	});
});
