import { afterEach, beforeEach, describe, expect, it } from "bun:test";
import { join } from "node:path";
import { $ } from "bun";
import { Core } from "../core/backlog.ts";
import type { Document, Task } from "../types/index.ts";
import { createUniqueTestDir, initializeTestProject, safeCleanup } from "./test-utils.ts";

let TEST_DIR: string;

const toPosixPath = (path: string): string => path.replace(/\\/g, "/");

describe("Core", () => {
	let core: Core;

	beforeEach(async () => {
		TEST_DIR = createUniqueTestDir("test-core");
		core = new Core(TEST_DIR);
		await core.filesystem.ensureBacklogStructure();

		// Initialize git repository for testing
		await $`git init -b main`.cwd(TEST_DIR).quiet();
		await $`git config user.name "Test User"`.cwd(TEST_DIR).quiet();
		await $`git config user.email test@example.com`.cwd(TEST_DIR).quiet();
	});

	afterEach(async () => {
		try {
			await safeCleanup(TEST_DIR);
		} catch {
			// Ignore cleanup errors - the unique directory names prevent conflicts
		}
	});

	describe("initialization", () => {
		it("should have filesystem and git operations available", () => {
			expect(core.filesystem).toBeDefined();
			expect(core.gitOps).toBeDefined();
		});

		it("should initialize project with default config", async () => {
			await initializeTestProject(core, "Test Project", true);

			const config = await core.filesystem.loadConfig();
			expect(config?.projectName).toBe("Test Project");
			expect(config?.statuses).toEqual(["To Do", "In Progress", "Done"]);
			expect(config?.defaultStatus).toBe("To Do");
		});

		it("should use root backlog.config.yml for custom backlog directories", async () => {
			await initializeTestProject(core, "Custom Root Project", false, "planning/backlog-data");

			expect(await Bun.file(join(TEST_DIR, "backlog.config.yml")).exists()).toBe(true);
			expect(await Bun.file(join(TEST_DIR, "planning", "backlog-data", "config.yml")).exists()).toBe(false);

			const freshCore = new Core(TEST_DIR);
			const config = await freshCore.filesystem.loadConfig();
			expect(config?.projectName).toBe("Custom Root Project");
			expect(freshCore.filesystem.backlogDirName).toBe("planning/backlog-data");
		});
	});

	describe("task operations", () => {
		const sampleTask: Task = {
			id: "task-1",
			title: "Test Task",
			status: "To Do",
			assignee: [],
			createdDate: "2025-06-07",
			labels: ["test"],
			dependencies: [],
			description: "This is a test task",
		};

		beforeEach(async () => {
			await initializeTestProject(core, "Test Project", true);
		});

		it("should create task without auto-commit", async () => {
			await core.createTask(sampleTask, false);

			const loadedTask = await core.filesystem.loadTask("task-1");
			expect(loadedTask?.id).toBe("TASK-1");
			expect(loadedTask?.title).toBe("Test Task");
		});

		it("should create task with auto-commit", async () => {
			await core.createTask(sampleTask, true);

			// Check if task file was created
			const loadedTask = await core.filesystem.loadTask("task-1");
			expect(loadedTask?.id).toBe("TASK-1");

			// Check git status to see if there are uncommitted changes
			const lastCommit = await core.gitOps.getLastCommitMessage();
			// For now, just check that we have a commit (could be initialization or task)
			expect(lastCommit).toBeDefined();
			expect(lastCommit.length).toBeGreaterThan(0);
		});

		it("should update task with auto-commit", async () => {
			await core.createTask(sampleTask, true);

			// Check original task
			const originalTask = await core.filesystem.loadTask("task-1");
			expect(originalTask?.title).toBe("Test Task");

			await core.updateTaskFromInput("task-1", { title: "Updated Task" }, true);

			// Check if task was updated
			const loadedTask = await core.filesystem.loadTask("task-1");
			expect(loadedTask?.title).toBe("Updated Task");

			const lastCommit = await core.gitOps.getLastCommitMessage();
			// For now, just check that we have a commit (could be initialization or task)
			expect(lastCommit).toBeDefined();
			expect(lastCommit.length).toBeGreaterThan(0);
		});

		it("should archive task with auto-commit", async () => {
			await core.createTask(sampleTask, true);

			const archived = await core.archiveTask("task-1", true);
			expect(archived).toBe(true);

			const lastCommit = await core.gitOps.getLastCommitMessage();
			expect(lastCommit).toContain("backlog: Archive task TASK-1");
		});

		it("should demote task with auto-commit", async () => {
			await core.createTask(sampleTask, true);

			const demoted = await core.demoteTask("task-1", true);
			expect(demoted).toBe(true);

			const lastCommit = await core.gitOps.getLastCommitMessage();
			expect(lastCommit).toContain("backlog: Demote task TASK-1");
		});

		it("should resolve tasks using flexible ID formats", async () => {
			const standardTask: Task = { ...sampleTask, id: "task-5", title: "Standard" };
			const paddedTask: Task = { ...sampleTask, id: "task-007", title: "Padded" };
			await core.createTask(standardTask, false);
			await core.createTask(paddedTask, false);

			const uppercase = await core.getTask("TASK-5");
			expect(uppercase?.id).toBe("TASK-5");

			const bare = await core.getTask("5");
			expect(bare?.id).toBe("TASK-5");

			const zeroPadded = await core.getTask("0007");
			expect(zeroPadded?.id).toBe("TASK-007");

			const mixedCase = await core.getTask("Task-007");
			expect(mixedCase?.id).toBe("TASK-007");
		});

		it("should resolve numeric-only IDs with custom prefix (BACK-364)", async () => {
			// Configure custom prefix
			const config = await core.filesystem.loadConfig();
			if (!config) {
				throw new Error("Expected config to be loaded");
			}
			await core.filesystem.saveConfig({
				...config,
				prefixes: { task: "back" },
			});

			// Create tasks with custom prefix
			const task1: Task = { ...sampleTask, id: "back-358", title: "Custom Prefix Task" };
			const task2: Task = { ...sampleTask, id: "back-5.1", title: "Custom Prefix Subtask" };
			await core.createTask(task1, false);
			await core.createTask(task2, false);

			// Numeric-only lookup should find task with custom prefix
			const byNumeric = await core.getTask("358");
			expect(byNumeric?.id).toBe("BACK-358");
			expect(byNumeric?.title).toBe("Custom Prefix Task");

			// Dotted numeric lookup should find subtask
			const byDotted = await core.getTask("5.1");
			expect(byDotted?.id).toBe("BACK-5.1");
			expect(byDotted?.title).toBe("Custom Prefix Subtask");

			// Full prefixed ID should also work (case-insensitive)
			const byFullId = await core.getTask("BACK-358");
			expect(byFullId?.id).toBe("BACK-358");

			const byLowercase = await core.getTask("back-358");
			expect(byLowercase?.id).toBe("BACK-358");
		});

		it("assigns ordinal 1000 to the first created task when none exist", async () => {
			const { task } = await core.createTaskFromInput({ title: "First ordinal task" });
			expect(task.ordinal).toBe(1000);

			const loadedTask = await core.filesystem.loadTask(task.id);
			expect(loadedTask?.ordinal).toBe(1000);
		});

		it("assigns the next tail ordinal when existing tasks already have ordinals", async () => {
			await core.createTask({
				...sampleTask,
				id: "task-10",
				title: "Seed 1000",
				ordinal: 1000,
			});
			await core.createTask({
				...sampleTask,
				id: "task-11",
				title: "Seed 4000",
				ordinal: 4000,
			});

			const { task } = await core.createTaskFromInput({ title: "Appended ordinal task" });
			expect(task.ordinal).toBe(5000);
		});

		it("preserves explicit ordinals on create input", async () => {
			const { task } = await core.createTaskFromInput({
				title: "Explicit ordinal task",
				ordinal: 2750,
			});
			expect(task.ordinal).toBe(2750);
		});

		it("rejects non-finite ordinals on create input", async () => {
			await expect(
				core.createTaskFromInput({
					title: "Invalid ordinal task",
					ordinal: Number.POSITIVE_INFINITY,
				}),
			).rejects.toThrow("Ordinal must be a non-negative number.");
		});

		it("ignores tasks without ordinals when computing the next tail ordinal", async () => {
			await core.createTask({
				...sampleTask,
				id: "task-20",
				title: "No ordinal seed",
			});
			await core.createTask({
				...sampleTask,
				id: "task-21",
				title: "Ordinal seed",
				ordinal: 3000,
			});

			const { task } = await core.createTaskFromInput({ title: "Mixed ordinal task" });
			expect(task.ordinal).toBe(4000);
		});

		it("preserves legacy no-ordinal ordering when no existing tasks have ordinals", async () => {
			await core.createTask({
				...sampleTask,
				id: "task-20",
				title: "Legacy no ordinal seed",
			});

			const { task } = await core.createTaskFromInput({ title: "Legacy appended task" });
			expect(task.ordinal).toBeUndefined();

			const loadedTask = await core.filesystem.loadTask(task.id);
			expect(loadedTask?.ordinal).toBeUndefined();
		});

		it("rejects non-finite ordinals on update input", async () => {
			const { task } = await core.createTaskFromInput({ title: "Ordinal update target" });

			await expect(core.updateTaskFromInput(task.id, { ordinal: Number.POSITIVE_INFINITY })).rejects.toThrow(
				"Ordinal must be a non-negative number.",
			);
		});

		it("should NOT match numeric ID with typos when using custom prefix (BACK-364)", async () => {
			// Configure custom prefix
			const config = await core.filesystem.loadConfig();
			if (!config) {
				throw new Error("Expected config to be loaded");
			}
			await core.filesystem.saveConfig({
				...config,
				prefixes: { task: "back" },
			});

			// Create task with custom prefix
			const task: Task = { ...sampleTask, id: "back-358", title: "Custom Prefix Task" };
			await core.createTask(task, false);

			// Typos should NOT match (prevent parseInt coercion bug)
			const withTypo = await core.getTask("358a");
			expect(withTypo).toBeNull();

			const withTypo2 = await core.getTask("35x8");
			expect(withTypo2).toBeNull();
		});

		it("should return false when archiving non-existent task", async () => {
			const archived = await core.archiveTask("non-existent", true);
			expect(archived).toBe(false);
		});

		it("should apply default status when task has empty status", async () => {
			const taskWithoutStatus: Task = {
				...sampleTask,
				status: "",
			};

			await core.createTask(taskWithoutStatus, false);

			const loadedTask = await core.filesystem.loadTask("task-1");
			expect(loadedTask?.status).toBe("To Do"); // Should use default from config
		});

		it("should not override existing status", async () => {
			const taskWithStatus: Task = {
				...sampleTask,
				status: "In Progress",
			};

			await core.createTask(taskWithStatus, false);

			const loadedTask = await core.filesystem.loadTask("task-1");
			expect(loadedTask?.status).toBe("In Progress");
		});

		it("should preserve description text when saving without header markers", async () => {
			const taskNoHeader: Task = {
				...sampleTask,
				id: "task-2",
				description: "Just text",
			};

			await core.createTask(taskNoHeader, false);
			const loaded = await core.filesystem.loadTask("task-2");
			expect(loaded?.description).toBe("Just text");
			const body = await core.getTaskContent("task-2");
			const matches = (body?.match(/## Description/g) ?? []).length;
			expect(matches).toBe(1);
		});

		it("should not duplicate description header in saved content", async () => {
			const taskWithHeader: Task = {
				...sampleTask,
				id: "task-3",
				description: "Existing",
			};

			await core.createTask(taskWithHeader, false);
			const body = await core.getTaskContent("task-3");
			const matches = (body?.match(/## Description/g) ?? []).length;
			expect(matches).toBe(1);
		});

		it("should handle task creation without auto-commit when git fails", async () => {
			// Create task in directory without git
			const nonGitCore = new Core(join(TEST_DIR, "no-git"));
			await nonGitCore.filesystem.ensureBacklogStructure();

			// This should succeed even without git
			await nonGitCore.createTask(sampleTask, false);

			const loadedTask = await nonGitCore.filesystem.loadTask("task-1");
			expect(loadedTask?.id).toBe("TASK-1");
		});

		it("should normalize assignee for string and array inputs", async () => {
			const stringTask = {
				...sampleTask,
				id: "task-2",
				title: "String Assignee",
				assignee: "@alice",
			} as unknown as Task;
			await core.createTask(stringTask, false);
			const loadedString = await core.filesystem.loadTask("task-2");
			expect(loadedString?.assignee).toEqual(["@alice"]);

			const arrayTask: Task = {
				...sampleTask,
				id: "task-3",
				title: "Array Assignee",
				assignee: ["@bob"],
			};
			await core.createTask(arrayTask, false);
			const loadedArray = await core.filesystem.loadTask("task-3");
			expect(loadedArray?.assignee).toEqual(["@bob"]);
		});

		it("should normalize assignee when updating tasks", async () => {
			await core.createTask(sampleTask, false);

			await core.updateTaskFromInput("task-1", { assignee: ["@carol"] }, false);
			let loaded = await core.filesystem.loadTask("task-1");
			expect(loaded?.assignee).toEqual(["@carol"]);

			await core.updateTaskFromInput("task-1", { assignee: ["@dave"] }, false);
			loaded = await core.filesystem.loadTask("task-1");
			expect(loaded?.assignee).toEqual(["@dave"]);
		});

		it("should create sub-tasks with proper hierarchical IDs", async () => {
			await initializeTestProject(core, "Subtask Project", true);

			// Create parent task
			const { task: parent } = await core.createTaskFromInput({
				title: "Parent Task",
				status: "To Do",
			});
			expect(parent.id).toBe("TASK-1");

			// Create first sub-task
			const { task: child1 } = await core.createTaskFromInput({
				title: "First Child",
				parentTaskId: parent.id,
				status: "To Do",
			});
			expect(child1.id).toBe("TASK-1.1");
			expect(child1.parentTaskId).toBe("TASK-1");

			// Create second sub-task
			const { task: child2 } = await core.createTaskFromInput({
				title: "Second Child",
				parentTaskId: parent.id,
				status: "To Do",
			});
			expect(child2.id).toBe("TASK-1.2");
			expect(child2.parentTaskId).toBe("TASK-1");

			// Create another parent task to ensure sequential numbering still works
			const { task: parent2 } = await core.createTaskFromInput({
				title: "Second Parent",
				status: "To Do",
			});
			expect(parent2.id).toBe("TASK-2");
		});
	});

	describe("document operations", () => {
		const baseDocument: Document = {
			id: "doc-1",
			title: "Operations Guide",
			type: "guide",
			createdDate: "2025-06-07",
			rawContent: "# Ops Guide",
		};

		beforeEach(async () => {
			await initializeTestProject(core, "Test Project", false);
		});

		it("updates a document title without leaving the previous file behind", async () => {
			await core.createDocument(baseDocument, false);

			const [initialFile] = await Array.fromAsync(
				new Bun.Glob("doc-*.md").scan({ cwd: core.filesystem.docsDir, followSymlinks: true }),
			);
			expect(initialFile).toBe("doc-1 - Operations-Guide.md");

			const documents = await core.filesystem.listDocuments();
			const existingDoc = documents[0];
			if (!existingDoc) {
				throw new Error("Expected document to exist after creation");
			}
			expect(existingDoc.title).toBe("Operations Guide");

			await core.updateDocument({ ...existingDoc, title: "Operations Guide Updated" }, "# Updated content", false);

			const docFiles = await Array.fromAsync(
				new Bun.Glob("doc-*.md").scan({ cwd: core.filesystem.docsDir, followSymlinks: true }),
			);
			expect(docFiles).toHaveLength(1);
			expect(docFiles[0]).toBe("doc-1 - Operations-Guide-Updated.md");

			const updatedDocs = await core.filesystem.listDocuments();
			expect(updatedDocs[0]?.title).toBe("Operations Guide Updated");
		});

		it("creates and moves documents through core input methods", async () => {
			const created = await core.createDocumentFromInput({
				title: "Setup Guide",
				content: "# Setup",
				type: "guide",
				path: "guides / setup",
				tags: ["setup", "guide"],
			});

			expect(created.id).toBe("doc-1");
			expect(created.path).toBe("guides/setup/doc-1 - Setup-Guide.md");

			const updated = await core.updateDocumentFromInput({
				id: created.id,
				title: "Install Guide",
				content: "# Install",
				path: "runbooks",
			});

			expect(updated.title).toBe("Install Guide");
			expect(updated.path).toBe("runbooks/doc-1 - Install-Guide.md");
			expect(updated.rawContent).toBe("# Install");

			const docFiles = await Array.fromAsync(
				new Bun.Glob("**/doc-*.md").scan({ cwd: core.filesystem.docsDir, followSymlinks: true }),
			);
			expect(docFiles.map(toPosixPath)).toEqual(["runbooks/doc-1 - Install-Guide.md"]);
		});

		it("preserves document path when updating without an explicit path", async () => {
			const created = await core.createDocumentFromInput({
				title: "Nested",
				content: "Initial",
				path: "guides",
			});

			const updated = await core.updateDocumentFromInput({
				id: created.id,
				content: "Updated",
				title: "Nested Updated",
			});

			expect(updated.path).toBe("guides/doc-1 - Nested-Updated.md");
		});

		it("rejects unsupported document types in core input methods", async () => {
			await expect(
				core.createDocumentFromInput({
					title: "Invalid",
					content: "Content",
					type: "unexpected",
				} as unknown as Parameters<typeof core.createDocumentFromInput>[0]),
			).rejects.toThrow("Document type must be one of");

			const created = await core.createDocumentFromInput({
				title: "Valid",
				content: "Content",
				type: "guide",
			});

			await expect(
				core.updateDocumentFromInput({
					id: created.id,
					content: "Updated",
					type: "unexpected",
				} as unknown as Parameters<typeof core.updateDocumentFromInput>[0]),
			).rejects.toThrow("Document type must be one of");
		});

		it("shows a git rename when the document title changes", async () => {
			await core.createDocument(baseDocument, true);

			const renamedDoc: Document = {
				...baseDocument,
				title: "Operations Guide Renamed",
			};

			await core.updateDocument(renamedDoc, "# Ops Guide", false);

			await $`git add -A`.cwd(TEST_DIR).quiet();
			const diffResult = await $`git diff --name-status -M HEAD`.cwd(TEST_DIR).quiet();
			const diff = diffResult.stdout.toString();
			const previousPath = "backlog/docs/doc-1 - Operations-Guide.md";
			const renamedPath = "backlog/docs/doc-1 - Operations-Guide-Renamed.md";
			const escapeForRegex = (value: string) => value.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&");
			expect(diff).toMatch(
				new RegExp(`^R\\d*\\t${escapeForRegex(previousPath)}\\t${escapeForRegex(renamedPath)}`, "m"),
			);
		});
	});

	describe("draft operations", () => {
		beforeEach(async () => {
			await initializeTestProject(core, "Draft Project", true);
		});

		it("should create draft without auto-commit", async () => {
			const { task: draft } = await core.createTaskFromInput(
				{
					title: "Draft Task",
					status: "Draft",
					description: "Draft task",
				},
				false,
			);

			const loaded = await core.filesystem.loadDraft(draft.id);
			expect(loaded?.id).toBe("DRAFT-1");
		});

		it("should create draft with auto-commit", async () => {
			const { task: draft } = await core.createTaskFromInput(
				{
					title: "Draft Task",
					status: "Draft",
					description: "Draft task",
				},
				true,
			);

			const loaded = await core.filesystem.loadDraft(draft.id);
			expect(loaded?.id).toBe("DRAFT-1");

			const lastCommit = await core.gitOps.getLastCommitMessage();
			expect(lastCommit).toBeDefined();
			expect(lastCommit.length).toBeGreaterThan(0);
		});

		it("should promote draft with auto-commit", async () => {
			const { task: draft } = await core.createTaskFromInput(
				{
					title: "Draft Task",
					status: "Draft",
					description: "Draft task",
				},
				true,
			);

			const promoted = await core.promoteDraft(draft.id, true);
			expect(promoted).toBe(true);

			const lastCommit = await core.gitOps.getLastCommitMessage();
			expect(lastCommit).toContain(`backlog: Promote draft ${draft.id.toUpperCase()}`);
		});

		it("should archive draft with auto-commit", async () => {
			const { task: draft } = await core.createTaskFromInput(
				{
					title: "Draft Task",
					status: "Draft",
					description: "Draft task",
				},
				true,
			);

			const archived = await core.archiveDraft(draft.id, true);
			expect(archived).toBe(true);

			const lastCommit = await core.gitOps.getLastCommitMessage();
			expect(lastCommit).toContain(`backlog: Archive draft ${draft.id.toUpperCase()}`);
		});

		it("should preserve draft metadata through the canonical create path", async () => {
			const { task: draft } = await core.createTaskFromInput(
				{
					title: "Draft Array",
					status: "Draft",
					description: "Draft task",
					assignee: ["@frank"],
					labels: ["draft"],
				},
				false,
			);

			const loaded = await core.filesystem.loadDraft(draft.id);
			expect(loaded?.assignee).toEqual(["@frank"]);
			expect(loaded?.labels).toEqual(["draft"]);
		});
	});

	describe("integration with config", () => {
		it("should use custom default status from config", async () => {
			// Initialize with custom config
			await initializeTestProject(core, "Custom Project");

			// Update config with custom default status
			const config = await core.filesystem.loadConfig();
			if (config) {
				config.defaultStatus = "Custom Status";
				await core.filesystem.saveConfig(config);
			}

			const taskWithoutStatus: Task = {
				id: "task-custom",
				title: "Custom Task",
				status: "",
				assignee: [],
				createdDate: "2025-06-07",
				labels: [],
				dependencies: [],
				description: "Task without status",
			};

			await core.createTask(taskWithoutStatus, false);

			const loadedTask = await core.filesystem.loadTask("task-custom");
			expect(loadedTask?.status).toBe("Custom Status");
		});

		it("should fall back to To Do when config has no default status", async () => {
			// Initialize project
			await initializeTestProject(core, "Fallback Project");

			// Update config to remove default status
			const config = await core.filesystem.loadConfig();
			if (config) {
				config.defaultStatus = undefined;
				await core.filesystem.saveConfig(config);
			}

			const taskWithoutStatus: Task = {
				id: "task-fallback",
				title: "Fallback Task",
				status: "",
				assignee: [],
				createdDate: "2025-06-07",
				labels: [],
				dependencies: [],
				description: "Task without status",
			};

			await core.createTask(taskWithoutStatus, false);

			const loadedTask = await core.filesystem.loadTask("task-fallback");
			expect(loadedTask?.status).toBe("To Do");
		});
	});

	describe("directory accessor integration", () => {
		it("should use FileSystem directory accessors for git operations", async () => {
			await initializeTestProject(core, "Accessor Test");

			const task: Task = {
				id: "task-accessor",
				title: "Accessor Test Task",
				status: "To Do",
				assignee: [],
				createdDate: "2025-06-07",
				labels: [],
				dependencies: [],
				description: "Testing directory accessors",
			};

			// Create task without auto-commit to avoid potential git timing issues
			await core.createTask(task, false);

			// Verify the task file was created in the correct directory
			// List all files to see what was actually created
			const allFiles = await core.filesystem.listTasks();

			// Check that a task with the expected ID exists
			const createdTask = allFiles.find((t) => t.id === "TASK-ACCESSOR");
			expect(createdTask).toBeDefined();
			expect(createdTask?.title).toBe("Accessor Test Task");
		}, 10000);
	});
});
