/**
 * uok-parity-exit-guarantee.test.mjs
 *
 * Verifies that every UOK kernel enter event in the parity log is
 * always matched by exactly one exit event, and that the status counter
 * in buildParityReport only tallies exit events (not enter events).
 */
import assert from "node:assert/strict";
import {
	existsSync,
	mkdirSync,
	mkdtempSync,
	readFileSync,
	rmSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, test } from "vitest";
import { runAutoLoopWithUok } from "../uok/kernel.js";
import {
	buildParityReport,
	parseParityEvents,
	writeParityHeartbeat,
} from "../uok/parity-report.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-exit-guarantee-"));
	tmpRoots.push(root);
	mkdirSync(join(root, ".sf", "runtime"), { recursive: true });
	return root;
}

function testDeps() {
	return {
		loadEffectiveSFPreferences() {
			return {
				preferences: {
					uok: { enabled: true, audit_envelope: { enabled: false } },
				},
			};
		},
	};
}

function testCtx(sessionId = "session-guarantee-test") {
	return {
		sessionManager: { getSessionId: () => sessionId },
	};
}

function readParityEvents(projectRoot) {
	const path = join(projectRoot, ".sf", "runtime", "uok-parity.jsonl");
	assert.equal(existsSync(path), true, "parity.jsonl should exist");
	return parseParityEvents(readFileSync(path, "utf-8"));
}

function countExitsByRunId(events) {
	const counts = new Map();
	for (const ev of events) {
		if (ev.phase === "exit" && ev.runId) {
			counts.set(ev.runId, (counts.get(ev.runId) ?? 0) + 1);
		}
	}
	return counts;
}

// ── Case 1: Normal exit — exactly one exit per runId ─────────────────────────
test("guarantee_normal_exit_exactly_one_exit_per_runId", async () => {
	const projectRoot = makeProject();
	const state = { basePath: projectRoot };

	await runAutoLoopWithUok({
		ctx: testCtx("session-normal"),
		pi: {},
		s: state,
		deps: testDeps(),
		async runKernelLoop() {
			// Normal completion — no conditional exit site fires.
		},
	});

	const events = readParityEvents(projectRoot);
	const enters = events.filter((e) => e.phase === "enter");
	const exits = events.filter((e) => e.phase === "exit");

	assert.equal(enters.length, 1, "exactly one enter event");
	assert.equal(exits.length, 1, "exactly one exit event");
	assert.equal(enters[0].runId, exits[0].runId, "enter and exit share runId");

	const exitsByRunId = countExitsByRunId(events);
	assert.equal(exitsByRunId.get(enters[0].runId), 1, "only one exit per runId");
});

// ── Case 2: Abnormal exit via finally (no conditional site) ──────────────────
test("guarantee_finally_writes_exit_with_reason_normal_termination", async () => {
	const projectRoot = makeProject();
	const state = { basePath: projectRoot };

	// The kernel loop completes without throwing — guaranteed-exit finally fires.
	await runAutoLoopWithUok({
		ctx: testCtx("session-finally"),
		pi: {},
		s: state,
		deps: testDeps(),
		async runKernelLoop() {
			// Simulate work; no explicit conditional exit site fires.
		},
	});

	const events = readParityEvents(projectRoot);
	const exitEvent = events.find((e) => e.phase === "exit");
	assert.ok(exitEvent, "exit event must be present");
	// The finally block sets __uokExitWritten=true, then calls recordUokKernelTermination
	// which writes the exit via writeParityHeartbeat with status "ok".
	assert.equal(exitEvent.status, "ok");
	assert.equal(exitEvent.runId, events.find((e) => e.phase === "enter").runId);
});

// ── Case 3: Signal handler writes exit with correct reason ────────────────────
// We test the once-flag + writeParityHeartbeat pattern that kernel.ts registers
// for signal handlers. We invoke the handler function directly (not via
// process.emit, which would disturb other signal listeners in the test process).
test("guarantee_signal_handler_writes_exit_with_signal_reason", async () => {
	const projectRoot = makeProject();
	const parityPath = join(projectRoot, ".sf", "runtime", "uok-parity.jsonl");
	const runId = "uok-sigterm-simulate-run";

	// Write an enter event as kernel.ts would.
	writeParityHeartbeat(projectRoot, {
		ts: new Date().toISOString(),
		runId,
		path: "uok-kernel",
		phase: "enter",
	});

	// Mirror the kernel.ts once-flag + handler pattern.
	let exitWritten = false;
	function writeExitOnce(reason, exitStatus) {
		if (exitWritten) return;
		exitWritten = true;
		writeParityHeartbeat(projectRoot, {
			ts: new Date().toISOString(),
			runId,
			path: "uok-kernel",
			phase: "exit",
			status: exitStatus ?? "ok",
			reason,
		});
	}

	// Invoke the SIGTERM branch directly (without process.emit to avoid
	// triggering other signal listeners in the test process).
	writeExitOnce("signal:SIGTERM", "signal");

	assert.equal(exitWritten, true, "exit must have been marked as written");

	// Verify the parity log has the exit event.
	assert.equal(existsSync(parityPath), true, "parity.jsonl should exist");
	const events = parseParityEvents(readFileSync(parityPath, "utf-8"));

	const enters = events.filter((e) => e.phase === "enter");
	const exits = events.filter((e) => e.phase === "exit");

	assert.equal(enters.length, 1, "one enter event");
	assert.equal(exits.length, 1, "one exit event from signal handler");
	assert.equal(exits[0].reason, "signal:SIGTERM");
	assert.equal(exits[0].status, "signal");
	assert.equal(exits[0].runId, runId);

	// Verify once-guard: calling writeExitOnce a second time must NOT write another exit.
	writeExitOnce("signal:SIGTERM", "signal");
	const eventsAfter = parseParityEvents(readFileSync(parityPath, "utf-8"));
	assert.equal(
		eventsAfter.filter((e) => e.phase === "exit").length,
		1,
		"once-flag prevents second exit write",
	);
});

