import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { test } from 'vitest';

import {
	clearRunawayGuardState,
	collectWorktreeFingerprint,
	evaluateRunawayGuard,
	type RunawayGuardConfig,
	resetRunawayGuardState,
} from "../auto-runaway-guard.ts";

function config(
	overrides: Partial<RunawayGuardConfig> = {},
): RunawayGuardConfig {
	return {
		enabled: true,
		toolCallWarning: 60,
		tokenWarning: 1_000_000,
		elapsedMs: 20 * 60 * 1000,
		changedFilesWarning: 75,
		diagnosticTurns: 2,
		hardPause: true,
		minIntervalMs: 0,
		...overrides,
	};
}

test("runaway guard warns on first unit-relative threshold crossing", () => {
	clearRunawayGuardState();
	resetRunawayGuardState("research-slice", "S04", {
		sessionTokens: 500_000,
		changedFiles: 4,
	});

	const decision = evaluateRunawayGuard(
		"research-slice",
		"S04",
		{
			toolCalls: 61,
			sessionTokens: 1_500_001,
			elapsedMs: 5 * 60 * 1000,
			changedFiles: 4,
			topTools: { read: 40, bash: 21 },
		},
		config(),
		1000,
	);

	assert.equal(decision.action, "warn");
	if (decision.action !== "warn") throw new Error("expected warning");
	assert.equal(decision.final, false);
	assert.match(decision.message, /61 tool calls/);
	assert.match(decision.message, /1\.00M session tokens/);
	assert.match(
		decision.message,
		/legitimately large, blocked, or stuck\/churning/,
	);
});

test("runaway guard pauses after diagnostic turns when budget keeps growing", () => {
	clearRunawayGuardState();
	resetRunawayGuardState("research-slice", "S04");
	const c = config();

	assert.equal(
		evaluateRunawayGuard(
			"research-slice",
			"S04",
			{
				toolCalls: 60,
				sessionTokens: 100_000,
				elapsedMs: 5 * 60 * 1000,
			},
			c,
			1000,
		).action,
		"warn",
	);

	const final = evaluateRunawayGuard(
		"research-slice",
		"S04",
		{
			toolCalls: 80,
			sessionTokens: 100_000,
			elapsedMs: 5 * 60 * 1000,
		},
		c,
		2000,
	);
	assert.equal(final.action, "warn");
	if (final.action !== "warn") throw new Error("expected final warning");
	assert.equal(final.final, true);

	const paused = evaluateRunawayGuard(
		"research-slice",
		"S04",
		{
			toolCalls: 100,
			sessionTokens: 100_000,
			elapsedMs: 5 * 60 * 1000,
		},
		c,
		3000,
	);

	assert.equal(paused.action, "pause");
	if (paused.action !== "pause") throw new Error("expected pause");
	assert.match(paused.reason, /budget kept growing/);
	assert.equal(paused.metadata.reason, paused.reason);
	assert.equal(paused.metadata.unitType, "research-slice");
	assert.equal(paused.metadata.unitId, "S04");
	assert.equal(paused.metadata.diagnosticTurns, 2);
	assert.equal(paused.metadata.warningsSent, 2);
	assert.equal(paused.metadata.metrics.toolCalls, 100);
	assert.equal(paused.metadata.lastWarningMetrics.toolCalls, 80);
	assert.deepEqual(paused.metadata.thresholds.toolCallWarning, 60);
	assert.ok(
		paused.metadata.thresholdReasons.some((reason) =>
			reason.includes("tool calls"),
		),
	);
});

test("runaway guard catches changed-file churn relative to unit start", () => {
	clearRunawayGuardState();
	resetRunawayGuardState("execute-task", "T09", { changedFiles: 12 });

	const decision = evaluateRunawayGuard(
		"execute-task",
		"T09",
		{
			toolCalls: 4,
			sessionTokens: 25_000,
			elapsedMs: 2 * 60 * 1000,
			changedFiles: 88,
		},
		config(),
		1000,
	);

	assert.equal(decision.action, "warn");
	if (decision.action !== "warn") throw new Error("expected warning");
	assert.match(decision.message, /76 new changed files/);
	assert.match(
		decision.message,
		/Active edits are not automatically healthy progress/,
	);
});

