/**
 * db-first-validation.test.mjs — VALIDATION.md is a DB projection.
 *
 * Purpose: prove that after the DB-first migration:
 * 1. Writing an assessments row with status=needs-attention and regenerating
 *    VALIDATION.md produces a file with verdict: needs-attention in frontmatter.
 * 2. The three read sites (complete-milestone, workspace-index, state-shared)
 *    return the DB value, not the parsed .md.
 * 3. If .md is hand-edited to disagree with DB, the next regeneration overrides.
 * 4. Doctor does NOT register db_projection_validation_drift (check is retired).
 */
import assert from "node:assert/strict";
import {
	mkdirSync,
	mkdtempSync,
	readFileSync,
	rmSync,
	writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, test } from "vitest";
import { checkEngineHealth } from "../doctor-engine-checks.js";
import {
	closeDatabase,
	getMilestoneValidationAssessment,
	insertAssessment,
	insertMilestone,
	insertSlice,
	insertTask,
	openDatabase,
} from "../sf-db.js";
import { invalidateStateCache } from "../state.js";
import { readMilestoneValidationVerdict } from "../state-shared.js";
import { handleCompleteMilestone } from "../tools/complete-milestone.js";
import { handleValidateMilestone } from "../tools/validate-milestone.js";
import { indexWorkspace } from "../workspace-index.js";

const tmpDirs = [];

afterEach(() => {
	closeDatabase();
	invalidateStateCache();
	while (tmpDirs.length > 0) {
		const dir = tmpDirs.pop();
		if (dir) rmSync(dir, { recursive: true, force: true });
	}
});

function makeProject() {
	const dir = mkdtempSync(join(tmpdir(), "sf-db-first-validation-"));
	tmpDirs.push(dir);
	mkdirSync(join(dir, ".sf", "milestones", "M001"), { recursive: true });
	openDatabase(join(dir, ".sf", "sf.db"));
	insertMilestone({
		id: "M001",
		title: "DB-first validation",
		status: "active",
	});
	insertSlice({
		milestoneId: "M001",
		id: "S01",
		title: "Done slice",
		status: "complete",
		sequence: 1,
	});
	insertTask({
		milestoneId: "M001",
		sliceId: "S01",
		id: "T01",
		title: "Done task",
		status: "complete",
	});
	return dir;
}

// ── Test 1: regeneration produces correct frontmatter ──────────────────────
test("handleValidateMilestone_regenerates_validation_md_with_db_verdict_in_frontmatter", async () => {
	const project = makeProject();

	const result = await handleValidateMilestone(
		{
			milestoneId: "M001",
			verdict: "needs-attention",
			remediationRound: 1,
			successCriteriaChecklist: "- [ ] criterion one",
			sliceDeliveryAudit: "S01: complete",
			crossSliceIntegration: "no issues",
			requirementCoverage: "100%",
			verdictRationale: "minor attention item",
		},
		project,
		{ uokGatesEnabled: false },
	);

	assert.equal(
		result.error,
		undefined,
		`handleValidateMilestone error: ${result.error}`,
	);
	assert.equal(result.verdict, "needs-attention");

	// The file must exist and have the generated header
	const validationPath = join(
		project,
		".sf",
		"milestones",
		"M001",
		"M001-VALIDATION.md",
	);
	const content = readFileSync(validationPath, "utf-8");
	assert.ok(
		content.startsWith("<!-- generated from .sf/sf.db"),
		"file must start with generated header",
	);
	assert.match(
		content,
		/verdict:\s*needs-attention/,
		"frontmatter must carry the verdict",
	);

	// DB row must agree
	const assessment = getMilestoneValidationAssessment("M001");
	assert.equal(assessment?.status, "needs-attention");
});

// ── Test 2a: readMilestoneValidationVerdict returns DB value ───────────────
test("readMilestoneValidationVerdict_returns_db_status_without_reading_file", async () => {
	const project = makeProject();
	const validationPath = join(
		project,
		".sf",
		"milestones",
		"M001",
		"M001-VALIDATION.md",
	);

	// Write STALE file with different verdict than DB
	writeFileSync(
		validationPath,
		"---\nverdict: needs-remediation\nremediation_round: 1\n---\n\n# stale",
	);
	// DB says needs-attention
	insertAssessment({
		path: validationPath,
		milestoneId: "M001",
		scope: "milestone-validation",
		status: "needs-attention",
		fullContent: "---\nverdict: needs-attention\n---\n",
	});

	const { terminal, verdict } = await readMilestoneValidationVerdict(
		project,
		"M001",
		(p) => readFileSync(p, "utf-8"),
	);

	assert.equal(
		verdict,
		"needs-attention",
		"verdict must come from DB, not stale file",
	);
	assert.equal(terminal, true);
});

