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, test, vi } from "vitest";
import { closeDatabase, insertLlmTaskOutcome, openDatabase } from "../sf-db.js";
import { ChaosMonkey } from "../uok/chaos-monkey.js";
import { CostGuardGate } from "../uok/cost-guard-gate.js";
import { OutcomeLearningGate } from "../uok/outcome-learning-gate.js";
import { SecurityGate } from "../uok/security-gate.js";

const tmpRoots = [];

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

function makeProject() {
	const root = mkdtempSync(join(tmpdir(), "sf-uok-gates-"));
	tmpRoots.push(root);
	return root;
}

// ─── SecurityGate ──────────────────────────────────────────────────────────

test("securityGate_when_script_missing_returns_pass_with_skip", async () => {
	const gate = new SecurityGate();
	const result = await gate.execute({ basePath: makeProject() }, 1);
	assert.equal(result.outcome, "pass");
	assert.ok(result.rationale.includes("skipped"));
});

test("securityGate_when_script_passes_returns_pass", async () => {
	const root = makeProject();
	const scriptDir = join(root, "scripts");
	mkdirSync(scriptDir, { recursive: true });
	writeFileSync(
		join(scriptDir, "secret-scan.sh"),
		"#!/bin/bash\nexit 0\n",
		"utf-8",
	);
	const gate = new SecurityGate();
	const result = await gate.execute({ basePath: root }, 1);
	assert.equal(result.outcome, "pass");
	assert.ok(result.rationale.includes("No secrets"));
});

test("securityGate_when_script_fails_returns_fail", async () => {
	const root = makeProject();
	const scriptDir = join(root, "scripts");
	mkdirSync(scriptDir, { recursive: true });
	writeFileSync(
		join(scriptDir, "secret-scan.sh"),
		"#!/bin/bash\necho 'found AWS key'\nexit 1\n",
		"utf-8",
	);
	const gate = new SecurityGate();
	const result = await gate.execute({ basePath: root }, 1);
	assert.equal(result.outcome, "fail");
	assert.equal(result.failureClass, "policy");
	assert.ok(result.rationale.includes("secrets"));
});

// ─── CostGuardGate ─────────────────────────────────────────────────────────

test("costGuardGate_when_no_data_returns_pass", async () => {
	openDatabase(":memory:");
	const gate = new CostGuardGate();
	const result = await gate.execute({
		modelId: "gpt-4o",
		unitType: "execute-task",
		unitId: "M001/S01/T01",
	});
	assert.equal(result.outcome, "pass");
});

test("costGuardGate_when_unit_cost_exceeded_returns_fail", async () => {
	openDatabase(":memory:");
	const gate = new CostGuardGate({ maxUsdPerUnit: 1.0 });
	const _now = new Date().toISOString();
	for (let i = 0; i < 3; i++) {
		insertLlmOutcome({
			modelId: "gpt-4o",
			provider: "openai",
			unitType: "execute-task",
			unitId: "M001/S01/T01",
			succeeded: 0,
			cost_usd: 0.5,
			recorded_at: new Date(Date.now() + i * 1000).toISOString(),
		});
	}
	const result = await gate.execute({
		modelId: "gpt-4o",
		unitType: "execute-task",
		unitId: "M001/S01/T01",
	});
	assert.equal(result.outcome, "fail");
	assert.ok(result.rationale.includes("$1.50"));
});

test("costGuardGate_when_hourly_cost_exceeded_returns_fail", async () => {
	openDatabase(":memory:");
	const gate = new CostGuardGate({ maxUsdPerHour: 1.0 });
	const _now = new Date().toISOString();
	for (let i = 0; i < 3; i++) {
		insertLlmOutcome({
			modelId: "gpt-4o",
			provider: "openai",
			unitType: "execute-task",
			unitId: `M001/S01/T0${i}`,
			succeeded: 1,
			cost_usd: 0.5,
			recorded_at: new Date(Date.now() + i * 1000).toISOString(),
		});
	}
	const result = await gate.execute({
		modelId: "gpt-4o",
		unitType: "execute-task",
		unitId: "M001/S01/T99",
	});
	assert.equal(result.outcome, "fail");
	assert.ok(result.rationale.includes("1-hour"));
});

