/**
 * Regression test for #2344: Auto-loop hangs after plan-slice completes
 * because postUnitPostVerification() never resolves.
 *
 * When postUnitPostVerification() hangs (e.g., due to a module import
 * deadlock or SQLite transaction hang), the auto-loop blocks forever
 * with no error message, no notification, and no recovery.
 *
 * The fix adds a timeout guard around postUnitPostVerification() in
 * runFinalize(). If it doesn't resolve within the timeout, the function
 * force-returns "continue" and logs an error, allowing the loop to
 * proceed to the next iteration.
 *
 * This test verifies the timeout utility used by the fix, since the
 * full runFinalize function has too many transitive dependencies for
 * isolated unit testing.
 */

import assert from "node:assert/strict";
import {
	FINALIZE_POST_TIMEOUT_MS,
	FINALIZE_PRE_TIMEOUT_MS,
	withTimeout,
} from "../auto/finalize-timeout.ts";
import { MAX_FINALIZE_TIMEOUTS } from "../auto/types.ts";
import { describe, it } from "vitest";
import { readFileSync } from "node:fs";

function getRunFinalizeBody(phasesSource: string): string {
	const fnIdx = phasesSource.indexOf("export async function runFinalize(");
	assert.ok(fnIdx > 0, "runFinalize function should exist in phases.ts");

	const nextExportIdx = phasesSource.indexOf("\nexport ", fnIdx + 1);
	return phasesSource.slice(
		fnIdx,
		nextExportIdx > fnIdx ? nextExportIdx : undefined,
	);
}

describe("withTimeout utility", () => {
	it("resolves when inner promise resolves promptly", async () => {
		const result = await withTimeout(Promise.resolve("ok"), 1000, "test-timeout");
		assert.strictEqual(result.value, "ok");
		assert.strictEqual(result.timedOut, false);
	});

	it("returns fallback when inner promise hangs", async () => {
		const startTime = Date.now();
		const result = await withTimeout(
			new Promise<string>(() => {
				// Never resolves
			}),
			100,
			"test-timeout",
		);
		const elapsed = Date.now() - startTime;

		assert.strictEqual(result.timedOut, true);
		assert.strictEqual(result.value, undefined);
		assert.ok(elapsed >= 90, `should wait at least 90ms (took ${elapsed}ms)`);
		assert.ok(elapsed < 500, `should not wait too long (took ${elapsed}ms)`);
	});

	it("propagates rejection from the inner promise", async () => {
		await assert.rejects(
			() => withTimeout(Promise.reject(new Error("boom")), 1000, "test-timeout"),
			(err: any) => {
				assert.strictEqual(err.message, "boom");
				return true;
			},
		);
	});

	it("cleans up timer on success", async () => {
		const result = await withTimeout(
			new Promise<string>((r) => setTimeout(() => r("delayed"), 50)),
			5000,
			"cleanup-test",
		);
		assert.strictEqual(result.value, "delayed");
		assert.strictEqual(result.timedOut, false);
	});
});

