import assert from "node:assert/strict";
import {
	mkdirSync,
	mkdtempSync,
	readdirSync,
	rmSync,
	writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, test } from "vitest";
import {
	clearRunawayRecoveredRuntimeRecords,
	clearUnitRuntimeRecord,
	decideUnitRuntimeDispatch,
	getRecoveryDiagnostics,
	getUnitRuntimeState,
	isTerminalUnitRuntimeStatus,
	listUnitRuntimeRecords,
	readUnitRuntimeRecord,
	UNIT_RUNTIME_STATUSES,
	UNIT_RUNTIME_TERMINAL_STATUSES,
	writeUnitRuntimeRecord,
} from "../uok/unit-runtime.js";

const tmpRoots = [];

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

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

// ─── Constants ─────────────────────────────────────────────────────────────

test("unitRuntimeStatuses_is_exhaustive", () => {
	assert.ok(UNIT_RUNTIME_STATUSES.includes("queued"));
	assert.ok(UNIT_RUNTIME_STATUSES.includes("completed"));
	assert.ok(UNIT_RUNTIME_STATUSES.includes("failed"));
	assert.ok(UNIT_RUNTIME_STATUSES.includes("runaway-recovered"));
});

test("terminalStatuses_is_subset_of_all_statuses", () => {
	for (const s of UNIT_RUNTIME_TERMINAL_STATUSES) {
		assert.ok(
			UNIT_RUNTIME_STATUSES.includes(s),
			`${s} should be in UNIT_RUNTIME_STATUSES`,
		);
	}
});

// ─── isTerminalUnitRuntimeStatus ───────────────────────────────────────────

test("isTerminal_returns_true_for_terminal_statuses", () => {
	for (const s of UNIT_RUNTIME_TERMINAL_STATUSES) {
		assert.equal(
			isTerminalUnitRuntimeStatus(s),
			true,
			`${s} should be terminal`,
		);
	}
});

test("isTerminal_returns_false_for_non_terminal_statuses", () => {
	const nonTerminal = UNIT_RUNTIME_STATUSES.filter(
		(s) => !UNIT_RUNTIME_TERMINAL_STATUSES.includes(s),
	);
	for (const s of nonTerminal) {
		assert.equal(
			isTerminalUnitRuntimeStatus(s),
			false,
			`${s} should not be terminal`,
		);
	}
});

// ─── getUnitRuntimeState ───────────────────────────────────────────────────

test("getUnitRuntimeState_normalizes_legacy_phase_only_record", () => {
	const state = getUnitRuntimeState({ phase: "dispatched" });
	assert.equal(state.status, "running");
});

test("getUnitRuntimeState_prefers_explicit_status_over_phase", () => {
	const state = getUnitRuntimeState({ phase: "dispatched", status: "queued" });
	assert.equal(state.status, "queued");
});

test("getUnitRuntimeState_maps_timeout_phase_to_stale", () => {
	const state = getUnitRuntimeState({ phase: "timeout" });
	assert.equal(state.status, "stale");
});

test("getUnitRuntimeState_maps_finalized_phase_to_completed", () => {
	const state = getUnitRuntimeState({ phase: "finalized" });
	assert.equal(state.status, "completed");
});

test("getUnitRuntimeState_computes_retryCount_from_recoveryAttempts", () => {
	const state = getUnitRuntimeState({ recoveryAttempts: 3 });
	assert.equal(state.retryCount, 3);
});

// ─── decideUnitRuntimeDispatch ─────────────────────────────────────────────

test("decide_when_no_record_returns_dispatch", () => {
	const d = decideUnitRuntimeDispatch(null);
	assert.equal(d.action, "dispatch");
	assert.equal(d.reasonCode, "no-runtime-record");
});

test("decide_when_queued_returns_dispatch", () => {
	const d = decideUnitRuntimeDispatch({ status: "queued" });
	assert.equal(d.action, "dispatch");
	assert.equal(d.reasonCode, "queued");
});