test("costGuardGate_when_highTier_fails_twice_returns_fail", async () => {
	openDatabase(":memory:");
	const gate = new CostGuardGate({ highTierFailureThreshold: 2 });
	const _now = new Date().toISOString();
	insertLlmOutcome({
		modelId: "claude-opus-4-6",
		provider: "anthropic",
		unitType: "execute-task",
		unitId: "M001/S01/T01",
		succeeded: 0,
		cost_usd: 2.0,
		recorded_at: new Date(Date.now()).toISOString(),
	});
	insertLlmOutcome({
		modelId: "claude-opus-4-6",
		provider: "anthropic",
		unitType: "execute-task",
		unitId: "M001/S01/T01",
		succeeded: 0,
		cost_usd: 2.0,
		recorded_at: new Date(Date.now() + 1000).toISOString(),
	});
	const result = await gate.execute({
		modelId: "claude-opus-4-6",
		unitType: "execute-task",
		unitId: "M001/S01/T01",
	});
	assert.equal(result.outcome, "fail");
	assert.ok(result.rationale.includes("high-tier"));
});

test("costGuardGate_when_suggesting_alternative_uses_known_cost_table_model", async () => {
	openDatabase(":memory:");
	const gate = new CostGuardGate({ highTierFailureThreshold: 2 });
	insertLlmOutcome({
		modelId: "claude-opus-4-6",
		provider: "anthropic",
		unitType: "execute-task",
		unitId: "M001/S01/T01",
		succeeded: 0,
		cost_usd: 0.1,
		recorded_at: Date.now(),
	});
	insertLlmOutcome({
		modelId: "claude-opus-4-6",
		provider: "anthropic",
		unitType: "execute-task",
		unitId: "M001/S01/T01",
		succeeded: 0,
		cost_usd: 0.1,
		recorded_at: Date.now() + 1000,
	});

	const result = await gate.execute({
		modelId: "claude-opus-4-6",
		unitType: "execute-task",
		unitId: "M001/S01/T01",
	});

	assert.equal(result.outcome, "fail");
	assert.ok(result.recommendation.includes("claude-haiku-4-5"));
	assert.ok(!result.recommendation.includes("claude-haiku-4-5-haiku"));
});

// ─── OutcomeLearningGate ───────────────────────────────────────────────────

test("outcomeLearningGate_when_no_data_returns_pass", async () => {
	openDatabase(":memory:");
	const gate = new OutcomeLearningGate();
	const result = await gate.execute({
		modelId: "gpt-4o",
		unitType: "execute-task",
		unitId: "M001/S01/T01",
	});
	assert.equal(result.outcome, "pass");
});

test("outcomeLearningGate_when_unit_failure_rate_high_returns_fail", async () => {
	openDatabase(":memory:");
	const gate = new OutcomeLearningGate({
		minSampleSize: 3,
		failureRateThreshold: 0.6,
	});
	const _now = new Date().toISOString();
	for (let i = 0; i < 3; i++) {
		insertLlmOutcome({
			modelId: "gpt-4o",
			provider: "openai",
			unitType: "execute-task",
			unitId: "M001/S01/T01",
			succeeded: 0,
			cost_usd: 0.1,
			recorded_at: new Date(Date.now() + i * 1000).toISOString(),
		});
	}
	const result = await gate.execute({
		modelId: "gpt-4o",
		unitType: "execute-task",
		unitId: "M001/S01/T01",
	});
	assert.equal(result.outcome, "fail");
	assert.ok(result.rationale.includes("pattern"));
	assert.ok(result.findings.includes("100%"));
});

test("outcomeLearningGate_when_model_failure_rate_high_returns_fail", async () => {
	openDatabase(":memory:");
	const gate = new OutcomeLearningGate({
		minSampleSize: 3,
		failureRateThreshold: 0.6,
	});
	const _now = new Date().toISOString();
	for (let i = 0; i < 4; i++) {
		insertLlmOutcome({
			modelId: "gpt-4o",
			provider: "openai",
			unitType: "execute-task",
			unitId: `M001/S01/T0${i}`,
			succeeded: 0,
			cost_usd: 0.1,
			recorded_at: new Date(Date.now() + i * 1000).toISOString(),
		});
	}
	const result = await gate.execute({
		modelId: "gpt-4o",
		unitType: "execute-task",
		unitId: "M001/S01/T99",
	});
	assert.equal(result.outcome, "fail");
	assert.ok(result.findings.includes("gpt-4o"));
	assert.ok(result.findings.includes("100%"));
});

