import assert from "node:assert/strict";
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, test } from "vitest";
import {
	closeDatabase,
	getGateCircuitBreaker,
	getGateLatencyStats,
	getGateRunStats,
	openDatabase,
	updateGateCircuitBreaker,
} from "../sf-db.js";
import { validateGate } from "../uok/contracts.js";
import { UokGateRunner } from "../uok/gate-runner.js";
import {
	buildMetricsText,
	invalidateMetricsCache,
} from "../uok/metrics-exposition.js";

const tmpRoots = [];

function restoreEnv(name, value) {
	if (value === undefined) {
		delete process.env[name];
		return;
	}
	process.env[name] = value;
}

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

function makeProject() {
	const root = mkdtempSync(join(tmpdir(), "sf-uok-runner-"));
	mkdirSync(join(root, ".sf"), { recursive: true });
	tmpRoots.push(root);
	return root;
}

function makeCtx(overrides = {}) {
	const basePath = makeProject();
	return {
		basePath,
		traceId: "trace-1",
		turnId: "turn-1",
		unitType: "execute-task",
		unitId: "M001/S01/T01",
		milestoneId: "M001",
		sliceId: "S01",
		taskId: "T01",
		...overrides,
	};
}

// ─── Registration ──────────────────────────────────────────────────────────

test("runner_register_adds_gate", () => {
	const runner = new UokGateRunner();
	const gate = {
		id: "test-gate",
		type: "policy",
		execute: async () => ({ outcome: "pass", rationale: "ok" }),
	};
	runner.register(gate);
	assert.equal(runner.list().length, 1);
	assert.equal(runner.list()[0].id, "test-gate");
});

// ─── Unknown gate ──────────────────────────────────────────────────────────

test("run_when_gate_not_registered_returns_manual_attention", async () => {
	openDatabase(":memory:");
	const runner = new UokGateRunner();
	const result = await runner.run("missing-gate", makeCtx());
	assert.equal(result.outcome, "manual-attention");
	assert.equal(result.gateId, "missing-gate");
	assert.equal(result.failureClass, "unknown");
	assert.equal(result.retryable, false);
});

test("run_when_gate_not_registered_persists_to_trace", async () => {
	const ctx = makeCtx();
	openDatabase(join(ctx.basePath, ".sf", "sf.db"));
	const runner = new UokGateRunner();
	await runner.run("missing-gate", ctx);
	const stats = getGateRunStats("missing-gate", 24);
	assert.equal(stats.total, 1);
	assert.equal(stats.manualAttention, 1);
});

// ─── Pass ──────────────────────────────────────────────────────────────────

test("run_when_gate_passes_returns_pass", async () => {
	openDatabase(":memory:");
	const runner = new UokGateRunner();
	const gate = {
		id: "pass-gate",
		type: "verification",
		execute: async () => ({ outcome: "pass", rationale: "all good" }),
	};
	runner.register(gate);
	const result = await runner.run("pass-gate", makeCtx());
	assert.equal(result.outcome, "pass");
	assert.equal(result.attempt, 1);
	assert.equal(result.retryable, false);
});

test("run_when_gate_passes_persists_pass_to_trace", async () => {
	const ctx = makeCtx();
	openDatabase(join(ctx.basePath, ".sf", "sf.db"));
	const runner = new UokGateRunner();
	runner.register({
		id: "pass-gate",
		type: "verification",
		execute: async () => ({ outcome: "pass", rationale: "ok" }),
	});
	await runner.run("pass-gate", ctx);
	const stats = getGateRunStats("pass-gate", 24);
	assert.equal(stats.total, 1);
	assert.equal(stats.pass, 1);
});

// ─── Fail without retry ────────────────────────────────────────────────────

test("run_when_policy_fail_returns_fail_no_retry", async () => {
	openDatabase(":memory:");
	const runner = new UokGateRunner();
	runner.register({
		id: "policy-gate",
		type: "policy",
		execute: async () => ({
			outcome: "fail",
			failureClass: "policy",
			rationale: "blocked",
		}),
	});
	const result = await runner.run("policy-gate", makeCtx());
	assert.equal(result.outcome, "fail");
	assert.equal(result.attempt, 1);
	assert.equal(result.retryable, false);
});

