/**
 * uok-purpose-coherence.test.mjs — verify the purpose-coherence-gate
 * verdict logic (ADR-0000 restoration, P4).
 *
 * Two layers:
 *   1. computePurposeCoherenceVerdict — pure function over the
 *      milestone/slice/task trace inputs. Covers the four states
 *      named in the parent contract: missing vision (fail),
 *      vision+slice-trace-null (warn), vision+slice-ok+task-null
 *      (warn), full chain (pass).
 *   2. evaluatePurposeCoherence — DB-backed adapter. Exercises the
 *      "column doesn't exist on legacy DB" downgrade path that the
 *      gate must survive while P2/P3 migrations land in parallel.
 */
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, test } from "vitest";
import { _getAdapter, closeDatabase, openDatabase } from "../sf-db.js";
import {
	computePurposeCoherenceVerdict,
	evaluatePurposeCoherence,
} from "../uok/purpose-coherence.js";

const tmpRoots = [];

afterEach(() => {
	closeDatabase();
	for (const dir of tmpRoots.splice(0)) {
		rmSync(dir, { recursive: true, force: true });
	}
});

function makeProject() {
	const root = mkdtempSync(join(tmpdir(), "sf-purpose-coherence-"));
	mkdirSync(join(root, ".sf"), { recursive: true });
	tmpRoots.push(root);
	return root;
}

// ─── computePurposeCoherenceVerdict (pure) ────────────────────────────

describe("computePurposeCoherenceVerdict", () => {
	test("missing milestone returns fail", () => {
		const verdict = computePurposeCoherenceVerdict({ milestone: null });
		expect(verdict.outcome).toBe("fail");
		expect(verdict.rationale).toMatch(/milestone vision missing/);
	});

	test("milestone with empty vision returns fail", () => {
		const verdict = computePurposeCoherenceVerdict({
			milestone: { id: "M001", vision: "" },
		});
		expect(verdict.outcome).toBe("fail");
		expect(verdict.failureClass).toBe("policy");
		expect(verdict.rationale).toMatch(/milestone vision missing/);
		expect(verdict.findings).toMatch(/M001/);
	});

	test("milestone with whitespace-only vision returns fail", () => {
		const verdict = computePurposeCoherenceVerdict({
			milestone: { id: "M001", vision: "   \n   " },
		});
		expect(verdict.outcome).toBe("fail");
	});

	test("vision present, no active slice or task returns pass", () => {
		const verdict = computePurposeCoherenceVerdict({
			milestone: { id: "M001", vision: "Deliver X" },
		});
		expect(verdict.outcome).toBe("pass");
		expect(verdict.failureClass).toBe("none");
	});

	test("vision present, slice trace NULL returns warn (allow)", () => {
		const verdict = computePurposeCoherenceVerdict({
			milestone: { id: "M001", vision: "Deliver X" },
			slice: { id: "S01", traces_vision_fragment: null },
		});
		expect(verdict.outcome).toBe("warn");
		expect(verdict.rationale).toMatch(
			/slice goal does not trace to milestone vision/,
		);
		expect(verdict.findings).toMatch(/S01/);
	});

	test("vision present, slice trace empty string returns warn", () => {
		const verdict = computePurposeCoherenceVerdict({
			milestone: { id: "M001", vision: "Deliver X" },
			slice: { id: "S01", traces_vision_fragment: "" },
		});
		expect(verdict.outcome).toBe("warn");
	});

	test("vision present, slice trace column missing returns warn", () => {
		const verdict = computePurposeCoherenceVerdict({
			milestone: { id: "M001", vision: "Deliver X" },
			slice: { id: "S01" },
			schema: { sliceTraceColumnMissing: true },
		});
		expect(verdict.outcome).toBe("warn");
		expect(verdict.rationale).toMatch(
			/slice\.traces_vision_fragment column not yet present/,
		);
	});

	test("vision present, slice trace ok, task trace NULL returns warn", () => {
		const verdict = computePurposeCoherenceVerdict({
			milestone: { id: "M001", vision: "Deliver X" },
			slice: { id: "S01", traces_vision_fragment: "frag-1" },
			task: { id: "T01", purpose_trace: null },
		});
		expect(verdict.outcome).toBe("warn");
		expect(verdict.rationale).toMatch(/task does not trace to slice goal/);
		expect(verdict.findings).toMatch(/T01/);
	});

	test("vision present, slice trace ok, task trace column missing returns warn", () => {
		const verdict = computePurposeCoherenceVerdict({
			milestone: { id: "M001", vision: "Deliver X" },
			slice: { id: "S01", traces_vision_fragment: "frag-1" },
			task: { id: "T01" },
			schema: { taskTraceColumnMissing: true },
		});
		expect(verdict.outcome).toBe("warn");
		expect(verdict.rationale).toMatch(
			/task\.purpose_trace column not yet present/,
		);
	});

	test("full chain present returns pass", () => {
		const verdict = computePurposeCoherenceVerdict({
			milestone: { id: "M001", vision: "Deliver X" },
			slice: { id: "S01", traces_vision_fragment: "frag-1" },
			task: { id: "T01", purpose_trace: "task-trace-1" },
		});
		expect(verdict.outcome).toBe("pass");
		expect(verdict.failureClass).toBe("none");
		expect(verdict.rationale).toMatch(/purpose chain intact/);
	});

	// Belt-and-braces: even if a task has a trace, a NULL slice trace still
	// breaks the chain at the slice level — we must report the slice gap.
	test("task trace present but slice trace null still warns about slice", () => {
		const verdict = computePurposeCoherenceVerdict({
			milestone: { id: "M001", vision: "Deliver X" },
			slice: { id: "S01", traces_vision_fragment: null },
			task: { id: "T01", purpose_trace: "task-trace-1" },
		});
		expect(verdict.outcome).toBe("warn");
		expect(verdict.rationale).toMatch(/slice/);
	});
});

