/**
 * Regression test for issue #909.
 *
 * When S##-PLAN.md exists (causing deriveState → phase:'executing') but the
 * individual task plan files (tasks/T01-PLAN.md, etc.) are absent, the dispatch
 * table must recover by re-running plan-slice — NOT hard-stop.
 *
 * Prior behaviour: action:"stop" → infinite loop on restart.
 * Fixed behaviour: action:"dispatch" unitType:"plan-slice".
 */

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 { test, afterEach } from 'vitest';
import type { DispatchContext } from "../auto-dispatch.ts";
import { resolveDispatch } from "../auto-dispatch.ts";
import {
	closeDatabase,
	getTask,
	insertMilestone,
	insertSlice,
	insertTask,
	openDatabase,
} from "../sf-db.ts";
import type { SFState } from "../types.ts";

function makeState(overrides: Partial<SFState> = {}): SFState {
	return {
		activeMilestone: { id: "M002", title: "Test Milestone" },
		activeSlice: { id: "S03", title: "Third Slice" },
		activeTask: { id: "T01", title: "First Task" },
		phase: "executing",
		recentDecisions: [],
		blockers: [],
		nextAction: "",
		registry: [],
		...overrides,
	};
}

function makeContext(
	basePath: string,
	stateOverrides?: Partial<SFState>,
): DispatchContext {
	return {
		basePath,
		mid: "M002",
		midTitle: "Test Milestone",
		state: makeState(stateOverrides),
		prefs: undefined,
	};
}

// ─── Scaffold helpers ──────────────────────────────────────────────────────

function scaffoldSlicePlan(basePath: string, mid: string, sid: string): void {
	const milestoneDir = join(basePath, ".sf", "milestones", mid);
	mkdirSync(milestoneDir, { recursive: true });
	writeFileSync(
		join(milestoneDir, `${mid}-CONTEXT.md`),
		["# Test Milestone Context", "", "Existing test fixture context.", ""].join(
			"\n",
		),
	);
	const dir = join(basePath, ".sf", "milestones", mid, "slices", sid);
	mkdirSync(dir, { recursive: true });
	writeFileSync(
		join(dir, `${sid}-PLAN.md`),
		[
			`# ${sid}: Third Slice`,
			"",
			"## Tasks",
			"- [ ] **T01: Do something** `est:1h`",
			"- [ ] **T02: Do another thing** `est:30m`",
			"",
		].join("\n"),
	);
}

function scaffoldTaskPlan(
	basePath: string,
	mid: string,
	sid: string,
	tid: string,
): void {
	const dir = join(basePath, ".sf", "milestones", mid, "slices", sid, "tasks");
	mkdirSync(dir, { recursive: true });
	writeFileSync(
		join(dir, `${tid}-PLAN.md`),
		[`# ${tid}: Do something`, "", "## Steps", "- [ ] Step 1", ""].join("\n"),
	);
}

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

test("dispatch: missing task plan triggers plan-slice (not stop) — issue #909", async (t) => {
	const tmp = mkdtempSync(join(tmpdir(), "sf-909-"));
	afterEach(() => rmSync(tmp, { recursive: true, force: true }));

	// Slice plan exists with tasks, but tasks/ directory is empty
	scaffoldSlicePlan(tmp, "M002", "S03");

	const ctx = makeContext(tmp);
	const result = await resolveDispatch(ctx);

	assert.equal(result.action, "dispatch", "should dispatch, not stop");
	assert.ok(
		result.action === "dispatch" && result.unitType === "plan-slice",
		`unitType should be plan-slice, got: ${result.action === "dispatch" ? result.unitType : "(stop)"}`,
	);
	assert.ok(
		result.action === "dispatch" && result.unitId === "M002/S03",
		`unitId should be M002/S03, got: ${result.action === "dispatch" ? result.unitId : "(stop)"}`,
	);
});

