// @vitest-environment node
/**
 * Integration tests for OpenCodeServerManager with AgentModule.
 *
 * Tests the full lifecycle of OpenCode servers managed by AgentModule:
 * - Server starts when workspace is added
 * - Server stops when workspace is removed
 * - All servers stop when project is closed
 * - Callbacks fire correctly for start/stop events
 */

import { request as httpRequest } from "http";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { OpenCodeServerManager } from "./server-manager";
import {
  createMockProcessRunner,
  type MockProcessRunner,
} from "../../../boundaries/platform/process.state-mock";
import { createMockPathProvider } from "../../../boundaries/platform/path-provider.test-utils";
import {
  createPortManagerMock,
  type MockPortManager,
} from "../../../boundaries/platform/network.test-utils";
import { SILENT_LOGGER } from "../../../boundaries/platform/logging";
import type { HttpClient } from "../../../boundaries/platform/network";
import type { PathProvider } from "../../../boundaries/platform/path-provider";
import { createMockConfig } from "../../../boundaries/platform/config.test-utils";

/**
 * Create a mock HttpClient with vitest spies.
 */
function createTestHttpClient(): HttpClient & { fetch: ReturnType<typeof vi.fn> } {
  const defaultResponse = new Response(JSON.stringify({ status: "ok" }), { status: 200 });
  return {
    fetch: vi.fn().mockResolvedValue(defaultResponse),
  };
}

/**
 * Create a mock PathProvider for testing.
 * Uses the standard createMockPathProvider which returns Path objects.
 */
function createTestPathProvider(): PathProvider {
  return createMockPathProvider();
}

/** Default config used by tests that don't pass an explicit override. */
const DEFAULT_CONFIG_DEFAULTS = { "version.opencode": "1.0.223" };

