/**
 * headless-triage-uok-gates.test.ts — verify slice 3a of "Make UOK the SF
 * Control Plane": runTriageApply emits gate_run trace events with schema-v2
 * metadata for the three triage gates.
 *
 * Test contract: each decision point (trusted-source check, plan-validation,
 * review-code review) writes exactly one gate_run trace event with
 * surface=headless, runControl=supervised, permissionProfile=high, traceId=
 * the flowId. The outcome reflects the decision (pass/fail/manual-attention).
 */
import {
	mkdirSync,
	mkdtempSync,
	readdirSync,
	readFileSync,
	rmSync,
	writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, test } from "vitest";
import { runTriageApply } from "../headless-triage.js";

const deciderPlan = [
	"```yaml",
	"decisions:",
	"  - id: sf-test-1",
	"    outcome: close",
	"    evidence_kind: human-clear",
	"    reason: stale",
	"```",
	"Self-feedback triage complete.",
].join("\n");

const tempDirs: string[] = [];
let originalAgentDir: string | undefined;

function makeProject(): string {
	const dir = mkdtempSync(join(tmpdir(), "sf-triage-uok-gates-"));
	tempDirs.push(dir);
	mkdirSync(join(dir, ".sf"), { recursive: true });
	return dir;
}

function readTraceEvents(project: string): Array<Record<string, unknown>> {
	const dir = join(project, ".sf", "traces");
	let files: string[];
	try {
		files = readdirSync(dir).filter(
			(f) => f.endsWith(".jsonl") && !f.startsWith("latest"),
		);
	} catch {
		return [];
	}
	const events: Array<Record<string, unknown>> = [];
	for (const f of files) {
		const content = readFileSync(join(dir, f), "utf-8");
		for (const line of content.split("\n")) {
			if (!line.trim()) continue;
			try {
				events.push(JSON.parse(line));
			} catch {
				// skip malformed lines
			}
		}
	}
	return events;
}

function gateRunsByGateId(
	project: string,
): Map<string, Array<Record<string, unknown>>> {
	const events = readTraceEvents(project).filter((e) => e.type === "gate_run");
	const map = new Map<string, Array<Record<string, unknown>>>();
	for (const ev of events) {
		const gid = typeof ev.gateId === "string" ? (ev.gateId as string) : null;
		if (!gid) continue;
		if (!map.has(gid)) map.set(gid, []);
		map.get(gid)!.push(ev);
	}
	return map;
}

beforeEach(() => {
	originalAgentDir = process.env.SF_CODING_AGENT_DIR;
	const dir = mkdtempSync(join(tmpdir(), "sf-triage-uok-gates-agent-"));
	tempDirs.push(dir);
	process.env.SF_CODING_AGENT_DIR = dir;
});

afterEach(() => {
	if (originalAgentDir === undefined) delete process.env.SF_CODING_AGENT_DIR;
	else process.env.SF_CODING_AGENT_DIR = originalAgentDir;
	while (tempDirs.length > 0) {
		rmSync(tempDirs.pop()!, { recursive: true, force: true });
	}
});

