import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test";
import { buildLocalBranchTaskIndex, loadLocalBranchTasks } from "../core/task-loader.ts";
import type { GitOperations } from "../git/operations.ts";
import type { BacklogConfig, Task } from "../types/index.ts";

// Mock GitOperations for testing
class MockGitOperations implements Partial<GitOperations> {
	private currentBranch = "main";

	async getCurrentBranch(): Promise<string> {
		return this.currentBranch;
	}

	async listRecentBranches(_daysAgo: number): Promise<string[]> {
		return ["main", "feature-a", "feature-b", "origin/main"];
	}

	async getBranchLastModifiedMap(_ref: string, _dir: string): Promise<Map<string, Date>> {
		const map = new Map<string, Date>();
		map.set("backlog/tasks/task-1 - Main Task.md", new Date("2025-06-13"));
		map.set("backlog/tasks/task-2 - Feature Task.md", new Date("2025-06-13"));
		map.set("backlog/tasks/task-3 - New Task.md", new Date("2025-06-13"));
		return map;
	}

	async listFilesInTree(ref: string, _path: string): Promise<string[]> {
		// Main branch has task-1 and task-2
		if (ref === "main") {
			return ["backlog/tasks/task-1 - Main Task.md", "backlog/tasks/task-2 - Feature Task.md"];
		}
		// feature-a has task-1 and task-3 (task-3 is new)
		if (ref === "feature-a") {
			return ["backlog/tasks/task-1 - Main Task.md", "backlog/tasks/task-3 - New Task.md"];
		}
		// feature-b has task-2
		if (ref === "feature-b") {
			return ["backlog/tasks/task-2 - Feature Task.md"];
		}
		return [];
	}

	async showFile(_ref: string, file: string): Promise<string> {
		if (file.includes("task-1")) {
			return `---
id: task-1
title: Main Task
status: To Do
assignee: []
created_date: 2025-06-13
labels: []
dependencies: []
---\n\n## Description\n\nMain task`;
		}
		if (file.includes("task-2")) {
			return `---
id: task-2
title: Feature Task
status: In Progress
assignee: []
created_date: 2025-06-13
labels: []
dependencies: []
---\n\n## Description\n\nFeature task`;
		}
		if (file.includes("task-3")) {
			return `---
id: task-3
title: New Task
status: To Do
assignee: []
created_date: 2025-06-13
labels: []
dependencies: []
---\n\n## Description\n\nNew task from feature-a branch`;
		}
		return "";
	}
}