test("decide_when_running_returns_skip", () => {
	const d = decideUnitRuntimeDispatch({ status: "running" });
	assert.equal(d.action, "skip");
	assert.equal(d.reasonCode, "active-or-claimed");
});

test("decide_when_completed_returns_notify", () => {
	const d = decideUnitRuntimeDispatch({ status: "completed" });
	assert.equal(d.action, "notify");
	assert.equal(d.reasonCode, "terminal-ready-to-notify");
});

test("decide_when_failed_with_budget_returns_retry", () => {
	const d = decideUnitRuntimeDispatch({
		status: "failed",
		retryCount: 0,
		maxRetries: 2,
	});
	assert.equal(d.action, "retry");
	assert.equal(d.reasonCode, "retry-budget-available");
});

test("decide_when_failed_without_budget_returns_block", () => {
	const d = decideUnitRuntimeDispatch({
		status: "failed",
		retryCount: 2,
		maxRetries: 1,
	});
	assert.equal(d.action, "block");
	assert.equal(d.reasonCode, "retry-budget-exhausted");
});

test("decide_when_stale_with_budget_returns_retry", () => {
	const d = decideUnitRuntimeDispatch({
		status: "stale",
		retryCount: 0,
		maxRetries: 1,
	});
	assert.equal(d.action, "retry");
});

test("decide_when_runaway_recovered_requires_explicit_reset", () => {
	const d = decideUnitRuntimeDispatch({
		status: "runaway-recovered",
		retryCount: 0,
		maxRetries: 1,
	});
	assert.equal(d.action, "block");
	assert.equal(d.reasonCode, "runaway-recovery-reset-required");
});

test("decide_when_blocked_returns_notify", () => {
	const d = decideUnitRuntimeDispatch({ status: "blocked" });
	assert.equal(d.action, "notify");
});

test("decide_when_cancelled_returns_notify", () => {
	const d = decideUnitRuntimeDispatch({ status: "cancelled" });
	assert.equal(d.action, "notify");
});

test("decide_when_synthetic_failed_returns_block", () => {
	const d = decideUnitRuntimeDispatch({
		status: "failed",
		unitType: "synthetic",
		retryCount: 0,
		maxRetries: 3,
	});
	assert.equal(d.action, "block");
	assert.equal(d.reasonCode, "synthetic-reset-required");
});

test("decide_when_notified_returns_skip", () => {
	const d = decideUnitRuntimeDispatch({ status: "notified" });
	assert.equal(d.action, "skip");
	assert.equal(d.reasonCode, "notified");
});

test("decide_when_notifiedAt_set_returns_skip", () => {
	const d = decideUnitRuntimeDispatch({
		status: "completed",
		notifiedAt: Date.now(),
	});
	assert.equal(d.action, "skip");
	assert.equal(d.reasonCode, "already-notified");
});

test("decide_retryBudgetRemaining_is_accurate", () => {
	const d = decideUnitRuntimeDispatch({
		status: "failed",
		retryCount: 1,
		maxRetries: 3,
	});
	assert.equal(d.retryBudgetRemaining, 2);
});

// ─── Record IO ─────────────────────────────────────────────────────────────

test("writeUnitRuntimeRecord_creates_file", () => {
	const root = makeProject();
	writeUnitRuntimeRecord(root, "execute-task", "M001/S01/T01", Date.now(), {
		status: "running",
	});
	const records = listUnitRuntimeRecords(root);
	assert.equal(records.length, 1);
	assert.equal(records[0].unitType, "execute-task");
	assert.equal(records[0].unitId, "M001/S01/T01");
	assert.equal(records[0].status, "running");
});

test("readUnitRuntimeRecord_returns_cached_record", () => {
	const root = makeProject();
	writeUnitRuntimeRecord(root, "execute-task", "M001/S01/T01", Date.now(), {
		status: "running",
	});
	const record = readUnitRuntimeRecord(root, "execute-task", "M001/S01/T01");
	assert.equal(record.status, "running");
});

