/**
 * auto-dispatch-canonical-plan.test.mjs - dispatch contract for canonical plans.
 *
 * Purpose: prove auto dispatch does not treat stale rendered ROADMAP.md rows as
 * executable slice state when structured DB rows are available.
 */
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, test, vi } from "vitest";

const parallelPromptMock = vi.hoisted(() =>
	vi.fn(async (_mid, _title, slices) => {
		return JSON.stringify({ slices });
	}),
);

vi.mock("../auto-prompts.js", () => ({
	buildCompleteMilestonePrompt: vi.fn(async () => "complete milestone"),
	buildCompleteSlicePrompt: vi.fn(async () => "complete slice"),
	buildDiscussMilestonePrompt: vi.fn(async () => "discuss milestone"),
	buildDiscussProjectPrompt: vi.fn(async () => "discuss project"),
	buildDiscussRequirementsPrompt: vi.fn(async () => "discuss requirements"),
	buildExecuteTaskPrompt: vi.fn(async () => "execute task"),
	buildGateEvaluatePrompt: vi.fn(async () => "gate evaluate"),
	buildParallelResearchSlicesPrompt: parallelPromptMock,
	buildPlanMilestonePrompt: vi.fn(async () => "plan milestone"),
	buildPlanSlicePrompt: vi.fn(async () => "plan slice"),
	buildReactiveExecutePrompt: vi.fn(async () => "reactive execute"),
	buildReassessRoadmapPrompt: vi.fn(async () => "reassess roadmap"),
	buildRefineSlicePrompt: vi.fn(async () => "refine slice"),
	buildReplanSlicePrompt: vi.fn(async () => "replan slice"),
	buildResearchProjectPrompt: vi.fn(async () => "research project"),
	buildResearchMilestonePrompt: vi.fn(async () => "research milestone"),
	buildResearchSlicePrompt: vi.fn(async () => "research slice"),
	buildRewriteDocsPrompt: vi.fn(async () => "rewrite docs"),
	buildRunUatPrompt: vi.fn(async () => "run uat"),
	buildValidateMilestonePrompt: vi.fn(async () => "validate milestone"),
	buildWorkflowPreferencesPrompt: vi.fn(async () => "workflow preferences"),
	checkNeedsReassessment: vi.fn(async () => null),
	checkNeedsRunUat: vi.fn(async () => null),
}));

import {
	closeDatabase,
	insertAssessment,
	insertMilestone,
	insertSlice,
	openDatabase,
} from "../sf-db.js";
import { resolveDispatch } from "../uok/auto-dispatch.js";

function makeTempDir(prefix) {
	return mkdtempSync(join(tmpdir(), prefix));
}

function cleanup(dir) {
	try {
		rmSync(dir, { recursive: true, force: true });
	} catch {}
}

function writeMilestoneFile(base, milestoneId, fileName, content) {
	const dir = join(base, ".sf", "milestones", milestoneId);
	mkdirSync(dir, { recursive: true });
	writeFileSync(join(dir, fileName), content);
}

