/**
 * Slice 3b of "Make UOK the SF Control Plane".
 *
 * Verifies that the autonomous loop's gate emissions now carry the
 * schema-v2 run-context fields (surface, runControl, permissionProfile,
 * parentTrace) through to the trace events that `sf headless status
 * uok --json` reads — flipping the gate's coverageStatus from
 * "legacy" to "ok".
 *
 * Layers covered here:
 *   1. UokGateRunner.run reads ctx.surface/runControl/permissionProfile/
 *      parentTrace and writes them into every gate_run trace event
 *      (unknown-gate, normal, and circuit-breaker-blocked paths).
 *   2. buildAutonomousUokContext pins surface="autonomous" / runControl=
 *      "autonomous" and derives permissionProfile from session+prefs.
 *   3. plan-slice's handlePlanSlice forwards an options.uokContext to
 *      sf-db-gates.insertGateRow.
 */

import assert from "node:assert/strict";
import {
	mkdirSync,
	mkdtempSync,
	readdirSync,
	readFileSync,
	rmSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, test } from "vitest";
import { _getAdapter } from "../sf-db/sf-db-core.js";
import {
	closeDatabase,
	insertMilestone,
	insertSlice,
	openDatabase,
	updateGateCircuitBreaker,
} from "../sf-db.js";
import {
	buildAutonomousUokContext,
	derivePermissionProfile,
	emitAutonomousGate,
} from "../uok/auto-uok-ctx.js";
import { UokGateRunner } from "../uok/gate-runner.js";

const tmpRoots = [];

function makeProject() {
	const root = mkdtempSync(join(tmpdir(), "sf-uok-slice3b-"));
	mkdirSync(join(root, ".sf", "traces"), { recursive: true });
	mkdirSync(join(root, ".sf", "milestones", "M1", "slices", "S1"), {
		recursive: true,
	});
	mkdirSync(join(root, ".sf", "milestones", "M2", "slices", "S2"), {
		recursive: true,
	});
	tmpRoots.push(root);
	return root;
}

function readGateRunEvents(basePath) {
	const dir = join(basePath, ".sf", "traces");
	const events = [];
	for (const name of readdirSync(dir)) {
		if (!name.endsWith(".jsonl")) continue;
		const lines = readFileSync(join(dir, name), "utf-8")
			.split("\n")
			.filter(Boolean);
		for (const line of lines) {
			try {
				const ev = JSON.parse(line);
				if (ev.type === "gate_run") events.push(ev);
			} catch {
				/* skip malformed lines */
			}
		}
	}
	return events;
}

function planningMeeting() {
	return {
		trigger: "test",
		pm: "Plan the slice.",
		researcher: "Schema exists.",
		partner: "Existing tools.",
		combatant: "Avoid duplication.",
		architect: "SQLite-backed.",
		moderator: "Proceed.",
		recommendedRoute: "planning",
		confidenceSummary: "high",
	};
}

beforeEach(() => {
	openDatabase(":memory:");
});

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

// ─── 1. UokGateRunner.run propagates schema-v2 ctx into gate_run events ───

