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

import showDiffExtension, { createRunReviewDiff } from "../index.ts";

function createExecStub(overrides: Record<string, { code: number; stdout: string; stderr: string }> = {}) {
	return async (_cmd: string, args: string[]) => {
		const key = args.join(" ");
		if (key in overrides) return overrides[key]!;
		if (key === "rev-parse --git-dir") return { code: 0, stdout: ".git\n", stderr: "" };
		if (key === "rev-parse --abbrev-ref HEAD") return { code: 0, stdout: "feature\n", stderr: "" };
		if (key === "for-each-ref --format=%(upstream:remotename) refs/heads/feature")
			return { code: 0, stdout: "origin\n", stderr: "" };
		if (key === "rev-parse --verify origin/main") return { code: 0, stdout: "abc\n", stderr: "" };
		if (key === "rev-parse --verify main") return { code: 0, stdout: "abc\n", stderr: "" };
		if (key === "status --porcelain") return { code: 0, stdout: "", stderr: "" };
		if (key === "diff origin/main...HEAD" || key === "diff main...HEAD") {
			return {
				code: 0,
				stdout: "diff --git a/a.ts b/a.ts\nindex 1111111..2222222 100644\n--- a/a.ts\n+++ b/a.ts\n@@ -1 +1 @@\n-old\n+new\n",
				stderr: "",
			};
		}
		if (key === "rev-parse --show-toplevel") return { code: 0, stdout: "/repo\n", stderr: "" };
		if (key.startsWith("show ")) return { code: 0, stdout: "file content\n", stderr: "" };
		return { code: 1, stdout: "", stderr: `unhandled exec: ${key}` };
	};
}

function createCtx() {
	const notifications: Array<{ message: string; level: "info" | "warning" | "error" }> = [];
	return {
		ctx: {
			cwd: "/repo",
			hasUI: true,
			ui: {
				notify: (message: string, level: "info" | "warning" | "error") => notifications.push({ message, level }),
			},
		},
		notifications,
	};
}

function createHarness(execImpl: (cmd: string, args: string[]) => Promise<{ code: number; stdout: string; stderr: string }>) {
	let command:
		| { name: string; handler: (args: string, ctx: ReturnType<typeof createCtx>["ctx"]) => Promise<void> }
		| undefined;
	let tool:
		| {
				name: string;
				execute: (
					_toolCallId: string,
					params: { base?: string },
					_signal: unknown,
					_onUpdate: unknown,
					ctx: ReturnType<typeof createCtx>["ctx"],
				) => Promise<{ content: Array<{ type: string; text: string }>; details: { outputPath?: string } }>;
		  }
		| undefined;

	showDiffExtension(
		{
			exec: execImpl,
			registerCommand: (name: string, def: { handler: (args: string, ctx: ReturnType<typeof createCtx>["ctx"]) => Promise<void> }) => {
				command = { name, handler: def.handler };
			},
			registerTool: (def: { name: string } & NonNullable<typeof tool>) => {
				tool = def;
			},
		} as never,
		{
			renderHtml: () => "<html />",
			writeHtml: async () => "/tmp/review.html",
			openBrowser: async () => ({ ok: true }),
		},
	);

	if (!command || !tool) throw new Error("extension failed to register command/tool");
	return { command, tool };
}

describe("createRunReviewDiff", () => {
	it("short-circuits with no-change message on empty diff", async () => {
		const runReviewDiff = createRunReviewDiff({
			renderHtml: () => {
				throw new Error("render should not run");
			},
			writeHtml: async () => {
				throw new Error("write should not run");
			},
			openBrowser: async () => {
				throw new Error("browser should not run");
			},
		});

		const result = await runReviewDiff(
			{ exec: createExecStub({ "diff origin/main...HEAD": { code: 0, stdout: "\n\n", stderr: "" } }) } as never,
			undefined,
			createCtx().ctx as never,
		);
		assert.deepEqual(result, { ok: true, message: "No changes between origin/main and HEAD" });
	});

	it("stays successful when browser open fails and emits both warnings", async () => {
		const fromCtx = createCtx();
		const runReviewDiff = createRunReviewDiff({
			renderHtml: () => "<html />",
			writeHtml: async () => "/tmp/review.html",
			openBrowser: async () => ({ ok: false, error: "blocked" }),
		});

		const result = await runReviewDiff(
			{ exec: createExecStub({ "status --porcelain": { code: 0, stdout: " M a.txt\n", stderr: "" } }) } as never,
			undefined,
			fromCtx.ctx as never,
		);

		assert.equal(result.ok, true);
		assert.equal(result.message, "HTML diff generated: /tmp/review.html");
		const warnings = fromCtx.notifications.filter((n) => n.level === "warning").map((n) => n.message);
		assert.ok(warnings.some((m) => m.includes("Working tree has staged/unstaged changes")));
		assert.ok(warnings.some((m) => m.includes("Failed to open browser automatically")));
	});

	it("passes FileContent array to renderHtml", async () => {
		let receivedFiles: unknown[] = [];
		const runReviewDiff = createRunReviewDiff({
			renderHtml: (files) => { receivedFiles = files; return "<html />"; },
			writeHtml: async () => "/tmp/review.html",
			openBrowser: async () => ({ ok: true }),
		});

		await runReviewDiff(
			{ exec: createExecStub() } as never,
			undefined,
			createCtx().ctx as never,
		);

		assert.equal(receivedFiles.length, 1);
		const file = receivedFiles[0] as { path: string; status: string; original: string; modified: string };
		assert.equal(file.path, "a.ts");
		assert.equal(file.status, "modified");
		assert.equal(typeof file.original, "string");
		assert.equal(typeof file.modified, "string");
	});
});

describe("show-diff command/tool", () => {
	it("registers /show-diff command and keeps internal show_review_diff tool contract", () => {
		const { command, tool } = createHarness(createExecStub());
		assert.equal(command.name, "show-diff");
		assert.equal(tool.name, "show_review_diff");
	});

	it("notifies success and returns generated output details", async () => {
		const { command, tool } = createHarness(createExecStub());
		const fromCommand = createCtx();
		const fromTool = createCtx();

		await command.handler("main", fromCommand.ctx);
		const toolResult = await tool.execute("id", { base: "main" }, undefined, undefined, fromTool.ctx);

		assert.equal(fromCommand.notifications.find((n) => n.level === "info")?.message, "HTML diff generated: /tmp/review.html");
		assert.equal(fromTool.notifications.find((n) => n.level === "info")?.message, "HTML diff generated: /tmp/review.html");
		assert.equal(toolResult.details.outputPath, "/tmp/review.html");
		assert.equal(toolResult.content[0]?.text, "HTML diff generated: /tmp/review.html");
	});

	it("propagates git diff failures for command and tool", async () => {
		const { command, tool } = createHarness(createExecStub({ "diff origin/main...HEAD": { code: 1, stdout: "", stderr: "bad diff" } }));
		const fromCommand = createCtx();
		const fromTool = createCtx();

		await command.handler("", fromCommand.ctx);
		const toolResult = await tool.execute("id", {}, undefined, undefined, fromTool.ctx);

		assert.equal(fromCommand.notifications.find((n) => n.level === "error")?.message, "git diff failed: bad diff");
		assert.equal(fromTool.notifications.find((n) => n.level === "error")?.message, "git diff failed: bad diff");
		assert.equal(toolResult.content[0]?.text, "git diff failed: bad diff");
	});
});