// ─── Fail with retry ───────────────────────────────────────────────────────

test("run_when_execution_fail_retries_once_then_fails", async () => {
	openDatabase(":memory:");
	const runner = new UokGateRunner();
	let calls = 0;
	runner.register({
		id: "exec-gate",
		type: "verification",
		execute: async () => {
			calls++;
			return {
				outcome: "fail",
				failureClass: "execution",
				rationale: "transient error",
			};
		},
	});
	const result = await runner.run("exec-gate", makeCtx());
	assert.equal(calls, 2);
	assert.equal(result.outcome, "fail");
	assert.equal(result.attempt, 2);
	assert.equal(result.retryable, false);
});

test("run_when_timeout_fail_retries_twice_then_fails", async () => {
	openDatabase(":memory:");
	const runner = new UokGateRunner();
	let calls = 0;
	runner.register({
		id: "timeout-gate",
		type: "verification",
		execute: async () => {
			calls++;
			return {
				outcome: "fail",
				failureClass: "timeout",
				rationale: "timed out",
			};
		},
	});
	const result = await runner.run("timeout-gate", makeCtx());
	assert.equal(calls, 3);
	assert.equal(result.outcome, "fail");
	assert.equal(result.attempt, 3);
	assert.equal(result.retryable, false);
});

// ─── Retry succeeds ────────────────────────────────────────────────────────

test("run_when_first_fail_then_pass_stops_early", async () => {
	openDatabase(":memory:");
	const runner = new UokGateRunner();
	let calls = 0;
	runner.register({
		id: "flaky-gate",
		type: "verification",
		execute: async () => {
			calls++;
			return calls === 1
				? { outcome: "fail", failureClass: "execution", rationale: "retry me" }
				: { outcome: "pass", rationale: "ok now" };
		},
	});
	const result = await runner.run("flaky-gate", makeCtx());
	assert.equal(calls, 2);
	assert.equal(result.outcome, "pass");
	assert.equal(result.attempt, 2);
});

// ─── Exception isolation ───────────────────────────────────────────────────

test("run_when_gate_throws_returns_fail_with_unknown_class", async () => {
	openDatabase(":memory:");
	const runner = new UokGateRunner();
	runner.register({
		id: "throw-gate",
		type: "verification",
		execute: async () => {
			throw new Error("boom");
		},
	});
	const result = await runner.run("throw-gate", makeCtx());
	assert.equal(result.outcome, "fail");
	assert.equal(result.failureClass, "unknown");
	assert.ok(result.rationale.includes("boom"));
});

// ─── Trace audit trail ─────────────────────────────────────────────────────

test("run_records_every_attempt_to_trace", async () => {
	const ctx = makeCtx();
	openDatabase(join(ctx.basePath, ".sf", "sf.db"));
	const runner = new UokGateRunner();
	let calls = 0;
	runner.register({
		id: "audit-gate",
		type: "verification",
		execute: async () => {
			calls++;
			return {
				outcome: "fail",
				failureClass: "execution",
				rationale: `attempt ${calls}`,
			};
		},
	});
	await runner.run("audit-gate", ctx);
	const stats = getGateRunStats("audit-gate", 24);
	assert.equal(stats.total, 2);
	assert.equal(stats.fail, 1);
	assert.equal(stats.retry, 1);
});

// ─── Gate metadata ─────────────────────────────────────────────────────────

test("run_result_includes_gate_id_type_and_timestamps", async () => {
	openDatabase(":memory:");
	const runner = new UokGateRunner();
	runner.register({
		id: "meta-gate",
		type: "policy",
		execute: async () => ({ outcome: "pass", rationale: "ok" }),
	});
	const result = await runner.run("meta-gate", makeCtx());
	assert.equal(result.gateId, "meta-gate");
	assert.equal(result.gateType, "policy");
	assert.ok(typeof result.evaluatedAt === "string");
	assert.ok(result.evaluatedAt.length > 0);
});

