/**
 * @file useTerminalSessions.test.ts
 * @description Unit tests for the useTerminalSessions hook
 */

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { renderHook, act, waitFor } from "@testing-library/react";
import { useTerminalSessions } from "./useTerminalSessions";
import type { Socket } from "socket.io-client";

// ==========================================
// Mock socket.io-client
// ==========================================

vi.mock("socket.io-client");

// ==========================================
// Mock lib/config
// ==========================================

vi.mock("@/lib/config", () => ({
  API_BASE_URL: "http://localhost:3001",
}));

// ==========================================
// Helpers
// ==========================================

interface MockSocket {
  on: ReturnType<typeof vi.fn>;
  off: ReturnType<typeof vi.fn>;
  emit: ReturnType<typeof vi.fn>;
  disconnect: ReturnType<typeof vi.fn>;
  connected: boolean;
}

describe("useTerminalSessions", () => {
  let mockSocket: MockSocket;
  let socketEventHandlers: Record<string, (data: unknown) => void>;
  let mockIo: ReturnType<typeof vi.fn>;

  beforeEach(async () => {
    socketEventHandlers = {};

    mockSocket = {
      on: vi.fn((event: string, handler: (data: unknown) => void) => {
        socketEventHandlers[event] = handler;
        return mockSocket;
      }),
      off: vi.fn(),
      emit: vi.fn(),
      disconnect: vi.fn(),
      connected: true,
    };

    const socketIo = await import("socket.io-client");
    mockIo = vi.mocked(socketIo.io);
    mockIo.mockReturnValue(mockSocket as unknown as Socket);
  });

  afterEach(() => {
    vi.clearAllMocks();
  });

  // ==========================================
  // Connection lifecycle
  // ==========================================

  describe("connection lifecycle", () => {
    it("should connect to the /terminal namespace with auth token", () => {
      renderHook(() => useTerminalSessions({ token: "test-token" }));

      expect(mockIo).toHaveBeenCalledWith(
        expect.stringContaining("/terminal"),
        expect.objectContaining({
          auth: { token: "test-token" },
        })
      );
    });

    it("should start disconnected", () => {
      const { result } = renderHook(() => useTerminalSessions({ token: "test-token" }));

      expect(result.current.isConnected).toBe(false);
    });

    it("should update isConnected when connect event fires", async () => {
      const { result } = renderHook(() => useTerminalSessions({ token: "test-token" }));

      act(() => {
        socketEventHandlers.connect?.(undefined);
      });

      await waitFor(() => {
        expect(result.current.isConnected).toBe(true);
      });
    });

    it("should set connectionError when connect_error fires", async () => {
      const { result } = renderHook(() => useTerminalSessions({ token: "test-token" }));

      act(() => {
        socketEventHandlers.connect_error?.(new Error("Connection refused"));
      });

      await waitFor(() => {
        expect(result.current.connectionError).toBe("Connection refused");
        expect(result.current.isConnected).toBe(false);
      });
    });

    it("should not connect when token is empty", () => {
      renderHook(() => useTerminalSessions({ token: "" }));

      expect(mockIo).not.toHaveBeenCalled();
    });

    it("should disconnect socket on unmount", () => {
      const { unmount } = renderHook(() => useTerminalSessions({ token: "test-token" }));

      unmount();

      expect(mockSocket.disconnect).toHaveBeenCalled();
    });
  });

  // ==========================================
  // Session creation
  // ==========================================

  describe("createSession", () => {
    it("should emit terminal:create when connected", () => {
      const { result } = renderHook(() => useTerminalSessions({ token: "test-token" }));

      act(() => {
        socketEventHandlers.connect?.(undefined);
      });

      act(() => {
        result.current.createSession({ name: "bash", cols: 120, rows: 40 });
      });

      expect(mockSocket.emit).toHaveBeenCalledWith("terminal:create", {
        name: "bash",
        cols: 120,
        rows: 40,
      });
    });

    it("should not emit terminal:create when disconnected", () => {
      mockSocket.connected = false;
      const { result } = renderHook(() => useTerminalSessions({ token: "test-token" }));

      act(() => {
        result.current.createSession();
      });

      expect(mockSocket.emit).not.toHaveBeenCalledWith("terminal:create", expect.anything());
    });

    it("should add session to sessions map when terminal:created fires", async () => {
      const { result } = renderHook(() => useTerminalSessions({ token: "test-token" }));

      act(() => {
        socketEventHandlers["terminal:created"]?.({
          sessionId: "session-1",
          name: "Terminal 1",
          cols: 80,
          rows: 24,
        });
      });

      await waitFor(() => {
        expect(result.current.sessions.has("session-1")).toBe(true);
        expect(result.current.sessions.get("session-1")?.name).toBe("Terminal 1");
        expect(result.current.sessions.get("session-1")?.status).toBe("active");
      });
    });

    it("should set first created session as active", async () => {
      const { result } = renderHook(() => useTerminalSessions({ token: "test-token" }));

      act(() => {
        socketEventHandlers["terminal:created"]?.({
          sessionId: "session-1",
          name: "Terminal 1",
          cols: 80,
          rows: 24,
        });
      });

      await waitFor(() => {
        expect(result.current.activeSessionId).toBe("session-1");
      });
    });

    it("should not change active session when a second session is created", async () => {
      const { result } = renderHook(() => useTerminalSessions({ token: "test-token" }));

      act(() => {
        socketEventHandlers["terminal:created"]?.({
          sessionId: "session-1",
          name: "Terminal 1",
          cols: 80,
          rows: 24,
        });
      });

      await waitFor(() => {
        expect(result.current.activeSessionId).toBe("session-1");
      });

      act(() => {
        socketEventHandlers["terminal:created"]?.({
          sessionId: "session-2",
          name: "Terminal 2",
          cols: 80,
          rows: 24,
        });
      });

      await waitFor(() => {
        expect(result.current.sessions.size).toBe(2);
        // Active session should remain session-1
        expect(result.current.activeSessionId).toBe("session-1");
      });
    });

    it("should manage multiple sessions in the sessions map", async () => {
      const { result } = renderHook(() => useTerminalSessions({ token: "test-token" }));

      act(() => {
        socketEventHandlers["terminal:created"]?.({
          sessionId: "session-1",
          name: "Terminal 1",
          cols: 80,
          rows: 24,
        });
        socketEventHandlers["terminal:created"]?.({
          sessionId: "session-2",
          name: "Terminal 2",
          cols: 80,
          rows: 24,
        });
      });

      await waitFor(() => {
        expect(result.current.sessions.size).toBe(2);
        expect(result.current.sessions.has("session-1")).toBe(true);
        expect(result.current.sessions.has("session-2")).toBe(true);
      });
    });
  });

  // ==========================================
  // Session close
  // ==========================================

  describe("closeSession", () => {
    it("should emit terminal:close and remove session from map", async () => {
      const { result } = renderHook(() => useTerminalSessions({ token: "test-token" }));

      act(() => {
        socketEventHandlers["terminal:created"]?.({
          sessionId: "session-1",
          name: "Terminal 1",
          cols: 80,
          rows: 24,
        });
      });

      await waitFor(() => {
        expect(result.current.sessions.has("session-1")).toBe(true);
      });

      act(() => {
        result.current.closeSession("session-1");
      });

      expect(mockSocket.emit).toHaveBeenCalledWith("terminal:close", {
        sessionId: "session-1",
      });

      await waitFor(() => {
        expect(result.current.sessions.has("session-1")).toBe(false);
      });
    });

    it("should switch active session to another when active is closed", async () => {
      const { result } = renderHook(() => useTerminalSessions({ token: "test-token" }));

      act(() => {
        socketEventHandlers["terminal:created"]?.({
          sessionId: "session-1",
          name: "Terminal 1",
          cols: 80,
          rows: 24,
        });
        socketEventHandlers["terminal:created"]?.({
          sessionId: "session-2",
          name: "Terminal 2",
          cols: 80,
          rows: 24,
        });
      });

      await waitFor(() => {
        expect(result.current.activeSessionId).toBe("session-1");
      });

      act(() => {
        result.current.closeSession("session-1");
      });

      await waitFor(() => {
        // Should switch to session-2
        expect(result.current.activeSessionId).toBe("session-2");
        expect(result.current.sessions.has("session-1")).toBe(false);
      });
    });

    it("should set activeSessionId to null when last session is closed", async () => {
      const { result } = renderHook(() => useTerminalSessions({ token: "test-token" }));

      act(() => {
        socketEventHandlers["terminal:created"]?.({
          sessionId: "session-1",
          name: "Terminal 1",
          cols: 80,
          rows: 24,
        });
      });

      await waitFor(() => {
        expect(result.current.activeSessionId).toBe("session-1");
      });

      act(() => {
        result.current.closeSession("session-1");
      });

      await waitFor(() => {
        expect(result.current.sessions.size).toBe(0);
        expect(result.current.activeSessionId).toBeNull();
      });
    });
  });

  // ==========================================
  // Rename session
  // ==========================================

  describe("renameSession", () => {
    it("should update the session name in the sessions map", async () => {
      const { result } = renderHook(() => useTerminalSessions({ token: "test-token" }));

      act(() => {
        socketEventHandlers["terminal:created"]?.({
          sessionId: "session-1",
          name: "Terminal 1",
          cols: 80,
          rows: 24,
        });
      });

      await waitFor(() => {
        expect(result.current.sessions.get("session-1")?.name).toBe("Terminal 1");
      });

      act(() => {
        result.current.renameSession("session-1", "My Custom Shell");
      });

      await waitFor(() => {
        expect(result.current.sessions.get("session-1")?.name).toBe("My Custom Shell");
      });
    });

    it("should not affect other session names", async () => {
      const { result } = renderHook(() => useTerminalSessions({ token: "test-token" }));

      act(() => {
        socketEventHandlers["terminal:created"]?.({
          sessionId: "session-1",
          name: "Terminal 1",
          cols: 80,
          rows: 24,
        });
        socketEventHandlers["terminal:created"]?.({
          sessionId: "session-2",
          name: "Terminal 2",
          cols: 80,
          rows: 24,
        });
      });

      act(() => {
        result.current.renameSession("session-1", "Custom");
      });

      await waitFor(() => {
        expect(result.current.sessions.get("session-1")?.name).toBe("Custom");
        expect(result.current.sessions.get("session-2")?.name).toBe("Terminal 2");
      });
    });
  });

  // ==========================================
  // setActiveSession
  // ==========================================

  describe("setActiveSession", () => {
    it("should update activeSessionId", async () => {
      const { result } = renderHook(() => useTerminalSessions({ token: "test-token" }));

      act(() => {
        socketEventHandlers["terminal:created"]?.({
          sessionId: "session-1",
          name: "Terminal 1",
          cols: 80,
          rows: 24,
        });
        socketEventHandlers["terminal:created"]?.({
          sessionId: "session-2",
          name: "Terminal 2",
          cols: 80,
          rows: 24,
        });
      });

      await waitFor(() => {
        expect(result.current.activeSessionId).toBe("session-1");
      });

      act(() => {
        result.current.setActiveSession("session-2");
      });

      await waitFor(() => {
        expect(result.current.activeSessionId).toBe("session-2");
      });
    });
  });

  // ==========================================
  // sendInput
  // ==========================================

  describe("sendInput", () => {
    it("should emit terminal:input with sessionId and data", () => {
      const { result } = renderHook(() => useTerminalSessions({ token: "test-token" }));

      act(() => {
        socketEventHandlers.connect?.(undefined);
      });

      act(() => {
        result.current.sendInput("session-1", "ls -la\n");
      });

      expect(mockSocket.emit).toHaveBeenCalledWith("terminal:input", {
        sessionId: "session-1",
        data: "ls -la\n",
      });
    });

    it("should not emit when disconnected", () => {
      mockSocket.connected = false;
      const { result } = renderHook(() => useTerminalSessions({ token: "test-token" }));

      act(() => {
        result.current.sendInput("session-1", "ls\n");
      });

      expect(mockSocket.emit).not.toHaveBeenCalledWith("terminal:input", expect.anything());
    });
  });

  // ==========================================
  // resize
  // ==========================================

  describe("resize", () => {
    it("should emit terminal:resize with sessionId, cols, and rows", () => {
      const { result } = renderHook(() => useTerminalSessions({ token: "test-token" }));

      act(() => {
        socketEventHandlers.connect?.(undefined);
      });

      act(() => {
        result.current.resize("session-1", 120, 40);
      });

      expect(mockSocket.emit).toHaveBeenCalledWith("terminal:resize", {
        sessionId: "session-1",
        cols: 120,
        rows: 40,
      });
    });
  });

  // ==========================================
  // Output callback routing
  // ==========================================

  describe("registerOutputCallback", () => {
    it("should call the registered callback when terminal:output fires for that session", () => {
      const { result } = renderHook(() => useTerminalSessions({ token: "test-token" }));
      const cb = vi.fn();

      act(() => {
        result.current.registerOutputCallback("session-1", cb);
      });

      act(() => {
        socketEventHandlers["terminal:output"]?.({
          sessionId: "session-1",
          data: "hello world\r\n",
        });
      });

      expect(cb).toHaveBeenCalledWith("hello world\r\n");
    });

    it("should not call callback for a different session", () => {
      const { result } = renderHook(() => useTerminalSessions({ token: "test-token" }));
      const cbSession1 = vi.fn();
      const cbSession2 = vi.fn();

      act(() => {
        result.current.registerOutputCallback("session-1", cbSession1);
        result.current.registerOutputCallback("session-2", cbSession2);
      });

      act(() => {
        socketEventHandlers["terminal:output"]?.({
          sessionId: "session-1",
          data: "output for session 1",
        });
      });

      expect(cbSession1).toHaveBeenCalledWith("output for session 1");
      expect(cbSession2).not.toHaveBeenCalled();
    });

    it("should stop calling callback after unsubscribing", () => {
      const { result } = renderHook(() => useTerminalSessions({ token: "test-token" }));
      const cb = vi.fn();
      let unsubscribe: (() => void) | undefined;

      act(() => {
        unsubscribe = result.current.registerOutputCallback("session-1", cb);
      });

      act(() => {
        unsubscribe?.();
      });

      act(() => {
        socketEventHandlers["terminal:output"]?.({
          sessionId: "session-1",
          data: "should not arrive",
        });
      });

      expect(cb).not.toHaveBeenCalled();
    });

    it("should support multiple callbacks for the same session", () => {
      const { result } = renderHook(() => useTerminalSessions({ token: "test-token" }));
      const cb1 = vi.fn();
      const cb2 = vi.fn();

      act(() => {
        result.current.registerOutputCallback("session-1", cb1);
        result.current.registerOutputCallback("session-1", cb2);
      });

      act(() => {
        socketEventHandlers["terminal:output"]?.({
          sessionId: "session-1",
          data: "broadcast",
        });
      });

      expect(cb1).toHaveBeenCalledWith("broadcast");
      expect(cb2).toHaveBeenCalledWith("broadcast");
    });
  });

  // ==========================================
  // Exit event
  // ==========================================

  describe("terminal:exit handling", () => {
    it("should mark session as exited when terminal:exit fires", async () => {
      const { result } = renderHook(() => useTerminalSessions({ token: "test-token" }));

      act(() => {
        socketEventHandlers["terminal:created"]?.({
          sessionId: "session-1",
          name: "Terminal 1",
          cols: 80,
          rows: 24,
        });
      });

      await waitFor(() => {
        expect(result.current.sessions.get("session-1")?.status).toBe("active");
      });

      act(() => {
        socketEventHandlers["terminal:exit"]?.({
          sessionId: "session-1",
          exitCode: 0,
        });
      });

      await waitFor(() => {
        expect(result.current.sessions.get("session-1")?.status).toBe("exited");
        expect(result.current.sessions.get("session-1")?.exitCode).toBe(0);
      });
    });

    it("should not remove the session from the map on exit", async () => {
      const { result } = renderHook(() => useTerminalSessions({ token: "test-token" }));

      act(() => {
        socketEventHandlers["terminal:created"]?.({
          sessionId: "session-1",
          name: "Terminal 1",
          cols: 80,
          rows: 24,
        });
      });

      act(() => {
        socketEventHandlers["terminal:exit"]?.({
          sessionId: "session-1",
          exitCode: 1,
        });
      });

      await waitFor(() => {
        // Session remains in map — user can restart or close it manually
        expect(result.current.sessions.has("session-1")).toBe(true);
      });
    });
  });

  // ==========================================
  // Disconnect handling
  // ==========================================

  describe("disconnect handling", () => {
    it("should mark all active sessions as exited on disconnect", async () => {
      const { result } = renderHook(() => useTerminalSessions({ token: "test-token" }));

      act(() => {
        socketEventHandlers["terminal:created"]?.({
          sessionId: "session-1",
          name: "Terminal 1",
          cols: 80,
          rows: 24,
        });
        socketEventHandlers["terminal:created"]?.({
          sessionId: "session-2",
          name: "Terminal 2",
          cols: 80,
          rows: 24,
        });
      });

      await waitFor(() => {
        expect(result.current.sessions.size).toBe(2);
      });

      act(() => {
        socketEventHandlers.disconnect?.(undefined);
      });

      await waitFor(() => {
        expect(result.current.isConnected).toBe(false);
        expect(result.current.sessions.get("session-1")?.status).toBe("exited");
        expect(result.current.sessions.get("session-2")?.status).toBe("exited");
      });
    });
  });
});