// ─── evaluatePurposeCoherence (DB-backed) ────────────────────────────

/**
 * Open a vanilla SF DB. This is the *legacy* shape for our purposes:
 * the schema created by sf-db.openDatabase() does NOT yet include
 * `slices.traces_vision_fragment` or `tasks.purpose_trace` — those
 * are added by the parallel P2/P3 migrations. Opening unaltered
 * exercises the missing-column downgrade path the gate must survive.
 */
function makeLegacyDb() {
	const base = makeProject();
	openDatabase(join(base, ".sf", "sf.db"));
	const db = _getAdapter();
	return { base, db };
}

/**
 * Open an SF DB. Post-P2/P3 merge the trace columns are already added
 * by the schema migration ladder (v70 = tasks.purpose_trace,
 * v72 = slices.traces_vision_fragment), so no manual ALTER is needed.
 * Test value-NULL paths by inserting rows with explicit NULL.
 */
function makeForwardDb() {
	const base = makeProject();
	openDatabase(join(base, ".sf", "sf.db"));
	const db = _getAdapter();
	return { base, db };
}

describe("evaluatePurposeCoherence (DB-backed)", () => {
	test("vision present, columns missing on legacy DB, slice active → warn (allow)", () => {
		const { db } = makeLegacyDb();
		db.prepare("INSERT INTO milestones (id, vision) VALUES (:id, :v)").run({
			":id": "M001",
			":v": "Deliver X",
		});
		db.prepare("INSERT INTO slices (milestone_id, id) VALUES (:mid, :sid)").run(
			{ ":mid": "M001", ":sid": "S01" },
		);
		const verdict = evaluatePurposeCoherence({
			state: {
				activeMilestone: { id: "M001" },
				activeSlice: { id: "S01" },
				activeTask: null,
			},
			db,
		});
		expect(verdict.outcome).toBe("warn");
		// Post-migration the column always exists; the warn message now
		// reports the NULL value rather than the absent column. The gate
		// itself still keeps the column-missing SqliteError catch so
		// projects that somehow predate the migration don't hard-fail.
		expect(verdict.rationale).toMatch(/slice goal does not trace/);
	});

	test("vision missing on legacy DB → fail", () => {
		const { db } = makeLegacyDb();
		db.prepare("INSERT INTO milestones (id, vision) VALUES (:id, :v)").run({
			":id": "M001",
			":v": "",
		});
		const verdict = evaluatePurposeCoherence({
			state: {
				activeMilestone: { id: "M001" },
				activeSlice: null,
				activeTask: null,
			},
			db,
		});
		expect(verdict.outcome).toBe("fail");
	});

	test("full chain present on forward DB → pass", () => {
		const { db } = makeForwardDb();
		db.prepare("INSERT INTO milestones (id, vision) VALUES (:id, :v)").run({
			":id": "M001",
			":v": "Deliver X",
		});
		db.prepare(
			"INSERT INTO slices (milestone_id, id, traces_vision_fragment) VALUES (:mid, :sid, :tv)",
		).run({ ":mid": "M001", ":sid": "S01", ":tv": "frag-1" });
		db.prepare(
			"INSERT INTO tasks (milestone_id, slice_id, id, purpose_trace) VALUES (:mid, :sid, :tid, :pt)",
		).run({
			":mid": "M001",
			":sid": "S01",
			":tid": "T01",
			":pt": "trace-1",
		});
		const verdict = evaluatePurposeCoherence({
			state: {
				activeMilestone: { id: "M001" },
				activeSlice: { id: "S01" },
				activeTask: { id: "T01" },
			},
			db,
		});
		expect(verdict.outcome).toBe("pass");
	});

	test("vision present, slice trace NULL on forward DB → warn (slice gap)", () => {
		const { db } = makeForwardDb();
		db.prepare("INSERT INTO milestones (id, vision) VALUES (:id, :v)").run({
			":id": "M001",
			":v": "Deliver X",
		});
		db.prepare(
			"INSERT INTO slices (milestone_id, id, traces_vision_fragment) VALUES (:mid, :sid, NULL)",
		).run({ ":mid": "M001", ":sid": "S01" });
		const verdict = evaluatePurposeCoherence({
			state: {
				activeMilestone: { id: "M001" },
				activeSlice: { id: "S01" },
				activeTask: null,
			},
			db,
		});
		expect(verdict.outcome).toBe("warn");
		expect(verdict.rationale).toMatch(/slice goal does not trace/);
	});

	test("vision + slice ok, task trace NULL on forward DB → warn (task gap)", () => {
		const { db } = makeForwardDb();
		db.prepare("INSERT INTO milestones (id, vision) VALUES (:id, :v)").run({
			":id": "M001",
			":v": "Deliver X",
		});
		db.prepare(
			"INSERT INTO slices (milestone_id, id, traces_vision_fragment) VALUES (:mid, :sid, :tv)",
		).run({ ":mid": "M001", ":sid": "S01", ":tv": "frag-1" });
		db.prepare(
			"INSERT INTO tasks (milestone_id, slice_id, id, purpose_trace) VALUES (:mid, :sid, :tid, NULL)",
		).run({ ":mid": "M001", ":sid": "S01", ":tid": "T01" });
		const verdict = evaluatePurposeCoherence({
			state: {
				activeMilestone: { id: "M001" },
				activeSlice: { id: "S01" },
				activeTask: { id: "T01" },
			},
			db,
		});
		expect(verdict.outcome).toBe("warn");
		expect(verdict.rationale).toMatch(/task does not trace/);
	});

	test("no active milestone → fail (no purpose root)", () => {
		const { db } = makeLegacyDb();
		const verdict = evaluatePurposeCoherence({
			state: {
				activeMilestone: null,
				activeSlice: null,
				activeTask: null,
			},
			db,
		});
		expect(verdict.outcome).toBe("fail");
	});
});