// ── Case 4: parity-report status counter only tallies exit events ────────────
test("parity_report_status_counter_only_counts_exit_events", () => {
	// Fixture: 3 enters + 2 exits (one ok, one error). Enter events have no
	// status field. The status bucket should show {ok:1, error:1}, no "unknown".
	const NOW = Date.parse("2026-05-17T00:00:00.000Z");
	const events = [
		// 3 enter events — deliberately no status field
		{
			schemaVersion: 1,
			ts: new Date(NOW - 10_000).toISOString(),
			runId: "uok-r1",
			path: "uok-kernel",
			phase: "enter",
		},
		{
			schemaVersion: 1,
			ts: new Date(NOW - 9_000).toISOString(),
			runId: "uok-r2",
			path: "uok-kernel",
			phase: "enter",
		},
		{
			schemaVersion: 1,
			ts: new Date(NOW - 8_000).toISOString(),
			runId: "uok-r3",
			path: "uok-kernel",
			phase: "enter",
		},
		// 2 exit events
		{
			schemaVersion: 1,
			ts: new Date(NOW - 7_000).toISOString(),
			runId: "uok-r1",
			path: "uok-kernel",
			phase: "exit",
			status: "ok",
		},
		{
			schemaVersion: 1,
			ts: new Date(NOW - 6_000).toISOString(),
			runId: "uok-r2",
			path: "uok-kernel",
			phase: "exit",
			status: "error",
			error: "boom",
		},
	];

	const report = buildParityReport(events, "/tmp/uok-parity.jsonl", NOW);

	// Status bucket must only contain exit statuses
	assert.deepEqual(
		report.statuses,
		{ ok: 1, error: 1 },
		"statuses must sum to exit-event count only",
	);
	assert.equal(
		"unknown" in report.statuses,
		false,
		"unknown bucket must not appear from enter-only events",
	);

	// Sanity: counts
	assert.equal(report.enterEvents, 3);
	assert.equal(report.exitEvents, 2);

	// uok-r3 has enter but no exit — should be in unmatched
	assert.equal(report.unmatchedRuns.length, 1);
	assert.equal(report.unmatchedRuns[0].runId, "uok-r3");
});

// ── Case 5: double exit (signal handler + finally both write) — deduplication ─
test("parity_report_handles_duplicate_exits_per_runId_as_balanced", () => {
	// If two exit events land for the same runId (e.g. signal handler fires and
	// then the finally also fires), the reporter must treat it as balanced, not
	// as mismatched in the other direction.
	const NOW = Date.parse("2026-05-17T00:00:00.000Z");
	const events = [
		{
			schemaVersion: 1,
			ts: new Date(NOW - 10_000).toISOString(),
			runId: "uok-double",
			path: "uok-kernel",
			phase: "enter",
		},
		{
			schemaVersion: 1,
			ts: new Date(NOW - 5_000).toISOString(),
			runId: "uok-double",
			path: "uok-kernel",
			phase: "exit",
			status: "signal",
		},
		{
			schemaVersion: 1,
			ts: new Date(NOW - 4_000).toISOString(),
			runId: "uok-double",
			path: "uok-kernel",
			phase: "exit",
			status: "ok",
		},
	];

	const report = buildParityReport(events, "/tmp/uok-parity.jsonl", NOW);

	// enterEvents=1, exitEvents=2 → not in unmatchedRuns (exitEvents >= enterEvents)
	assert.equal(
		report.unmatchedRuns.length,
		0,
		"no unmatched runs with double-exit",
	);
	assert.equal(report.missingExitEvents, 0);
	// statuses bucket has both exits counted
	assert.deepEqual(report.statuses, { signal: 1, ok: 1 });
});