test("outcomeLearningGate_when_escalation_pattern_detected_returns_fail", async () => {
	openDatabase(":memory:");
	const gate = new OutcomeLearningGate({
		minSampleSize: 1,
		lookbackHours: 168,
	});
	const _now = new Date().toISOString();
	for (let i = 0; i < 3; i++) {
		insertLlmOutcome({
			modelId: "gpt-4o",
			provider: "openai",
			unitType: "execute-task",
			unitId: `M001/S01/T0${i}`,
			succeeded: 1,
			escalated: 1,
			cost_usd: 0.1,
			recorded_at: new Date(Date.now() + i * 1000).toISOString(),
		});
	}
	const result = await gate.execute({
		modelId: "gpt-4o",
		unitType: "execute-task",
		unitId: "M001/S01/T99",
	});
	assert.equal(result.outcome, "fail");
	assert.ok(result.findings.includes("escalated"));
});

// ─── ChaosMonkey ───────────────────────────────────────────────────────────

test("chaosMonkey_when_inactive_does_nothing", async () => {
	const monkey = new ChaosMonkey({ active: false });
	await monkey.strike("test");
	assert.deepEqual(monkey.getInjectedEvents(), []);
});

test("chaosMonkey_when_active_may_inject_latency", async () => {
	const monkey = new ChaosMonkey({
		active: true,
		latencyProbability: 1.0,
		maxLatencyMs: 50,
		partialFailureProbability: 0,
		diskStressProbability: 0,
		memoryStressProbability: 0,
	});
	const start = Date.now();
	await monkey.strike("test");
	const elapsed = Date.now() - start;
	assert.ok(elapsed >= 30, `expected latency injection, elapsed ${elapsed}ms`);
	const events = monkey.getInjectedEvents();
	assert.equal(events.length, 1);
	assert.equal(events[0].type, "latency");
	assert.equal(events[0].phase, "test");
});

test("chaosMonkey_when_active_may_throw_retryable_error", async () => {
	const monkey = new ChaosMonkey({
		active: true,
		latencyProbability: 0,
		partialFailureProbability: 1.0,
		diskStressProbability: 0,
		memoryStressProbability: 0,
	});
	try {
		await monkey.strike("test");
		assert.fail("expected partial failure throw");
	} catch (err) {
		assert.equal(err.code, "CHAOS_INJECTED");
		assert.equal(err.retryable, true);
	}
	const events = monkey.getInjectedEvents();
	assert.equal(events.length, 1);
	assert.equal(events[0].type, "partial-failure");
});

test("chaosMonkey_disk_stress_writes_and_cleans_up", async () => {
	const monkey = new ChaosMonkey({
		active: true,
		latencyProbability: 0,
		partialFailureProbability: 0,
		diskStressProbability: 1.0,
		diskStressMb: 5,
		memoryStressProbability: 0,
	});
	await monkey.strike("test");
	const events = monkey.getInjectedEvents();
	assert.equal(events.length, 1);
	assert.equal(events[0].type, "disk-stress");
	assert.ok(events[0].sizeMb >= 1 && events[0].sizeMb <= 5);
});

test("chaosMonkey_clearEvents_resets_history", async () => {
	const monkey = new ChaosMonkey({
		active: true,
		latencyProbability: 1.0,
		maxLatencyMs: 10,
		partialFailureProbability: 0,
		diskStressProbability: 0,
		memoryStressProbability: 0,
	});
	await monkey.strike("test");
	assert.equal(monkey.getInjectedEvents().length, 1);
	monkey.clearEvents();
	assert.equal(monkey.getInjectedEvents().length, 0);
});

// ─── Helpers ───────────────────────────────────────────────────────────────

function insertLlmOutcome(overrides) {
	return insertLlmTaskOutcome({
		modelId: overrides.modelId ?? "gpt-4o",
		provider: overrides.provider ?? "openai",
		unitType: overrides.unitType ?? "execute-task",
		unitId: overrides.unitId ?? "M001/S01/T01",
		succeeded: overrides.succeeded ?? 1,
		retries: overrides.retries ?? 0,
		escalated: overrides.escalated ?? 0,
		verification_passed: overrides.verification_passed ?? null,
		blocker_discovered: overrides.blocker_discovered ?? 0,
		duration_ms: overrides.duration_ms ?? 1000,
		tokens_total: overrides.tokens_total ?? 1000,
		cost_usd: overrides.cost_usd ?? 0.1,
		recorded_at: overrides.recorded_at ?? new Date().toISOString(),
		...overrides,
	});
}