test("readUnitRuntimeRecord_returns_null_for_missing", () => {
	const root = makeProject();
	const record = readUnitRuntimeRecord(root, "execute-task", "MISSING");
	assert.equal(record, null);
});

test("clearRunawayRecoveredRuntimeRecords_clears_only_runaway_terminal_rows", () => {
	const root = makeProject();
	const t0 = Date.now();
	writeUnitRuntimeRecord(root, "discuss-milestone", "M001-X", t0, {
		status: "runaway-recovered",
		phase: "paused",
		runawayGuardPause: { reason: "test pause" },
	});
	writeUnitRuntimeRecord(root, "execute-task", "M001/S01/T01", t0, {
		status: "failed",
		phase: "timeout",
	});
	assert.equal(listUnitRuntimeRecords(root).length, 2);
	assert.equal(clearRunawayRecoveredRuntimeRecords(root), 1);
	const after = listUnitRuntimeRecords(root);
	assert.equal(after.length, 1);
	assert.equal(after[0].unitType, "execute-task");
});

test("clearRunawayRecoveredRuntimeRecords_returns_zero_when_dir_missing", () => {
	const root = makeProject();
	assert.equal(clearRunawayRecoveredRuntimeRecords(root), 0);
});

test("clearUnitRuntimeRecord_removes_file_and_cache", () => {
	const root = makeProject();
	writeUnitRuntimeRecord(root, "execute-task", "M001/S01/T01", Date.now(), {
		status: "running",
	});
	clearUnitRuntimeRecord(root, "execute-task", "M001/S01/T01");
	const record = readUnitRuntimeRecord(root, "execute-task", "M001/S01/T01");
	assert.equal(record, null);
	assert.equal(listUnitRuntimeRecords(root).length, 0);
});

test("writeUnitRuntimeRecord_merges_updates", () => {
	const root = makeProject();
	const t = Date.now();
	writeUnitRuntimeRecord(root, "execute-task", "M001/S01/T01", t, {
		status: "running",
		progressCount: 1,
	});
	writeUnitRuntimeRecord(root, "execute-task", "M001/S01/T01", t, {
		status: "progress",
		progressCount: 2,
	});
	const record = readUnitRuntimeRecord(root, "execute-task", "M001/S01/T01");
	assert.equal(record.status, "progress");
	assert.equal(record.progressCount, 2);
});

test("writeUnitRuntimeRecord_when_lineage_event_started_persists_current_worker", () => {
	const root = makeProject();
	const record = writeUnitRuntimeRecord(
		root,
		"execute-task",
		"M001/S01/T01",
		Date.now(),
		{
			status: "running",
			lineageEvent: {
				status: "started",
				workerSessionId: "worker-1",
				spawnId: "spawn-1",
				ts: "2026-05-08T00:00:00.000Z",
			},
		},
	);
	assert.equal(record.lineage.status, "started");
	assert.equal(record.lineage.currentWorkerSessionId, "worker-1");
	assert.deepEqual(record.lineage.workerSessionIds, ["worker-1"]);
	assert.equal(record.lineage.events[0].spawnId, "spawn-1");
});

test("writeUnitRuntimeRecord_when_lineage_event_completed_clears_current_worker", () => {
	const root = makeProject();
	const t = Date.now();
	writeUnitRuntimeRecord(root, "execute-task", "M001/S01/T01", t, {
		status: "running",
		lineageEvent: {
			status: "started",
			workerSessionId: "worker-1",
		},
	});
	const record = writeUnitRuntimeRecord(
		root,
		"execute-task",
		"M001/S01/T01",
		t,
		{
			status: "completed",
			lineageEvent: {
				status: "completed",
				workerSessionId: "worker-1",
			},
		},
	);
	assert.equal(record.lineage.status, "completed");
	assert.equal(record.lineage.currentWorkerSessionId, null);
	assert.equal(record.lineage.completedWorkerSessionId, "worker-1");
	assert.equal(record.lineage.events.length, 2);
});

