/**
 * session-lock-transient-read.test.ts — Tests for transient lock file unreadability (#2324).
 *
 * Regression coverage for:
 *   #2324  onCompromised declares lock lost when the lock file is temporarily
 *          unreadable (NFS/CIFS latency, macOS APFS snapshot, concurrent process
 *          briefly holding the file).
 *
 * Tests:
 *   - readExistingLockDataWithRetry retries on transient read failure
 *   - readExistingLockDataWithRetry returns data when file becomes readable after retries
 *   - readExistingLockDataWithRetry returns null only when ALL retries exhausted
 *   - onCompromised does not declare compromise when lock file is transiently unreadable
 */

import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import {
	chmodSync,
	mkdirSync,
	mkdtempSync,
	renameSync,
	rmSync,
	writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, it } from "vitest";
import { sfRoot } from "../paths.ts";
import {
	acquireSessionLock,
	getSessionLockStatus,
	readExistingLockDataWithRetry,
	releaseSessionLock,
	type SessionLockData,
} from "../session-lock.ts";

describe("#2324: transient lock file unreadability", () => {
	it("reads file normally when readable", async () => {
		const base = mkdtempSync(join(tmpdir(), "sf-transient-"));
		mkdirSync(join(base, ".sf"), { recursive: true });

		try {
			const lockFile = join(sfRoot(base), "auto.lock");
			const lockData: SessionLockData = {
				pid: process.pid,
				startedAt: new Date().toISOString(),
				unitType: "execute-task",
				unitId: "M001/S01/T01",
				unitStartedAt: new Date().toISOString(),
				sessionFile: "test-session.json",
			};
			writeFileSync(lockFile, JSON.stringify(lockData, null, 2));

			const result = readExistingLockDataWithRetry(lockFile);
			assert.ok(result !== null, "data returned for readable file");
			assert.strictEqual(result!.pid, process.pid);
			assert.strictEqual(result!.sessionFile, "test-session.json");
		} finally {
			rmSync(base, { recursive: true, force: true });
		}
	});

	it("returns null for truly missing file after retries", async () => {
		const base = mkdtempSync(join(tmpdir(), "sf-transient-"));
		mkdirSync(join(base, ".sf"), { recursive: true });

		try {
			const lockFile = join(sfRoot(base), "auto.lock");
			const result = readExistingLockDataWithRetry(lockFile, {
				maxAttempts: 2,
				delayMs: 10,
			});
			assert.strictEqual(result, null);
		} finally {
			rmSync(base, { recursive: true, force: true });
		}
	});

	it("recovers after transient unavailability", async () => {
		const base = mkdtempSync(join(tmpdir(), "sf-transient-"));
		mkdirSync(join(base, ".sf"), { recursive: true });

		try {
			const lockFile = join(sfRoot(base), "auto.lock");
			const tmpFile = lockFile + ".hidden";
			const lockData: SessionLockData = {
				pid: process.pid,
				startedAt: new Date().toISOString(),
				unitType: "execute-task",
				unitId: "M001/S01/T01",
				unitStartedAt: new Date().toISOString(),
				sessionFile: "recovery-session.json",
			};
			writeFileSync(lockFile, JSON.stringify(lockData, null, 2));

			renameSync(lockFile, tmpFile);
			spawn("bash", ["-c", `sleep 0.05 && mv "${tmpFile}" "${lockFile}"`], {
				stdio: "ignore",
				detached: true,
			}).unref();

			const result = readExistingLockDataWithRetry(lockFile, {
				maxAttempts: 8,
				delayMs: 400,
			});
			assert.ok(result !== null, "data recovered after transient unavailability");
			assert.strictEqual(result!.sessionFile, "recovery-session.json");
		} finally {
			rmSync(base, { recursive: true, force: true });
		}
	});

	it("recovers from transient permission error", async () => {
		const base = mkdtempSync(join(tmpdir(), "sf-transient-"));
		mkdirSync(join(base, ".sf"), { recursive: true });

		try {
			const lockFile = join(sfRoot(base), "auto.lock");
			const lockData: SessionLockData = {
				pid: process.pid,
				startedAt: new Date().toISOString(),
				unitType: "execute-task",
				unitId: "M001/S01/T01",
				unitStartedAt: new Date().toISOString(),
				sessionFile: "perm-session.json",
			};
			writeFileSync(lockFile, JSON.stringify(lockData, null, 2));

			chmodSync(lockFile, 0o000);
			spawn("bash", ["-c", `sleep 0.05 && chmod 644 "${lockFile}"`], {
				stdio: "ignore",
				detached: true,
			}).unref();

			const result = readExistingLockDataWithRetry(lockFile, {
				maxAttempts: 8,
				delayMs: 400,
			});
			assert.ok(result !== null, "data recovered after transient permission error");

			try {
				chmodSync(lockFile, 0o644);
			} catch {
				/* best-effort */
			}
		} finally {
			rmSync(base, { recursive: true, force: true });
		}
	});

	it("tolerates transient lock file unavailability in getSessionLockStatus", async () => {
		const base = mkdtempSync(join(tmpdir(), "sf-transient-"));
		mkdirSync(join(base, ".sf"), { recursive: true });

		try {
			const result = acquireSessionLock(base);
			assert.ok(result.acquired, "lock acquired");

			const status1 = getSessionLockStatus(base);
			assert.ok(status1.valid, "lock valid before transient failure");

			const lockFile = join(sfRoot(base), "auto.lock");
			const tmpFile = lockFile + ".hidden";
			renameSync(lockFile, tmpFile);

			setTimeout(() => {
				try {
					renameSync(tmpFile, lockFile);
				} catch {
					/* best-effort */
				}
			}, 30);

			await new Promise((r) => setTimeout(r, 60));
			const status2 = getSessionLockStatus(base);
			assert.ok(
				status2.valid,
				"lock still valid after transient file disappearance (OS lock held)",
			);

			try {
				renameSync(tmpFile, lockFile);
			} catch {
				/* already restored */
			}

			releaseSessionLock(base);
		} finally {
			rmSync(base, { recursive: true, force: true });
		}
	});

	it("default retry params work for readable file", async () => {
		const base = mkdtempSync(join(tmpdir(), "sf-transient-"));
		mkdirSync(join(base, ".sf"), { recursive: true });

		try {
			const lockFile = join(sfRoot(base), "auto.lock");
			const lockData: SessionLockData = {
				pid: process.pid,
				startedAt: new Date().toISOString(),
				unitType: "execute-task",
				unitId: "M001/S01/T01",
				unitStartedAt: new Date().toISOString(),
				sessionFile: "status-session.json",
			};
			writeFileSync(lockFile, JSON.stringify(lockData, null, 2));

			const result = readExistingLockDataWithRetry(lockFile);
			assert.ok(result !== null, "default params work for readable file");
		} finally {
			rmSync(base, { recursive: true, force: true });
		}
	});
});