describe("UokGateRunner emits schema-v2 fields into gate_run trace events", () => {
	test("normal-pass path carries surface/runControl/permissionProfile/parentTrace", async () => {
		const basePath = makeProject();
		const runner = new UokGateRunner();
		runner.register({
			id: "demo-gate",
			type: "verification",
			execute: async () => ({ outcome: "pass", rationale: "ok" }),
		});
		await runner.run("demo-gate", {
			basePath,
			traceId: "trace-1",
			turnId: "turn-1",
			unitType: "execute-task",
			unitId: "M1/S1/T1",
			milestoneId: "M1",
			sliceId: "S1",
			taskId: "T1",
			surface: "autonomous",
			runControl: "autonomous",
			permissionProfile: "high",
			parentTrace: "flow-parent",
		});
		const events = readGateRunEvents(basePath);
		assert.equal(events.length, 1);
		const ev = events[0];
		assert.equal(ev.surface, "autonomous");
		assert.equal(ev.runControl, "autonomous");
		assert.equal(ev.permissionProfile, "high");
		assert.equal(ev.parentTrace, "flow-parent");
		assert.equal(ev.gateId, "demo-gate");
		assert.equal(ev.outcome, "pass");
	});

	test("legacy ctx (no v2 fields) omits them from gate_run event", async () => {
		const basePath = makeProject();
		const runner = new UokGateRunner();
		runner.register({
			id: "legacy-gate",
			type: "verification",
			execute: async () => ({ outcome: "pass", rationale: "ok" }),
		});
		await runner.run("legacy-gate", {
			basePath,
			traceId: "trace-legacy",
			turnId: "turn-1",
		});
		const events = readGateRunEvents(basePath);
		assert.equal(events.length, 1);
		const ev = events[0];
		assert.equal(ev.surface, undefined);
		assert.equal(ev.runControl, undefined);
		assert.equal(ev.permissionProfile, undefined);
		assert.equal(ev.parentTrace, undefined);
	});

	test("unknown-gate path also carries schema-v2 ctx fields", async () => {
		const basePath = makeProject();
		const runner = new UokGateRunner();
		const result = await runner.run("not-registered", {
			basePath,
			traceId: "trace-unknown",
			turnId: "turn-1",
			surface: "autonomous",
			runControl: "autonomous",
			permissionProfile: "low",
			parentTrace: "flow-x",
		});
		assert.equal(result.outcome, "manual-attention");
		const events = readGateRunEvents(basePath);
		assert.equal(events.length, 1);
		assert.equal(events[0].surface, "autonomous");
		assert.equal(events[0].runControl, "autonomous");
		assert.equal(events[0].permissionProfile, "low");
		assert.equal(events[0].parentTrace, "flow-x");
	});

	test("circuit-breaker-blocked path also carries schema-v2 ctx fields", async () => {
		const basePath = makeProject();
		const runner = new UokGateRunner();
		runner.register({
			id: "cb-block-v2",
			type: "verification",
			execute: async () => ({ outcome: "pass", rationale: "ok" }),
		});
		updateGateCircuitBreaker("cb-block-v2", {
			state: "open",
			failureStreak: 5,
			openedAt: new Date().toISOString(),
		});
		await runner.run("cb-block-v2", {
			basePath,
			traceId: "trace-cb",
			turnId: "turn-1",
			surface: "autonomous",
			runControl: "autonomous",
			permissionProfile: "high",
		});
		const events = readGateRunEvents(basePath);
		assert.equal(events.length, 1);
		assert.equal(events[0].surface, "autonomous");
		assert.equal(events[0].runControl, "autonomous");
		assert.equal(events[0].permissionProfile, "high");
		assert.equal(events[0].outcome, "fail");
	});
});

// ─── 2. buildAutonomousUokContext pins autonomous defaults ────────────────

describe("buildAutonomousUokContext autonomous-loop defaults", () => {
	test("returns surface=autonomous and runControl=autonomous", () => {
		const ctx = buildAutonomousUokContext({
			s: { isYolo: () => false },
			prefs: undefined,
			traceId: "flow-1",
		});
		assert.equal(ctx?.surface, "autonomous");
		assert.equal(ctx?.runControl, "autonomous");
		assert.equal(ctx?.permissionProfile, "high");
		assert.equal(ctx?.traceId, "flow-1");
	});

	test("YOLO session forces permissionProfile=low", () => {
		const ctx = buildAutonomousUokContext({
			s: { isYolo: () => true },
			prefs: undefined,
			traceId: "flow-yolo",
		});
		assert.equal(ctx?.permissionProfile, "low");
	});

	test("prefs.permissionLevel=medium propagates to permissionProfile", () => {
		const ctx = buildAutonomousUokContext({
			s: {},
			prefs: { permissionLevel: "medium" },
			traceId: "flow-med",
		});
		assert.equal(ctx?.permissionProfile, "medium");
	});

	test("prefs.permissionLevel=low or minimal propagates to permissionProfile=low", () => {
		assert.equal(
			buildAutonomousUokContext({
				s: {},
				prefs: { permissionLevel: "low" },
				traceId: "flow-low",
			})?.permissionProfile,
			"low",
		);
		assert.equal(
			buildAutonomousUokContext({
				s: {},
				prefs: { permissionLevel: "minimal" },
				traceId: "flow-min",
			})?.permissionProfile,
			"low",
		);
	});

	test("returns null when traceId is missing", () => {
		assert.equal(buildAutonomousUokContext({ s: {}, prefs: {} }), null);
	});

	test("derivePermissionProfile session probe failure falls back to prefs", () => {
		const throwingSession = {
			isYolo: () => {
				throw new Error("session probe boom");
			},
		};
		assert.equal(
			derivePermissionProfile(throwingSession, { permissionLevel: "medium" }),
			"medium",
		);
		assert.equal(derivePermissionProfile(throwingSession, undefined), "high");
	});
});

