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

import { parseDiff, unquotePath } from "../diff-parser.ts";

const modifiedDiff = `\
diff --git a/src/index.ts b/src/index.ts
index abc1234..def5678 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,4 +1,5 @@
 const x = 1;
-const y = 2;
+const y = 3;
+const z = 4;
`;

const addedDiff = `\
diff --git a/src/new-file.ts b/src/new-file.ts
new file mode 100644
index 0000000..abc1234
--- /dev/null
+++ b/src/new-file.ts
@@ -0,0 +1,3 @@
+export const foo = 1;
+export const bar = 2;
+export const baz = 3;
`;

const deletedDiff = `\
diff --git a/src/old-file.ts b/src/old-file.ts
deleted file mode 100644
index abc1234..0000000
--- a/src/old-file.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export const removed = true;
-export const gone = true;
`;

const renamedDiff = `\
diff --git a/src/old-name.ts b/src/new-name.ts
similarity index 100%
rename from src/old-name.ts
rename to src/new-name.ts
`;

const renamedWithChangesDiff = `\
diff --git a/src/old-name.ts b/src/new-name.ts
similarity index 80%
rename from src/old-name.ts
rename to src/new-name.ts
index abc1234..def5678 100644
--- a/src/old-name.ts
+++ b/src/new-name.ts
@@ -1,3 +1,4 @@
 const a = 1;
-const b = 2;
+const b = 3;
+const c = 4;
`;

const multiFileDiff = `\
diff --git a/src/a.ts b/src/a.ts
index abc1234..def5678 100644
--- a/src/a.ts
+++ b/src/a.ts
@@ -1,2 +1,3 @@
 const x = 1;
+const y = 2;
diff --git a/src/b.ts b/src/b.ts
new file mode 100644
index 0000000..abc1234
--- /dev/null
+++ b/src/b.ts
@@ -0,0 +1,2 @@
+export const foo = 1;
+export const bar = 2;
diff --git a/src/c.ts b/src/c.ts
deleted file mode 100644
index abc1234..0000000
--- a/src/c.ts
+++ /dev/null
@@ -1,1 +0,0 @@
-export const old = true;
`;

const binaryDiff = `\
diff --git a/assets/image.png b/assets/image.png
index abc1234..def5678 100644
Binary files a/assets/image.png and b/assets/image.png differ
`;

const pathWithSpacesDiff = `\
diff --git a/src/my file.ts b/src/my file.ts
index abc1234..def5678 100644
--- a/src/my file.ts
+++ b/src/my file.ts
@@ -1,2 +1,3 @@
 const x = 1;
+const y = 2;
`;

const unicodeDiff = `\
diff --git a/src/日本語.ts b/src/日本語.ts
index abc1234..def5678 100644
--- a/src/日本語.ts
+++ b/src/日本語.ts
@@ -1,2 +1,3 @@
 const x = 1;
+const y = 2;
`;

const pathWithBSlashDiff = `\
diff --git a/src/foo b/bar.ts b/src/foo b/bar.ts
index abc1234..def5678 100644
--- a/src/foo b/bar.ts
+++ b/src/foo b/bar.ts
@@ -1,2 +1,3 @@
 const x = 1;
+const y = 2;
`;

const modeOnlyDiff = `\
diff --git a/src/script.ts b/src/script.ts
old mode 100644
new mode 100755
`;

const noNewlineDiff = `\
diff --git a/src/index.ts b/src/index.ts
index abc1234..def5678 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,1 +1,1 @@
-const x = 1;
\\ No newline at end of file
+const x = 2;
\\ No newline at end of file
`;

const renamedNewPathWithBSlashDiff = `\
diff --git a/src/old.ts b/src/new b/path.ts
similarity index 100%
rename from src/old.ts
rename to src/new b/path.ts
`;

