// @vitest-environment node
/**
 * Integration tests for ClaudeCodeServerManager.
 *
 * Tests the core functionality:
 * - Hook -> status mapping
 * - Multi-workspace routing
 * - Server lifecycle (start first workspace, stop last workspace)
 * - Config file generation
 */

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { ClaudeCodeServerManager } from "./server-manager";
import {
  createPortManagerMock,
  type MockPortManager,
} from "../../../boundaries/platform/network.test-utils";
import { createMockPathProvider } from "../../../boundaries/platform/path-provider.test-utils";
import {
  createFileSystemMock,
  directory,
} from "../../../boundaries/platform/filesystem.state-mock";
import { SILENT_LOGGER } from "../../../boundaries/platform/logging";
import type { PathProvider } from "../../../boundaries/platform/path-provider";
import type { MockFileSystemBoundary } from "../../../boundaries/platform/filesystem.state-mock";
import type { AgentStatus } from "../types";

/**
 * Send a hook to the bridge server.
 */
async function sendHook(
  port: number,
  hookName: string,
  payload: Record<string, unknown>
): Promise<Response> {
  return fetch(`http://127.0.0.1:${port}/hook/${hookName}`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Connection: "close", // Disable keep-alive to prevent server isolation issues
    },
    body: JSON.stringify(payload),
  });
}

