/**
 * detector-crash-loop-classifier.test.mjs — M048/S03 crash-loop contracts.
 *
 * Purpose: prove R066 quarantine fires only for repeated fast failures on the
 * same source hash with a last-green baseline to compare against.
 */
import assert from "node:assert/strict";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, test } from "vitest";

import {
	CRASH_LOOP_THRESHOLD,
	detectCrashLoop,
} from "../detectors/crash-loop-classifier.js";

const tmpDirs = [];

afterEach(() => {
	while (tmpDirs.length > 0)
		rmSync(tmpDirs.pop(), { recursive: true, force: true });
});

function tmpProject() {
	const dir = mkdtempSync(join(tmpdir(), "sf-crash-loop-"));
	tmpDirs.push(dir);
	return dir;
}

function lastGreen() {
	return {
		checksum: "green-checksum",
		result: {
			ok: true,
			exitCode: 0,
			outputLength: 100,
			elapsedMs: 10_000,
		},
	};
}

function failure(overrides = {}) {
	return {
		ok: false,
		exitCode: 1,
		elapsedMs: 12_000,
		output: "boom",
		sourceHash: "source-a",
		...overrides,
	};
}

test("detectCrashLoop_when_no_last_green_returns_missing_baseline", () => {
	const result = detectCrashLoop(tmpProject(), failure(), {
		lastGreen: null,
		persist: false,
	});

	assert.equal(result.stuck, false);
	assert.equal(result.reason, "missing-last-green");
});

test("detectCrashLoop_when_source_hash_missing_does_not_fire", () => {
	const result = detectCrashLoop(
		tmpProject(),
		failure({ sourceHash: undefined }),
		{ lastGreen: lastGreen(), persist: false },
	);

	assert.equal(result.stuck, false);
	assert.equal(result.reason, "missing-source-hash");
});

test("detectCrashLoop_when_first_failure_seen_does_not_fire", () => {
	const result = detectCrashLoop(tmpProject(), failure(), {
		lastGreen: lastGreen(),
		history: [],
		persist: false,
	});

	assert.equal(result.stuck, false);
	assert.equal(result.signature.eventCount, 1);
});

test("detectCrashLoop_when_same_hash_fails_three_times_under_window_fires", () => {
	const now = Date.parse("2026-05-17T12:00:00.000Z");
	const history = [
		failure({ finishedAt: now - 20_000 }),
		failure({ finishedAt: now - 10_000 }),
	].map((row) => ({
		sourceHash: row.sourceHash,
		exitCode: row.exitCode,
		elapsedMs: row.elapsedMs,
		failed: true,
		finishedAt: row.finishedAt,
	}));

	const result = detectCrashLoop(tmpProject(), failure({ finishedAt: now }), {
		lastGreen: lastGreen(),
		history,
		now,
		persist: false,
	});

	assert.equal(result.stuck, true);
	assert.equal(result.reason, "crash-loop-regression");
	assert.equal(result.signature.eventCount, CRASH_LOOP_THRESHOLD);
	assert.equal(result.signature.sourceHash, "source-a");
	assert.equal(result.signature.delta.ok_delta, false);
});

test("detectCrashLoop_when_hash_differs_does_not_mix_failures", () => {
	const now = Date.now();
	const history = [
		{
			sourceHash: "source-b",
			failed: true,
			exitCode: 1,
			elapsedMs: 1,
			finishedAt: now,
		},
		{
			sourceHash: "source-b",
			failed: true,
			exitCode: 1,
			elapsedMs: 1,
			finishedAt: now,
		},
	];

	const result = detectCrashLoop(tmpProject(), failure({ finishedAt: now }), {
		lastGreen: lastGreen(),
		history,
		persist: false,
	});

	assert.equal(result.stuck, false);
	assert.equal(result.signature.eventCount, 1);
});

test("detectCrashLoop_when_failure_is_slow_does_not_fire", () => {
	const now = Date.now();
	const history = [
		{
			sourceHash: "source-a",
			failed: true,
			exitCode: 1,
			elapsedMs: 120_000,
			finishedAt: now,
		},
		{
			sourceHash: "source-a",
			failed: true,
			exitCode: 1,
			elapsedMs: 120_000,
			finishedAt: now,
		},
	];

	const result = detectCrashLoop(
		tmpProject(),
		failure({ elapsedMs: 120_000, finishedAt: now }),
		{ lastGreen: lastGreen(), history, persist: false },
	);

	assert.equal(result.stuck, false);
	assert.equal(result.signature.eventCount, 0);
});

test("detectCrashLoop_when_current_matches_last_green_does_not_quarantine", () => {
	const now = Date.now();
	const history = [
		{
			sourceHash: "source-a",
			failed: true,
			exitCode: 1,
			elapsedMs: 1,
			finishedAt: now,
		},
		{
			sourceHash: "source-a",
			failed: true,
			exitCode: 1,
			elapsedMs: 1,
			finishedAt: now,
		},
	];
	const baseline = {
		result: { ok: false, exitCode: 1, outputLength: 4, elapsedMs: 12_000 },
		checksum: "known-red",
	};

	const result = detectCrashLoop(tmpProject(), failure({ output: "boom" }), {
		lastGreen: baseline,
		history,
		persist: false,
	});

	assert.equal(result.stuck, false);
	assert.equal(result.signature.eventCount, 3);
	assert.equal(result.signature.delta.ok_delta, true);
});

test("detectCrashLoop_persists_events_for_subsequent_calls", () => {
	const dir = tmpProject();
	const now = Date.now();
	const opts = { lastGreen: lastGreen(), now, threshold: 2 };

	detectCrashLoop(dir, failure({ finishedAt: now - 1_000 }), opts);
	const result = detectCrashLoop(dir, failure({ finishedAt: now }), opts);

	assert.equal(result.stuck, true);
	assert.equal(result.signature.eventCount, 2);
});