describe("parseDiff", () => {
	it("returns empty array for empty string", () => {
		assert.deepEqual(parseDiff(""), []);
	});

	it("returns empty array for whitespace-only string", () => {
		assert.deepEqual(parseDiff("   \n\t\n  "), []);
	});

	it("parses a single modified file", () => {
		const result = parseDiff(modifiedDiff);
		assert.equal(result.length, 1);
		assert.deepEqual(result[0], {
			path: "src/index.ts",
			oldPath: "src/index.ts",
			newPath: "src/index.ts",
			status: "modified",
			additions: 2,
			deletions: 1,
			binary: false,
		});
	});

	it("parses an added file", () => {
		const result = parseDiff(addedDiff);
		assert.equal(result.length, 1);
		assert.deepEqual(result[0], {
			path: "src/new-file.ts",
			oldPath: "",
			newPath: "src/new-file.ts",
			status: "added",
			additions: 3,
			deletions: 0,
			binary: false,
		});
	});

	it("parses a deleted file", () => {
		const result = parseDiff(deletedDiff);
		assert.equal(result.length, 1);
		assert.deepEqual(result[0], {
			path: "src/old-file.ts",
			oldPath: "src/old-file.ts",
			newPath: "",
			status: "deleted",
			additions: 0,
			deletions: 2,
			binary: false,
		});
	});

	it("parses a renamed file with no content changes", () => {
		const result = parseDiff(renamedDiff);
		assert.equal(result.length, 1);
		assert.deepEqual(result[0], {
			path: "src/new-name.ts",
			oldPath: "src/old-name.ts",
			newPath: "src/new-name.ts",
			status: "renamed",
			additions: 0,
			deletions: 0,
			binary: false,
		});
	});

	it("parses a renamed file with content changes", () => {
		const result = parseDiff(renamedWithChangesDiff);
		assert.equal(result.length, 1);
		assert.deepEqual(result[0], {
			path: "src/new-name.ts",
			oldPath: "src/old-name.ts",
			newPath: "src/new-name.ts",
			status: "renamed",
			additions: 2,
			deletions: 1,
			binary: false,
		});
	});

	it("parses multiple files in order", () => {
		const result = parseDiff(multiFileDiff);
		assert.equal(result.length, 3);
		assert.equal(result[0].path, "src/a.ts");
		assert.equal(result[0].status, "modified");
		assert.equal(result[0].additions, 1);
		assert.equal(result[0].deletions, 0);

		assert.equal(result[1].path, "src/b.ts");
		assert.equal(result[1].status, "added");
		assert.equal(result[1].additions, 2);
		assert.equal(result[1].deletions, 0);

		assert.equal(result[2].path, "src/c.ts");
		assert.equal(result[2].status, "deleted");
		assert.equal(result[2].additions, 0);
		assert.equal(result[2].deletions, 1);
	});

	it("parses a binary file entry with zero additions/deletions", () => {
		const result = parseDiff(binaryDiff);
		assert.equal(result.length, 1);
		assert.equal(result[0].path, "assets/image.png");
		assert.equal(result[0].status, "modified");
		assert.equal(result[0].additions, 0);
		assert.equal(result[0].deletions, 0);
		assert.equal(result[0].binary, true);
	});

	it("handles paths with spaces", () => {
		const result = parseDiff(pathWithSpacesDiff);
		assert.equal(result.length, 1);
		assert.equal(result[0].path, "src/my file.ts");
		assert.equal(result[0].additions, 1);
	});

	it("handles unicode paths", () => {
		const result = parseDiff(unicodeDiff);
		assert.equal(result.length, 1);
		assert.equal(result[0].path, "src/日本語.ts");
		assert.equal(result[0].additions, 1);
	});

	it("handles path containing ' b/' in filename", () => {
		const result = parseDiff(pathWithBSlashDiff);
		assert.equal(result.length, 1);
		assert.equal(result[0].path, "src/foo b/bar.ts");
		assert.equal(result[0].oldPath, "src/foo b/bar.ts");
		assert.equal(result[0].newPath, "src/foo b/bar.ts");
		assert.equal(result[0].additions, 1);
	});

	it("parses mode-only change as modified with zero hunks", () => {
		const result = parseDiff(modeOnlyDiff);
		assert.equal(result.length, 1);
		assert.equal(result[0].path, "src/script.ts");
		assert.equal(result[0].status, "modified");
		assert.equal(result[0].additions, 0);
		assert.equal(result[0].deletions, 0);
	});

	it("does not count '\\\\  No newline at end of file' as a change", () => {
		const result = parseDiff(noNewlineDiff);
		assert.equal(result.length, 1);
		assert.equal(result[0].additions, 1);
		assert.equal(result[0].deletions, 1);
	});

	it("parses rename where new path contains ' b/'", () => {
		const result = parseDiff(renamedNewPathWithBSlashDiff);
		assert.equal(result.length, 1);
		assert.equal(result[0].path, "src/new b/path.ts");
		assert.equal(result[0].oldPath, "src/old.ts");
		assert.equal(result[0].newPath, "src/new b/path.ts");
		assert.equal(result[0].status, "renamed");
	});
});

describe("unquotePath", () => {
	it("returns unquoted paths unchanged", () => {
		assert.equal(unquotePath("src/file.ts"), "src/file.ts");
	});

	it("decodes octal escapes", () => {
		assert.equal(unquotePath('"src/caf\\303\\251.ts"'), "src/café.ts");
	});

	it('decodes \\\\ to \\', () => {
		assert.equal(unquotePath('"src/back\\\\slash.ts"'), "src/back\\slash.ts");
	});

	it('decodes \\" to "', () => {
		assert.equal(unquotePath('"src/q\\"uote.ts"'), 'src/q"uote.ts');
	});

	it("decodes \\n to newline", () => {
		assert.equal(unquotePath('"src/new\\nline.ts"'), "src/new\nline.ts");
	});

	it("decodes \\t to tab", () => {
		assert.equal(unquotePath('"src/tab\\there.ts"'), "src/tab\there.ts");
	});

	it("decodes \\a to BEL (0x07)", () => {
		assert.equal(unquotePath('"src/bel\\a.ts"'), "src/bel\x07.ts");
	});

	it("decodes \\b to BS (0x08)", () => {
		assert.equal(unquotePath('"src/bs\\b.ts"'), "src/bs\x08.ts");
	});

	it("handles mixed escapes", () => {
		assert.equal(
			unquotePath('"src/\\303\\251\\t\\\\name.ts"'),
			"src/é\t\\name.ts",
		);
	});
});