// ─── Latency tracking ──────────────────────────────────────────────────────

test("run_records_duration_ms_in_trace", async () => {
	const ctx = makeCtx();
	openDatabase(join(ctx.basePath, ".sf", "sf.db"));
	const runner = new UokGateRunner();
	runner.register({
		id: "slow-gate",
		type: "verification",
		execute: async () => {
			await new Promise((r) => setTimeout(r, 50));
			return { outcome: "pass", rationale: "ok" };
		},
	});
	await runner.run("slow-gate", ctx);
	const stats = getGateLatencyStats("slow-gate", 24);
	assert.equal(stats.total, 1);
	assert.ok(stats.avgMs >= 40, `expected avgMs >= 40, got ${stats.avgMs}`);
});

// ─── Circuit breaker ───────────────────────────────────────────────────────

test("circuitBreaker_when_passes_resets_failure_streak", async () => {
	openDatabase(":memory:");
	const runner = new UokGateRunner();
	runner.register({
		id: "cb-pass",
		type: "verification",
		execute: async () => ({ outcome: "pass", rationale: "ok" }),
	});
	updateGateCircuitBreaker("cb-pass", { state: "closed", failureStreak: 3 });
	await runner.run("cb-pass", makeCtx());
	const breaker = getGateCircuitBreaker("cb-pass");
	assert.equal(breaker.state, "closed");
	assert.equal(breaker.failureStreak, 0);
});

test("circuitBreaker_when_fails_incrementally_opens_after_threshold", async () => {
	openDatabase(":memory:");
	const runner = new UokGateRunner();
	let calls = 0;
	runner.register({
		id: "cb-fail",
		type: "verification",
		execute: async () => {
			calls++;
			return { outcome: "fail", failureClass: "execution", rationale: "nope" };
		},
	});
	// Threshold is 5; run 5 times to open the breaker
	for (let i = 0; i < 5; i++) {
		await runner.run("cb-fail", makeCtx());
	}
	const breaker = getGateCircuitBreaker("cb-fail");
	assert.equal(breaker.state, "open");
	assert.equal(breaker.failureStreak, 5);
	assert.ok(breaker.openedAt != null);
	assert.equal(calls, 10);
});

test("circuitBreaker_when_open_blocks_execution", async () => {
	openDatabase(":memory:");
	const runner = new UokGateRunner();
	let calls = 0;
	runner.register({
		id: "cb-block",
		type: "verification",
		execute: async () => {
			calls++;
			return { outcome: "pass", rationale: "ok" };
		},
	});
	updateGateCircuitBreaker("cb-block", {
		state: "open",
		failureStreak: 5,
		openedAt: new Date().toISOString(),
	});
	const result = await runner.run("cb-block", makeCtx());
	assert.equal(calls, 0);
	assert.equal(result.outcome, "fail");
	assert.ok(result.rationale.includes("OPEN"));
});

test("circuitBreaker_when_open_and_cooled_down_transitions_to_half_open", async () => {
	openDatabase(":memory:");
	const runner = new UokGateRunner();
	let calls = 0;
	runner.register({
		id: "cb-cool",
		type: "verification",
		execute: async () => {
			calls++;
			return { outcome: "pass", rationale: "ok" };
		},
	});
	updateGateCircuitBreaker("cb-cool", {
		state: "open",
		failureStreak: 5,
		openedAt: new Date(Date.now() - 120_000).toISOString(),
	});
	const result = await runner.run("cb-cool", makeCtx());
	assert.equal(calls, 1);
	assert.equal(result.outcome, "pass");
	const breaker = getGateCircuitBreaker("cb-cool");
	assert.equal(breaker.state, "closed");
});

