import { spawn } from "node:child_process";
import { describe, expect, test } from "vitest";
import {
	isSessionPidAlive,
	terminateExistingSession,
} from "../session-lock.js";

function spawnSleeper(seconds = 30) {
	// `sleep` is a deliberate cooperative target: it exits on SIGTERM, which
	// lets us exercise the graceful path. For the SIGKILL escalation test we
	// spawn a child that ignores SIGTERM via `trap '' TERM`.
	const child = spawn("/bin/sh", ["-c", `sleep ${seconds}`], {
		stdio: "ignore",
		detached: false,
	});
	return child;
}

function spawnIgnoreSigterm(seconds = 30) {
	// A Node child that installs an explicit SIGTERM handler that does
	// nothing. Unlike `sh -c "trap '' TERM; sleep N"` (where the shell
	// tail-call-exec's sleep so SIGTERM hits sleep directly), this child
	// IS the long-lived process and reliably ignores SIGTERM until the
	// SIGKILL escalation. This lets us assert the escalation path.
	const child = spawn(
		process.execPath,
		[
			"-e",
			`process.on('SIGTERM', () => {}); setTimeout(() => process.exit(0), ${seconds * 1000});`,
		],
		{ stdio: "ignore", detached: false },
	);
	return child;
}

describe("terminateExistingSession", () => {
	test("returns alreadyDead=true when pid is invalid", async () => {
		const result = await terminateExistingSession(0);
		expect(result.terminated).toBe(false);
		expect(result.alreadyDead).toBe(true);
	});

	test("refuses to terminate the current process", async () => {
		const result = await terminateExistingSession(process.pid);
		expect(result.terminated).toBe(false);
	});

	test("returns alreadyDead=true for a dead pid", async () => {
		// PID 1 is alive but not ours; use a value that's almost certainly
		// not assigned. 2^31 - 1 is well above any plausible PID.
		const result = await terminateExistingSession(2147483646);
		expect(result.alreadyDead).toBe(true);
		expect(result.terminated).toBe(true);
	});

	test("gracefully terminates a process that respects SIGTERM", async () => {
		const child = spawnSleeper(60);
		try {
			expect(isSessionPidAlive(child.pid)).toBe(true);
			const result = await terminateExistingSession(child.pid, {
				gracePeriodMs: 3000,
				reapWaitMs: 1000,
				pollIntervalMs: 50,
			});
			expect(result.terminated).toBe(true);
			expect(result.escalated).toBe(false);
			expect(isSessionPidAlive(child.pid)).toBe(false);
		} finally {
			try {
				child.kill("SIGKILL");
			} catch {
				/* may already be dead */
			}
		}
	});

	test("escalates to SIGKILL when the process ignores SIGTERM", async () => {
		const child = spawnIgnoreSigterm(60);
		// Give the child a moment to register its SIGTERM handler before we
		// send SIGTERM. Without this, the kill may arrive before
		// process.on('SIGTERM', …) executes and Node uses the default handler
		// (exit on signal), which makes the test look like graceful exit.
		await new Promise((resolve) => setTimeout(resolve, 250));
		try {
			expect(isSessionPidAlive(child.pid)).toBe(true);
			const result = await terminateExistingSession(child.pid, {
				gracePeriodMs: 750,
				reapWaitMs: 2000,
				pollIntervalMs: 50,
			});
			expect(result.terminated).toBe(true);
			expect(result.escalated).toBe(true);
			expect(isSessionPidAlive(child.pid)).toBe(false);
		} finally {
			try {
				child.kill("SIGKILL");
			} catch {
				/* may already be dead */
			}
		}
	});
});

describe("isSessionPidAlive", () => {
	test("returns false for current process (self-check is intentionally disabled)", () => {
		// isPidAlive specifically excludes the current PID to prevent
		// false-positive self-detection in the lock takeover flow.
		expect(isSessionPidAlive(process.pid)).toBe(false);
	});

	test("returns false for clearly-dead pid", () => {
		expect(isSessionPidAlive(2147483646)).toBe(false);
	});

	test("returns true for a live child", async () => {
		const child = spawnSleeper(30);
		try {
			expect(isSessionPidAlive(child.pid)).toBe(true);
		} finally {
			try {
				child.kill("SIGKILL");
			} catch {
				/* may already be dead */
			}
		}
	});

	test("returns false for non-integer or non-positive inputs", () => {
		expect(isSessionPidAlive(0)).toBe(false);
		expect(isSessionPidAlive(-1)).toBe(false);
		expect(isSessionPidAlive("nope")).toBe(false);
		expect(isSessionPidAlive(null)).toBe(false);
	});
});
