import assert from "node:assert/strict";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import {
	_getAdapter,
	closeDatabase,
	copyWorktreeDb,
	getDecisionById,
	getRequirementById,
	insertArtifact,
	insertDecision,
	insertRequirement,
	isDbAvailable,
	openDatabase,
	reconcileWorktreeDb,
} from "../sf-db.ts";
import { describe, it } from "vitest";

function tempDir(): string {
	return fs.mkdtempSync(path.join(os.tmpdir(), "sf-wt-test-"));
}

function cleanup(...dirs: string[]): void {
	closeDatabase();
	for (const dir of dirs) {
		try {
			fs.rmSync(dir, { recursive: true, force: true });
		} catch {
			// best effort
		}
	}
}

function seedMainDb(dbPath: string): void {
	openDatabase(dbPath);
	insertDecision({
		id: "D001",
		when_context: "2025-01-01",
		scope: "M001/S01",
		decision: "Use SQLite",
		choice: "node:sqlite",
		rationale: "Built-in",
		revisable: "yes",
		made_by: "agent",
		superseded_by: null,
	});
	insertRequirement({
		id: "R001",
		class: "functional",
		status: "active",
		description: "Must store decisions",
		why: "Core feature",
		source: "design",
		primary_owner: "S01",
		supporting_slices: "",
		validation: "test",
		notes: "",
		full_content: "Full requirement text",
		superseded_by: null,
	});
	insertArtifact({
		path: "docs/arch.md",
		artifact_type: "plan",
		milestone_id: "M001",
		slice_id: null,
		task_id: null,
		full_content: "Architecture document",
	});
}

describe("worktree-db: copyWorktreeDb", () => {
	it("copies DB file and data is queryable", () => {
		const srcDir = tempDir();
		const destDir = tempDir();
		const srcDb = path.join(srcDir, "sf.db");
		const destDb = path.join(destDir, "nested", "sf.db");

		seedMainDb(srcDb);
		closeDatabase();

		const result = copyWorktreeDb(srcDb, destDb);
		assert.strictEqual(result, true);
		assert.ok(fs.existsSync(destDb));

		openDatabase(destDb);
		const d = getDecisionById("D001");
		assert.ok(d !== null);
		assert.strictEqual(d?.choice, "node:sqlite");

		const r = getRequirementById("R001");
		assert.ok(r !== null);
		assert.strictEqual(r?.description, "Must store decisions");

		cleanup(srcDir, destDir);
	});

	it("skips -wal and -shm files", () => {
		const srcDir = tempDir();
		const destDir = tempDir();
		const srcDb = path.join(srcDir, "sf.db");
		const destDb = path.join(destDir, "sf.db");

		seedMainDb(srcDb);
		closeDatabase();

		fs.writeFileSync(srcDb + "-wal", "fake wal data");
		fs.writeFileSync(srcDb + "-shm", "fake shm data");

		copyWorktreeDb(srcDb, destDb);

		assert.ok(fs.existsSync(destDb));
		assert.ok(!fs.existsSync(destDb + "-wal"));
		assert.ok(!fs.existsSync(destDb + "-shm"));

		cleanup(srcDir, destDir);
	});

	it("returns false when source doesn't exist", () => {
		const destDir = tempDir();
		const result = copyWorktreeDb(
			"/nonexistent/path/sf.db",
			path.join(destDir, "sf.db"),
		);
		assert.strictEqual(result, false);
		cleanup(destDir);
	});

	it("creates dest directory if needed", () => {
		const srcDir = tempDir();
		const destDir = tempDir();
		const srcDb = path.join(srcDir, "sf.db");
		const deepDest = path.join(destDir, "a", "b", "c", "sf.db");

		seedMainDb(srcDb);
		closeDatabase();

		const result = copyWorktreeDb(srcDb, deepDest);
		assert.strictEqual(result, true);
		assert.ok(fs.existsSync(deepDest));

		cleanup(srcDir, destDir);
	});
});

