import { type ChildProcess, spawn } from "node:child_process";
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
	acquireInteractiveSessionLock,
	type InteractiveSessionLockResult,
} from "../interactive-session-lock.js";

describe("interactive session lock", () => {
	let dirs: string[] = [];
	let children: ChildProcess[] = [];
	let handles: InteractiveSessionLockResult[] = [];

	afterEach(() => {
		for (const handle of handles) {
			if (handle.acquired) handle.release();
		}
		handles = [];
		for (const child of children) {
			if (!child.killed) child.kill("SIGTERM");
		}
		children = [];
		for (const dir of dirs) {
			rmSync(dir, { recursive: true, force: true });
		}
		dirs = [];
	});

	it("acquires and releases a repo-scoped interactive lock", () => {
		const dir = makeProjectDir();
		const result = acquireInteractiveSessionLock(dir, "/tmp/session.jsonl");
		handles.push(result);

		expect(result.acquired).toBe(true);
		if (!result.acquired) return;
		const data = JSON.parse(readFileSync(result.lockPath, "utf-8"));
		expect(data.pid).toBe(process.pid);
		expect(data.cwd).toBe(resolve(dir));

		result.release();
		expect(() => readFileSync(result.lockPath, "utf-8")).toThrow();
	});

	it("refuses a second live interactive owner for the same repo", () => {
		const dir = makeProjectDir();
		const child = spawn(
			process.execPath,
			["-e", "setInterval(() => {}, 1000)"],
			{
				cwd: dir,
				stdio: "ignore",
			},
		);
		children.push(child);
		writeLock(dir, {
			pid: child.pid ?? 0,
			ppid: process.pid,
			cwd: resolve(dir),
			startedAt: "2026-05-05T00:00:00.000Z",
			sessionFile: "/tmp/live-session.jsonl",
			token: testLockToken("live-owner"),
		});

		const result = acquireInteractiveSessionLock(dir);
		handles.push(result);

		expect(result.acquired).toBe(false);
		if (result.acquired) return;
		expect(result.pid).toBe(child.pid);
		expect(result.message).toContain("Another interactive sf session");
	});

	it("recovers automatically from a stale interactive lock", () => {
		const dir = makeProjectDir();
		writeLock(dir, {
			pid: 9_999_999,
			ppid: 1,
			cwd: resolve(dir),
			startedAt: "2026-05-05T00:00:00.000Z",
			token: testLockToken("dead-owner"),
		});

		const result = acquireInteractiveSessionLock(dir);
		handles.push(result);

		expect(result.acquired).toBe(true);
		if (!result.acquired) return;
		const data = JSON.parse(readFileSync(result.lockPath, "utf-8"));
		expect(data.pid).toBe(process.pid);
		expect(data.token).not.toBe("dead-owner");
	});

	it("treats a live pid from another cwd as stale pid reuse", () => {
		const dir = makeProjectDir();
		writeLock(dir, {
			pid: process.pid,
			ppid: process.ppid,
			cwd: "/tmp/not-this-repo",
			startedAt: "2026-05-05T00:00:00.000Z",
			token: testLockToken("reused-pid"),
		});

		const result = acquireInteractiveSessionLock(dir);
		handles.push(result);

		expect(result.acquired).toBe(true);
	});

	function makeProjectDir(): string {
		const dir = join(
			tmpdir(),
			`sf-interactive-lock-${process.pid}-${Date.now()}-${dirs.length}`,
		);
		mkdirSync(dir, { recursive: true });
		dirs.push(dir);
		return dir;
	}

	function writeLock(
		dir: string,
		data: {
			pid: number;
			ppid: number;
			cwd: string;
			startedAt: string;
			sessionFile?: string;
			token: string;
		},
	): void {
		const sfDir = join(dir, ".sf");
		mkdirSync(join(sfDir, "interactive.lock.d"), { recursive: true });
		writeFileSync(
			join(sfDir, "interactive.lock"),
			JSON.stringify(data, null, 2),
			"utf-8",
		);
	}

	function testLockToken(value: string): string {
		return value;
	}
});