test("circuitBreaker_half_open_fail_reopens", async () => {
	openDatabase(":memory:");
	const runner = new UokGateRunner();
	runner.register({
		id: "cb-reopen",
		type: "verification",
		execute: async () => ({
			outcome: "fail",
			failureClass: "execution",
			rationale: "nope",
		}),
	});
	updateGateCircuitBreaker("cb-reopen", {
		state: "half-open",
		failureStreak: 5,
		halfOpenAttempts: 1,
	});
	await runner.run("cb-reopen", makeCtx());
	const breaker = getGateCircuitBreaker("cb-reopen");
	assert.equal(breaker.state, "open");
});

test("circuitBreaker_half_open_limit_exceeded_reopens", async () => {
	openDatabase(":memory:");
	const runner = new UokGateRunner();
	let calls = 0;
	runner.register({
		id: "cb-limit",
		type: "verification",
		execute: async () => {
			calls++;
			return { outcome: "pass", rationale: "ok" };
		},
	});
	updateGateCircuitBreaker("cb-limit", {
		state: "half-open",
		failureStreak: 5,
		halfOpenAttempts: 3,
	});
	const result = await runner.run("cb-limit", makeCtx());
	assert.equal(calls, 0);
	assert.equal(result.outcome, "fail");
	assert.ok(result.rationale.includes("reopening"));
});

// ─── Contract validation ───────────────────────────────────────────────────

test("validateGate_when_valid_returns_true", () => {
	const result = validateGate({
		id: "good",
		type: "policy",
		execute: async () => ({ outcome: "pass", rationale: "ok" }),
	});
	assert.equal(result.valid, true);
});

test("validateGate_when_null_returns_false", () => {
	const result = validateGate(null);
	assert.equal(result.valid, false);
	assert.ok(result.reason.includes("object"));
});

test("validateGate_when_missing_id_returns_false", () => {
	const result = validateGate({ type: "policy", execute: async () => ({}) });
	assert.equal(result.valid, false);
	assert.ok(result.reason.includes("id"));
});

test("validateGate_when_empty_id_returns_false", () => {
	const result = validateGate({
		id: "",
		type: "policy",
		execute: async () => ({}),
	});
	assert.equal(result.valid, false);
	assert.ok(result.reason.includes("id"));
});

test("validateGate_when_missing_type_returns_false", () => {
	const result = validateGate({ id: "x", execute: async () => ({}) });
	assert.equal(result.valid, false);
	assert.ok(result.reason.includes("type"));
});

test("validateGate_when_missing_execute_returns_false", () => {
	const result = validateGate({ id: "x", type: "policy" });
	assert.equal(result.valid, false);
	assert.ok(result.reason.includes("execute"));
});

test("validateGate_when_execute_not_function_returns_false", () => {
	const result = validateGate({ id: "x", type: "policy", execute: "not-a-fn" });
	assert.equal(result.valid, false);
	assert.ok(result.reason.includes("execute"));
});

test("runner_register_when_invalid_gate_throws", () => {
	const runner = new UokGateRunner();
	assert.throws(
		() =>
			runner.register({ id: "", type: "policy", execute: async () => ({}) }),
		/id/,
	);
	assert.throws(
		() => runner.register({ id: "x", type: "", execute: async () => ({}) }),
		/type/,
	);
	assert.throws(() => runner.register({ id: "x", type: "policy" }), /execute/);
	assert.throws(() => runner.register(null), /object/);
});

// ─── Adaptive cooldown ─────────────────────────────────────────────────────