describe("worktree-db: reconcileWorktreeDb", () => {
	it("merges new decisions from worktree into main", () => {
		const mainDir = tempDir();
		const wtDir = tempDir();
		const mainDb = path.join(mainDir, "sf.db");
		const wtDb = path.join(wtDir, "sf.db");

		seedMainDb(mainDb);
		closeDatabase();

		copyWorktreeDb(mainDb, wtDb);
		openDatabase(wtDb);
		insertDecision({
			id: "D002",
			when_context: "2025-02-01",
			scope: "M001/S02",
			decision: "Use WAL mode",
			choice: "WAL",
			rationale: "Performance",
			revisable: "yes",
			made_by: "agent",
			superseded_by: null,
		});
		closeDatabase();

		openDatabase(mainDb);
		const result = reconcileWorktreeDb(mainDb, wtDb);

		assert.ok(result.decisions > 0);
		const d2 = getDecisionById("D002");
		assert.ok(d2 !== null);
		assert.strictEqual(d2?.choice, "WAL");

		cleanup(mainDir, wtDir);
	});

	it("merges new requirements from worktree into main", () => {
		const mainDir = tempDir();
		const wtDir = tempDir();
		const mainDb = path.join(mainDir, "sf.db");
		const wtDb = path.join(wtDir, "sf.db");

		seedMainDb(mainDb);
		closeDatabase();
		copyWorktreeDb(mainDb, wtDb);

		openDatabase(wtDb);
		insertRequirement({
			id: "R002",
			class: "non-functional",
			status: "active",
			description: "Must be fast",
			why: "UX",
			source: "design",
			primary_owner: "S02",
			supporting_slices: "",
			validation: "benchmark",
			notes: "",
			full_content: "Performance requirement",
			superseded_by: null,
		});
		closeDatabase();

		openDatabase(mainDb);
		const result = reconcileWorktreeDb(mainDb, wtDb);

		assert.ok(result.requirements > 0);
		const r2 = getRequirementById("R002");
		assert.ok(r2 !== null);
		assert.strictEqual(r2?.description, "Must be fast");

		cleanup(mainDir, wtDir);
	});

	it("detects conflicts and applies INSERT OR REPLACE (worktree wins)", () => {
		const mainDir = tempDir();
		const wtDir = tempDir();
		const mainDb = path.join(mainDir, "sf.db");
		const wtDb = path.join(wtDir, "sf.db");

		seedMainDb(mainDb);
		closeDatabase();
		copyWorktreeDb(mainDb, wtDb);

		openDatabase(mainDb);
		_getAdapter()!
			.prepare(`UPDATE decisions SET choice = 'better-sqlite3' WHERE id = 'D001'`)
			.run();
		closeDatabase();

		openDatabase(wtDb);
		_getAdapter()!
			.prepare(`UPDATE decisions SET choice = 'sql.js' WHERE id = 'D001'`)
			.run();
		closeDatabase();

		openDatabase(mainDb);
		const result = reconcileWorktreeDb(mainDb, wtDb);

		assert.ok(result.conflicts.length > 0);
		assert.ok(
			result.conflicts.some((c) => c.includes("D001")),
		);

		const d1 = getDecisionById("D001");
		assert.strictEqual(d1?.choice, "sql.js");

		cleanup(mainDir, wtDir);
	});

	it("handles missing worktree DB gracefully", () => {
		const mainDir = tempDir();
		const mainDb = path.join(mainDir, "sf.db");

		seedMainDb(mainDb);

		const result = reconcileWorktreeDb(mainDb, "/nonexistent/worktree.db");
		assert.strictEqual(result.decisions, 0);
		assert.strictEqual(result.requirements, 0);
		assert.strictEqual(result.artifacts, 0);
		assert.strictEqual(result.conflicts.length, 0);

		cleanup(mainDir);
	});

	it("main DB is usable after reconciliation", () => {
		const mainDir = tempDir();
		const wtDir = tempDir();
		const mainDb = path.join(mainDir, "sf.db");
		const wtDb = path.join(wtDir, "sf.db");

		seedMainDb(mainDb);
		closeDatabase();
		copyWorktreeDb(mainDb, wtDb);

		openDatabase(mainDb);
		reconcileWorktreeDb(mainDb, wtDb);

		assert.ok(isDbAvailable());

		insertDecision({
			id: "D099",
			when_context: "2025-12-01",
			scope: "test",
			decision: "Post-reconcile insert",
			choice: "works",
			rationale: "Verify DETACH cleanup",
			revisable: "no",
			made_by: "agent",
			superseded_by: null,
		});

		const d99 = getDecisionById("D099");
		assert.ok(d99 !== null);
		assert.strictEqual(d99?.choice, "works");

		const adapter = _getAdapter()!;
		let wtAccessible = false;
		try {
			adapter.prepare("SELECT count(*) FROM wt.decisions").get();
			wtAccessible = true;
		} catch {
			// Expected — wt should be detached
		}
		assert.ok(!wtAccessible, "wt database is detached after reconciliation");

		cleanup(mainDir, wtDir);
	});

	it("reconcile with empty worktree DB produces no conflicts", () => {
		const mainDir = tempDir();
		const wtDir = tempDir();
		const mainDb = path.join(mainDir, "sf.db");
		const wtDb = path.join(wtDir, "sf.db");

		seedMainDb(mainDb);
		closeDatabase();
		copyWorktreeDb(mainDb, wtDb);

		openDatabase(mainDb);
		const result = reconcileWorktreeDb(mainDb, wtDb);

		assert.strictEqual(result.conflicts.length, 0);
		assert.ok(isDbAvailable());

		cleanup(mainDir, wtDir);
	});
});