describe("runTriageApply emits gate_run trace events with schema-v2 metadata", () => {
	test("agree_path_emits_three_pass_gates_with_uok_v2_metadata", async () => {
		const project = makeProject();
		const result = await runTriageApply(project, "triage prompt", {
			candidateCount: 1,
			allowUntrustedRunner: true,
			agentRunner: async (agent) => {
				if (agent.name === "triage-decider") {
					return { ok: true, output: deciderPlan, exitCode: 0 };
				}
				return { ok: true, output: "review-code: agree", exitCode: 0 };
			},
		});

		expect(result.agreed).toBe(true);

		const gates = gateRunsByGateId(project);
		const trustEvents = gates.get("trusted-agent-source-gate") ?? [];
		const validEvents = gates.get("triage-plan-validation-gate") ?? [];
		const reviewEvents = gates.get("triage-apply-review-gate") ?? [];

		expect(trustEvents).toHaveLength(1);
		expect(trustEvents[0].outcome).toBe("pass");
		expect(trustEvents[0].surface).toBe("headless");
		expect(trustEvents[0].runControl).toBe("supervised");
		expect(trustEvents[0].permissionProfile).toBe("high");
		expect(typeof trustEvents[0].traceId).toBe("string");
		expect((trustEvents[0].traceId as string).startsWith("triage-apply-")).toBe(
			true,
		);

		expect(validEvents).toHaveLength(1);
		expect(validEvents[0].outcome).toBe("pass");
		expect(validEvents[0].surface).toBe("headless");

		expect(reviewEvents).toHaveLength(1);
		expect(reviewEvents[0].outcome).toBe("pass");
		expect(reviewEvents[0].surface).toBe("headless");

		// Mutation gate fires after the agree-path apply phase (codex audit
		// 2026-05-14 follow-up). Since the test project has no real ledger
		// entry for sf-test-1, markResolved rejects it and the gate lands
		// "fail" with rejectedCount=1, not "pass".
		const mutationEvents = gates.get("triage-apply-mutation-gate") ?? [];
		expect(mutationEvents).toHaveLength(1);
		expect(mutationEvents[0].outcome).toBe("fail");
		expect(mutationEvents[0].surface).toBe("headless");
		expect(mutationEvents[0].rejectedCount).toBe(1);
		expect(mutationEvents[0].resolvedCount).toBe(0);
	});

	test("disagree_path_emits_review_gate_as_fail", async () => {
		const project = makeProject();
		await runTriageApply(project, "triage prompt", {
			allowUntrustedRunner: true,
			agentRunner: async (agent) => {
				if (agent.name === "triage-decider") {
					return { ok: true, output: deciderPlan, exitCode: 0 };
				}
				return {
					ok: true,
					output: "## Concern 1:\nbad close",
					exitCode: 0,
				};
			},
		});

		const gates = gateRunsByGateId(project);
		const reviewEvents = gates.get("triage-apply-review-gate") ?? [];
		expect(reviewEvents).toHaveLength(1);
		expect(reviewEvents[0].outcome).toBe("fail");
		expect(reviewEvents[0].surface).toBe("headless");

		// Mutation gate must NOT fire on disagree path — no mutations means
		// no apply phase ran.
		const mutationEvents = gates.get("triage-apply-mutation-gate") ?? [];
		expect(mutationEvents).toHaveLength(0);
	});

	test("unknown_id_in_plan_emits_validation_gate_fail_and_no_review_gate", async () => {
		const project = makeProject();
		await runTriageApply(project, "triage prompt", {
			candidateCount: 1,
			expectedIds: ["sf-expected-1"],
			allowUntrustedRunner: true,
			agentRunner: async () => ({
				ok: true,
				output: deciderPlan,
				exitCode: 0,
			}),
		});

		const gates = gateRunsByGateId(project);
		const validEvents = gates.get("triage-plan-validation-gate") ?? [];
		const reviewEvents = gates.get("triage-apply-review-gate") ?? [];

		expect(validEvents).toHaveLength(1);
		expect(validEvents[0].outcome).toBe("fail");
		expect((validEvents[0].rationale as string).toLowerCase()).toContain(
			"not in the candidate set",
		);
		// Review gate must NOT fire because plan validation blocked the flow.
		expect(reviewEvents).toHaveLength(0);
	});

	test("trace_write_failure_fails_closed_before_agents_run", async () => {
		const project = makeProject();
		writeFileSync(join(project, ".sf", "traces"), "not a directory");
		let calls = 0;

		const result = await runTriageApply(project, "triage prompt", {
			candidateCount: 1,
			allowUntrustedRunner: true,
			agentRunner: async () => {
				calls += 1;
				return { ok: true, output: deciderPlan, exitCode: 0 };
			},
		});

		expect(result.ok).toBe(false);
		expect(result.error).toContain(
			"UOK gate emission failed for trusted-agent-source-gate",
		);
		expect(calls).toBe(0);
	});
});
