/**
 * db-driven-runtime-state.test.mjs — DB authority for runtime state.
 *
 * Purpose: prove DB-backed projects choose active slices/tasks from structured
 * SQLite rows even when generated markdown/JSON projections are stale.
 */
import assert from "node:assert/strict";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, test } from "vitest";
import {
	closeDatabase,
	getAllMilestones,
	getSliceTasks,
	insertMilestone,
	insertSlice,
	insertTask,
	isDbAvailable,
	openDatabase,
} from "../sf-db.js";
import { deriveState, invalidateStateCache } from "../state.js";

const tmpDirs = [];

afterEach(() => {
	closeDatabase();
	if (!isDbAvailable()) {
		openDatabase(":memory:");
		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-runtime-state-"));
	tmpDirs.push(dir);
	mkdirSync(join(dir, ".sf", "milestones", "M777", "slices", "S01"), {
		recursive: true,
	});
	mkdirSync(join(dir, ".sf", "milestones", "M777", "slices", "S02"), {
		recursive: true,
	});
	openDatabase(join(dir, ".sf", "sf.db"));
	insertMilestone({
		id: "M777",
		title: "DB runtime authority",
		status: "active",
	});
	insertSlice({
		milestoneId: "M777",
		id: "S02",
		title: "DB first slice",
		status: "pending",
		sequence: 1,
	});
	insertSlice({
		milestoneId: "M777",
		id: "S01",
		title: "Stale file first slice",
		status: "pending",
		sequence: 2,
	});
	insertTask({
		milestoneId: "M777",
		sliceId: "S02",
		id: "T02",
		title: "DB first task",
		status: "pending",
		sequence: 1,
	});
	insertTask({
		milestoneId: "M777",
		sliceId: "S02",
		id: "T01",
		title: "DB second task",
		status: "pending",
		sequence: 2,
	});
	return dir;
}

test("deriveState_when_generated_projections_are_stale_uses_db_slice_and_task_sequence", async () => {
	const project = makeProject();
	const milestoneDir = join(project, ".sf", "milestones", "M777");
	writeFileSync(
		join(milestoneDir, "M777-ROADMAP.md"),
		[
			"# M777: stale generated roadmap",
			"",
			"## Slice Overview",
			"| ID | Slice | Risk | Depends | Done | After this |",
			"|----|-------|------|---------|------|------------|",
			"| S01 | Stale file first slice | low | - |  | stale first |",
			"| S02 | DB first slice | low | - |  | should still be first by DB |",
			"",
		].join("\n"),
	);
	writeFileSync(
		join(milestoneDir, "M777-ROADMAP.json"),
		JSON.stringify(
			{
				origin: "stale-test-projection",
				slices: [
					{ id: "S01", title: "Stale file first slice", sequence: 1 },
					{ id: "S02", title: "DB first slice", sequence: 2 },
				],
			},
			null,
			2,
		),
	);
	writeFileSync(
		join(milestoneDir, "slices", "S02", "S02-PLAN.md"),
		[
			"# S02: stale generated plan",
			"",
			"## Tasks",
			"",
			"- [ ] T99: stale task that should not replace DB rows",
			"",
		].join("\n"),
	);

	const state = await deriveState(project);

	assert.equal(state.phase, "executing");
	assert.equal(state.activeMilestone?.id, "M777");
	assert.equal(state.activeSlice?.id, "S02");
	assert.equal(state.activeTask?.id, "T02");
});

test("deriveState_when_all_slices_cancelled_replans_without_selecting_cancelled_slice", async () => {
	const project = mkdtempSync(join(tmpdir(), "sf-db-runtime-state-"));
	tmpDirs.push(project);
	mkdirSync(join(project, ".sf", "milestones", "M779", "slices", "S01"), {
		recursive: true,
	});
	openDatabase(join(project, ".sf", "sf.db"));
	insertMilestone({
		id: "M779",
		title: "Cancelled slice selection",
		status: "active",
	});
	insertSlice({
		milestoneId: "M779",
		id: "S01",
		title: "Cancelled work",
		status: "cancelled",
		sequence: 1,
	});

	const state = await deriveState(project);

	assert.equal(state.phase, "pre-planning");
	assert.equal(state.activeMilestone?.id, "M779");
	assert.equal(state.activeSlice, null);
	assert.deepEqual(state.blockers, []);
	assert.match(state.nextAction, /pre-planning ladder|cancelled|inactive/i);
});

test("deriveState_when_db_has_no_tasks_refuses_runtime_plan_file_import", async () => {
	const project = mkdtempSync(join(tmpdir(), "sf-db-runtime-state-"));
	tmpDirs.push(project);
	mkdirSync(join(project, ".sf", "milestones", "M778", "slices", "S01"), {
		recursive: true,
	});
	openDatabase(join(project, ".sf", "sf.db"));
	insertMilestone({
		id: "M778",
		title: "DB planning authority",
		status: "active",
	});
	insertSlice({
		milestoneId: "M778",
		id: "S01",
		title: "Plan exists without DB tasks",
		status: "pending",
		sequence: 1,
	});
	writeFileSync(
		join(project, ".sf", "milestones", "M778", "M778-ROADMAP.md"),
		[
			"# M778: DB planning authority",
			"",
			"## Slice Overview",
			"| ID | Slice | Risk | Depends | Done | After this |",
			"|----|-------|------|---------|------|------------|",
			"| S01 | Plan exists without DB tasks | low | - |  | keep planning |",
			"",
		].join("\n"),
	);
	writeFileSync(
		join(project, ".sf", "milestones", "M778", "slices", "S01", "S01-PLAN.md"),
		[
			"# S01: stale generated plan",
			"",
			"## Tasks",
			"",
			"- [ ] T01: stale file task",
			"",
		].join("\n"),
	);

	const state = await deriveState(project);

	assert.equal(state.phase, "planning");
	assert.equal(state.activeMilestone?.id, "M778");
	assert.equal(state.activeSlice?.id, "S01");
	assert.equal(state.activeTask, null);
	assert.equal(getSliceTasks("M778", "S01").length, 0);
	assert.match(state.nextAction, /has a plan file but no tasks/i);
});

test("deriveState_when_db_has_no_milestones_refuses_runtime_roadmap_import", async () => {
	const project = mkdtempSync(join(tmpdir(), "sf-db-runtime-state-"));
	tmpDirs.push(project);
	const milestoneDir = join(project, ".sf", "milestones", "M779");
	mkdirSync(milestoneDir, { recursive: true });
	openDatabase(join(project, ".sf", "sf.db"));
	writeFileSync(
		join(milestoneDir, "M779-ROADMAP.md"),
		[
			"# M779: stale disk-only roadmap",
			"",
			"## Slice Overview",
			"| ID | Slice | Risk | Depends | Done | After this |",
			"|----|-------|------|---------|------|------------|",
			"| S01 | Should not import | low | - |  | no DB mutation |",
			"",
		].join("\n"),
	);

	const state = await deriveState(project);

	assert.equal(state.phase, "pre-planning");
	assert.equal(state.activeMilestone, null);
	assert.deepEqual(getAllMilestones(), []);
});

test("deriveState_when_task_summary_exists_keeps_db_task_status_authoritative", async () => {
	const project = makeProject();
	const taskDir = join(
		project,
		".sf",
		"milestones",
		"M777",
		"slices",
		"S02",
		"tasks",
	);
	mkdirSync(taskDir, { recursive: true });
	writeFileSync(
		join(taskDir, "T02-SUMMARY.md"),
		[
			"# T02: stale summary projection",
			"",
			"This file is not DB state.",
			"",
		].join("\n"),
	);

	const state = await deriveState(project);
	const [firstTask] = getSliceTasks("M777", "S02");

	assert.equal(state.phase, "executing");
	assert.equal(state.activeSlice?.id, "S02");
	assert.equal(state.activeTask?.id, "T02");
	assert.equal(firstTask.id, "T02");
	assert.equal(firstTask.status, "pending");
});

test("deriveState_when_db_bootstrap_failed_refuses_legacy_markdown_runtime_fallback", async () => {
	const project = mkdtempSync(join(tmpdir(), "sf-db-runtime-state-"));
	tmpDirs.push(project);
	const milestoneDir = join(project, ".sf", "milestones", "M780");
	mkdirSync(milestoneDir, { recursive: true });
	writeFileSync(
		join(milestoneDir, "M780-ROADMAP.md"),
		[
			"# M780: legacy roadmap only",
			"",
			"## Slice Overview",
			"| ID | Slice | Risk | Depends | Done | After this |",
			"|----|-------|------|---------|------|------------|",
			"| S01 | Should require recovery | low | - |  | migrate first |",
			"",
		].join("\n"),
	);

	try {
		openDatabase(join(project, ".sf"));
	} catch {}

	const state = await deriveState(project);

	assert.equal(state.phase, "blocked");
	assert.equal(state.activeMilestone, null);
	assert.match(state.blockers[0], /does not fall back to markdown authority/i);
	assert.match(state.nextAction, /sf recover/i);
	assert.match(state.nextAction, /sf migrate/i);
});