test("circuitBreaker_adaptive_cooldown_doubles_on_reopen", async () => {
	openDatabase(":memory:");
	const prevThreshold = process.env.SF_CIRCUIT_BREAKER_FAILURE_THRESHOLD;
	const prevOpenMs = process.env.SF_CIRCUIT_BREAKER_OPEN_DURATION_MS;
	const prevMaxMs = process.env.SF_CIRCUIT_BREAKER_MAX_OPEN_DURATION_MS;
	process.env.SF_CIRCUIT_BREAKER_FAILURE_THRESHOLD = "1";
	process.env.SF_CIRCUIT_BREAKER_OPEN_DURATION_MS = "50";
	process.env.SF_CIRCUIT_BREAKER_MAX_OPEN_DURATION_MS = "200";

	try {
		const runner = new UokGateRunner();
		runner.register({
			id: "adaptive-cb",
			type: "verification",
			execute: async () => ({
				outcome: "fail",
				failureClass: "execution",
				rationale: "no",
			}),
		});

		await runner.run("adaptive-cb", makeCtx());

		const r2 = await runner.run("adaptive-cb", makeCtx());
		assert.ok(
			r2.rationale.includes("50ms"),
			`Expected 50ms in rationale, got: ${r2.rationale}`,
		);

		await new Promise((r) => setTimeout(r, 60));
		await runner.run("adaptive-cb", makeCtx());
		const r3 = await runner.run("adaptive-cb", makeCtx());
		assert.ok(
			r3.rationale.includes("100ms"),
			`Expected 100ms in rationale, got: ${r3.rationale}`,
		);

		await new Promise((r) => setTimeout(r, 110));
		await runner.run("adaptive-cb", makeCtx());
		const r4 = await runner.run("adaptive-cb", makeCtx());
		assert.ok(
			r4.rationale.includes("200ms"),
			`Expected 200ms in rationale, got: ${r4.rationale}`,
		);

		await new Promise((r) => setTimeout(r, 210));
		await runner.run("adaptive-cb", makeCtx());
		const r5 = await runner.run("adaptive-cb", makeCtx());
		assert.ok(
			r5.rationale.includes("200ms"),
			`Expected 200ms capped in rationale, got: ${r5.rationale}`,
		);
	} finally {
		restoreEnv("SF_CIRCUIT_BREAKER_FAILURE_THRESHOLD", prevThreshold);
		restoreEnv("SF_CIRCUIT_BREAKER_OPEN_DURATION_MS", prevOpenMs);
		restoreEnv("SF_CIRCUIT_BREAKER_MAX_OPEN_DURATION_MS", prevMaxMs);
	}
});

test("circuitBreaker_per_gate_thresholds_respected", async () => {
	openDatabase(":memory:");
	const prevA = process.env.SF_CIRCUIT_BREAKER_GATE_A_THRESHOLD;
	const prevB = process.env.SF_CIRCUIT_BREAKER_GATE_B_THRESHOLD;
	process.env.SF_CIRCUIT_BREAKER_GATE_A_THRESHOLD = "2";
	process.env.SF_CIRCUIT_BREAKER_GATE_B_THRESHOLD = "5";

	try {
		const runner = new UokGateRunner();
		runner.register({
			id: "gate-a",
			type: "verification",
			execute: async () => ({
				outcome: "fail",
				failureClass: "execution",
				rationale: "no",
			}),
		});
		runner.register({
			id: "gate-b",
			type: "verification",
			execute: async () => ({
				outcome: "fail",
				failureClass: "execution",
				rationale: "no",
			}),
		});

		// gate-a threshold=2: open after 2 failures
		await runner.run("gate-a", makeCtx());
		await runner.run("gate-a", makeCtx());
		const cbA = getGateCircuitBreaker("gate-a");
		assert.equal(cbA.state, "open");

		// gate-b threshold=5: still closed after 2 failures
		await runner.run("gate-b", makeCtx());
		await runner.run("gate-b", makeCtx());
		const cbB = getGateCircuitBreaker("gate-b");
		assert.equal(cbB.state, "closed");
	} finally {
		restoreEnv("SF_CIRCUIT_BREAKER_GATE_A_THRESHOLD", prevA);
		restoreEnv("SF_CIRCUIT_BREAKER_GATE_B_THRESHOLD", prevB);
	}
});

// ─── Metrics caching ───────────────────────────────────────────────────────

test("metrics_cache_returns_same_text_within_ttl", async () => {
	openDatabase(":memory:");
	invalidateMetricsCache();
	const first = buildMetricsText();
	const second = buildMetricsText();
	assert.equal(first, second);
});