test("runaway guard warns early when execute-task spends budget with no new file changes", () => {
	clearRunawayGuardState();
	resetRunawayGuardState("execute-task", "M002/S03/T01", {
		changedFiles: 10,
		worktreeFingerprint: "same",
	});
	const c = config();

	const first = evaluateRunawayGuard(
		"execute-task",
		"M002/S03/T01",
		{
			toolCalls: 25,
			sessionTokens: 500_000,
			elapsedMs: 2 * 60 * 1000,
			changedFiles: 10,
			worktreeFingerprint: "same",
			topTools: { read: 15, bash: 10 },
		},
		c,
		1000,
	);

	assert.equal(first.action, "warn");
	if (first.action !== "warn") throw new Error("expected warning");
	assert.equal(first.final, false);
	assert.match(first.message, /no new file changes after 25 tool calls/);
	assert.match(first.message, /0 new changed files since this execute-task/);

	const final = evaluateRunawayGuard(
		"execute-task",
		"M002/S03/T01",
		{
			toolCalls: 40,
			sessionTokens: 750_000,
			elapsedMs: 3 * 60 * 1000,
			changedFiles: 10,
			worktreeFingerprint: "same",
		},
		c,
		2000,
	);
	assert.equal(final.action, "warn");
	if (final.action !== "warn") throw new Error("expected final warning");
	assert.equal(final.final, true);

	const paused = evaluateRunawayGuard(
		"execute-task",
		"M002/S03/T01",
		{
			toolCalls: 55,
			sessionTokens: 1_000_000,
			elapsedMs: 4 * 60 * 1000,
			changedFiles: 10,
			worktreeFingerprint: "same",
		},
		c,
		3000,
	);
	assert.equal(paused.action, "pause");
	if (paused.action !== "pause") throw new Error("expected pause");
	assert.match(paused.reason, /0 new changed files/);
});

test("runaway guard does not treat edits to already-dirty files as no progress", () => {
	clearRunawayGuardState();
	resetRunawayGuardState("execute-task", "M002/S03/T01", {
		changedFiles: 10,
		worktreeFingerprint: "before",
	});

	const decision = evaluateRunawayGuard(
		"execute-task",
		"M002/S03/T01",
		{
			toolCalls: 25,
			sessionTokens: 500_000,
			elapsedMs: 2 * 60 * 1000,
			changedFiles: 10,
			worktreeFingerprint: "after",
		},
		config(),
		1000,
	);

	assert.deepEqual(decision, { action: "none" });
});

test("worktree fingerprint changes when untracked file content changes", () => {
	const dir = mkdtempSync(join(tmpdir(), "sf-runaway-"));
	try {
		execFileSync("git", ["init"], {
			cwd: dir,
			stdio: ["ignore", "ignore", "ignore"],
		});
		writeFileSync(join(dir, "scratch.txt"), "before\n");
		const before = collectWorktreeFingerprint(dir);
		writeFileSync(join(dir, "scratch.txt"), "after\n");
		const after = collectWorktreeFingerprint(dir);

		assert.ok(before);
		assert.ok(after);
		assert.notEqual(after, before);
	} finally {
		rmSync(dir, { recursive: true, force: true });
	}
});

test("worktree fingerprint changes when ignored durable sf artifacts change", () => {
	const dir = mkdtempSync(join(tmpdir(), "sf-runaway-"));
	try {
		execFileSync("git", ["init"], {
			cwd: dir,
			stdio: ["ignore", "ignore", "ignore"],
		});
		writeFileSync(join(dir, ".gitignore"), ".sf/\n");
		execFileSync("git", ["add", ".gitignore"], {
			cwd: dir,
			stdio: ["ignore", "ignore", "ignore"],
		});
		execFileSync("git", ["commit", "-m", "init"], {
			cwd: dir,
			env: {
				...process.env,
				GIT_AUTHOR_NAME: "SF Test",
				GIT_AUTHOR_EMAIL: "sf@example.com",
				GIT_COMMITTER_NAME: "SF Test",
				GIT_COMMITTER_EMAIL: "sf@example.com",
			},
			stdio: ["ignore", "ignore", "ignore"],
		});
		mkdirSync(join(dir, ".sf", "milestones", "M004", "slices", "S03"), {
			recursive: true,
		});
		writeFileSync(
			join(dir, ".sf", "milestones", "M004", "slices", "S03", "evidence.md"),
			"before\n",
		);

		const before = collectWorktreeFingerprint(dir);
		writeFileSync(
			join(dir, ".sf", "milestones", "M004", "slices", "S03", "evidence.md"),
			"after\n",
		);
		const after = collectWorktreeFingerprint(dir);

		assert.ok(before);
		assert.ok(after);
		assert.notEqual(after, before);
	} finally {
		rmSync(dir, { recursive: true, force: true });
	}
});

test("runaway guard is disabled when configured off", () => {
	clearRunawayGuardState();

	const decision = evaluateRunawayGuard(
		"research-slice",
		"S04",
		{
			toolCalls: 10_000,
			sessionTokens: 100_000_000,
			elapsedMs: 4 * 60 * 60 * 1000,
			changedFiles: 500,
		},
		config({ enabled: false }),
		1000,
	);

	assert.deepEqual(decision, { action: "none" });
});