describe("timeout constants", () => {
	it("FINALIZE_PRE_TIMEOUT_MS is defined and reasonable", () => {
		assert.ok(
			typeof FINALIZE_PRE_TIMEOUT_MS === "number",
			"FINALIZE_PRE_TIMEOUT_MS should be a number",
		);
		assert.ok(
			FINALIZE_PRE_TIMEOUT_MS >= 30_000,
			`pre timeout should be >= 30s (got ${FINALIZE_PRE_TIMEOUT_MS}ms)`,
		);
		assert.ok(
			FINALIZE_PRE_TIMEOUT_MS <= 120_000,
			`pre timeout should be <= 120s (got ${FINALIZE_PRE_TIMEOUT_MS}ms)`,
		);
	});

	it("FINALIZE_POST_TIMEOUT_MS is defined and reasonable", () => {
		assert.ok(
			typeof FINALIZE_POST_TIMEOUT_MS === "number",
			"FINALIZE_POST_TIMEOUT_MS should be a number",
		);
		assert.ok(
			FINALIZE_POST_TIMEOUT_MS >= 30_000,
			`timeout should be >= 30s (got ${FINALIZE_POST_TIMEOUT_MS}ms)`,
		);
		assert.ok(
			FINALIZE_POST_TIMEOUT_MS <= 120_000,
			`timeout should be <= 120s (got ${FINALIZE_POST_TIMEOUT_MS}ms)`,
		);
	});

	it("MAX_FINALIZE_TIMEOUTS is defined and reasonable", () => {
		assert.ok(
			typeof MAX_FINALIZE_TIMEOUTS === "number",
			"MAX_FINALIZE_TIMEOUTS should be a number",
		);
		assert.ok(
			MAX_FINALIZE_TIMEOUTS >= 2,
			`threshold should be >= 2 (got ${MAX_FINALIZE_TIMEOUTS})`,
		);
		assert.ok(
			MAX_FINALIZE_TIMEOUTS <= 10,
			`threshold should be <= 10 (got ${MAX_FINALIZE_TIMEOUTS})`,
		);
	});
});

describe("runFinalize timeout guards in phases.ts", () => {
	const phasesSource = readFileSync(
		new URL("../auto/phases.ts", import.meta.url),
		"utf-8",
	);
	const fnBody = getRunFinalizeBody(phasesSource);

	it("wraps postUnitPreVerification with withTimeout", () => {
		const preTimeoutIdx = fnBody.indexOf("withTimeout(");
		assert.ok(preTimeoutIdx > 0, "withTimeout should appear in runFinalize");

		const preVerIdx = fnBody.indexOf("postUnitPreVerification");
		assert.ok(
			preVerIdx > 0,
			"postUnitPreVerification should appear in runFinalize",
		);

		const firstWithTimeout = fnBody.slice(preTimeoutIdx, preTimeoutIdx + 200);
		assert.ok(
			firstWithTimeout.includes("postUnitPreVerification"),
			"first withTimeout in runFinalize should wrap postUnitPreVerification",
		);
	});

	it("wraps postUnitPostVerification with withTimeout", () => {
		const postVerIdx = fnBody.indexOf("postUnitPostVerification");
		assert.ok(
			postVerIdx > 0,
			"postUnitPostVerification should appear in runFinalize",
		);

		const timeoutCount = (fnBody.match(/withTimeout\(/g) || []).length;
		assert.ok(
			timeoutCount >= 2,
			`runFinalize should have at least 2 withTimeout guards (found ${timeoutCount})`,
		);
	});

	it("increments consecutiveFinalizeTimeouts in both pre and post handlers", () => {
		const incrementCount = (
			fnBody.match(/consecutiveFinalizeTimeouts\+\+/g) || []
		).length;
		assert.ok(
			incrementCount >= 2,
			`should increment consecutiveFinalizeTimeouts in both pre and post handlers (found ${incrementCount})`,
		);
	});

	it("checks MAX_FINALIZE_TIMEOUTS in both timeout handlers", () => {
		const escalationCount = (fnBody.match(/MAX_FINALIZE_TIMEOUTS/g) || [])
			.length;
		assert.ok(
			escalationCount >= 2,
			`should check MAX_FINALIZE_TIMEOUTS in both handlers (found ${escalationCount})`,
		);
	});

	it("detaches s.currentUnit in both timeout handlers", () => {
		const detachCount = (fnBody.match(/s\.currentUnit\s*=\s*null/g) || [])
			.length;
		assert.ok(
			detachCount >= 2,
			`should detach s.currentUnit in both timeout handlers (found ${detachCount})`,
		);
	});

	it("resets consecutiveFinalizeTimeouts on successful finalize", () => {
		assert.ok(
			fnBody.includes("consecutiveFinalizeTimeouts = 0"),
			"should reset consecutiveFinalizeTimeouts on successful finalize",
		);
	});
});
