/**
 * doctor-flow-audit-auto-cleanup.test.mjs - optional child cleanup.
 *
 * Purpose: prove startup flow audit can clean stale optional child processes
 * without surfacing non-actionable warnings to the operator.
 */
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, describe, test } from "vitest";
import { runFlowAudit } from "../doctor.js";
import {
	markResolved,
	readAllSelfFeedback,
	recordSelfFeedback,
} from "../self-feedback.js";

const tmpDirs = [];

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

function makeProject() {
	const dir = mkdtempSync(join(tmpdir(), "sf-flow-audit-cleanup-"));
	tmpDirs.push(dir);
	mkdirSync(join(dir, ".sf"), { recursive: true });
	// Pretend this is the forge repo so self-feedback stays project-local
	writeFileSync(
		join(dir, "package.json"),
		JSON.stringify({ name: "singularity-forge" }),
		"utf-8",
	);
	return dir;
}

describe("flow audit optional child cleanup", () => {
	test("runFlowAudit_when_optional_orphan_is_over_budget_and_cleanup_enabled_kills_without_warning", async () => {
		const project = makeProject();
		const killed = [];
		const report = await runFlowAudit(project, {
			nowMs: Date.parse("2026-05-06T12:00:00.000Z"),
			optionalChildBudgetMs: 30 * 60 * 1000,
			killOverBudgetChildren: true,
			killProcess(pid) {
				killed.push(pid);
			},
			psOutput: [
				" 12345     1  7200 next-server (v16.2.3)",
				" 23456 12345  7200 sf",
			].join("\n"),
		});

		assert.deepEqual(killed, [12345, 23456]);
		assert.equal(report.ok, true);
		assert.deepEqual(report.warnings, []);
		assert.equal(report.recommendedAction, "No flow-auditor action needed.");
		assert.deepEqual(
			report.childProcesses.map((p) => ({
				pid: p.pid,
				classification: p.classification,
				overBudget: p.overBudget,
				action: p.action,
				killed: p.killed,
			})),
			[
				{
					pid: 12345,
					classification: "orphan",
					overBudget: true,
					action: "kill",
					killed: true,
				},
				{
					pid: 23456,
					classification: "orphan",
					overBudget: true,
					action: "kill",
					killed: true,
				},
			],
		);
	});

	test("runFlowAudit_when_bare_sf_child_of_web_host_is_over_budget_cleans_it", async () => {
		const project = makeProject();
		const killed = [];
		const report = await runFlowAudit(project, {
			nowMs: Date.parse("2026-05-06T12:00:00.000Z"),
			optionalChildBudgetMs: 30 * 60 * 1000,
			killOverBudgetChildren: true,
			killProcess(pid) {
				killed.push(pid);
			},
			psOutput: [
				" 30001     1  7200 next-server (v1)",
				" 30002 30001  7200 sf",
			].join("\n"),
		});

		assert.deepEqual(killed, [30001, 30002]);
		assert.equal(report.ok, true);
		assert.deepEqual(report.warnings, []);
		assert.deepEqual(
			report.childProcesses.map((p) => ({
				pid: p.pid,
				classification: p.classification,
				action: p.action,
				killed: p.killed,
			})),
			[
				{
					pid: 30001,
					classification: "orphan",
					action: "kill",
					killed: true,
				},
				{
					pid: 30002,
					classification: "orphan",
					action: "kill",
					killed: true,
				},
			],
		);
	});

	test("runFlowAudit_when_only_old_errors_exist_does_not_warn_before_dispatch", async () => {
		const project = makeProject();
		writeFileSync(
			join(project, ".sf", "notifications.jsonl"),
			[
				JSON.stringify({
					ts: "2026-05-06T10:00:00.000Z",
					severity: "error",
					message: "Autonomous mode paused: old provider failure",
				}),
				JSON.stringify({
					ts: "2026-05-06T10:45:00.000Z",
					severity: "warning",
					message:
						"Flow audit: Review recent errors before dispatching another unit.",
				}),
			].join("\n") + "\n",
			"utf-8",
		);

		const report = await runFlowAudit(project, {
			nowMs: Date.parse("2026-05-06T12:00:00.000Z"),
			psOutput: "",
		});

		assert.equal(report.ok, true);
		assert.deepEqual(report.lastErrors, []);
		assert.equal(report.recommendedAction, "No flow-auditor action needed.");
	});

	test("runFlowAudit_when_recent_error_exists_warns_before_dispatch", async () => {
		const project = makeProject();
		writeFileSync(
			join(project, ".sf", "notifications.jsonl"),
			JSON.stringify({
				ts: "2026-05-06T11:45:00.000Z",
				severity: "error",
				message: "Autonomous mode paused: recent provider failure",
			}) + "\n",
			"utf-8",
		);

		const report = await runFlowAudit(project, {
			nowMs: Date.parse("2026-05-06T12:00:00.000Z"),
			psOutput: "",
		});

		assert.equal(report.ok, false);
		assert.deepEqual(report.lastErrors, [
			"Autonomous mode paused: recent provider failure",
		]);
		assert.equal(
			report.recommendedAction,
			"Review recent errors before dispatching another unit.",
		);
	});

	test("runFlowAudit_when_repeated_failures_on_same_milestone_files_rollup_with_enriched_evidence", async () => {
		const project = makeProject();
		const nowMs = Date.parse("2026-05-06T12:00:00.000Z");
		const uniqueMid = `MTEST${Date.now()}`;

		// Seed 3 unresolved self-feedback entries via the real channel (DB or JSONL)
		const createdIds = [];
		for (let i = 1; i <= 3; i++) {
			const result = recordSelfFeedback(
				{
					kind: "runaway-guard-hard-pause",
					severity: "medium",
					summary: `Runaway guard paused execute-task ${uniqueMid}/S01/T0${i}`,
					occurredIn: { milestone: uniqueMid, slice: "S01", task: `T0${i}` },
					source: "detector",
				},
				project,
			);
			assert.ok(result, `recordSelfFeedback call ${i} should succeed`);
			createdIds.push(result.entry.id);
		}

		// Set active unit in auto.lock so flow audit has context to enrich
		writeFileSync(
			join(project, ".sf", "auto.lock"),
			JSON.stringify({
				unitType: "execute-task",
				unitId: `${uniqueMid}/S01/T01`,
				phase: "running",
				startedAt: nowMs - 10 * 60 * 1000,
				sessionFile: ".sf/sessions/session-abc.json",
			}),
			"utf-8",
		);

		const report = await runFlowAudit(project, { nowMs, psOutput: "" });

		assert.equal(report.repeatedFailureRollup?.filed, true);
		assert.equal(report.repeatedFailureRollup?.milestoneId, uniqueMid);
		assert.equal(report.repeatedFailureRollup?.count, 3);

		// Read back the self-feedback and verify enriched evidence
		const allFeedback = readAllSelfFeedback(project);
		const rollup = allFeedback.find(
			(e) =>
				e.kind === "flow-audit:repeated-milestone-failure" &&
				e.occurredIn?.milestone === uniqueMid,
		);
		assert.ok(rollup, "rollup entry should exist");
		assert.ok(
			rollup.evidence.includes("Flow Audit Context"),
			"evidence should include Flow Audit Context header",
		);
		assert.ok(
			rollup.evidence.includes(`Active milestone: ${uniqueMid}`),
			"evidence should include active milestone",
		);
		assert.ok(
			rollup.evidence.includes(`execute-task ${uniqueMid}/S01/T01`),
			"evidence should include active unit",
		);
		assert.ok(
			rollup.evidence.includes("Session pointer:"),
			"evidence should include session pointer",
		);
		assert.ok(
			rollup.evidence.includes("Runaway history:"),
			"evidence should include runaway history",
		);
		assert.ok(
			rollup.evidence.includes("Underlying Failures"),
			"evidence should include underlying failures",
		);

		// Clean up: resolve all entries we created
		for (const id of createdIds) {
			markResolved(
				id,
				{ reason: "test cleanup", evidence: { kind: "auto-version-bump" } },
				project,
			);
		}
		if (rollup) {
			markResolved(
				rollup.id,
				{ reason: "test cleanup", evidence: { kind: "auto-version-bump" } },
				project,
			);
		}
	});
});