test("dispatch: present task plan proceeds to execute-task normally", async (t) => {
	const tmp = mkdtempSync(join(tmpdir(), "sf-909-ok-"));
	afterEach(() => rmSync(tmp, { recursive: true, force: true }));

	scaffoldSlicePlan(tmp, "M002", "S03");
	scaffoldTaskPlan(tmp, "M002", "S03", "T01");

	const ctx = makeContext(tmp);
	const result = await resolveDispatch(ctx);

	assert.equal(result.action, "dispatch");
	assert.ok(
		result.action === "dispatch" && result.unitType === "execute-task",
		`unitType should be execute-task, got: ${result.action === "dispatch" ? result.unitType : "(stop)"}`,
	);
	assert.ok(
		result.action === "dispatch" && result.unitId === "M002/S03/T01",
		`unitId should be M002/S03/T01, got: ${result.action === "dispatch" ? result.unitId : "(stop)"}`,
	);
});

test("dispatch: stale Docker Compose staging task is auto-skipped when DB can advance state", async (t) => {
	const tmp = mkdtempSync(join(tmpdir(), "sf-stale-staging-"));
	afterEach(() => {
		closeDatabase();
		rmSync(tmp, { recursive: true, force: true });
	});
	mkdirSync(join(tmp, ".sf"), { recursive: true });
	openDatabase(join(tmp, ".sf", "sf.db"));

	writeFileSync(
		join(tmp, "AGENTS.md"),
		[
			"## Pilot Test Infrastructure",
			"There is no staging environment.",
			"`deploy/staging/` Docker Compose is a historical artifact.",
			"Do not treat them as the deploy target unless explicitly asked for local compose validation.",
			"Do not assume docker-compose is the only or primary option.",
			"",
		].join("\n"),
	);
	scaffoldSlicePlan(tmp, "M002", "S03");
	scaffoldTaskPlan(tmp, "M002", "S03", "T01");
	insertMilestone({ id: "M002", title: "Test Milestone" });
	insertSlice({
		id: "S03",
		milestoneId: "M002",
		title: "Third Slice",
		status: "active",
	});
	insertTask({
		id: "T01",
		sliceId: "S03",
		milestoneId: "M002",
		title: "Validate docker-compose staging stack starts cleanly",
		status: "pending",
	});
	writeFileSync(
		join(
			tmp,
			".sf",
			"milestones",
			"M002",
			"slices",
			"S03",
			"tasks",
			"T01-PLAN.md",
		),
		[
			"# T01: Validate docker-compose staging stack starts cleanly",
			"",
			"Start the full staging stack via docker-compose, poll for portal health, verify postgres connectivity, then shut down cleanly.",
			"",
			"verify: docker compose up -d && poll for health and then docker compose down",
			"",
		].join("\n"),
	);

	const result = await resolveDispatch(makeContext(tmp));

	assert.equal(result.action, "skip");
	assert.equal(
		getTask("M002", "S03", "T01")?.status,
		"skipped",
		"stale staging task should be closed before skip re-derives state",
	);
});

test("dispatch: plan-slice recovery loop — second call after plan-slice still recovers cleanly", async (t) => {
	// Simulate: plan-slice ran but T01-PLAN.md is still missing (e.g. agent crashed mid-write).
	// Dispatch should still re-dispatch plan-slice, not hard-stop.
	const tmp = mkdtempSync(join(tmpdir(), "sf-909-loop-"));
	afterEach(() => rmSync(tmp, { recursive: true, force: true }));

	scaffoldSlicePlan(tmp, "M002", "S03");

	const ctx = makeContext(tmp);
	const r1 = await resolveDispatch(ctx);
	assert.equal(r1.action, "dispatch");
	assert.ok(r1.action === "dispatch" && r1.unitType === "plan-slice");

	// Still no task plan written — dispatch again
	const r2 = await resolveDispatch(ctx);
	assert.equal(r2.action, "dispatch");
	assert.ok(
		r2.action === "dispatch" && r2.unitType === "plan-slice",
		"should keep dispatching plan-slice until task plans appear",
	);
});