describe("Local branch task discovery", () => {
	let consoleDebugSpy: ReturnType<typeof spyOn>;

	beforeEach(() => {
		consoleDebugSpy = spyOn(console, "debug");
	});

	afterEach(() => {
		consoleDebugSpy?.mockRestore();
	});

	describe("buildLocalBranchTaskIndex", () => {
		it("should build index from local branches excluding current branch", async () => {
			const mockGit = new MockGitOperations() as unknown as GitOperations;
			const branches = ["main", "feature-a", "feature-b", "origin/main"];

			const index = await buildLocalBranchTaskIndex(mockGit, branches, "main", "backlog");

			// Should find TASK-3 from feature-a (not in main)
			expect(index.has("TASK-3")).toBe(true);
			const task3Entries = index.get("TASK-3");
			expect(task3Entries?.length).toBe(1);
			expect(task3Entries?.[0]?.branch).toBe("feature-a");

			// Should find task-1 and task-2 from other branches
			expect(index.has("TASK-1")).toBe(true);
			expect(index.has("TASK-2")).toBe(true);
		});

		it("should exclude origin/ branches", async () => {
			const mockGit = new MockGitOperations() as unknown as GitOperations;
			const branches = ["main", "feature-a", "origin/feature-a"];

			const index = await buildLocalBranchTaskIndex(mockGit, branches, "main", "backlog");

			// Should only have entries from feature-a (local), not origin/feature-a
			const task1Entries = index.get("TASK-1");
			expect(task1Entries?.every((e) => e.branch === "feature-a")).toBe(true);
		});

		it("should exclude current branch", async () => {
			const mockGit = new MockGitOperations() as unknown as GitOperations;
			const branches = ["main", "feature-a"];

			const index = await buildLocalBranchTaskIndex(mockGit, branches, "main", "backlog");

			// task-1 should only be from feature-a, not main
			const task1Entries = index.get("TASK-1");
			expect(task1Entries?.every((e) => e.branch !== "main")).toBe(true);
		});
	});

	describe("loadLocalBranchTasks", () => {
		it("should discover tasks from other local branches", async () => {
			const mockGit = new MockGitOperations() as unknown as GitOperations;

			const progressMessages: string[] = [];
			const localBranchTasks = await loadLocalBranchTasks(mockGit, null, (msg: string) => {
				progressMessages.push(msg);
			});

			// Should find TASK-3 which only exists in feature-a
			const task3 = localBranchTasks.find((t) => t.id === "TASK-3");
			expect(task3).toBeDefined();
			expect(task3?.title).toBe("New Task");
			expect(task3?.source).toBe("local-branch");
			expect(task3?.branch).toBe("feature-a");

			// Progress should mention other local branches
			expect(progressMessages.some((msg) => msg.includes("other local branches"))).toBe(true);
		});

		it("should skip tasks that exist in filesystem when provided", async () => {
			const mockGit = new MockGitOperations() as unknown as GitOperations;

			// Simulate that task-1 already exists in filesystem
			const localTasks: Task[] = [
				{
					id: "task-1",
					title: "Main Task (local)",
					status: "To Do",
					assignee: [],
					createdDate: "2025-06-13",
					labels: [],
					dependencies: [],
					source: "local",
				},
			];

			const localBranchTasks = await loadLocalBranchTasks(mockGit, null, undefined, localTasks);

			// TASK-3 should be found (not in local tasks)
			expect(localBranchTasks.some((t) => t.id === "TASK-3")).toBe(true);

			// task-1 should not be hydrated since it exists locally
			// (unless the remote version is newer, which in this mock it's not)
			// The behavior depends on whether the remote version is newer
		});

		it("should return empty array when on detached HEAD", async () => {
			const mockGit = {
				getCurrentBranch: async () => "",
			} as unknown as GitOperations;

			const tasks = await loadLocalBranchTasks(mockGit, null);
			expect(tasks).toEqual([]);
		});

		it("should return empty when only current branch exists", async () => {
			const mockGit = {
				getCurrentBranch: async () => "main",
				listRecentBranches: async () => ["main"],
			} as unknown as GitOperations;

			const tasks = await loadLocalBranchTasks(mockGit, null);
			expect(tasks).toEqual([]);
		});

		it("should match local tasks with uppercase IDs to index keys (custom prefix)", async () => {
			// Mock git operations for custom prefix (JIRA)
			const mockGit = {
				getCurrentBranch: async () => "main",
				listRecentBranches: async () => ["main", "feature-a"],
				listFilesInTree: async (ref: string) => {
					if (ref === "feature-a") {
						return ["backlog/tasks/jira-123 - Remote Task.md"];
					}
					return [];
				},
				getBranchLastModifiedMap: async () => {
					const map = new Map<string, Date>();
					map.set("backlog/tasks/jira-123 - Remote Task.md", new Date("2025-06-10")); // Older than local
					return map;
				},
				showFile: async () => `---
id: JIRA-123
title: Remote Task
status: To Do
assignee: []
created_date: 2025-06-10
labels: []
dependencies: []
---

## Description

Task from feature branch`,
			} as unknown as GitOperations;

			// Local task has uppercase ID (canonical format)
			const localTasks: Task[] = [
				{
					id: "JIRA-123", // Uppercase canonical ID
					title: "Local Task",
					status: "In Progress", // More progressed than remote
					assignee: [],
					createdDate: "2025-06-13",
					updatedDate: "2025-06-15", // Newer than remote
					labels: [],
					dependencies: [],
					source: "local",
				},
			];

			const config: BacklogConfig = {
				projectName: "Test",
				statuses: ["To Do", "In Progress", "Done"],
				labels: [],
				milestones: [],
				dateFormat: "YYYY-MM-DD",
				prefixes: { task: "jira" },
			};
			const localBranchTasks = await loadLocalBranchTasks(mockGit, config, undefined, localTasks);

			// JIRA-123 exists locally with more progress, so it should NOT be hydrated from other branch
			// This tests that uppercase "JIRA-123" in localById matches normalized index IDs
			expect(localBranchTasks.find((t) => t.id === "JIRA-123")).toBeUndefined();
		});

		it("should hydrate tasks that do not exist locally with custom prefix", async () => {
			const mockGit = {
				getCurrentBranch: async () => "main",
				listRecentBranches: async () => ["main", "feature-a"],
				listFilesInTree: async (ref: string) => {
					if (ref === "feature-a") {
						return ["backlog/tasks/jira-456 - New Remote Task.md"];
					}
					return [];
				},
				getBranchLastModifiedMap: async () => {
					const map = new Map<string, Date>();
					map.set("backlog/tasks/jira-456 - New Remote Task.md", new Date("2025-06-13"));
					return map;
				},
				showFile: async () => `---
id: JIRA-456
title: New Remote Task
status: To Do
assignee: []
created_date: 2025-06-13
labels: []
dependencies: []
---

## Description

New task from feature branch`,
			} as unknown as GitOperations;

			// Local tasks do NOT include JIRA-456
			const localTasks: Task[] = [
				{
					id: "JIRA-123",
					title: "Local Task",
					status: "To Do",
					assignee: [],
					createdDate: "2025-06-13",
					labels: [],
					dependencies: [],
					source: "local",
				},
			];

			const config: BacklogConfig = {
				projectName: "Test",
				statuses: ["To Do", "In Progress", "Done"],
				labels: [],
				milestones: [],
				dateFormat: "YYYY-MM-DD",
				prefixes: { task: "jira" },
			};
			const localBranchTasks = await loadLocalBranchTasks(mockGit, config, undefined, localTasks);

			// JIRA-456 should be hydrated since it doesn't exist locally
			const task456 = localBranchTasks.find((t) => t.id === "JIRA-456");
			expect(task456).toBeDefined();
			expect(task456?.title).toBe("New Remote Task");
			expect(task456?.source).toBe("local-branch");
		});

		it("should index and hydrate via pinned SHA when the branch is deleted after resolving", async () => {
			// Regression for the "fatal: failed to stat '<branch>:<path>'" race:
			// once the branch resolves to a SHA, every later index/hydrate operation
			// must use the SHA so branch deletion cannot suppress hydration.
			const SHA = "deadbeefcafe0000deadbeefcafe0000deadbeef";
			let showFileCalls = 0;
			const mockGit = {
				getCurrentBranch: async () => "main",
				listRecentBranches: async () => ["main", "feature-gone"],
				listFilesInTree: async (ref: string) => {
					if (ref !== SHA) {
						throw new Error(`unexpected list ref ${ref}`);
					}
					return ["backlog/tasks/task-741 - Long Title.md"];
				},
				getBranchLastModifiedMap: async (ref: string) => {
					if (ref !== SHA) {
						throw new Error(`unexpected timestamp ref ${ref}`);
					}
					const map = new Map<string, Date>();
					map.set("backlog/tasks/task-741 - Long Title.md", new Date("2025-06-13"));
					return map;
				},
				resolveCommit: async (ref: string) => (ref === "feature-gone" ? SHA : null),
				showFile: async (ref: string, _file: string) => {
					showFileCalls++;
					// Simulate the branch having been deleted: only the SHA still resolves.
					if (ref === "feature-gone") {
						throw new Error("fatal: failed to stat 'feature-gone:backlog/tasks/task-741 - Long Title.md'");
					}
					if (ref !== SHA) {
						throw new Error(`unexpected ref ${ref}`);
					}
					return `---
id: task-741
title: Long Title
status: To Do
assignee: []
created_date: 2025-06-13
labels: []
dependencies: []
---\n\n## Description\n\nPinned task`;
				},
			} as unknown as GitOperations;

			const localTasks: Task[] = [
				{
					id: "TASK-741",
					title: "Local Long Title",
					status: "To Do",
					assignee: [],
					createdDate: "2025-06-01",
					updatedDate: "2025-06-01",
					labels: [],
					dependencies: [],
					source: "local",
				},
			];

			const tasks = await loadLocalBranchTasks(mockGit, null, undefined, localTasks);

			const task741 = tasks.find((t) => t.id === "TASK-741");
			expect(task741).toBeDefined();
			expect(task741?.title).toBe("Long Title");
			// Display branch must remain the human-readable name, not the SHA.
			expect(task741?.branch).toBe("feature-gone");
			expect(showFileCalls).toBe(1);
		});

		it("should still hydrate via branch name when resolveCommit is unavailable (fallback)", async () => {
			// Older GitOperations / mocks without resolveCommit must keep working.
			const mockGit = {
				getCurrentBranch: async () => "main",
				listRecentBranches: async () => ["main", "feature-a"],
				listFilesInTree: async (ref: string) => (ref === "feature-a" ? ["backlog/tasks/task-9 - Plain.md"] : []),
				getBranchLastModifiedMap: async () => {
					const map = new Map<string, Date>();
					map.set("backlog/tasks/task-9 - Plain.md", new Date("2025-06-13"));
					return map;
				},
				showFile: async (ref: string) => {
					if (ref !== "feature-a") throw new Error(`unexpected ref ${ref}`);
					return `---
id: task-9
title: Plain
status: To Do
assignee: []
created_date: 2025-06-13
labels: []
dependencies: []
---\n\n## Description\n\nPlain task`;
				},
			} as unknown as GitOperations;

			const tasks = await loadLocalBranchTasks(mockGit, null);
			expect(tasks.find((t) => t.id === "TASK-9")?.title).toBe("Plain");
		});
	});
});