test("writeUnitRuntimeRecord_sanitizes_path_characters", () => {
	const root = makeProject();
	writeUnitRuntimeRecord(root, "exec/task", "M001/S01/T01", Date.now(), {
		status: "running",
	});
	const dir = join(root, ".sf", "runtime", "units");
	const files = readdirSync(dir);
	assert.ok(files.every((f) => !f.includes("/")));
});

// ─── listUnitRuntimeRecords ────────────────────────────────────────────────

test("listUnitRuntimeRecords_skips_malformed_files", () => {
	const root = makeProject();
	const dir = join(root, ".sf", "runtime", "units");
	mkdirSync(dir, { recursive: true });
	writeFileSync(join(dir, "bad.json"), "not json", "utf-8");
	writeUnitRuntimeRecord(root, "execute-task", "M001/S01/T01", Date.now(), {
		status: "running",
	});
	const records = listUnitRuntimeRecords(root);
	assert.equal(records.length, 1);
});

test("listUnitRuntimeRecords_returns_empty_when_dir_missing", () => {
	const root = makeProject();
	const records = listUnitRuntimeRecords(root);
	assert.deepEqual(records, []);
});

// ─── getRecoveryDiagnostics ────────────────────────────────────────────────

test("getRecoveryDiagnostics_returns_null_for_missing_record", () => {
	const root = makeProject();
	const diagnostics = getRecoveryDiagnostics(root, "execute-task", "MISSING");
	assert.equal(diagnostics, null);
});

test("getRecoveryDiagnostics_returns_structured_object_for_record_with_recovery", () => {
	const root = makeProject();
	const t = Date.now();
	writeUnitRuntimeRecord(root, "execute-task", "M001/S01/T01", t, {
		status: "failed",
		recoveryAttempts: 2,
		retryCount: 2,
		maxRetries: 3,
		lastRecoveryReason: "timeout",
		progressCount: 5,
		lastProgressKind: "checkpoint",
		lineageEvent: {
			status: "started",
			workerSessionId: "worker-1",
		},
	});
	const diagnostics = getRecoveryDiagnostics(
		root,
		"execute-task",
		"M001/S01/T01",
	);
	assert.ok(diagnostics);
	assert.equal(diagnostics.unitType, "execute-task");
	assert.equal(diagnostics.unitId, "M001/S01/T01");
	assert.equal(diagnostics.status, "failed");
	assert.equal(diagnostics.retryCount, 2);
	assert.equal(diagnostics.maxRetries, 3);
	assert.equal(diagnostics.lastRecoveryReason, "timeout");
	assert.equal(diagnostics.progressCount, 5);
	assert.equal(diagnostics.lastProgressKind, "checkpoint");
	assert.equal(diagnostics.recoveryAttempts, 2);
	assert.ok(diagnostics.lineageSummary);
	assert.equal(diagnostics.lineageSummary.status, "started");
	assert.equal(diagnostics.lineageSummary.workerCount, 1);
	assert.equal(diagnostics.lineageSummary.eventCount, 1);
	assert.equal(diagnostics.startedAt, t);
	assert.ok(diagnostics.updatedAt);
});

test("getRecoveryDiagnostics_returns_minimal_object_for_record_without_recovery", () => {
	const root = makeProject();
	const t = Date.now();
	writeUnitRuntimeRecord(root, "execute-task", "M001/S01/T02", t, {
		status: "running",
	});
	const diagnostics = getRecoveryDiagnostics(
		root,
		"execute-task",
		"M001/S01/T02",
	);
	assert.ok(diagnostics);
	assert.equal(diagnostics.status, "running");
	assert.equal(diagnostics.retryCount, 0);
	assert.equal(diagnostics.maxRetries, 1);
	assert.equal(diagnostics.lastRecoveryReason, null);
	assert.equal(diagnostics.progressCount, 0);
	assert.equal(diagnostics.lastProgressKind, "dispatch");
	assert.equal(diagnostics.recoveryAttempts, 0);
	assert.equal(diagnostics.lineageSummary, null);
});