// ─── 3. emitAutonomousGate / phase wiring contract ────────────────────────

describe("emitAutonomousGate forwards uokContext into UokGateRunner.run", () => {
	test("emitted gate_run trace event carries surface/runControl/permissionProfile/parentTrace", async () => {
		const basePath = makeProject();
		const uokContext = buildAutonomousUokContext({
			s: { isYolo: () => false },
			prefs: undefined,
			traceId: "pre-dispatch:flow-A",
			parentTrace: "flow-A",
			unitType: "pre-dispatch",
			unitId: "iter-3",
		});
		assert.ok(uokContext);
		const result = await emitAutonomousGate({
			basePath,
			gateId: "resource-version-guard",
			gateType: "policy",
			outcome: "pass",
			failureClass: "none",
			rationale: "ok",
			traceId: "pre-dispatch:flow-A",
			turnId: "iter-3",
			unitType: "pre-dispatch",
			unitId: "iter-3",
			milestoneId: "M1",
			uokContext,
		});
		assert.equal(result.outcome, "pass");
		const events = readGateRunEvents(basePath);
		assert.equal(events.length, 1);
		const ev = events[0];
		assert.equal(ev.surface, "autonomous");
		assert.equal(ev.runControl, "autonomous");
		assert.equal(ev.permissionProfile, "high");
		assert.equal(ev.parentTrace, "flow-A");
		assert.equal(ev.gateId, "resource-version-guard");
		assert.equal(ev.unitType, "pre-dispatch");
		assert.equal(ev.unitId, "iter-3");
		assert.equal(ev.milestoneId, "M1");
	});

	test("runnerOverride seam: phase code passes the same ctx every time", async () => {
		// Spy: record each .run call so we can assert the phases always
		// pass the v2 ctx through. This is the dependency-injection
		// contract the slice 3b plan calls for.
		const spy = {
			calls: [],
			register() {},
			async run(id, ctx) {
				this.calls.push({ id, ctx });
				return { outcome: "pass", gateId: id, gateType: "policy" };
			},
		};
		const uokContext = buildAutonomousUokContext({
			s: { isYolo: () => true },
			prefs: { permissionLevel: "medium" },
			traceId: "guard:flow-B",
			parentTrace: "flow-B",
		});
		assert.equal(uokContext?.permissionProfile, "low"); // YOLO wins over prefs
		await emitAutonomousGate({
			basePath: "/tmp/no-write",
			gateId: "plan-gate",
			gateType: "policy",
			outcome: "pass",
			traceId: "guard:flow-B",
			turnId: "iter-1",
			uokContext,
			runnerOverride: spy,
		});
		assert.equal(spy.calls.length, 1);
		assert.equal(spy.calls[0].id, "plan-gate");
		assert.equal(spy.calls[0].ctx.surface, "autonomous");
		assert.equal(spy.calls[0].ctx.runControl, "autonomous");
		assert.equal(spy.calls[0].ctx.permissionProfile, "low");
		assert.equal(spy.calls[0].ctx.parentTrace, "flow-B");
	});
});

// ─── 4. plan-slice forwards uokContext to insertGateRow ───────────────────

