import assert from "node:assert/strict";
import {
	mkdirSync,
	mkdtempSync,
	readFileSync,
	rmSync,
	writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { afterEach, test } from "vitest";

const projectRoot = process.cwd();

const cliWeb = await import("../../cli-web-branch.ts");
const webMode = await import("../../web-mode.ts");

test("parseCliArgs recognizes server explicitly", () => {
	const flags = cliWeb.parseCliArgs(["node", "dist/loader.js", "server"]);
	assert.deepEqual(flags.messages, ["server"]);
	assert.equal(flags.print, undefined);
	assert.equal(flags.mode, undefined);
});

test("package hooks declare a concrete staged web host", () => {
	const rootPackage = JSON.parse(
		readFileSync(join(projectRoot, "package.json"), "utf-8"),
	);
	assert.equal(
		rootPackage.scripts["stage:web-host"],
		"node scripts/stage-web-standalone.cjs",
	);
	assert.equal(
		rootPackage.scripts["build:web-host"],
		"npm --prefix web run build && npm run stage:web-host",
	);
	assert.equal(rootPackage.scripts["sf"], "node scripts/dev-cli.js");
	assert.equal(rootPackage.scripts["sf:server"], "node scripts/dev-server.js");
	assert.equal(
		rootPackage.scripts["sf:server:dist"],
		"node packages/daemon/dist/cli.js",
	);
	assert.equal(rootPackage.scripts["sf:web"], undefined);
	assert.equal(rootPackage.scripts["sf:web:stop"], undefined);
	assert.equal(rootPackage.scripts["sf:web:stop:all"], undefined);
	assert.ok(rootPackage.files.includes("dist/web"));

	const webPackage = JSON.parse(
		readFileSync(join(projectRoot, "web", "package.json"), "utf-8"),
	);
	assert.equal(
		webPackage.scripts["start:standalone"],
		"node .next/standalone/web/server.js",
	);
});

test("server mode launcher defines or imports a browser opener", () => {
	const source = readFileSync(join(projectRoot, "src", "web-mode.ts"), "utf-8");
	// openBrowser is now defined directly in web-mode.ts (was previously imported from onboarding.js)
	assert.match(source, /openBrowser/);
});

test("cli.ts branches to server mode before interactive startup and preserves cwd-scoped launch inputs", async (_t) => {
	const tmp = mkdtempSync(join(tmpdir(), "sf-web-cli-"));
	const cwd = join(tmp, "project space");
	mkdirSync(cwd, { recursive: true });

	let launchInputs:
		| { cwd: string; projectSessionsDir: string; agentDir: string }
		| undefined;

	afterEach(() => {
		rmSync(tmp, { recursive: true, force: true });
	});

	const cliSource = readFileSync(join(projectRoot, "src", "cli.ts"), "utf-8");
	const branchIndex = cliSource.indexOf(
		"const webBranch = await runWebCliBranch",
	);
	const modelRegistryIndex = cliSource.indexOf("const modelRegistry =");
	assert.ok(
		branchIndex !== -1,
		"cli.ts contains an explicit server branch handoff",
	);
	assert.ok(
		modelRegistryIndex !== -1,
		"cli.ts still contains the model-registry startup path",
	);
	assert.ok(
		branchIndex < modelRegistryIndex,
		"server branch runs before interactive startup state is constructed",
	);

	const result = await cliWeb.runWebCliBranch(
		cliWeb.parseCliArgs(["node", "dist/loader.js", "server"]),
		{
			cwd: () => cwd,
			runWebMode: async (options) => {
				launchInputs = options;
				return {
					mode: "web",
					ok: true,
					cwd: options.cwd,
					projectSessionsDir: options.projectSessionsDir,
					host: "127.0.0.1",
					port: 43123,
					url: "http://127.0.0.1:43123",
					hostKind: "source-dev",
					hostPath: "/tmp/fake-web/package.json",
					hostRoot: "/tmp/fake-web",
				};
			},
		},
	);

	assert.equal(result.handled, true);
	if (!result.handled) throw new Error("expected server branch to be handled");
	assert.equal(result.exitCode, 0);
	assert.deepEqual(launchInputs, {
		cwd,
		projectSessionsDir: cliWeb.getProjectSessionsDir(cwd),
		agentDir: join(process.env.HOME || "", ".sf", "agent"),
		host: undefined,
		port: undefined,
		allowedOrigins: undefined,
	});
});

test("launchWebMode prefers the packaged standalone host and opens the resolved URL", async (_t) => {
	const tmp = mkdtempSync(join(tmpdir(), "sf-web-host-"));
	const standaloneRoot = join(tmp, "dist", "web", "standalone");
	const serverPath = join(standaloneRoot, "server.js");
	mkdirSync(standaloneRoot, { recursive: true });
	writeFileSync(serverPath, 'console.log("stub")\n');

	let initResourcesCalled = false;
	let unrefCalled = false;
	let openedUrl = "";
	let stderrOutput = "";
	let spawnInvocation:
		| { command: string; args: readonly string[]; options: Record<string, any> }
		| undefined;
	let writtenPid: { path: string; pid: number } | undefined;

	const pidFilePath = join(tmp, "web-server.pid");

	afterEach(() => {
		rmSync(tmp, { recursive: true, force: true });
	});

	const status = await webMode.launchWebMode(
		{
			cwd: "/tmp/current-project",
			projectSessionsDir: "/tmp/.sf/sessions/--tmp-current-project--",
			agentDir: "/tmp/.sf/agent",
			packageRoot: tmp,
		},
		{
			initResources: () => {
				initResourcesCalled = true;
			},
			resolvePort: async () => 45123,
			execPath: "/custom/node",
			env: { TEST_ENV: "1" },
			spawn: (command, args, options) => {
				spawnInvocation = {
					command,
					args,
					options: options as Record<string, any>,
				};
				return {
					pid: 99999,
					once: () => undefined,
					unref: () => {
						unrefCalled = true;
					},
				} as any;
			},
			waitForBootReady: async () => undefined,
			openBrowser: (url) => {
				openedUrl = url;
			},
			pidFilePath,
			writePidFile: (path, pid) => {
				writtenPid = { path, pid };
				webMode.writePidFile(path, pid);
			},
			stderr: {
				write(chunk: string) {
					stderrOutput += chunk;
					return true;
				},
			},
		},
	);

	assert.equal(status.ok, true);
	if (!status.ok) throw new Error("expected successful web launch status");
	assert.equal(status.hostKind, "packaged-standalone");
	assert.equal(status.hostPath, serverPath);
	assert.equal(status.url, "http://127.0.0.1:45123");
	assert.equal(initResourcesCalled, true);
	assert.equal(unrefCalled, true);
	// The browser URL now includes a random auth token as a fragment
	assert.match(openedUrl, /^http:\/\/127\.0\.0\.1:45123\/#token=[a-f0-9]{64}$/);
	// Extract the auth token the launcher generated so we can verify it was
	// passed consistently to both the env and the browser URL.
	const authToken = openedUrl.replace("http://127.0.0.1:45123/#token=", "");
	assert.deepEqual(spawnInvocation, {
		command: "/custom/node",
		args: [serverPath],
		options: {
			cwd: standaloneRoot,
			detached: true,
			stdio: "ignore",
			windowsHide: true,
			shell: false,
			env: {
				TEST_ENV: "1",
				HOSTNAME: "127.0.0.1",
				PORT: "45123",
				SF_WEB_HOST: "127.0.0.1",
				SF_WEB_PORT: "45123",
				SF_WEB_AUTH_TOKEN: authToken,
				SF_WEB_PROJECT_CWD: "/tmp/current-project",
				SF_WEB_PROJECT_SESSIONS_DIR:
					"/tmp/.sf/sessions/--tmp-current-project--",
				SF_WEB_PACKAGE_ROOT: tmp,
				SF_WEB_HOST_KIND: "packaged-standalone",
			},
		},
	});
	assert.match(stderrOutput, /status=started/);
	assert.match(stderrOutput, /port=45123/);
	// PID file must be written with the spawned process's PID
	assert.deepEqual(writtenPid, { path: pidFilePath, pid: 99999 });
	assert.equal(webMode.readPidFile(pidFilePath), 99999);
});

test("stopWebMode kills process by PID and removes PID file", (_t) => {
	const tmp = mkdtempSync(join(tmpdir(), "sf-web-stop-"));
	const pidFilePath = join(tmp, "web-server.pid");
	let stderrOutput = "";
	let _killedPid: number | undefined;

	afterEach(() => {
		rmSync(tmp, { recursive: true, force: true });
	});

	webMode.writePidFile(pidFilePath, 12345);

	const result = webMode.stopWebMode({
		pidFilePath,
		readPidFile: webMode.readPidFile,
		deletePidFile: webMode.deletePidFile,
		stderr: {
			write: (chunk: string) => {
				stderrOutput += chunk;
				return true;
			},
		},
		// Override process.kill to avoid killing a real process in tests
	});

	// Since PID 12345 is almost certainly dead, stopWebMode should succeed by treating ESRCH as "already gone"
	assert.equal(result.ok, true);
	assert.match(stderrOutput, /pid=12345/);
});

test("stopWebMode reports error when no PID file exists", (_t) => {
	const tmp = mkdtempSync(join(tmpdir(), "sf-web-stop-nopid-"));
	const pidFilePath = join(tmp, "web-server.pid");
	let stderrOutput = "";

	afterEach(() => {
		rmSync(tmp, { recursive: true, force: true });
	});

	const result = webMode.stopWebMode({
		pidFilePath,
		readPidFile: webMode.readPidFile,
		deletePidFile: webMode.deletePidFile,
		stderr: {
			write: (chunk: string) => {
				stderrOutput += chunk;
				return true;
			},
		},
	});

	assert.equal(result.ok, false);
	assert.equal(result.reason, "no-pid-file");
	assert.match(stderrOutput, /not running/);
});

test('runWebCliBranch handles "server stop" subcommand without server flag', async (_t) => {
	const tmp = mkdtempSync(join(tmpdir(), "sf-web-branch-stop-"));
	const pidFilePath = join(tmp, "web-server.pid");
	let _stderrOutput = "";

	afterEach(() => {
		rmSync(tmp, { recursive: true, force: true });
	});

	const flags = cliWeb.parseCliArgs([
		"node",
		"dist/loader.js",
		"server",
		"stop",
	]);
	assert.equal(flags.messages.includes("server"), true);
	assert.deepEqual(flags.messages, ["server", "stop"]);

	const result = await cliWeb.runWebCliBranch(flags, {
		stopWebMode: (deps) => {
			return webMode.stopWebMode({ ...deps, pidFilePath });
		},
		stderr: {
			write: (chunk: string) => {
				_stderrOutput += chunk;
				return true;
			},
		},
	});

	assert.equal(result.handled, true);
	if (!result.handled) throw new Error("expected server stop to be handled");
	assert.equal(result.exitCode, 1); // no PID file — expected failure
	if (result.action !== "stop") throw new Error("expected action=stop");
	assert.equal(result.stopResult.ok, false);
});

// ─── Path argument tests ──────────────────────────────────────────────

test("parseCliArgs captures server <path>", () => {
	const flags = cliWeb.parseCliArgs([
		"node",
		"dist/loader.js",
		"server",
		"/tmp/my-project",
	]);
	assert.equal(flags.webPath, undefined);
	assert.deepEqual(flags.messages, ["server", "/tmp/my-project"]);
});

test("parseCliArgs captures server with relative path", () => {
	const flags = cliWeb.parseCliArgs([
		"node",
		"dist/loader.js",
		"server",
		"../other-project",
	]);
	assert.equal(flags.webPath, undefined);
	assert.deepEqual(flags.messages, ["server", "../other-project"]);
});

test("parseCliArgs does not capture server followed by a flag as path", () => {
	const flags = cliWeb.parseCliArgs([
		"node",
		"dist/loader.js",
		"server",
		"--model",
		"test",
	]);
	assert.equal(flags.webPath, undefined);
	assert.equal(flags.model, "test");
	assert.deepEqual(flags.messages, ["server"]);
});

test("sf server <path> is handled as server start with path", async (_t) => {
	const tmp = mkdtempSync(join(tmpdir(), "sf-web-path-"));
	const projectDir = join(tmp, "my-project");
	mkdirSync(projectDir, { recursive: true });
	let launchedCwd = "";

	afterEach(() => {
		rmSync(tmp, { recursive: true, force: true });
	});

	const flags = cliWeb.parseCliArgs([
		"node",
		"dist/loader.js",
		"server",
		projectDir,
	]);
	assert.deepEqual(flags.messages, ["server", projectDir]);

	const result = await cliWeb.runWebCliBranch(flags, {
		runWebMode: async (options) => {
			launchedCwd = options.cwd;
			return {
				mode: "web",
				ok: true,
				cwd: options.cwd,
				projectSessionsDir: options.projectSessionsDir,
				host: "127.0.0.1",
				port: 43124,
				url: "http://127.0.0.1:43124",
				hostKind: "source-dev",
				hostPath: "/tmp/fake-web/package.json",
				hostRoot: "/tmp/fake-web",
			};
		},
	});

	assert.equal(result.handled, true);
	if (!result.handled) throw new Error("expected server branch to be handled");
	assert.equal(result.exitCode, 0);
	assert.equal(launchedCwd, projectDir);
});

test("sf server start <path> resolves path and launches", async (_t) => {
	const tmp = mkdtempSync(join(tmpdir(), "sf-web-start-path-"));
	const projectDir = join(tmp, "another-project");
	mkdirSync(projectDir, { recursive: true });
	let launchedCwd = "";

	afterEach(() => {
		rmSync(tmp, { recursive: true, force: true });
	});

	const flags = cliWeb.parseCliArgs([
		"node",
		"dist/loader.js",
		"server",
		"start",
		projectDir,
	]);
	assert.deepEqual(flags.messages, ["server", "start", projectDir]);

	const result = await cliWeb.runWebCliBranch(flags, {
		runWebMode: async (options) => {
			launchedCwd = options.cwd;
			return {
				mode: "web",
				ok: true,
				cwd: options.cwd,
				projectSessionsDir: options.projectSessionsDir,
				host: "127.0.0.1",
				port: 43125,
				url: "http://127.0.0.1:43125",
				hostKind: "source-dev",
				hostPath: "/tmp/fake-web/package.json",
				hostRoot: "/tmp/fake-web",
			};
		},
	});

	assert.equal(result.handled, true);
	if (!result.handled) throw new Error("expected server branch to be handled");
	assert.equal(result.exitCode, 0);
	assert.equal(launchedCwd, projectDir);
});

test("sf server <path> resolves path and launches", async (_t) => {
	const tmp = mkdtempSync(join(tmpdir(), "sf-web-flag-path-"));
	const projectDir = join(tmp, "flagged-project");
	mkdirSync(projectDir, { recursive: true });
	let launchedCwd = "";

	afterEach(() => {
		rmSync(tmp, { recursive: true, force: true });
	});

	const flags = cliWeb.parseCliArgs([
		"node",
		"dist/loader.js",
		"server",
		projectDir,
	]);
	assert.equal(flags.webPath, undefined);
	assert.deepEqual(flags.messages, ["server", projectDir]);

	const result = await cliWeb.runWebCliBranch(flags, {
		runWebMode: async (options) => {
			launchedCwd = options.cwd;
			return {
				mode: "web",
				ok: true,
				cwd: options.cwd,
				projectSessionsDir: options.projectSessionsDir,
				host: "127.0.0.1",
				port: 43126,
				url: "http://127.0.0.1:43126",
				hostKind: "source-dev",
				hostPath: "/tmp/fake-web/package.json",
				hostRoot: "/tmp/fake-web",
			};
		},
	});

	assert.equal(result.handled, true);
	if (!result.handled) throw new Error("expected server branch to be handled");
	assert.equal(result.exitCode, 0);
	assert.equal(launchedCwd, projectDir);
});

test("sf server <nonexistent-path> fails with clear error", async () => {
	let stderrOutput = "";

	const flags = cliWeb.parseCliArgs([
		"node",
		"dist/loader.js",
		"server",
		"/tmp/nonexistent-sf-test-path-xyz",
	]);
	const result = await cliWeb.runWebCliBranch(flags, {
		stderr: {
			write: (chunk: string) => {
				stderrOutput += chunk;
				return true;
			},
		},
	});

	assert.equal(result.handled, true);
	if (!result.handled) throw new Error("expected server branch to be handled");
	assert.equal(result.exitCode, 1);
	if (result.action !== "start") throw new Error("expected action=start");
	assert.equal(result.status.ok, false);
	if (result.status.ok) throw new Error("expected failed status");
	assert.match(result.status.failureReason, /does not exist/);
	assert.match(stderrOutput, /does not exist/);
});

test("launch failure surfaces status and reason before browser open", async (_t) => {
	const tmp = mkdtempSync(join(tmpdir(), "sf-web-missing-host-"));
	let openedUrl = "";
	let stderrOutput = "";

	afterEach(() => {
		rmSync(tmp, { recursive: true, force: true });
	});

	const status = await webMode.launchWebMode(
		{
			cwd: "/tmp/current-project",
			projectSessionsDir: "/tmp/.sf/sessions/--tmp-current-project--",
			agentDir: "/tmp/.sf/agent",
			packageRoot: tmp,
		},
		{
			openBrowser: (url) => {
				openedUrl = url;
			},
			stderr: {
				write(chunk: string) {
					stderrOutput += chunk;
					return true;
				},
			},
		},
	);

	assert.equal(status.ok, false);
	if (status.ok) throw new Error("expected failed web launch status");
	assert.equal(status.hostPath, null);
	assert.equal(status.url, null);
	assert.equal(openedUrl, "");
	assert.match(status.failureReason, /host bootstrap not found/);
	assert.match(stderrOutput, /status=failed/);
	assert.match(stderrOutput, /reason=host bootstrap not found/);
});

// ─── Instance registry tests ─────────────────────────────────────────

test("registerInstance and readInstanceRegistry round-trip", (_t) => {
	const tmp = mkdtempSync(join(tmpdir(), "sf-web-registry-"));
	const registryPath = join(tmp, "web-instances.json");

	afterEach(() => {
		rmSync(tmp, { recursive: true, force: true });
	});

	webMode.registerInstance(
		"/tmp/project-a",
		{ pid: 1001, port: 3000, url: "http://127.0.0.1:3000" },
		registryPath,
	);
	webMode.registerInstance(
		"/tmp/project-b",
		{ pid: 1002, port: 3001, url: "http://127.0.0.1:3001" },
		registryPath,
	);

	const registry = webMode.readInstanceRegistry(registryPath);
	assert.equal(Object.keys(registry).length, 2);
	assert.equal(registry[resolve("/tmp/project-a")]?.pid, 1001);
	assert.equal(registry[resolve("/tmp/project-b")]?.port, 3001);
	assert.ok(registry[resolve("/tmp/project-a")]?.startedAt);
});

test("unregisterInstance removes a single entry", (_t) => {
	const tmp = mkdtempSync(join(tmpdir(), "sf-web-unreg-"));
	const registryPath = join(tmp, "web-instances.json");

	afterEach(() => {
		rmSync(tmp, { recursive: true, force: true });
	});

	webMode.registerInstance(
		"/tmp/project-a",
		{ pid: 1001, port: 3000, url: "http://127.0.0.1:3000" },
		registryPath,
	);
	webMode.registerInstance(
		"/tmp/project-b",
		{ pid: 1002, port: 3001, url: "http://127.0.0.1:3001" },
		registryPath,
	);
	webMode.unregisterInstance("/tmp/project-a", registryPath);

	const registry = webMode.readInstanceRegistry(registryPath);
	assert.equal(Object.keys(registry).length, 1);
	assert.equal(registry[resolve("/tmp/project-a")], undefined);
	assert.equal(registry[resolve("/tmp/project-b")]?.pid, 1002);
});

test("stopWebMode with projectCwd reports not-found when not in registry", () => {
	let stderrOutput = "";

	const result = webMode.stopWebMode(
		{
			stderr: {
				write: (chunk: string) => {
					stderrOutput += chunk;
					return true;
				},
			},
		},
		{ projectCwd: "/tmp/nonexistent-project-for-stop-test" },
	);

	assert.equal(result.ok, false);
	assert.equal(result.reason, "not-found");
	assert.match(stderrOutput, /No SF server running/);
});

test("sf server stop all is parsed and dispatched", async () => {
	let stopOptions: { projectCwd?: string; all?: boolean } | undefined;

	const flags = cliWeb.parseCliArgs([
		"node",
		"dist/loader.js",
		"server",
		"stop",
		"all",
	]);
	assert.deepEqual(flags.messages, ["server", "stop", "all"]);

	const result = await cliWeb.runWebCliBranch(flags, {
		stopWebMode: (_deps, opts) => {
			stopOptions = opts;
			return { ok: true, stoppedCount: 2 };
		},
		stderr: { write: () => true },
	});

	assert.equal(result.handled, true);
	if (!result.handled) throw new Error("expected handled");
	assert.equal(result.exitCode, 0);
	assert.equal(stopOptions?.all, true);
	assert.equal(stopOptions?.projectCwd, undefined);
});

test("sf server stop <path> is parsed and dispatched with resolved path", async (_t) => {
	const tmp = mkdtempSync(join(tmpdir(), "sf-web-stop-path-"));
	let stopOptions: { projectCwd?: string; all?: boolean } | undefined;

	afterEach(() => {
		rmSync(tmp, { recursive: true, force: true });
	});

	const flags = cliWeb.parseCliArgs([
		"node",
		"dist/loader.js",
		"server",
		"stop",
		tmp,
	]);
	const result = await cliWeb.runWebCliBranch(flags, {
		cwd: () => "/",
		stopWebMode: (_deps, opts) => {
			stopOptions = opts;
			return { ok: true, stoppedCount: 1 };
		},
		stderr: { write: () => true },
	});

	assert.equal(result.handled, true);
	if (!result.handled) throw new Error("expected handled");
	assert.equal(result.exitCode, 0);
	assert.equal(stopOptions?.projectCwd, tmp);
	assert.equal(stopOptions?.all, false);
});

// ─── Context-aware launch detection tests ──────────────────────────────

test("resolveContextAwareCwd returns project cwd when inside a project under dev root", (_t) => {
	const tmp = mkdtempSync(join(tmpdir(), "sf-ctx-aware-"));
	const devRoot = join(tmp, "devroot");
	const projectA = join(devRoot, "projectA");
	const prefsPath = join(tmp, "web-preferences.json");

	afterEach(() => {
		rmSync(tmp, { recursive: true, force: true });
	});

	mkdirSync(projectA, { recursive: true });
	writeFileSync(prefsPath, JSON.stringify({ devRoot }));

	const result = cliWeb.resolveContextAwareCwd(projectA, prefsPath);
	assert.equal(result, projectA);
});

test("resolveContextAwareCwd returns cwd unchanged when AT dev root", (_t) => {
	const tmp = mkdtempSync(join(tmpdir(), "sf-ctx-aware-"));
	const devRoot = join(tmp, "devroot");
	const prefsPath = join(tmp, "web-preferences.json");

	afterEach(() => {
		rmSync(tmp, { recursive: true, force: true });
	});

	mkdirSync(devRoot, { recursive: true });
	writeFileSync(prefsPath, JSON.stringify({ devRoot }));

	const result = cliWeb.resolveContextAwareCwd(devRoot, prefsPath);
	assert.equal(result, devRoot);
});

test("resolveContextAwareCwd returns cwd unchanged when no dev root configured", (_t) => {
	const tmp = mkdtempSync(join(tmpdir(), "sf-ctx-aware-"));
	const prefsPath = join(tmp, "web-preferences.json");
	const cwd = join(tmp, "somedir");

	afterEach(() => {
		rmSync(tmp, { recursive: true, force: true });
	});

	mkdirSync(cwd, { recursive: true });
	writeFileSync(prefsPath, JSON.stringify({ theme: "dark" }));

	const result = cliWeb.resolveContextAwareCwd(cwd, prefsPath);
	assert.equal(result, cwd);
});

test("resolveContextAwareCwd returns cwd unchanged when prefs file missing", (_t) => {
	const tmp = mkdtempSync(join(tmpdir(), "sf-ctx-aware-"));
	const prefsPath = join(tmp, "nonexistent-prefs.json");
	const cwd = join(tmp, "somedir");

	afterEach(() => {
		rmSync(tmp, { recursive: true, force: true });
	});

	mkdirSync(cwd, { recursive: true });

	const result = cliWeb.resolveContextAwareCwd(cwd, prefsPath);
	assert.equal(result, cwd);
});

test("resolveContextAwareCwd returns cwd unchanged when dev root path is stale", (_t) => {
	const tmp = mkdtempSync(join(tmpdir(), "sf-ctx-aware-"));
	const prefsPath = join(tmp, "web-preferences.json");
	const cwd = join(tmp, "somedir");
	const staleDevRoot = join(tmp, "nonexistent-devroot");

	afterEach(() => {
		rmSync(tmp, { recursive: true, force: true });
	});

	mkdirSync(cwd, { recursive: true });
	writeFileSync(prefsPath, JSON.stringify({ devRoot: staleDevRoot }));

	const result = cliWeb.resolveContextAwareCwd(cwd, prefsPath);
	assert.equal(result, cwd);
});

test("resolveContextAwareCwd resolves nested cwd to one-level-deep project", (_t) => {
	const tmp = mkdtempSync(join(tmpdir(), "sf-ctx-aware-"));
	const devRoot = join(tmp, "devroot");
	const projectA = join(devRoot, "projectA");
	const nested = join(projectA, "src", "components", "deep");
	const prefsPath = join(tmp, "web-preferences.json");

	afterEach(() => {
		rmSync(tmp, { recursive: true, force: true });
	});

	mkdirSync(nested, { recursive: true });
	writeFileSync(prefsPath, JSON.stringify({ devRoot }));

	const result = cliWeb.resolveContextAwareCwd(nested, prefsPath);
	assert.equal(result, projectA);
});

test("resolveContextAwareCwd returns cwd unchanged when outside dev root", (_t) => {
	const tmp = mkdtempSync(join(tmpdir(), "sf-ctx-aware-"));
	const devRoot = join(tmp, "devroot");
	const outsideDir = join(tmp, "elsewhere");
	const prefsPath = join(tmp, "web-preferences.json");

	afterEach(() => {
		rmSync(tmp, { recursive: true, force: true });
	});

	mkdirSync(devRoot, { recursive: true });
	mkdirSync(outsideDir, { recursive: true });
	writeFileSync(prefsPath, JSON.stringify({ devRoot }));

	const result = cliWeb.resolveContextAwareCwd(outsideDir, prefsPath);
	assert.equal(result, outsideDir);
});

// ─── Stale instance cleanup tests ─────────────────────────────────────

test("launchWebMode kills stale instance for same cwd before spawning", async (_t) => {
	const tmp = mkdtempSync(join(tmpdir(), "sf-web-stale-"));
	const standaloneRoot = join(tmp, "dist", "web", "standalone");
	const serverPath = join(standaloneRoot, "server.js");
	mkdirSync(standaloneRoot, { recursive: true });
	writeFileSync(serverPath, 'console.log("stub")\n');

	const registryPath = join(tmp, "web-instances.json");
	const pidFilePath = join(tmp, "web-server.pid");
	const cwd = "/tmp/stale-project";

	// Pre-register a stale instance for the same cwd
	webMode.registerInstance(
		cwd,
		{ pid: 77777, port: 3000, url: "http://127.0.0.1:3000" },
		registryPath,
	);

	let stderrOutput = "";
	let spawnCalled = false;

	afterEach(() => {
		rmSync(tmp, { recursive: true, force: true });
	});

	const status = await webMode.launchWebMode(
		{
			cwd,
			projectSessionsDir: "/tmp/.sf/sessions/stale",
			agentDir: "/tmp/.sf/agent",
			packageRoot: tmp,
		},
		{
			initResources: () => {},
			resolvePort: async () => 45200,
			execPath: "/custom/node",
			env: { TEST_ENV: "1" },
			spawn: (_command, _args, _options) => {
				spawnCalled = true;
				return {
					pid: 88888,
					once: () => undefined,
					unref: () => {},
				} as any;
			},
			waitForBootReady: async () => undefined,
			openBrowser: () => {},
			pidFilePath,
			writePidFile: webMode.writePidFile,
			registryPath,
			stderr: {
				write(chunk: string) {
					stderrOutput += chunk;
					return true;
				},
			},
		},
	);

	assert.equal(status.ok, true);
	assert.equal(spawnCalled, true);
	// Stale instance for same cwd should have been cleaned up
	assert.match(stderrOutput, /Cleaning up stale/);
	// New instance should be registered
	const registry = webMode.readInstanceRegistry(registryPath);
	assert.equal(registry[resolve(cwd)]?.pid, 88888);
});

test("launchWebMode does not log cleanup when no stale instance exists", async (_t) => {
	const tmp = mkdtempSync(join(tmpdir(), "sf-web-no-stale-"));
	const standaloneRoot = join(tmp, "dist", "web", "standalone");
	const serverPath = join(standaloneRoot, "server.js");
	mkdirSync(standaloneRoot, { recursive: true });
	writeFileSync(serverPath, 'console.log("stub")\n');

	const registryPath = join(tmp, "web-instances.json");
	const pidFilePath = join(tmp, "web-server.pid");

	let stderrOutput = "";

	afterEach(() => {
		rmSync(tmp, { recursive: true, force: true });
	});

	const status = await webMode.launchWebMode(
		{
			cwd: "/tmp/clean-project",
			projectSessionsDir: "/tmp/.sf/sessions/clean",
			agentDir: "/tmp/.sf/agent",
			packageRoot: tmp,
		},
		{
			initResources: () => {},
			resolvePort: async () => 45201,
			execPath: "/custom/node",
			env: { TEST_ENV: "1" },
			spawn: () =>
				({
					pid: 88889,
					once: () => undefined,
					unref: () => {},
				}) as any,
			waitForBootReady: async () => undefined,
			openBrowser: () => {},
			pidFilePath,
			writePidFile: webMode.writePidFile,
			registryPath,
			stderr: {
				write(chunk: string) {
					stderrOutput += chunk;
					return true;
				},
			},
		},
	);

	assert.equal(status.ok, true);
	// No cleanup message when no stale instance exists
	assert.equal(stderrOutput.includes("Cleaning up stale"), false);
});

// ─── Orphan process reaper tests (sf-mooe4m5k-6fm7z9) ──────────────────

test("reapOrphanedNextServerProcesses returns zero reaped on non-Linux platforms", () => {
	// This test verifies the function structure exists and returns safely
	// on platforms where /proc is unavailable. On Linux CI it may actually
	// find processes, so we only assert the shape is correct.
	const stderrChunks: string[] = [];
	const result = webMode.reapOrphanedNextServerProcesses({
		write: (chunk: string) => {
			stderrChunks.push(chunk);
			return true;
		},
	});

	assert.equal(typeof result.reaped, "number");
	assert.equal(Array.isArray(result.errors), true);
	// Should not throw; reaped count is platform-dependent
});

test("reapOrphanedNextServerProcesses is exported and callable", () => {
	assert.equal(typeof webMode.reapOrphanedNextServerProcesses, "function");
});

test("reapOrphanedNextServerProcesses kills orphaned standalone next-server", () => {
	const killed: Array<{ pid: number; signal: string }> = [];
	const stderrChunks: string[] = [];
	const packageRoot = "/tmp/sf-package";
	const result = webMode.reapOrphanedNextServerProcesses(
		{
			write: (chunk: string) => {
				stderrChunks.push(chunk);
				return true;
			},
		},
		packageRoot,
		{
			platform: "linux",
			execSync: (() =>
				[
					"123 1 node /tmp/sf-package/dist/web/standalone/node_modules/next/dist/server/next-server.js node",
					"124 999 node /tmp/sf-package/dist/web/standalone/node_modules/next/dist/server/next-server.js node",
					"125 1 node /elsewhere/next-server.js node",
				].join("\n")) as any,
			readlinkSync: ((path: string) => {
				if (path === "/proc/123/cwd")
					return "/tmp/sf-package/dist/web/standalone";
				if (path === "/proc/124/cwd")
					return "/tmp/sf-package/dist/web/standalone";
				return "/elsewhere";
			}) as any,
			kill: ((pid: number, signal: string) => {
				killed.push({ pid, signal });
				return true;
			}) as any,
		},
	);

	assert.equal(result.reaped, 1);
	assert.deepEqual(killed, [{ pid: 123, signal: "SIGTERM" }]);
	assert.match(stderrChunks.join(""), /Reaped orphaned next-server/);
});
