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, safeCleanup } from "./test-utils.ts";

let TEST_DIR: string;

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 core.initializeProject("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");
		});
	});

	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 core.initializeProject("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 _hasChanges = await core.gitOps.hasUncommittedChanges();

			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 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 core.initializeProject("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 core.initializeProject("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 }));
			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 }));
			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("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", () => {
		const sampleDraft: Task = {
			id: "task-draft",
			title: "Draft Task",
			status: "Draft",
			assignee: [],
			createdDate: "2025-06-07",
			labels: [],
			dependencies: [],
			description: "Draft task",
		};

		beforeEach(async () => {
			await core.initializeProject("Draft Project", true);
		});

		it("should create draft without auto-commit", async () => {
			await core.createDraft(sampleDraft, false);

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

		it("should create draft with auto-commit", async () => {
			await core.createDraft(sampleDraft, true);

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

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

		it("should promote draft with auto-commit", async () => {
			await core.createDraft(sampleDraft, true);

			const promoted = await core.promoteDraft("task-draft", true);
			expect(promoted).toBe(true);

			const lastCommit = await core.gitOps.getLastCommitMessage();
			expect(lastCommit).toContain("backlog: Promote draft task-draft");
		});

		it("should archive draft with auto-commit", async () => {
			await core.createDraft(sampleDraft, true);

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

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

		it("should normalize assignee for string and array inputs", async () => {
			const draftString = {
				...sampleDraft,
				id: "task-draft-1",
				title: "Draft String",
				assignee: "@erin",
			} as unknown as Task;
			await core.createDraft(draftString, false);
			const loadedString = await core.filesystem.loadDraft("task-draft-1");
			expect(loadedString?.assignee).toEqual(["@erin"]);

			const draftArray: Task = {
				...sampleDraft,
				id: "task-draft-2",
				title: "Draft Array",
				assignee: ["@frank"],
			};
			await core.createDraft(draftArray, false);
			const loadedArray = await core.filesystem.loadDraft("task-draft-2");
			expect(loadedArray?.assignee).toEqual(["@frank"]);
		});
	});

	describe("integration with config", () => {
		it("should use custom default status from config", async () => {
			// Initialize with custom config
			await core.initializeProject("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 core.initializeProject("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 core.initializeProject("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
			const _tasksDir = core.filesystem.tasksDir;

			// 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);
	});
});
