/**
 * drift-detection-gate.test.mjs — ADR-0075 drift detection gate contracts.
 *
 * Purpose: prove task completion drift is detected from mocked task rows and a
 * temporary working tree without requiring a live sf.db.
 */
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import driftDetectionGate from "../uok/drift-detection-gate.js";

const tmpRoots = [];

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

function makeProject() {
	const root = mkdtempSync(join(tmpdir(), "sf-drift-detection-gate-"));
	tmpRoots.push(root);
	return root;
}

function writeProjectFile(root, relPath, content = "") {
	const fullPath = join(root, relPath);
	mkdirSync(dirname(fullPath), { recursive: true });
	writeFileSync(fullPath, content, "utf-8");
	return fullPath;
}

function taskRow(overrides = {}) {
	return {
		milestone_id: "M014",
		slice_id: "S03",
		id: "R082",
		status: "complete",
		key_files: JSON.stringify(["docs/plans/R082.md"]),
		narrative: "Implemented and verified.",
		verification_result: "pass",
		known_issues: "None.",
		deviations: "None.",
		full_summary_md: "# R082\nComplete.",
		...overrides,
	};
}

async function runSingle(root, row) {
	return driftDetectionGate.execute({
		basePath: root,
		scope: "single-task",
		taskRow: row,
	});
}

function expectGateResultShape(result) {
	expect(result).toHaveProperty("outcome");
	expect(result).toHaveProperty("failureClass");
	expect(result).toHaveProperty("rationale");
	expect(["pass", "fail", "retry", "manual-attention"]).toContain(
		result.outcome,
	);
	expect(result.failureClass).toBe("verification");
	expect(typeof result.rationale).toBe("string");
	expect(result.rationale.length).toBeGreaterThan(0);
}

describe("driftDetectionGate", () => {
	it("exports_adr0075_gate_contract", () => {
		expect(driftDetectionGate.id).toBe("drift-detection");
		expect(driftDetectionGate.type).toBe("verification");
		expect(typeof driftDetectionGate.execute).toBe("function");
	});

	it("checkArtifactsExist_when_complete_artifact_missing_returns_fail", async () => {
		const root = makeProject();
		const result = await runSingle(
			root,
			taskRow({ key_files: JSON.stringify(["docs/plans/missing.md"]) }),
		);

		expectGateResultShape(result);
		expect(result.outcome).toBe("fail");
		expect(result.rationale).toContain(
			"task marked complete but artifact at docs/plans/missing.md missing on disk",
		);
		expect(result.findings).toEqual(["docs/plans/missing.md"]);
		expect(result.recommendation).toBe(
			"revert task status to in_progress + re-dispatch",
		);
	});

	it("checkArtifactsExist_when_complete_artifact_exists_returns_pass", async () => {
		const root = makeProject();
		writeProjectFile(root, "docs/plans/R082.md", "# plan\n");

		const result = await runSingle(root, taskRow());

		expectGateResultShape(result);
		expect(result.outcome).toBe("pass");
	});

	it("detectProseStatusMismatch_when_complete_prose_says_blocked_returns_manual_attention", async () => {
		const root = makeProject();
		writeProjectFile(root, "docs/plans/R082.md", "# plan\n");

		const result = await runSingle(
			root,
			taskRow({
				narrative: "Implementation is blocked on missing schema evidence.",
			}),
		);

		expectGateResultShape(result);
		expect(result.outcome).toBe("manual-attention");
		expect(result.rationale).toContain("prose indicates blocked");
		expect(result.findings[0]).toContain("blocked");
	});

	it("detectProseStatusMismatch_when_complete_prose_is_clean_returns_pass", async () => {
		const root = makeProject();
		writeProjectFile(root, "docs/plans/R082.md", "# plan\n");

		const result = await runSingle(
			root,
			taskRow({
				narrative: "Implemented the verification gate.",
				full_summary_md: "Verification passed and artifacts are present.",
			}),
		);

		expectGateResultShape(result);
		expect(result.outcome).toBe("pass");
	});

	it("checkImportsResolve_when_src_artifact_import_missing_returns_fail", async () => {
		const root = makeProject();
		writeProjectFile(
			root,
			"src/resources/extensions/sf/uok/example.js",
			'import { missing } from "./missing.js";\nexport const value = missing;\n',
		);

		const result = await runSingle(
			root,
			taskRow({
				key_files: JSON.stringify([
					"src/resources/extensions/sf/uok/example.js",
				]),
			}),
		);

		expectGateResultShape(result);
		expect(result.outcome).toBe("fail");
		expect(result.rationale).toBe(
			"import to ./missing.js resolves to missing file",
		);
		expect(result.findings[0]).toContain("example.js:1 -> ./missing.js");
	});

	it("checkImportsResolve_when_src_artifact_import_exists_returns_pass", async () => {
		const root = makeProject();
		writeProjectFile(
			root,
			"src/resources/extensions/sf/uok/example.js",
			'import { present } from "./present.js";\nexport const value = present;\n',
		);
		writeProjectFile(
			root,
			"src/resources/extensions/sf/uok/present.js",
			"export const present = true;\n",
		);

		const result = await runSingle(
			root,
			taskRow({
				key_files: JSON.stringify([
					"src/resources/extensions/sf/uok/example.js",
				]),
			}),
		);

		expectGateResultShape(result);
		expect(result.outcome).toBe("pass");
	});

	it("execute_when_sweep_mode_iterates_mock_db_rows_and_returns_worst_outcome", async () => {
		const root = makeProject();
		writeProjectFile(root, "docs/plans/clean.md", "# clean\n");
		const rows = [
			taskRow({
				id: "T01",
				key_files: JSON.stringify(["docs/plans/clean.md"]),
			}),
			taskRow({
				id: "T02",
				key_files: JSON.stringify(["docs/plans/missing.md"]),
			}),
			taskRow({
				id: "T03",
				key_files: JSON.stringify(["docs/plans/clean.md"]),
				narrative: "Needs review before it can really be considered done.",
			}),
		];
		const seen = [];

		const result = await driftDetectionGate.execute({
			basePath: root,
			scope: "sweep",
			dbRead: (query) => {
				seen.push(query);
				return rows;
			},
		});

		expect(seen).toHaveLength(1);
		expect(seen[0].kind).toBe("recently-completed-tasks");
		expectGateResultShape(result);
		expect(result.outcome).toBe("fail");
		expect(result.findings).toContain("docs/plans/missing.md");
	});

	it("execute_when_single_task_missing_task_row_returns_manual_attention_shape", async () => {
		const root = makeProject();

		const result = await driftDetectionGate.execute({
			basePath: root,
			scope: "single-task",
		});

		expectGateResultShape(result);
		expect(result.outcome).toBe("manual-attention");
		expect(result.rationale).toContain("ctx.taskRow");
	});
});
