/**
 * iter-completion-reconciler.test.mjs — Layer A iter↔DB reconciliation sweeper tests.
 *
 * Purpose: verify reconcileIterCompletions and iterCompletionReconcilerGate
 * behave correctly against tmp fixtures; never touches real .sf/ data.
 */
import {
	mkdirSync,
	mkdtempSync,
	readFileSync,
	rmSync,
	writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { DatabaseSync } from "node:sqlite";
import { afterEach, describe, expect, it } from "vitest";
import {
	iterCompletionReconcilerGate,
	reconcileIterCompletions,
} from "../sf-db/iteration-completion-reconciler.js";

// ─── Fixtures ───────────────────────────────────────────────────────────────

const tmpRoots = [];

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

function makeProject() {
	const root = mkdtempSync(join(tmpdir(), "sf-iter-reconciler-"));
	tmpRoots.push(root);
	mkdirSync(join(root, ".sf", "runtime", "autonomous-solver"), {
		recursive: true,
	});
	return root;
}

function writeIterJsonl(root, entries) {
	const path = join(
		root,
		".sf",
		"runtime",
		"autonomous-solver",
		"iterations.jsonl",
	);
	const content = entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
	writeFileSync(path, content, "utf-8");
}

/**
 * Create a minimal in-memory SQLite DB with a tasks table.
 * Returns the adapter object (prepare/exec/close interface).
 */
function makeMemDb() {
	const raw = new DatabaseSync(":memory:");
	raw.exec(`
		CREATE TABLE IF NOT EXISTS tasks (
			milestone_id TEXT NOT NULL,
			slice_id TEXT NOT NULL,
			id TEXT NOT NULL,
			status TEXT NOT NULL DEFAULT 'pending',
			completed_at TEXT DEFAULT NULL,
			verification_status TEXT NOT NULL DEFAULT '',
			PRIMARY KEY (milestone_id, slice_id, id)
		)
	`);

	// Minimal adapter interface matching sf-db-core createAdapter output
	const stmtCache = new Map();
	function wrapStmt(raw2) {
		return {
			run(...params) {
				return raw2.run(...params);
			},
			get(...params) {
				const r = raw2.get(...params);
				return r == null ? undefined : { ...r };
			},
			all(...params) {
				return raw2.all(...params).map((r) => ({ ...r }));
			},
		};
	}
	return {
		exec(sql) {
			raw.exec(sql);
		},
		prepare(sql) {
			let cached = stmtCache.get(sql);
			if (cached) return cached;
			cached = wrapStmt(raw.prepare(sql));
			stmtCache.set(sql, cached);
			return cached;
		},
		close() {
			stmtCache.clear();
			raw.close();
		},
		// Helper for tests
		insertTask(milestoneId, sliceId, id, status, completedAt) {
			raw
				.prepare(
					`INSERT INTO tasks (milestone_id, slice_id, id, status, completed_at)
					 VALUES (?, ?, ?, ?, ?)`,
				)
				.run(milestoneId, sliceId, id, status, completedAt ?? null);
		},
		getTask(milestoneId, sliceId, id) {
			const r = raw
				.prepare(
					"SELECT * FROM tasks WHERE milestone_id = ? AND slice_id = ? AND id = ?",
				)
				.get(milestoneId, sliceId, id);
			return r == null ? undefined : { ...r };
		},
	};
}

const TS_X = "2026-05-17T12:42:05.618Z";
const TS_OLDER = "2026-05-17T11:00:00.000Z";
const TS_NEWER = "2026-05-17T14:00:00.000Z";

// ─── Tests ──────────────────────────────────────────────────────────────────

describe("reconcileIterCompletions", () => {
	it("empty_iter_and_empty_db_returns_no_reconciliations", async () => {
		const root = makeProject();
		const db = makeMemDb();
		// No iter file written → empty

		const result = await reconcileIterCompletions(root, { db });

		expect(result.reconciled).toEqual([]);
		expect(result.totalChecked).toBe(0);
		expect(typeof result.durationMs).toBe("number");
		db.close();
	});

	it("iter_complete_db_pending_reconciler_flips_db_to_complete", async () => {
		const root = makeProject();
		const db = makeMemDb();

		db.insertTask("M999", "S99", "T99", "pending", null);

		writeIterJsonl(root, [
			{ ts: TS_X, unitId: "M999/S99/T99", outcome: "complete", iteration: 1 },
		]);

		const result = await reconcileIterCompletions(root, { db });

		expect(result.reconciled).toHaveLength(1);
		expect(result.reconciled[0].unitId).toBe("M999/S99/T99");
		expect(result.reconciled[0].oldStatus).toBe("pending");
		expect(result.reconciled[0].newStatus).toBe("complete");
		expect(result.reconciled[0].source).toBe("iterations.jsonl");
		expect(result.totalChecked).toBe(1);

		const row = db.getTask("M999", "S99", "T99");
		expect(row.status).toBe("complete");
		expect(row.completed_at).toBe(TS_X);
		expect(row.verification_status).toBe("all_pass");

		db.close();
	});

	it("iter_complete_db_complete_with_newer_completed_at_no_change", async () => {
		const root = makeProject();
		const db = makeMemDb();

		// DB already has a newer completed_at — DB is ahead of iter, leave it
		db.insertTask("M999", "S99", "T99", "complete", TS_NEWER);

		writeIterJsonl(root, [
			{ ts: TS_X, unitId: "M999/S99/T99", outcome: "complete", iteration: 1 },
		]);

		const result = await reconcileIterCompletions(root, { db });

		expect(result.reconciled).toHaveLength(0);
		expect(result.totalChecked).toBe(1);

		// DB row should be untouched
		const row = db.getTask("M999", "S99", "T99");
		expect(row.completed_at).toBe(TS_NEWER);

		db.close();
	});

	it("iter_research_slice_unit_no_T_part_is_skipped", async () => {
		const root = makeProject();
		const db = makeMemDb();

		writeIterJsonl(root, [
			{
				ts: TS_X,
				unitId: "M999/parallel-research",
				outcome: "complete",
				iteration: 1,
			},
		]);

		const result = await reconcileIterCompletions(root, { db });

		// totalChecked = 0 because the unit has no T-segment
		expect(result.reconciled).toHaveLength(0);
		expect(result.totalChecked).toBe(0);

		db.close();
	});

	it("dryRun_true_returns_reconciliations_but_no_db_writes", async () => {
		const root = makeProject();
		const db = makeMemDb();

		db.insertTask("M999", "S99", "T99", "pending", null);

		writeIterJsonl(root, [
			{ ts: TS_X, unitId: "M999/S99/T99", outcome: "complete", iteration: 1 },
		]);

		const result = await reconcileIterCompletions(root, { db, dryRun: true });

		// Reports the drift but does not write
		expect(result.reconciled).toHaveLength(1);
		expect(result.reconciled[0].unitId).toBe("M999/S99/T99");

		// DB must remain untouched
		const row = db.getTask("M999", "S99", "T99");
		expect(row.status).toBe("pending");
		expect(row.completed_at).toBeNull();

		db.close();
	});
});

describe("iterCompletionReconcilerGate", () => {
	it("exports_adr0075_gate_contract_shape", () => {
		expect(iterCompletionReconcilerGate.id).toBe("iter-completion-reconciler");
		expect(iterCompletionReconcilerGate.type).toBe("verification");
		expect(typeof iterCompletionReconcilerGate.execute).toBe("function");
	});

	it("execute_returns_pass_when_no_drift", async () => {
		const root = makeProject();
		const db = makeMemDb();

		// No iter file — nothing to reconcile
		const result = await iterCompletionReconcilerGate.execute({
			basePath: root,
			options: { db },
		});

		expect(result.outcome).toBe("pass");
		expect(result.failureClass).toBeNull();
		expect(typeof result.rationale).toBe("string");
		expect(result.rationale).toContain("0 of");

		db.close();
	});

	it("execute_returns_manual_attention_with_findings_when_drift_reconciled", async () => {
		const root = makeProject();
		const db = makeMemDb();

		db.insertTask("M999", "S99", "T99", "pending", null);
		writeIterJsonl(root, [
			{ ts: TS_X, unitId: "M999/S99/T99", outcome: "complete", iteration: 1 },
		]);

		const result = await iterCompletionReconcilerGate.execute({
			basePath: root,
			options: { db },
		});

		expect(result.outcome).toBe("manual-attention");
		expect(result.failureClass).toBe("verification");
		expect(typeof result.rationale).toBe("string");
		expect(result.rationale).toContain("1 of");
		expect(Array.isArray(result.findings)).toBe(true);
		expect(result.findings[0].unitId).toBe("M999/S99/T99");

		db.close();
	});
});