describe("ClaudeCodeServerManager integration", () => {
  let serverManager: ClaudeCodeServerManager;
  let mockPortManager: MockPortManager;
  let mockPathProvider: PathProvider;
  let mockFileSystem: MockFileSystemBoundary;

  beforeEach(() => {
    vi.clearAllMocks();

    // Provide enough ports for tests
    mockPortManager = createPortManagerMock([15001, 15002, 15003, 15004, 15005]);
    mockPathProvider = createMockPathProvider();
    mockFileSystem = createFileSystemMock({
      entries: {
        "/app-data": directory(),
        "/app-data/claude": directory(),
        "/app-data/claude/configs": directory(),
      },
    });

    serverManager = new ClaudeCodeServerManager({
      portManager: mockPortManager,
      pathProvider: mockPathProvider,
      fileSystem: mockFileSystem,
      logger: SILENT_LOGGER,
      config: {
        hookHandlerPath: "/mock/hook-handler.js",
      },
    });
  });

  afterEach(async () => {
    await serverManager.dispose();
  });

  describe("workspace lifecycle", () => {
    it("starts server on first workspace, returns same port for subsequent", async () => {
      const port1 = await serverManager.startServer("/workspace/feature-a");
      const port2 = await serverManager.startServer("/workspace/feature-b");

      // Both should get the same port (single server for all workspaces)
      expect(port1).toBe(15001);
      expect(port2).toBe(15001);
      expect(serverManager.getPort("/workspace/feature-a")).toBe(15001);
      expect(serverManager.getPort("/workspace/feature-b")).toBe(15001);
    });

    it("returns existing port when starting same workspace twice", async () => {
      const port1 = await serverManager.startServer("/workspace/feature-a");
      const port2 = await serverManager.startServer("/workspace/feature-a");

      expect(port1).toBe(port2);
      expect(port1).toBe(15001);
    });

    it("server stops only when last workspace is removed", async () => {
      await serverManager.startServer("/workspace/feature-a");
      await serverManager.startServer("/workspace/feature-b");

      // Stop first workspace - server should still be running
      await serverManager.stopServer("/workspace/feature-a");
      expect(serverManager.getPort("/workspace/feature-a")).toBeUndefined();
      expect(serverManager.getPort("/workspace/feature-b")).toBe(15001);

      // Stop second workspace - server should stop
      await serverManager.stopServer("/workspace/feature-b");
      expect(serverManager.getPort("/workspace/feature-b")).toBeUndefined();
    });

    it("isRunning reflects workspace registration", async () => {
      expect(serverManager.isRunning("/workspace/feature-a")).toBe(false);

      await serverManager.startServer("/workspace/feature-a");
      expect(serverManager.isRunning("/workspace/feature-a")).toBe(true);

      await serverManager.stopServer("/workspace/feature-a");
      expect(serverManager.isRunning("/workspace/feature-a")).toBe(false);
    });
  });

  describe("callback wiring", () => {
    it("onServerStarted fires for each workspace", async () => {
      const startedCallback = vi.fn();
      serverManager.onServerStarted(startedCallback);

      await serverManager.startServer("/workspace/feature-a");
      await serverManager.startServer("/workspace/feature-b");

      expect(startedCallback).toHaveBeenCalledTimes(2);
      expect(startedCallback).toHaveBeenCalledWith("/workspace/feature-a", 15001);
      expect(startedCallback).toHaveBeenCalledWith("/workspace/feature-b", 15001);
    });

    it("onServerStopped fires for each workspace", async () => {
      const stoppedCallback = vi.fn();
      serverManager.onServerStopped(stoppedCallback);

      await serverManager.startServer("/workspace/feature-a");
      await serverManager.startServer("/workspace/feature-b");
      await serverManager.stopServer("/workspace/feature-a");
      await serverManager.stopServer("/workspace/feature-b");

      expect(stoppedCallback).toHaveBeenCalledTimes(2);
      expect(stoppedCallback).toHaveBeenCalledWith("/workspace/feature-a", false);
      expect(stoppedCallback).toHaveBeenCalledWith("/workspace/feature-b", false);
    });

    it("unsubscribe works", async () => {
      const startedCallback = vi.fn();
      const unsubscribe = serverManager.onServerStarted(startedCallback);

      await serverManager.startServer("/workspace/feature-a");
      expect(startedCallback).toHaveBeenCalledTimes(1);

      unsubscribe();
      await serverManager.startServer("/workspace/feature-b");
      expect(startedCallback).toHaveBeenCalledTimes(1);
    });

    it("onWorkspaceReady fires when status changes to idle", async () => {
      const readyCallback = vi.fn();
      serverManager.onWorkspaceReady(readyCallback);

      const port = await serverManager.startServer("/workspace/feature-a");

      // WrapperStart sets status to idle
      await sendHook(port, "WrapperStart", { workspacePath: "/workspace/feature-a" });
      expect(readyCallback).toHaveBeenCalledTimes(1);
      expect(readyCallback).toHaveBeenCalledWith("/workspace/feature-a");

      // SessionStart also sets status to idle, but status is already idle so no change
      readyCallback.mockClear();
      await sendHook(port, "SessionStart", { workspacePath: "/workspace/feature-a" });
      expect(readyCallback).not.toHaveBeenCalled(); // No change, already idle
    });

    it("markActiveHandler is called when status becomes idle", async () => {
      const markActiveHandler = vi.fn();
      serverManager.setMarkActiveHandler(markActiveHandler);

      const port = await serverManager.startServer("/workspace/feature-a");

      // WrapperStart sets status to idle, should trigger markActiveHandler
      await sendHook(port, "WrapperStart", { workspacePath: "/workspace/feature-a" });

      expect(markActiveHandler).toHaveBeenCalledWith("/workspace/feature-a");

      // Make busy then idle again
      await sendHook(port, "UserPromptSubmit", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "Stop", { workspacePath: "/workspace/feature-a" });

      expect(markActiveHandler).toHaveBeenCalledTimes(2);
    });
  });

  describe("hook handling", () => {
    it("routes hooks to correct workspace based on workspacePath", async () => {
      const port = await serverManager.startServer("/workspace/feature-a");
      await serverManager.startServer("/workspace/feature-b");

      const statusChangesA: AgentStatus[] = [];
      const statusChangesB: AgentStatus[] = [];

      serverManager.onStatusChange("/workspace/feature-a", (status) => {
        statusChangesA.push(status);
      });
      serverManager.onStatusChange("/workspace/feature-b", (status) => {
        statusChangesB.push(status);
      });

      // Send SessionStart to workspace A
      await sendHook(port, "SessionStart", {
        workspacePath: "/workspace/feature-a",
        session_id: "session-a",
      });

      // Send UserPromptSubmit to workspace B
      await sendHook(port, "UserPromptSubmit", {
        workspacePath: "/workspace/feature-b",
      });

      expect(statusChangesA).toEqual(["idle"]);
      expect(statusChangesB).toEqual(["busy"]);
    });

    it.each([
      {
        hookName: "WrapperStart",
        setupHooks: [] as string[],
        extraPayload: {},
        expectedChanges: ["idle"],
        finalStatus: "idle",
      },
      {
        hookName: "WrapperEnd",
        setupHooks: ["SessionStart"],
        extraPayload: {},
        expectedChanges: ["idle", "none"],
        finalStatus: "none",
      },
      {
        hookName: "SessionStart",
        setupHooks: [] as string[],
        extraPayload: { session_id: "test-session" },
        expectedChanges: ["idle"],
        finalStatus: "idle",
      },
      {
        hookName: "UserPromptSubmit",
        setupHooks: ["SessionStart"],
        extraPayload: {},
        expectedChanges: ["idle", "busy"],
        finalStatus: "busy",
      },
      {
        hookName: "PermissionRequest",
        setupHooks: ["SessionStart", "UserPromptSubmit"],
        extraPayload: {},
        expectedChanges: ["idle", "busy", "idle"],
        finalStatus: "idle",
      },
      {
        hookName: "PreCompact",
        setupHooks: ["SessionStart"],
        extraPayload: {},
        expectedChanges: ["idle", "busy"],
        finalStatus: "busy",
      },
      {
        hookName: "Stop",
        setupHooks: ["SessionStart", "UserPromptSubmit"],
        extraPayload: {},
        expectedChanges: ["idle", "busy", "idle"],
        finalStatus: "idle",
      },
      {
        hookName: "StopFailure",
        setupHooks: ["SessionStart", "UserPromptSubmit"],
        extraPayload: {},
        expectedChanges: ["idle", "busy", "idle"],
        finalStatus: "idle",
      },
      {
        hookName: "SessionEnd",
        setupHooks: ["SessionStart"],
        extraPayload: {},
        expectedChanges: ["idle", "none"],
        finalStatus: "none",
      },
    ])(
      "$hookName -> $finalStatus",
      async ({ hookName, setupHooks, extraPayload, expectedChanges, finalStatus }) => {
        const port = await serverManager.startServer("/workspace/feature-a");
        const statusChanges: AgentStatus[] = [];
        serverManager.onStatusChange("/workspace/feature-a", (status) => {
          statusChanges.push(status);
        });

        for (const hook of setupHooks) {
          await sendHook(port, hook, { workspacePath: "/workspace/feature-a" });
        }
        await sendHook(port, hookName, {
          workspacePath: "/workspace/feature-a",
          ...extraPayload,
        });

        expect(statusChanges).toEqual(expectedChanges);
        expect(serverManager.getStatus("/workspace/feature-a")).toBe(finalStatus);
        if (hookName === "SessionStart") {
          expect(serverManager.getSessionId("/workspace/feature-a")).toBe("test-session");
        }
      }
    );

    it("SessionStart during automatic compaction stays busy", async () => {
      const port = await serverManager.startServer("/workspace/feature-a");
      const statusChanges: AgentStatus[] = [];
      serverManager.onStatusChange("/workspace/feature-a", (status) => {
        statusChanges.push(status);
      });

      // Agent is working (busy)
      await sendHook(port, "SessionStart", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "UserPromptSubmit", { workspacePath: "/workspace/feature-a" });
      // Automatic compaction mid-turn: PreCompact while busy sets flag
      await sendHook(port, "PreCompact", { workspacePath: "/workspace/feature-a" });
      // SessionStart after compaction should stay busy (flag consumed)
      await sendHook(port, "SessionStart", { workspacePath: "/workspace/feature-a" });

      // No false idle transition — status stays busy throughout
      expect(statusChanges).toEqual(["idle", "busy"]);
      expect(serverManager.getStatus("/workspace/feature-a")).toBe("busy");
    });

    it("Stop between PreCompact and SessionStart stays busy", async () => {
      const port = await serverManager.startServer("/workspace/feature-a");
      const statusChanges: AgentStatus[] = [];
      serverManager.onStatusChange("/workspace/feature-a", (status) => {
        statusChanges.push(status);
      });

      await sendHook(port, "SessionStart", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "UserPromptSubmit", { workspacePath: "/workspace/feature-a" });
      // Automatic compaction mid-turn: PreCompact while busy sets flag
      await sendHook(port, "PreCompact", { workspacePath: "/workspace/feature-a" });
      // Inner session ends (Stop) while compaction is in progress — must not go idle
      await sendHook(port, "Stop", { workspacePath: "/workspace/feature-a" });
      // Compaction continues with a fresh SessionStart, still busy
      await sendHook(port, "SessionStart", { workspacePath: "/workspace/feature-a" });

      expect(statusChanges).toEqual(["idle", "busy"]);
      expect(serverManager.getStatus("/workspace/feature-a")).toBe("busy");
    });

    it("StopFailure between PreCompact and SessionStart stays busy", async () => {
      const port = await serverManager.startServer("/workspace/feature-a");
      const statusChanges: AgentStatus[] = [];
      serverManager.onStatusChange("/workspace/feature-a", (status) => {
        statusChanges.push(status);
      });

      await sendHook(port, "SessionStart", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "UserPromptSubmit", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "PreCompact", { workspacePath: "/workspace/feature-a" });
      // Auto-compaction wrapper observes a non-zero exit during the swap
      await sendHook(port, "StopFailure", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "SessionStart", { workspacePath: "/workspace/feature-a" });

      expect(statusChanges).toEqual(["idle", "busy"]);
      expect(serverManager.getStatus("/workspace/feature-a")).toBe("busy");
    });

    it("manual compact: SessionStart goes idle normally", async () => {
      const port = await serverManager.startServer("/workspace/feature-a");
      const statusChanges: AgentStatus[] = [];
      serverManager.onStatusChange("/workspace/feature-a", (status) => {
        statusChanges.push(status);
      });

      // Agent is idle (waiting for user), user runs /compact
      await sendHook(port, "SessionStart", { workspacePath: "/workspace/feature-a" });
      // PreCompact while idle does NOT set flag
      await sendHook(port, "PreCompact", { workspacePath: "/workspace/feature-a" });
      // SessionStart after compaction should go idle normally
      await sendHook(port, "SessionStart", { workspacePath: "/workspace/feature-a" });

      expect(statusChanges).toEqual(["idle", "busy", "idle"]);
      expect(serverManager.getStatus("/workspace/feature-a")).toBe("idle");
    });

    it("compacting flag cleared after use", async () => {
      const port = await serverManager.startServer("/workspace/feature-a");
      const statusChanges: AgentStatus[] = [];
      serverManager.onStatusChange("/workspace/feature-a", (status) => {
        statusChanges.push(status);
      });

      // Automatic compaction mid-turn
      await sendHook(port, "SessionStart", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "UserPromptSubmit", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "PreCompact", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "SessionStart", { workspacePath: "/workspace/feature-a" });

      // Agent finishes, stops
      await sendHook(port, "Stop", { workspacePath: "/workspace/feature-a" });

      // Next SessionStart should go idle normally (flag was consumed)
      await sendHook(port, "SessionStart", { workspacePath: "/workspace/feature-a" });

      expect(statusChanges).toEqual(["idle", "busy", "idle"]);
      expect(serverManager.getStatus("/workspace/feature-a")).toBe("idle");
    });

    it("WrapperEnd clears ignoreNextSessionStart flag", async () => {
      const port = await serverManager.startServer("/workspace/feature-a");
      const statusChanges: AgentStatus[] = [];
      serverManager.onStatusChange("/workspace/feature-a", (status) => {
        statusChanges.push(status);
      });

      // Automatic compaction sets flag
      await sendHook(port, "SessionStart", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "UserPromptSubmit", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "PreCompact", { workspacePath: "/workspace/feature-a" });

      // Claude exits before SessionStart (abnormal exit clears flag)
      await sendHook(port, "WrapperEnd", { workspacePath: "/workspace/feature-a" });

      // New session should go idle (flag was defensively cleared)
      await sendHook(port, "WrapperStart", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "SessionStart", { workspacePath: "/workspace/feature-a" });

      expect(statusChanges).toEqual(["idle", "busy", "none", "idle"]);
      expect(serverManager.getStatus("/workspace/feature-a")).toBe("idle");
    });

    it("Notification(idle_prompt) recovers from failed compaction", async () => {
      const port = await serverManager.startServer("/workspace/feature-a");
      const statusChanges: AgentStatus[] = [];
      serverManager.onStatusChange("/workspace/feature-a", (status) => {
        statusChanges.push(status);
      });

      // Automatic compaction: busy → PreCompact (stays busy, sets flag)
      await sendHook(port, "SessionStart", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "UserPromptSubmit", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "PreCompact", { workspacePath: "/workspace/feature-a" });

      // Compaction fails — no SessionStart follows, only a Notification
      await sendHook(port, "Notification", {
        workspacePath: "/workspace/feature-a",
        notification_type: "idle_prompt",
      });

      expect(statusChanges).toEqual(["idle", "busy", "idle"]);
      expect(serverManager.getStatus("/workspace/feature-a")).toBe("idle");
    });

    it("Notification(idle_prompt) clears ignoreNextSessionStart flag", async () => {
      const port = await serverManager.startServer("/workspace/feature-a");
      const statusChanges: AgentStatus[] = [];
      serverManager.onStatusChange("/workspace/feature-a", (status) => {
        statusChanges.push(status);
      });

      // Automatic compaction sets flag
      await sendHook(port, "SessionStart", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "UserPromptSubmit", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "PreCompact", { workspacePath: "/workspace/feature-a" });

      // idle_prompt clears the flag
      await sendHook(port, "Notification", {
        workspacePath: "/workspace/feature-a",
        notification_type: "idle_prompt",
      });

      // Next SessionStart should go idle normally (flag was cleared)
      await sendHook(port, "UserPromptSubmit", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "SessionStart", { workspacePath: "/workspace/feature-a" });

      expect(statusChanges).toEqual(["idle", "busy", "idle", "busy", "idle"]);
    });

    it("Notification(permission_prompt) transitions to idle", async () => {
      const port = await serverManager.startServer("/workspace/feature-a");
      const statusChanges: AgentStatus[] = [];
      serverManager.onStatusChange("/workspace/feature-a", (status) => {
        statusChanges.push(status);
      });

      await sendHook(port, "SessionStart", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "UserPromptSubmit", { workspacePath: "/workspace/feature-a" });

      await sendHook(port, "Notification", {
        workspacePath: "/workspace/feature-a",
        notification_type: "permission_prompt",
      });

      expect(statusChanges).toEqual(["idle", "busy", "idle"]);
      expect(serverManager.getStatus("/workspace/feature-a")).toBe("idle");
    });

    it("Notification(elicitation_dialog) transitions to idle", async () => {
      const port = await serverManager.startServer("/workspace/feature-a");
      const statusChanges: AgentStatus[] = [];
      serverManager.onStatusChange("/workspace/feature-a", (status) => {
        statusChanges.push(status);
      });

      await sendHook(port, "SessionStart", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "UserPromptSubmit", { workspacePath: "/workspace/feature-a" });

      await sendHook(port, "Notification", {
        workspacePath: "/workspace/feature-a",
        notification_type: "elicitation_dialog",
      });

      expect(statusChanges).toEqual(["idle", "busy", "idle"]);
      expect(serverManager.getStatus("/workspace/feature-a")).toBe("idle");
    });

    it("Notification(auth_success) does not change status", async () => {
      const port = await serverManager.startServer("/workspace/feature-a");
      const statusChanges: AgentStatus[] = [];
      serverManager.onStatusChange("/workspace/feature-a", (status) => {
        statusChanges.push(status);
      });

      await sendHook(port, "SessionStart", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "UserPromptSubmit", { workspacePath: "/workspace/feature-a" });

      await sendHook(port, "Notification", {
        workspacePath: "/workspace/feature-a",
        notification_type: "auth_success",
      });

      expect(statusChanges).toEqual(["idle", "busy"]);
      expect(serverManager.getStatus("/workspace/feature-a")).toBe("busy");
    });

    it("Notification(idle_prompt) is no-op when already idle", async () => {
      const port = await serverManager.startServer("/workspace/feature-a");
      const statusChanges: AgentStatus[] = [];
      serverManager.onStatusChange("/workspace/feature-a", (status) => {
        statusChanges.push(status);
      });

      // Get to idle state
      await sendHook(port, "SessionStart", { workspacePath: "/workspace/feature-a" });

      // idle_prompt when already idle should not fire callback
      await sendHook(port, "Notification", {
        workspacePath: "/workspace/feature-a",
        notification_type: "idle_prompt",
      });

      expect(statusChanges).toEqual(["idle"]);
      expect(serverManager.getStatus("/workspace/feature-a")).toBe("idle");
    });

    it("PreToolUse does not change status without prior PermissionRequest", async () => {
      const port = await serverManager.startServer("/workspace/feature-a");
      const statusChanges: AgentStatus[] = [];
      serverManager.onStatusChange("/workspace/feature-a", (status) => {
        statusChanges.push(status);
      });

      // Make busy
      await sendHook(port, "SessionStart", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "UserPromptSubmit", { workspacePath: "/workspace/feature-a" });

      // PreToolUse without PermissionRequest should not change status
      await sendHook(port, "PreToolUse", {
        workspacePath: "/workspace/feature-a",
        tool_name: "bash",
      });

      // Status should remain busy
      expect(statusChanges).toEqual(["idle", "busy"]);
      expect(serverManager.getStatus("/workspace/feature-a")).toBe("busy");
    });

    it("PreToolUse transitions to busy after PermissionRequest", async () => {
      const port = await serverManager.startServer("/workspace/feature-a");
      const statusChanges: AgentStatus[] = [];
      serverManager.onStatusChange("/workspace/feature-a", (status) => {
        statusChanges.push(status);
      });

      // Start session and make busy
      await sendHook(port, "SessionStart", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "UserPromptSubmit", { workspacePath: "/workspace/feature-a" });
      // Permission request puts us in idle
      await sendHook(port, "PermissionRequest", { workspacePath: "/workspace/feature-a" });

      expect(serverManager.getStatus("/workspace/feature-a")).toBe("idle");

      // PreToolUse after PermissionRequest should transition to busy
      await sendHook(port, "PreToolUse", {
        workspacePath: "/workspace/feature-a",
        tool_name: "bash",
      });

      expect(statusChanges).toEqual(["idle", "busy", "idle", "busy"]);
      expect(serverManager.getStatus("/workspace/feature-a")).toBe("busy");
    });

    it("PreToolUse flag is cleared after use", async () => {
      const port = await serverManager.startServer("/workspace/feature-a");
      const statusChanges: AgentStatus[] = [];
      serverManager.onStatusChange("/workspace/feature-a", (status) => {
        statusChanges.push(status);
      });

      // Start session and make busy
      await sendHook(port, "SessionStart", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "UserPromptSubmit", { workspacePath: "/workspace/feature-a" });
      // Permission request
      await sendHook(port, "PermissionRequest", { workspacePath: "/workspace/feature-a" });
      // First PreToolUse clears the flag
      await sendHook(port, "PreToolUse", {
        workspacePath: "/workspace/feature-a",
        tool_name: "bash",
      });

      // Second PreToolUse should NOT change status (flag already cleared)
      await sendHook(port, "PreToolUse", {
        workspacePath: "/workspace/feature-a",
        tool_name: "bash",
      });

      // Should only have 4 status changes, not 5
      expect(statusChanges).toEqual(["idle", "busy", "idle", "busy"]);
    });

    it("ignores hooks for unknown workspaces", async () => {
      const port = await serverManager.startServer("/workspace/feature-a");

      // Send hook for unknown workspace - should not throw
      const response = await sendHook(port, "SessionStart", {
        workspacePath: "/unknown/workspace",
      });

      expect(response.ok).toBe(true);
    });

    it("returns 400 for invalid hook name", async () => {
      const port = await serverManager.startServer("/workspace/feature-a");

      const response = await sendHook(port, "InvalidHook", {
        workspacePath: "/workspace/feature-a",
      });

      expect(response.status).toBe(400);
    });

    it("returns 400 for invalid JSON body", async () => {
      const port = await serverManager.startServer("/workspace/feature-a");

      const response = await fetch(`http://127.0.0.1:${port}/hook/SessionStart`, {
        method: "POST",
        headers: { "Content-Type": "application/json", Connection: "close" },
        body: "not json",
      });

      expect(response.status).toBe(400);
    });

    it("returns 405 for non-POST requests", async () => {
      const port = await serverManager.startServer("/workspace/feature-a");

      const response = await fetch(`http://127.0.0.1:${port}/hook/SessionStart`, {
        method: "GET",
        headers: { Connection: "close" },
      });

      expect(response.status).toBe(405);
    });
  });

  describe("project close cleanup", () => {
    it("stops all workspaces for a project", async () => {
      await serverManager.startServer("/project/.worktrees/feature-a");
      await serverManager.startServer("/project/.worktrees/feature-b");
      await serverManager.startServer("/other-project/feature-c");

      // All should be registered
      expect(serverManager.isRunning("/project/.worktrees/feature-a")).toBe(true);
      expect(serverManager.isRunning("/project/.worktrees/feature-b")).toBe(true);
      expect(serverManager.isRunning("/other-project/feature-c")).toBe(true);

      // Stop all for /project
      await serverManager.stopAllForProject("/project");

      // Project workspaces should be stopped
      expect(serverManager.isRunning("/project/.worktrees/feature-a")).toBe(false);
      expect(serverManager.isRunning("/project/.worktrees/feature-b")).toBe(false);
      // Other project should still be running
      expect(serverManager.isRunning("/other-project/feature-c")).toBe(true);
    });
  });

  describe("restartServer", () => {
    it("restarts workspace and preserves port", async () => {
      const port = await serverManager.startServer("/workspace/feature-a");

      const result = await serverManager.restartServer("/workspace/feature-a");

      expect(result.success).toBe(true);
      if (result.success) {
        expect(result.port).toBe(port);
      }
    });

    it("fires stop and start callbacks during restart", async () => {
      const startedCallback = vi.fn();
      const stoppedCallback = vi.fn();
      const callOrder: string[] = [];

      serverManager.onServerStarted(() => {
        callOrder.push("started");
        startedCallback();
      });
      serverManager.onServerStopped(() => {
        callOrder.push("stopped");
        stoppedCallback();
      });

      await serverManager.startServer("/workspace/feature-a");
      callOrder.length = 0; // Reset for restart test

      await serverManager.restartServer("/workspace/feature-a");

      expect(stoppedCallback).toHaveBeenCalled();
      expect(startedCallback).toHaveBeenCalledTimes(2); // Initial + restart
      expect(callOrder).toEqual(["stopped", "started"]);
    });

    it("stopped callback has isRestart=true during restart", async () => {
      const stoppedCallback = vi.fn();
      serverManager.onServerStopped(stoppedCallback);

      await serverManager.startServer("/workspace/feature-a");
      await serverManager.restartServer("/workspace/feature-a");

      expect(stoppedCallback).toHaveBeenCalledWith("/workspace/feature-a", true);
    });

    it("fails for unregistered workspace", async () => {
      const result = await serverManager.restartServer("/unknown/workspace");

      expect(result.success).toBe(false);
      if (!result.success) {
        expect(result.error).toContain("not registered");
        expect(result.serverStopped).toBe(false);
      }
    });

    it("resets status and preserves callbacks", async () => {
      const port = await serverManager.startServer("/workspace/feature-a");
      const statusChanges: AgentStatus[] = [];
      serverManager.onStatusChange("/workspace/feature-a", (status) => {
        statusChanges.push(status);
      });

      // Make busy
      await sendHook(port, "SessionStart", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "UserPromptSubmit", { workspacePath: "/workspace/feature-a" });
      expect(serverManager.getStatus("/workspace/feature-a")).toBe("busy");

      // Restart
      await serverManager.restartServer("/workspace/feature-a");

      // Status should be reset to none
      expect(serverManager.getStatus("/workspace/feature-a")).toBe("none");

      // But callback should still work
      await sendHook(port, "SessionStart", { workspacePath: "/workspace/feature-a" });
      expect(statusChanges).toContain("idle");
    });
  });

  describe("config file generation", () => {
    it("generates hooks config file", async () => {
      await serverManager.startServer("/workspace/feature-a");

      // Find the generated config file
      const entries = [...mockFileSystem.$.entries.entries()];
      const hooksConfig = entries.find(([path]) => path.includes("codehydra-hooks.json"));

      expect(hooksConfig).toBeDefined();
      if (hooksConfig) {
        const [, entry] = hooksConfig;
        expect(entry.type).toBe("file");
        if (entry.type === "file") {
          const content =
            typeof entry.content === "string" ? entry.content : entry.content.toString();
          // Should contain hook handler path
          expect(content).toContain("/mock/hook-handler.js");
          // Should contain hook definitions
          expect(content).toContain("SessionStart");
          expect(content).toContain("UserPromptSubmit");
        }
      }
    });

    it("generates MCP config file", async () => {
      serverManager.setMcpConfig({ port: 9999 });
      await serverManager.startServer("/workspace/feature-a");

      // Find the generated config file
      const entries = [...mockFileSystem.$.entries.entries()];
      const mcpConfig = entries.find(([path]) => path.includes("codehydra-mcp.json"));

      expect(mcpConfig).toBeDefined();
      if (mcpConfig) {
        const [, entry] = mcpConfig;
        expect(entry.type).toBe("file");
        if (entry.type === "file") {
          const content =
            typeof entry.content === "string" ? entry.content : entry.content.toString();
          // Should contain MCP port
          expect(content).toContain("9999");
          // Should contain workspace path
          expect(content).toContain("/workspace/feature-a");
        }
      }
    });
  });

  describe("config path getters", () => {
    it("returns consistent paths for hooks config", async () => {
      await serverManager.startServer("/workspace/feature-a");

      const path1 = serverManager.getHooksConfigPath("/workspace/feature-a");
      const path2 = serverManager.getHooksConfigPath("/workspace/feature-a");

      expect(path1.toString()).toBe(path2.toString());
      expect(path1.toString()).toContain("codehydra-hooks.json");
    });

    it("returns consistent paths for MCP config", async () => {
      await serverManager.startServer("/workspace/feature-a");

      const path1 = serverManager.getMcpConfigPath("/workspace/feature-a");
      const path2 = serverManager.getMcpConfigPath("/workspace/feature-a");

      expect(path1.toString()).toBe(path2.toString());
      expect(path1.toString()).toContain("codehydra-mcp.json");
    });

    it("returns different paths for different workspaces", async () => {
      await serverManager.startServer("/workspace/feature-a");
      await serverManager.startServer("/workspace/feature-b");

      const pathA = serverManager.getHooksConfigPath("/workspace/feature-a");
      const pathB = serverManager.getHooksConfigPath("/workspace/feature-b");

      expect(pathA.toString()).not.toBe(pathB.toString());
    });
  });

  describe("dispose", () => {
    it("stops all workspaces and server", async () => {
      await serverManager.startServer("/workspace/feature-a");
      await serverManager.startServer("/workspace/feature-b");

      await serverManager.dispose();

      expect(serverManager.isRunning("/workspace/feature-a")).toBe(false);
      expect(serverManager.isRunning("/workspace/feature-b")).toBe(false);
    });

    it("is safe to call multiple times", async () => {
      await serverManager.startServer("/workspace/feature-a");

      await serverManager.dispose();
      await serverManager.dispose(); // Should not throw
    });
  });

  describe("initial prompt", () => {
    it("setInitialPrompt stores path retrievable via getInitialPromptPath", async () => {
      await serverManager.startServer("/workspace/feature-a");

      await serverManager.setInitialPrompt("/workspace/feature-a", {
        prompt: "Hello, Claude!",
      });

      const path = serverManager.getInitialPromptPath("/workspace/feature-a");
      expect(path).toBeDefined();
      expect(path?.toString()).toContain("initial-prompt.json");
    });

    it("initial prompt file contains correct JSON structure", async () => {
      await serverManager.startServer("/workspace/feature-a");

      await serverManager.setInitialPrompt("/workspace/feature-a", {
        prompt: "Test prompt",
        agent: "coder",
        model: { providerID: "anthropic", modelID: "claude-sonnet" },
      });

      const path = serverManager.getInitialPromptPath("/workspace/feature-a");
      expect(path).toBeDefined();

      // Read file from mock filesystem
      const content = await mockFileSystem.readFile(path!);
      const parsed = JSON.parse(content);

      expect(parsed.prompt).toBe("Test prompt");
      expect(parsed.agent).toBe("coder");
      expect(parsed.model).toBe("claude-sonnet"); // modelID only, not full object
    });

    it("getInitialPromptPath returns undefined when no prompt set", async () => {
      await serverManager.startServer("/workspace/feature-a");

      const path = serverManager.getInitialPromptPath("/workspace/feature-a");
      expect(path).toBeUndefined();
    });

    it("getInitialPromptPath returns undefined for unknown workspace", async () => {
      const path = serverManager.getInitialPromptPath("/workspace/unknown");
      expect(path).toBeUndefined();
    });

    it("setInitialPrompt logs warning for unknown workspace", async () => {
      // Should not throw, just log warning and return
      await expect(
        serverManager.setInitialPrompt("/workspace/unknown", { prompt: "Test" })
      ).resolves.not.toThrow();
    });

    it("WrapperStart with non-plan initial prompt sets status to busy", async () => {
      const port = await serverManager.startServer("/workspace/feature-a");
      const statusChanges: AgentStatus[] = [];
      serverManager.onStatusChange("/workspace/feature-a", (status) => {
        statusChanges.push(status);
      });

      const readyCallback = vi.fn();
      serverManager.onWorkspaceReady(readyCallback);

      const markActiveHandler = vi.fn();
      serverManager.setMarkActiveHandler(markActiveHandler);

      // Set initial prompt without plan agent (agent undefined → non-plan)
      await serverManager.setInitialPrompt("/workspace/feature-a", {
        prompt: "Build a feature",
      });

      // WrapperStart should set status to busy instead of idle
      await sendHook(port, "WrapperStart", { workspacePath: "/workspace/feature-a" });

      expect(statusChanges).toEqual(["busy"]);
      expect(serverManager.getStatus("/workspace/feature-a")).toBe("busy");
      expect(readyCallback).toHaveBeenCalledWith("/workspace/feature-a");
      expect(markActiveHandler).toHaveBeenCalledWith("/workspace/feature-a");
    });

    it("SessionStart stays busy with non-plan initial prompt", async () => {
      const port = await serverManager.startServer("/workspace/feature-a");
      const statusChanges: AgentStatus[] = [];
      serverManager.onStatusChange("/workspace/feature-a", (status) => {
        statusChanges.push(status);
      });

      await serverManager.setInitialPrompt("/workspace/feature-a", {
        prompt: "Build a feature",
      });

      // Full startup sequence: WrapperStart → SessionStart → UserPromptSubmit
      await sendHook(port, "WrapperStart", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "SessionStart", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "UserPromptSubmit", { workspacePath: "/workspace/feature-a" });

      // Should stay busy throughout — no idle blip
      expect(statusChanges).toEqual(["busy"]);
      expect(serverManager.getStatus("/workspace/feature-a")).toBe("busy");
    });

    it("WrapperStart with plan initial prompt sets status to idle", async () => {
      const port = await serverManager.startServer("/workspace/feature-a");
      const statusChanges: AgentStatus[] = [];
      serverManager.onStatusChange("/workspace/feature-a", (status) => {
        statusChanges.push(status);
      });

      // Set initial prompt with plan agent
      await serverManager.setInitialPrompt("/workspace/feature-a", {
        prompt: "Plan a feature",
        agent: "plan",
      });

      // WrapperStart should set status to idle (normal behavior)
      await sendHook(port, "WrapperStart", { workspacePath: "/workspace/feature-a" });

      expect(statusChanges).toEqual(["idle"]);
      expect(serverManager.getStatus("/workspace/feature-a")).toBe("idle");
    });

    it("WrapperStart without initial prompt sets status to idle", async () => {
      const port = await serverManager.startServer("/workspace/feature-a");
      const statusChanges: AgentStatus[] = [];
      serverManager.onStatusChange("/workspace/feature-a", (status) => {
        statusChanges.push(status);
      });

      // No setInitialPrompt called

      // WrapperStart should set status to idle (normal behavior)
      await sendHook(port, "WrapperStart", { workspacePath: "/workspace/feature-a" });

      expect(statusChanges).toEqual(["idle"]);
      expect(serverManager.getStatus("/workspace/feature-a")).toBe("idle");
    });

    it("flag consumed on SessionStart, subsequent session goes idle", async () => {
      const port = await serverManager.startServer("/workspace/feature-a");
      const statusChanges: AgentStatus[] = [];
      serverManager.onStatusChange("/workspace/feature-a", (status) => {
        statusChanges.push(status);
      });

      await serverManager.setInitialPrompt("/workspace/feature-a", {
        prompt: "Build a feature",
      });

      // First session: flag consumed on SessionStart
      await sendHook(port, "WrapperStart", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "SessionStart", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "UserPromptSubmit", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "Stop", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "WrapperEnd", { workspacePath: "/workspace/feature-a" });

      // Second session: normal idle behavior
      await sendHook(port, "WrapperStart", { workspacePath: "/workspace/feature-a" });

      expect(statusChanges).toEqual(["busy", "idle", "none", "idle"]);
      expect(serverManager.getStatus("/workspace/feature-a")).toBe("idle");
    });

    it("setInitialPrompt handles mkdtemp failure gracefully", async () => {
      await serverManager.startServer("/workspace/feature-a");

      // Make mkdtemp throw an error
      mockFileSystem.$.mkdtempShouldFail = true;

      // Should not throw - logs error and continues
      await expect(
        serverManager.setInitialPrompt("/workspace/feature-a", { prompt: "Test" })
      ).resolves.not.toThrow();

      // Path should not be set since mkdtemp failed
      const path = serverManager.getInitialPromptPath("/workspace/feature-a");
      expect(path).toBeUndefined();

      // Reset for other tests
      mockFileSystem.$.mkdtempShouldFail = false;
    });
  });

  describe("no-session marker", () => {
    it("setNoSessionMarker stores path retrievable via getNoSessionMarkerPath", async () => {
      await serverManager.startServer("/workspace/feature-a");

      await serverManager.setNoSessionMarker("/workspace/feature-a");

      const path = serverManager.getNoSessionMarkerPath("/workspace/feature-a");
      expect(path).toBeDefined();
      expect(path?.toString()).toContain("claude/no-session/");
    });

    it("getNoSessionMarkerPath returns undefined when no marker set", async () => {
      await serverManager.startServer("/workspace/feature-a");

      const path = serverManager.getNoSessionMarkerPath("/workspace/feature-a");
      expect(path).toBeUndefined();
    });

    it("getNoSessionMarkerPath returns undefined for unknown workspace", () => {
      const path = serverManager.getNoSessionMarkerPath("/workspace/unknown");
      expect(path).toBeUndefined();
    });

    it("setNoSessionMarker logs warning for unknown workspace", async () => {
      await expect(serverManager.setNoSessionMarker("/workspace/unknown")).resolves.not.toThrow();
    });

    it("marker file is created as empty file", async () => {
      await serverManager.startServer("/workspace/feature-a");

      await serverManager.setNoSessionMarker("/workspace/feature-a");

      const path = serverManager.getNoSessionMarkerPath("/workspace/feature-a");
      expect(path).toBeDefined();

      const content = await mockFileSystem.readFile(path!);
      expect(content).toBe("");
    });
  });

  describe("sub-agent tracking", () => {
    it("StopFailure suppressed when sub-agents are active", async () => {
      const port = await serverManager.startServer("/workspace/feature-a");
      const statusChanges: AgentStatus[] = [];
      serverManager.onStatusChange("/workspace/feature-a", (status) => {
        statusChanges.push(status);
      });

      await sendHook(port, "SessionStart", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "UserPromptSubmit", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "SubagentStart", {
        workspacePath: "/workspace/feature-a",
        agent_id: "sub-1",
      });
      // Main agent hits API error, but sub-agent is still running
      await sendHook(port, "StopFailure", { workspacePath: "/workspace/feature-a" });

      // Should stay busy — StopFailure suppressed
      expect(statusChanges).toEqual(["idle", "busy"]);
      expect(serverManager.getStatus("/workspace/feature-a")).toBe("busy");
    });

    it("StopFailure without sub-agents transitions to idle", async () => {
      const port = await serverManager.startServer("/workspace/feature-a");
      const statusChanges: AgentStatus[] = [];
      serverManager.onStatusChange("/workspace/feature-a", (status) => {
        statusChanges.push(status);
      });

      await sendHook(port, "SessionStart", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "UserPromptSubmit", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "StopFailure", { workspacePath: "/workspace/feature-a" });

      expect(statusChanges).toEqual(["idle", "busy", "idle"]);
    });

    it("Stop suppressed to busy when sub-agents are active", async () => {
      const port = await serverManager.startServer("/workspace/feature-a");
      const statusChanges: AgentStatus[] = [];
      serverManager.onStatusChange("/workspace/feature-a", (status) => {
        statusChanges.push(status);
      });

      await sendHook(port, "SessionStart", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "UserPromptSubmit", { workspacePath: "/workspace/feature-a" });
      // Sub-agent spawned
      await sendHook(port, "SubagentStart", {
        workspacePath: "/workspace/feature-a",
        agent_id: "sub-1",
      });
      // Main agent stops, but sub-agent is still running
      await sendHook(port, "Stop", { workspacePath: "/workspace/feature-a" });

      // Should stay busy — Stop suppressed
      expect(statusChanges).toEqual(["idle", "busy"]);
      expect(serverManager.getStatus("/workspace/feature-a")).toBe("busy");
    });

    it("last SubagentStop stays busy, subsequent Stop goes idle", async () => {
      const port = await serverManager.startServer("/workspace/feature-a");
      const statusChanges: AgentStatus[] = [];
      serverManager.onStatusChange("/workspace/feature-a", (status) => {
        statusChanges.push(status);
      });

      await sendHook(port, "SessionStart", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "UserPromptSubmit", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "SubagentStart", {
        workspacePath: "/workspace/feature-a",
        agent_id: "sub-1",
      });
      await sendHook(port, "SubagentStart", {
        workspacePath: "/workspace/feature-a",
        agent_id: "sub-2",
      });
      // Main agent stops
      await sendHook(port, "Stop", { workspacePath: "/workspace/feature-a" });

      // First sub-agent stops — still one active
      await sendHook(port, "SubagentStop", {
        workspacePath: "/workspace/feature-a",
        agent_id: "sub-1",
      });
      expect(serverManager.getStatus("/workspace/feature-a")).toBe("busy");

      // Last sub-agent stops — stays busy (main agent will resume to process result)
      await sendHook(port, "SubagentStop", {
        workspacePath: "/workspace/feature-a",
        agent_id: "sub-2",
      });
      expect(serverManager.getStatus("/workspace/feature-a")).toBe("busy");

      // Main agent resumes, processes result, then stops → real idle
      await sendHook(port, "UserPromptSubmit", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "Stop", { workspacePath: "/workspace/feature-a" });

      expect(statusChanges).toEqual(["idle", "busy", "idle"]);
      expect(serverManager.getStatus("/workspace/feature-a")).toBe("idle");
    });

    it("SubagentStop without prior SubagentStart is a safe no-op", async () => {
      const port = await serverManager.startServer("/workspace/feature-a");
      const statusChanges: AgentStatus[] = [];
      serverManager.onStatusChange("/workspace/feature-a", (status) => {
        statusChanges.push(status);
      });

      await sendHook(port, "SessionStart", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "UserPromptSubmit", { workspacePath: "/workspace/feature-a" });

      // SubagentStop for unknown agent — should not crash or change status
      await sendHook(port, "SubagentStop", {
        workspacePath: "/workspace/feature-a",
        agent_id: "unknown-agent",
      });

      expect(statusChanges).toEqual(["idle", "busy"]);
      expect(serverManager.getStatus("/workspace/feature-a")).toBe("busy");
    });

    it("SubagentStart without agent_id is ignored", async () => {
      const port = await serverManager.startServer("/workspace/feature-a");
      const statusChanges: AgentStatus[] = [];
      serverManager.onStatusChange("/workspace/feature-a", (status) => {
        statusChanges.push(status);
      });

      await sendHook(port, "SessionStart", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "UserPromptSubmit", { workspacePath: "/workspace/feature-a" });

      // SubagentStart without agent_id — no tracking
      await sendHook(port, "SubagentStart", { workspacePath: "/workspace/feature-a" });

      // Stop should go idle normally (no sub-agents tracked)
      await sendHook(port, "Stop", { workspacePath: "/workspace/feature-a" });

      expect(statusChanges).toEqual(["idle", "busy", "idle"]);
    });

    it("sub-agent result processing: full cycle stays busy until final Stop", async () => {
      const port = await serverManager.startServer("/workspace/feature-a");
      const statusChanges: AgentStatus[] = [];
      serverManager.onStatusChange("/workspace/feature-a", (status) => {
        statusChanges.push(status);
      });

      // Matches real log trace: SubagentStart → Stop(suppressed) → SubagentStop → UserPromptSubmit → Stop(idle)
      await sendHook(port, "SessionStart", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "UserPromptSubmit", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "SubagentStart", {
        workspacePath: "/workspace/feature-a",
        agent_id: "sub-1",
      });
      await sendHook(port, "Stop", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "SubagentStop", {
        workspacePath: "/workspace/feature-a",
        agent_id: "sub-1",
      });
      // Main agent resumes to process sub-agent result
      await sendHook(port, "UserPromptSubmit", { workspacePath: "/workspace/feature-a" });
      // Main agent finishes processing → real idle
      await sendHook(port, "Stop", { workspacePath: "/workspace/feature-a" });

      // Only one idle transition — no false idle blip after SubagentStop
      expect(statusChanges).toEqual(["idle", "busy", "idle"]);
    });

    it("WrapperEnd clears sub-agent tracking", async () => {
      const port = await serverManager.startServer("/workspace/feature-a");
      const statusChanges: AgentStatus[] = [];
      serverManager.onStatusChange("/workspace/feature-a", (status) => {
        statusChanges.push(status);
      });

      await sendHook(port, "SessionStart", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "UserPromptSubmit", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "SubagentStart", {
        workspacePath: "/workspace/feature-a",
        agent_id: "sub-1",
      });
      // Main agent stops (suppressed)
      await sendHook(port, "Stop", { workspacePath: "/workspace/feature-a" });

      // WrapperEnd — clears all tracking
      await sendHook(port, "WrapperEnd", { workspacePath: "/workspace/feature-a" });

      expect(statusChanges).toEqual(["idle", "busy", "none"]);

      // New session: Stop should go idle normally (sub-agent tracking was cleared)
      await sendHook(port, "WrapperStart", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "SessionStart", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "UserPromptSubmit", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "Stop", { workspacePath: "/workspace/feature-a" });

      expect(statusChanges).toEqual(["idle", "busy", "none", "idle", "busy", "idle"]);
    });

    it("SessionEnd clears sub-agent tracking", async () => {
      const port = await serverManager.startServer("/workspace/feature-a");
      const statusChanges: AgentStatus[] = [];
      serverManager.onStatusChange("/workspace/feature-a", (status) => {
        statusChanges.push(status);
      });

      await sendHook(port, "SessionStart", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "UserPromptSubmit", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "SubagentStart", {
        workspacePath: "/workspace/feature-a",
        agent_id: "sub-1",
      });
      // Main agent stops (suppressed)
      await sendHook(port, "Stop", { workspacePath: "/workspace/feature-a" });

      // SessionEnd — clears all tracking
      await sendHook(port, "SessionEnd", { workspacePath: "/workspace/feature-a" });

      expect(statusChanges).toEqual(["idle", "busy", "none"]);
    });

    it("stale sub-agent IDs from previous turn do not block next Stop", async () => {
      const port = await serverManager.startServer("/workspace/feature-a");
      const statusChanges: AgentStatus[] = [];
      serverManager.onStatusChange("/workspace/feature-a", (status) => {
        statusChanges.push(status);
      });

      // Turn 1: sub-agent spawned but SubagentStop never fires (e.g., sub-agent crashes)
      await sendHook(port, "SessionStart", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "UserPromptSubmit", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "SubagentStart", {
        workspacePath: "/workspace/feature-a",
        agent_id: "sub-orphan",
      });
      // Turn completes via Notification (idle_prompt) — bypasses sub-agent guard
      await sendHook(port, "Notification", {
        workspacePath: "/workspace/feature-a",
        notification_type: "idle_prompt",
      });

      expect(serverManager.getStatus("/workspace/feature-a")).toBe("idle");

      // Turn 2: normal turn, no sub-agents — Stop must go idle
      await sendHook(port, "UserPromptSubmit", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "Stop", { workspacePath: "/workspace/feature-a" });

      expect(statusChanges).toEqual(["idle", "busy", "idle", "busy", "idle"]);
    });

    it("Stop without sub-agents still transitions to idle normally", async () => {
      const port = await serverManager.startServer("/workspace/feature-a");
      const statusChanges: AgentStatus[] = [];
      serverManager.onStatusChange("/workspace/feature-a", (status) => {
        statusChanges.push(status);
      });

      await sendHook(port, "SessionStart", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "UserPromptSubmit", { workspacePath: "/workspace/feature-a" });
      await sendHook(port, "Stop", { workspacePath: "/workspace/feature-a" });

      // Normal flow — no sub-agents, Stop goes idle
      expect(statusChanges).toEqual(["idle", "busy", "idle"]);
    });
  });
});
