import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, test } from "vitest";
import { autoLoop } from "../auto/loop.js";
import {
	handleAutonomousSolverEval,
	loadAutonomousSolverEvalCases,
	parseAutonomousSolverEvalArgs,
	runAutomaticAutonomousSolverEval,
	runAutonomousSolverEval,
	sampleAutonomousSolverEvalCases,
} from "../autonomous-solver-eval.js";
import {
	closeDatabase,
	getSolverEvalCaseResults,
	getSolverEvalRun,
	listHeadlessRuns,
	listSolverEvalRuns,
	openDatabase,
	recordHeadlessRun,
	recordSolverEvalRun,
} from "../sf-db.js";

let tempDirs = [];

function makeProject() {
	const dir = mkdtempSync(join(tmpdir(), "sf-solver-eval-"));
	tempDirs.push(dir);
	return dir;
}

function makeCtx() {
	const notices = [];
	return {
		notices,
		ctx: {
			ui: {
				notify(message, level) {
					notices.push({ message, level });
				},
			},
		},
	};
}

afterEach(() => {
	closeDatabase();
	for (const dir of tempDirs) {
		rmSync(dir, { recursive: true, force: true });
	}
	tempDirs = [];
});

describe("autonomous solver eval", () => {
	test("parseAutonomousSolverEvalArgs_defaults_to_sample", () => {
		expect(parseAutonomousSolverEvalArgs("run")).toEqual({
			action: "run",
			sample: true,
			casesPath: null,
			runId: null,
			limit: 10,
		});
		expect(
			parseAutonomousSolverEvalArgs("--cases cases.jsonl --run-id abc"),
		).toEqual({
			action: "run",
			sample: false,
			casesPath: "cases.jsonl",
			runId: "abc",
			limit: 10,
		});
		expect(parseAutonomousSolverEvalArgs("history --limit 3")).toMatchObject({
			action: "history",
			limit: 3,
		});
		expect(parseAutonomousSolverEvalArgs("show run-1")).toMatchObject({
			action: "show",
			runId: "run-1",
		});
	});

	test("loadAutonomousSolverEvalCases_reads_jsonl_contract", () => {
		const project = makeProject();
		const casesPath = join(project, "cases.jsonl");
		writeFileSync(
			casesPath,
			`${JSON.stringify({
				id: "case-a",
				files: { "input.txt": "start" },
				rawCommand: [process.execPath, "-e", ""],
				sfCommand: [process.execPath, "-e", ""],
				assertions: [{ kind: "exists", path: "input.txt" }],
			})}\n`,
		);

		const cases = loadAutonomousSolverEvalCases(casesPath);

		expect(cases).toHaveLength(1);
		expect(cases[0].id).toBe("case-a");
		expect(cases[0].assertions[0]).toEqual({
			kind: "exists",
			path: "input.txt",
			value: undefined,
		});
	});

	test("runAutonomousSolverEval_compares_raw_and_sf_solver_signals", () => {
		const project = makeProject();
		const report = runAutonomousSolverEval({
			basePath: project,
			runId: "sample-run",
			cases: sampleAutonomousSolverEvalCases(),
		});

		expect(report.summary.cases).toBe(1);
		expect(report.summary.sfWins).toBe(1);
		expect(report.summary.rawFalseCompletes).toBe(1);
		expect(report.summary.sfFalseCompletes).toBe(0);
		const sfResult = report.results.find((r) => r.mode === "sf");
		expect(sfResult.passed).toBe(true);
		expect(sfResult.solverSignals.hasCheckpoint).toBe(true);
		expect(sfResult.solverSignals.pddComplete).toBe(true);
	});

	test("recordSolverEvalRun_persists_queryable_run_and_case_rows", () => {
		const project = makeProject();
		openDatabase(":memory:");
		const report = runAutonomousSolverEval({
			basePath: project,
			runId: "db-run",
			cases: sampleAutonomousSolverEvalCases(),
		});

		recordSolverEvalRun({ ...report, dbRecorded: true });

		const run = getSolverEvalRun("db-run");
		const runs = listSolverEvalRuns(5);
		const cases = getSolverEvalCaseResults("db-run");
		expect(run.runId).toBe("db-run");
		expect(run.summary.sfWins).toBe(1);
		expect(runs[0].runId).toBe("db-run");
		expect(cases).toHaveLength(2);
		expect(cases.find((r) => r.mode === "raw").falseComplete).toBe(true);
		expect(cases.find((r) => r.mode === "sf").pddComplete).toBe(true);
	});

	test("recordHeadlessRun_persists_timeout_outcome_in_db", () => {
		openDatabase(":memory:");

		recordHeadlessRun({
			runId: "headless-timeout-test",
			command: "/autonomous",
			status: "timeout",
			exitCode: 1,
			timedOut: true,
			interrupted: false,
			restartCount: 0,
			maxRestarts: 3,
			durationMs: 90_000,
			totalEvents: 314,
			toolCalls: 15,
			solverEvalRunId: "auto-db-run",
			solverEvalReportPath:
				".sf/evals/autonomous-solver/auto-db-run/report.json",
			details: { outputFormat: "stream-json" },
		});

		const runs = listHeadlessRuns(1);
		expect(runs).toHaveLength(1);
		expect(runs[0]).toMatchObject({
			runId: "headless-timeout-test",
			command: "/autonomous",
			status: "timeout",
			exitCode: 1,
			timedOut: true,
			restartCount: 0,
			solverEvalRunId: "auto-db-run",
		});
		expect(runs[0].details.outputFormat).toBe("stream-json");
	});

	test("handleAutonomousSolverEval_records_and_reads_db_history", async () => {
		const project = makeProject();
		mkdirSync(join(project, ".sf"), { recursive: true });
		const { ctx, notices } = makeCtx();

		await handleAutonomousSolverEval("--sample --run-id cmd-run", ctx, project);
		await handleAutonomousSolverEval("history --limit 1", ctx, project);
		await handleAutonomousSolverEval("show cmd-run", ctx, project);

		expect(notices[0].message).toContain("DB recorded: yes");
		expect(notices[1].message).toContain("cmd-run");
		expect(notices[2].message).toContain("Autonomous solver eval: cmd-run");
		expect(notices[2].message).toContain("raw");
		expect(notices[2].message).toContain("sf");
	});

	test("runAutomaticAutonomousSolverEval_records_lifecycle_evidence", async () => {
		const project = makeProject();
		mkdirSync(join(project, ".sf"), { recursive: true });
		const events = [];

		const result = await runAutomaticAutonomousSolverEval({
			basePath: project,
			reason: "test-exit",
			emitJournalEvent: (event) => events.push(event),
		});

		expect(result.ok).toBe(true);
		expect(result.report.dbRecorded).toBe(true);
		expect(result.report.suiteSource).toBe("auto-sample");
		expect(events.map((event) => event.eventType)).toEqual([
			"solver-eval-auto-start",
			"solver-eval-auto-complete",
		]);
		const run = getSolverEvalRun(result.report.runId);
		const cases = getSolverEvalCaseResults(result.report.runId);
		expect(run.summary.sfWins).toBe(1);
		expect(cases).toHaveLength(2);
	});

	test("runAutomaticAutonomousSolverEval_can_be_disabled_by_preference", async () => {
		const project = makeProject();
		const events = [];

		const result = await runAutomaticAutonomousSolverEval({
			basePath: project,
			enabled: false,
			emitJournalEvent: (event) => events.push(event),
		});

		expect(result).toMatchObject({ ok: true, skipped: true });
		expect(events[0].eventType).toBe("solver-eval-auto-skipped");
	});

	test("autoLoop_runs_solver_eval_on_autonomous_exit", async () => {
		const project = makeProject();
		mkdirSync(join(project, ".sf"), { recursive: true });
		const events = [];
		const { ctx, notices } = makeCtx();

		await autoLoop(
			ctx,
			{},
			{ active: true, basePath: project, cmdCtx: null },
			{
				loadEffectiveSFPreferences: () => ({
					preferences: {
						auto_supervisor: { solver_eval_on_autonomous_exit: true },
					},
				}),
				emitJournalEvent: (event) => events.push(event),
				uokObserver: null,
			},
		);

		expect(
			events.some((event) => event.eventType === "solver-eval-auto-start"),
		).toBe(true);
		const complete = events.find(
			(event) => event.eventType === "solver-eval-auto-complete",
		);
		expect(complete?.data?.dbRecorded).toBe(true);
		expect(notices.at(-1).message).toContain("Autonomous solver eval recorded");
		const runs = listSolverEvalRuns(1);
		expect(runs[0].suiteSource).toBe("auto-sample");
	});
});