describe("plan-slice forwards uokContext through to insertGateRow", () => {
	test("handlePlanSlice options.uokContext lands on every seeded gate row", async () => {
		// Real sf-db setup so the transaction commits and we can inspect
		// the seeded quality_gates rows.
		closeDatabase();
		const project = makeProject();
		openDatabase(join(project, ".sf", "sf.db"));
		insertMilestone({ id: "M1", title: "demo", status: "active" });
		insertSlice({
			milestoneId: "M1",
			id: "S1",
			title: "demo slice",
			status: "pending",
		});
		const { handlePlanSlice } = await import("../tools/plan-slice.js");
		const uokContext = buildAutonomousUokContext({
			s: { isYolo: () => false },
			prefs: undefined,
			traceId: "flow-from-test",
			parentTrace: "flow-from-test",
		});
		assert.ok(
			uokContext,
			"test setup: buildAutonomousUokContext must return a ctx",
		);
		await handlePlanSlice(
			{
				milestoneId: "M1",
				sliceId: "S1",
				goal: "do the thing",
				successCriteria: "it works",
				proofLevel: "tested",
				integrationClosure: "merged",
				observabilityImpact: "logs",
				adversarialReview: {
					partner: "P review",
					combatant: "C review",
					architect: "A review",
				},
				planningMeeting: planningMeeting(),
				tasks: [
					{
						taskId: "T01",
						title: "Task one",
						description: "Do task one carefully",
						estimate: "small",
						files: ["src/foo.ts"],
						verify: "tests pass",
						inputs: ["spec"],
						expectedOutput: ["green tests"],
						observabilityImpact: "more logs",
					},
				],
			},
			project,
			{ uokContext },
		);
		const db = _getAdapter();
		const rows = db
			.prepare(
				"SELECT gate_id, surface, run_control, permission_profile, trace_id, parent_trace FROM quality_gates WHERE milestone_id = ? AND slice_id = ? ORDER BY gate_id, task_id",
			)
			.all("M1", "S1");
		assert.ok(rows.length >= 4, `expected ≥4 gate rows, got ${rows.length}`);
		for (const row of rows) {
			assert.equal(
				row.surface,
				"autonomous",
				`gate ${row.gate_id} missing surface`,
			);
			assert.equal(row.run_control, "autonomous");
			assert.equal(row.permission_profile, "high");
			assert.equal(row.trace_id, "flow-from-test");
			assert.equal(row.parent_trace, "flow-from-test");
		}
	});

	test("handlePlanSlice without uokContext leaves gate rows in legacy null shape", async () => {
		closeDatabase();
		const project = makeProject();
		openDatabase(join(project, ".sf", "sf.db"));
		insertMilestone({ id: "M2", title: "legacy", status: "active" });
		insertSlice({
			milestoneId: "M2",
			id: "S2",
			title: "legacy slice",
			status: "pending",
		});
		const { handlePlanSlice } = await import("../tools/plan-slice.js");
		await handlePlanSlice(
			{
				milestoneId: "M2",
				sliceId: "S2",
				goal: "legacy path",
				successCriteria: "ok",
				proofLevel: "tested",
				integrationClosure: "merged",
				observabilityImpact: "logs",
				adversarialReview: {
					partner: "P review",
					combatant: "C review",
					architect: "A review",
				},
				planningMeeting: planningMeeting(),
				tasks: [
					{
						taskId: "T01",
						title: "legacy task",
						description: "describes legacy task",
						estimate: "small",
						files: ["x.ts"],
						verify: "ok",
						inputs: ["i"],
						expectedOutput: ["o"],
					},
				],
			},
			project,
			// no options.uokContext
		);
		const db = _getAdapter();
		const rows = db
			.prepare(
				"SELECT gate_id, surface, run_control, permission_profile, trace_id FROM quality_gates WHERE milestone_id = ? AND slice_id = ? ORDER BY gate_id",
			)
			.all("M2", "S2");
		assert.ok(rows.length >= 4);
		for (const row of rows) {
			assert.equal(
				row.surface,
				null,
				`gate ${row.gate_id} unexpectedly carries surface`,
			);
			assert.equal(row.run_control, null);
			assert.equal(row.permission_profile, null);
			assert.equal(row.trace_id, null);
		}
	});
});