describe("OpenCodeServerManager integration", () => {
  let serverManager: OpenCodeServerManager;
  let mockProcessRunner: MockProcessRunner;
  let mockPortManager: MockPortManager;
  let mockHttpClient: ReturnType<typeof createTestHttpClient>;
  let mockPathProvider: PathProvider;
  let processCount: number;

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

    // Create a mock process runner that returns new processes for each call
    mockProcessRunner = createMockProcessRunner({
      onSpawn: () => ({
        pid: 1000 + processCount++,
        killResult: { success: true, reason: "SIGTERM" },
      }),
    });
    // Provide enough ports for tests (max ~5 concurrent servers in some tests)
    mockPortManager = createPortManagerMock([
      14001, 14002, 14003, 14004, 14005, 14006, 14007, 14008, 14009, 14010,
    ]);
    mockHttpClient = createTestHttpClient();
    mockPathProvider = createTestPathProvider();

    serverManager = new OpenCodeServerManager(
      mockProcessRunner,
      mockPortManager,
      mockHttpClient,
      mockPathProvider,
      createMockConfig({ defaults: DEFAULT_CONFIG_DEFAULTS }),
      SILENT_LOGGER
    );
  });

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

  describe("callback wiring", () => {
    it("onServerStarted callback is fired with path and port", async () => {
      const startedCallback = vi.fn();
      serverManager.onServerStarted(startedCallback);

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

      // Callback should have been fired (with undefined pending prompt)
      expect(startedCallback).toHaveBeenCalledWith("/workspace/feature-a", 14001, undefined);
    });

    it("onServerStopped callback is fired when server stops", async () => {
      const stoppedCallback = vi.fn();
      serverManager.onServerStopped(stoppedCallback);

      // Start and stop server
      await serverManager.startServer("/workspace/feature-a");
      await serverManager.stopServer("/workspace/feature-a");

      // Callback should have been fired with isRestart=false
      expect(stoppedCallback).toHaveBeenCalledWith("/workspace/feature-a", false);
    });

    it("onServerStopped callback receives correct workspace path", async () => {
      const receivedPaths: string[] = [];
      serverManager.onServerStopped((path) => {
        receivedPaths.push(path);
      });

      // Start and stop two servers
      await serverManager.startServer("/workspace/feature-a");
      await serverManager.startServer("/workspace/feature-b");
      await serverManager.stopServer("/workspace/feature-a");
      await serverManager.stopServer("/workspace/feature-b");

      // Both paths should have been received in order
      expect(receivedPaths).toEqual(["/workspace/feature-a", "/workspace/feature-b"]);
    });
  });

  describe("workspace lifecycle", () => {
    it("server starts on add, stops on remove", async () => {
      // Start
      const port = await serverManager.startServer("/workspace/feature-a");
      expect(port).toBe(14001);
      expect(serverManager.getPort("/workspace/feature-a")).toBe(14001);

      // Stop
      await serverManager.stopServer("/workspace/feature-a");
      expect(serverManager.getPort("/workspace/feature-a")).toBeUndefined();
    });
  });

  describe("multiple workspaces", () => {
    it("each gets own server and port", async () => {
      const port1 = await serverManager.startServer("/workspace/feature-a");
      const port2 = await serverManager.startServer("/workspace/feature-b");
      const port3 = await serverManager.startServer("/workspace/feature-c");

      expect(port1).toBe(14001);
      expect(port2).toBe(14002);
      expect(port3).toBe(14003);

      expect(serverManager.getPort("/workspace/feature-a")).toBe(14001);
      expect(serverManager.getPort("/workspace/feature-b")).toBe(14002);
      expect(serverManager.getPort("/workspace/feature-c")).toBe(14003);
    });
  });

  describe("project close cleanup", () => {
    it("all servers killed when project is closed", async () => {
      // Start multiple servers for the same project
      await serverManager.startServer("/project/.worktrees/feature-a");
      await serverManager.startServer("/project/.worktrees/feature-b");
      await serverManager.startServer("/project/.worktrees/feature-c");

      // All should be running
      expect(serverManager.getPort("/project/.worktrees/feature-a")).toBeDefined();
      expect(serverManager.getPort("/project/.worktrees/feature-b")).toBeDefined();
      expect(serverManager.getPort("/project/.worktrees/feature-c")).toBeDefined();

      // Close project
      await serverManager.stopAllForProject("/project");

      // All should be stopped
      expect(serverManager.getPort("/project/.worktrees/feature-a")).toBeUndefined();
      expect(serverManager.getPort("/project/.worktrees/feature-b")).toBeUndefined();
      expect(serverManager.getPort("/project/.worktrees/feature-c")).toBeUndefined();

      // All processes should have been killed
      expect(mockProcessRunner.$.spawned(0)).toHaveBeenKilled();
      expect(mockProcessRunner.$.spawned(1)).toHaveBeenKilled();
      expect(mockProcessRunner.$.spawned(2)).toHaveBeenKilled();
    });
  });

  describe("restartServer", () => {
    it("restartServer returns same port", async () => {
      // Start server
      const originalPort = await serverManager.startServer("/workspace/feature-a");
      expect(originalPort).toBe(14001);

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

      expect(result.success).toBe(true);
      if (result.success) {
        expect(result.port).toBe(14001);
      }
      expect(serverManager.getPort("/workspace/feature-a")).toBe(14001);
    });

    it("restartServer fails if server not running", async () => {
      const result = await serverManager.restartServer("/workspace/feature-a");

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

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

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

      // Start server (fires started)
      await serverManager.startServer("/workspace/feature-a");
      expect(startedCallback).toHaveBeenCalledTimes(1);
      callOrder = []; // Reset for restart test

      // Restart server (fires stopped then started)
      await serverManager.restartServer("/workspace/feature-a");

      expect(stoppedCallback).toHaveBeenCalledTimes(1);
      expect(startedCallback).toHaveBeenCalledTimes(2); // 1 for start, 1 for restart
      expect(callOrder).toEqual(["stopped", "started"]);
    });

    it("restartServer during starting state waits then restarts", async () => {
      // Start a server that will resolve
      const startPromise = serverManager.startServer("/workspace/feature-a");

      // Immediately try to restart (while starting)
      const restartPromise = serverManager.restartServer("/workspace/feature-a");

      // Wait for both
      await startPromise;
      const result = await restartPromise;

      // Should succeed
      expect(result.success).toBe(true);
      if (result.success) {
        expect(result.port).toBe(14001);
      }
    });

    it("restartServer during restarting state returns in-progress promise", async () => {
      // Start server
      await serverManager.startServer("/workspace/feature-a");

      // Start two restarts concurrently
      const restartPromise1 = serverManager.restartServer("/workspace/feature-a");
      const restartPromise2 = serverManager.restartServer("/workspace/feature-a");

      // They should be the same promise
      expect(restartPromise1).toBe(restartPromise2);

      // Wait for completion
      const result = await restartPromise1;
      expect(result.success).toBe(true);
    });

    it("restartServer fails with port conflict", async () => {
      // Create a new server manager with short timeout for this test
      const shortTimeoutManager = new OpenCodeServerManager(
        mockProcessRunner,
        mockPortManager,
        mockHttpClient,
        mockPathProvider,
        createMockConfig({ defaults: DEFAULT_CONFIG_DEFAULTS }),
        SILENT_LOGGER,
        { healthCheckTimeoutMs: 100, healthCheckIntervalMs: 10 }
      );

      try {
        // Start server
        await shortTimeoutManager.startServer("/workspace/feature-a");

        // Make health check fail (simulating port conflict)
        mockHttpClient.fetch.mockRejectedValue(new Error("Connection refused"));

        // Restart server - should fail because health check fails
        const result = await shortTimeoutManager.restartServer("/workspace/feature-a");

        expect(result.success).toBe(false);
        if (!result.success) {
          expect(result.serverStopped).toBe(true);
        }
      } finally {
        await shortTimeoutManager.dispose();
      }
    });
  });

  describe("bridge server", () => {
    it("starts lazily on first startServer and exposes bridge port", async () => {
      expect(serverManager.getBridgePort()).toBeNull();

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

      // Bridge port is OS-assigned (port 0), so just check it's a valid port
      const bridgePort = serverManager.getBridgePort();
      expect(bridgePort).not.toBeNull();
      expect(bridgePort).toBeGreaterThan(0);
    });

    it("reuses bridge server across multiple workspaces", async () => {
      await serverManager.startServer("/workspace/feature-a");
      const bridgePort1 = serverManager.getBridgePort();

      await serverManager.startServer("/workspace/feature-b");
      const bridgePort2 = serverManager.getBridgePort();

      expect(bridgePort1).toBe(bridgePort2);
    });

    it("fires onWorkspaceReady callback when WrapperStart notification received", async () => {
      const readyCallback = vi.fn();
      serverManager.onWorkspaceReady(readyCallback);

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

      const bridgePort = serverManager.getBridgePort()!;
      const payload = JSON.stringify({ workspacePath: "/workspace/feature-a" });

      // Send WrapperStart notification to bridge server
      await new Promise<void>((resolve, reject) => {
        const req = httpRequest(
          {
            hostname: "127.0.0.1",
            port: bridgePort,
            path: "/hook/WrapperStart",
            method: "POST",
            headers: {
              "Content-Type": "application/json",
              "Content-Length": Buffer.byteLength(payload),
            },
          },
          (res) => {
            let body = "";
            res.on("data", (chunk: Buffer) => {
              body += chunk.toString();
            });
            res.on("end", () => {
              try {
                const parsed = JSON.parse(body) as { success: boolean };
                expect(parsed.success).toBe(true);
                resolve();
              } catch (e) {
                reject(e);
              }
            });
          }
        );
        req.on("error", reject);
        req.write(payload);
        req.end();
      });

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

    it("calls markActiveHandler when WrapperStart notification received", async () => {
      const markActiveHandler = vi.fn();
      serverManager.setMarkActiveHandler(markActiveHandler);

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

      const bridgePort = serverManager.getBridgePort()!;
      const payload = JSON.stringify({ workspacePath: "/workspace/feature-a" });

      // Send WrapperStart notification to bridge server
      await new Promise<void>((resolve, reject) => {
        const req = httpRequest(
          {
            hostname: "127.0.0.1",
            port: bridgePort,
            path: "/hook/WrapperStart",
            method: "POST",
            headers: {
              "Content-Type": "application/json",
              "Content-Length": Buffer.byteLength(payload),
            },
          },
          (res) => {
            let body = "";
            res.on("data", (chunk: Buffer) => {
              body += chunk.toString();
            });
            res.on("end", () => {
              try {
                const parsed = JSON.parse(body) as { success: boolean };
                expect(parsed.success).toBe(true);
                resolve();
              } catch (e) {
                reject(e);
              }
            });
          }
        );
        req.on("error", reject);
        req.write(payload);
        req.end();
      });

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

    it("cleans up bridge server on dispose", async () => {
      await serverManager.startServer("/workspace/feature-a");
      expect(serverManager.getBridgePort()).not.toBeNull();

      await serverManager.dispose();

      expect(serverManager.getBridgePort()).toBeNull();
    });
  });

  describe("rapid workspace add/remove cycles", () => {
    it("are stable", async () => {
      // Rapid add/remove cycles
      for (let i = 0; i < 5; i++) {
        await serverManager.startServer(`/workspace/feature-${i}`);
        await serverManager.stopServer(`/workspace/feature-${i}`);
      }

      // No servers should be running
      for (let i = 0; i < 5; i++) {
        expect(serverManager.getPort(`/workspace/feature-${i}`)).toBeUndefined();
      }
    });

    it("handles concurrent starts/stops", async () => {
      // Start multiple concurrently
      const [port1, port2, port3] = await Promise.all([
        serverManager.startServer("/workspace/feature-a"),
        serverManager.startServer("/workspace/feature-b"),
        serverManager.startServer("/workspace/feature-c"),
      ]);

      expect(port1).toBeDefined();
      expect(port2).toBeDefined();
      expect(port3).toBeDefined();
      expect(new Set([port1, port2, port3]).size).toBe(3); // All unique

      // Stop multiple concurrently
      await Promise.all([
        serverManager.stopServer("/workspace/feature-a"),
        serverManager.stopServer("/workspace/feature-b"),
        serverManager.stopServer("/workspace/feature-c"),
      ]);

      // All should be stopped
      expect(serverManager.getPort("/workspace/feature-a")).toBeUndefined();
      expect(serverManager.getPort("/workspace/feature-b")).toBeUndefined();
      expect(serverManager.getPort("/workspace/feature-c")).toBeUndefined();
    });
  });
});