// ── Test 2b: workspace-index validationVerdict comes from DB ──────────────
test("buildWorkspaceIndex_populates_validationVerdict_from_db_not_file", async () => {
	const project = makeProject();
	const validationPath = join(
		project,
		".sf",
		"milestones",
		"M001",
		"M001-VALIDATION.md",
	);

	// Stale file says needs-remediation, DB says pass
	writeFileSync(
		validationPath,
		"---\nverdict: needs-remediation\nremediation_round: 1\n---\n\n# stale",
	);
	insertAssessment({
		path: validationPath,
		milestoneId: "M001",
		scope: "milestone-validation",
		status: "pass",
		fullContent: "---\nverdict: pass\n---\n",
	});

	// Add ROADMAP.md so workspace-index can build the milestone entry
	writeFileSync(
		join(project, ".sf", "milestones", "M001", "M001-ROADMAP.md"),
		"# M001: DB-first validation\n\n## Slice Overview\n| ID | Slice | Risk | Depends | Done | After this |\n|----|-------|------|---------|------|------------|\n| S01 | Done slice | low | - | ✅ | done |\n",
	);

	const index = await indexWorkspace(project);

	const milestone = index.milestones?.find((m) => m.id === "M001");
	assert.ok(milestone, "milestone must be in workspace index");
	assert.equal(
		milestone.validationVerdict,
		"pass",
		"validationVerdict must come from DB (pass), not stale file (needs-remediation)",
	);
});

// ── Test 2c: complete-milestone blocks on DB verdict, not file ────────────
test("handleCompleteMilestone_blocks_when_db_verdict_is_not_pass_regardless_of_file", async () => {
	const project = makeProject();
	const validationPath = join(
		project,
		".sf",
		"milestones",
		"M001",
		"M001-VALIDATION.md",
	);

	// File says pass, DB says needs-attention → must block on DB verdict
	writeFileSync(
		validationPath,
		"---\nverdict: pass\nremediation_round: 1\n---\n\n# file says pass but is stale",
	);
	insertAssessment({
		path: validationPath,
		milestoneId: "M001",
		scope: "milestone-validation",
		status: "needs-attention",
		fullContent: "---\nverdict: needs-attention\n---\n",
	});

	const result = await handleCompleteMilestone(
		{
			milestoneId: "M001",
			title: "DB-first validation",
			verificationPassed: true,
			oneLiner: "Should be blocked",
			narrative: "DB verdict is needs-attention.",
			successCriteriaResults: "Checked.",
			definitionOfDoneResults: "Checked.",
		},
		project,
	);

	assert.ok(
		result.error?.includes("needs-attention"),
		`expected block on DB needs-attention verdict, got: ${result.error}`,
	);
});

// ── Test 3: stale hand-edit overridden by next regeneration ───────────────
test("handleValidateMilestone_overwrites_hand_edited_file_with_db_values", async () => {
	const project = makeProject();
	const validationPath = join(
		project,
		".sf",
		"milestones",
		"M001",
		"M001-VALIDATION.md",
	);

	// Simulate hand-edit with wrong verdict
	writeFileSync(
		validationPath,
		"---\nverdict: needs-remediation\nremediation_round: 99\n---\n\n# hand edited",
	);

	// Regenerate via tool (DB-first write then disk render)
	const result = await handleValidateMilestone(
		{
			milestoneId: "M001",
			verdict: "pass",
			remediationRound: 1,
			successCriteriaChecklist: "- [x] all good",
			sliceDeliveryAudit: "S01: complete",
			crossSliceIntegration: "none",
			requirementCoverage: "100%",
			verdictRationale: "everything passed",
		},
		project,
		{ uokGatesEnabled: false },
	);

	assert.equal(result.error, undefined);
	const content = readFileSync(validationPath, "utf-8");
	assert.match(
		content,
		/verdict:\s*pass/,
		"regeneration must override hand-edit",
	);
	assert.ok(
		!content.includes("needs-remediation"),
		"stale verdict must be gone after regeneration",
	);
});

// ── Test 4: db_projection_validation_drift is not registered ──────────────
test("checkEngineHealth_does_not_report_db_projection_validation_drift", async () => {
	const project = makeProject();
	const milestoneDir = join(project, ".sf", "milestones", "M001");
	const validationPath = join(milestoneDir, "M001-VALIDATION.md");

	// Set up a scenario that would previously have triggered drift:
	// file says needs-remediation, DB says pass
	writeFileSync(
		validationPath,
		"---\nverdict: needs-remediation\n---\n\n# stale",
	);
	insertAssessment({
		path: validationPath,
		milestoneId: "M001",
		scope: "milestone-validation",
		status: "pass",
		fullContent: "---\nverdict: pass\n---\n",
	});

	const issues = [];
	await checkEngineHealth(project, issues, [], () => false);

	assert.equal(
		issues.some((i) => i.code === "db_projection_validation_drift"),
		false,
		"db_projection_validation_drift must not be emitted — check is retired",
	);
});