describe("resolveDispatch canonical milestone plan", () => {
	afterEach(() => {
		closeDatabase();
		parallelPromptMock.mockClear();
	});

	test("parallel_research_when_roadmap_markdown_is_stale_uses_db_slices", async () => {
		const base = makeTempDir("sf-dispatch-canonical-");
		try {
			mkdirSync(join(base, ".sf"), { recursive: true });
			openDatabase(join(base, ".sf", "sf.db"));
			insertMilestone({
				id: "M324",
				title: "Canonical dispatch",
				status: "active",
				planning: {
					vision: "Dispatch from DB rows.",
					successCriteria: ["DB slices are executable state."],
				},
			});
			insertSlice({
				milestoneId: "M324",
				id: "S01",
				title: "DB first slice",
				status: "pending",
				risk: "low",
				depends: [],
				demo: "First DB slice.",
				sequence: 1,
			});
			insertSlice({
				milestoneId: "M324",
				id: "S02",
				title: "DB second slice",
				status: "pending",
				risk: "low",
				depends: [],
				demo: "Second DB slice.",
				sequence: 2,
			});
			writeMilestoneFile(
				base,
				"M324",
				"M324-ROADMAP.md",
				[
					"# M324: Canonical dispatch",
					"",
					"Two slices:",
					"",
					"## Slice Overview",
					"",
					"| Slice | Title | Status |",
					"| --- | --- | --- |",
					"| S99 | Stale Markdown first | pending |",
					"| S100 | Stale Markdown second | pending |",
					"",
				].join("\n"),
			);

			const result = await resolveDispatch({
				state: {
					phase: "planning",
					activeSlice: { id: "S01", title: "DB first slice" },
				},
				mid: "M324",
				midTitle: "Canonical dispatch",
				basePath: base,
				prefs: { phases: {} },
				session: {},
				pipelineVariant: "standard",
			});

			expect(result).toMatchObject({
				action: "dispatch",
				unitType: "research-slice",
				unitId: "M324/parallel-research",
			});
			expect(parallelPromptMock).toHaveBeenCalledTimes(1);
			expect(parallelPromptMock.mock.calls[0][2]).toEqual([
				{ id: "S01", title: "DB first slice" },
				{ id: "S02", title: "DB second slice" },
			]);
		} finally {
			closeDatabase();
			cleanup(base);
		}
	});

	test("pre_planning_when_db_has_milestone_context_dispatches_plan_milestone", async () => {
		const base = makeTempDir("sf-dispatch-db-context-plan-");
		try {
			mkdirSync(join(base, ".sf"), { recursive: true });
			openDatabase(join(base, ".sf", "sf.db"));
			insertMilestone({
				id: "M325",
				title: "DB context replan",
				status: "active",
				planning: {
					vision: "Replan from DB-backed milestone purpose.",
					successCriteria: ["Planner receives enough context from DB."],
				},
			});
			insertSlice({
				milestoneId: "M325",
				id: "S01",
				title: "Cancelled stale slice",
				status: "cancelled",
				goal: "Old slice still records what the milestone was trying to do.",
				sequence: 1,
			});

			const result = await resolveDispatch({
				state: {
					phase: "pre-planning",
					activeMilestone: { id: "M325", title: "DB context replan" },
					activeSlice: null,
					activeTask: null,
				},
				mid: "M325",
				midTitle: "DB context replan",
				basePath: base,
				prefs: { phases: {} },
				session: {},
				pipelineVariant: "standard",
			});

			expect(result).toMatchObject({
				action: "dispatch",
				unitType: "plan-milestone",
				unitId: "M325",
				prompt: "plan milestone",
			});
		} finally {
			closeDatabase();
			cleanup(base);
		}
	});

	test("completing_milestone_when_validation_needs_attention_without_plan_dispatches_remediation", async () => {
		const base = makeTempDir("sf-dispatch-validation-attention-");
		try {
			mkdirSync(join(base, ".sf"), { recursive: true });
			openDatabase(join(base, ".sf", "sf.db"));
			insertMilestone({
				id: "M900",
				title: "Validation self healing",
				status: "active",
			});
			const validationContent = [
				"---",
				"verdict: needs-attention",
				"remediation_round: 0",
				"---",
				"",
				"# Milestone Validation: M900",
				"",
				"## Verdict Rationale",
				"The validation looks stale: it says only S01 is complete even though DB-backed slice state may have moved on.",
				"Address or explicitly defer the finding and rerun validation.",
				"",
			].join("\n");
			insertAssessment({
				path: ".sf/milestones/M900/M900-VALIDATION.md",
				milestoneId: "M900",
				scope: "milestone-validation",
				status: "needs-attention",
				fullContent: validationContent,
			});

			const result = await resolveDispatch({
				state: {
					phase: "completing-milestone",
				},
				mid: "M900",
				midTitle: "Validation self healing",
				basePath: base,
				prefs: { phases: {} },
				session: {},
				pipelineVariant: "standard",
			});

			expect(result).toMatchObject({
				action: "dispatch",
				unitType: "rewrite-docs",
				unitId: "M900/validation-attention",
			});
			expect(result.reason).toBeUndefined();
			expect(result.prompt).toContain(
				"No structured remediation section was present",
			);
			expect(result.prompt).toContain("ready for revalidation");
		} finally {
			closeDatabase();
			cleanup(base);
		}
	});

	test("completing_milestone_when_validation_projection_advances_round_uses_file_and_revalidates", async () => {
		const base = makeTempDir("sf-dispatch-validation-revalidate-");
		try {
			mkdirSync(join(base, ".sf"), { recursive: true });
			openDatabase(join(base, ".sf", "sf.db"));
			insertMilestone({
				id: "M901",
				title: "Validation revalidation",
				status: "active",
			});
			const validationPath = ".sf/milestones/M901/M901-VALIDATION.md";
			insertAssessment({
				path: validationPath,
				milestoneId: "M901",
				scope: "milestone-validation",
				status: "needs-attention",
				fullContent: [
					"---",
					"verdict: needs-attention",
					"remediation_round: 0",
					"---",
					"",
					"# Old Validation",
				].join("\n"),
			});
			writeMilestoneFile(
				base,
				"M901",
				"M901-VALIDATION.md",
				[
					"---",
					"verdict: needs-remediation",
					"remediation_round: 1",
					"---",
					"",
					"# Updated Validation",
					"Validation attention remediated; ready for revalidation.",
				].join("\n"),
			);

			const result = await resolveDispatch({
				state: {
					phase: "completing-milestone",
				},
				mid: "M901",
				midTitle: "Validation revalidation",
				basePath: base,
				prefs: { phases: {} },
				session: {},
				pipelineVariant: "standard",
			});

			expect(result).toMatchObject({
				action: "dispatch",
				unitType: "validate-milestone",
				unitId: "M901",
				prompt: "validate milestone",
			});
		} finally {
			closeDatabase();
			cleanup(base);
		}
	});
});
