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

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { renderHook, act, waitFor } from "@testing-library/react";
import { useTerminal } from "./useTerminal";
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("useTerminal", () => {
  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
  // ==========================================

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

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

    it("should start disconnected and update when connected event fires", async () => {
      const { result } = renderHook(() =>
        useTerminal({
          token: "test-token",
        })
      );

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

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

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

    it("should update sessionId when terminal:created event fires", async () => {
      const { result } = renderHook(() =>
        useTerminal({
          token: "test-token",
        })
      );

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

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

    it("should clear sessionId when disconnect event fires", async () => {
      const { result } = renderHook(() =>
        useTerminal({
          token: "test-token",
        })
      );

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

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

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

      await waitFor(() => {
        expect(result.current.isConnected).toBe(false);
        expect(result.current.sessionId).toBeNull();
      });
    });

    it("should set connectionError when connect_error fires", async () => {
      const { result } = renderHook(() =>
        useTerminal({
          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(() =>
        useTerminal({
          token: "",
        })
      );

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

  // ==========================================
  // Output and exit callbacks
  // ==========================================

  describe("event callbacks", () => {
    it("should call onOutput when terminal:output fires", () => {
      const onOutput = vi.fn();

      renderHook(() =>
        useTerminal({
          token: "test-token",
          onOutput,
        })
      );

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

      expect(onOutput).toHaveBeenCalledWith("session-abc", "hello world\r\n");
    });

    it("should call onExit when terminal:exit fires and clear sessionId", async () => {
      const onExit = vi.fn();

      const { result } = renderHook(() =>
        useTerminal({
          token: "test-token",
          onExit,
        })
      );

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

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

      await waitFor(() => {
        expect(onExit).toHaveBeenCalledWith({ sessionId: "session-abc", exitCode: 0 });
        expect(result.current.sessionId).toBeNull();
      });
    });

    it("should call onError when terminal:error fires", () => {
      const onError = vi.fn();

      renderHook(() =>
        useTerminal({
          token: "test-token",
          onError,
        })
      );

      act(() => {
        socketEventHandlers["terminal:error"]?.({
          message: "PTY spawn failed",
        });
      });

      expect(onError).toHaveBeenCalledWith("PTY spawn failed");
    });
  });

  // ==========================================
  // Control functions
  // ==========================================

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

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

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

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

    it("should not emit terminal:create when disconnected", () => {
      mockSocket.connected = false;

      const { result } = renderHook(() =>
        useTerminal({
          token: "test-token",
        })
      );

      act(() => {
        result.current.createSession({ cols: 80, rows: 24 });
      });

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

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

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

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

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

    it("should not emit when no sessionId is set", () => {
      const { result } = renderHook(() =>
        useTerminal({
          token: "test-token",
        })
      );

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

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

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

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

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

      act(() => {
        result.current.resize(100, 30);
      });

      expect(mockSocket.emit).toHaveBeenCalledWith("terminal:resize", {
        sessionId: "session-abc",
        cols: 100,
        rows: 30,
      });
    });
  });

  describe("closeSession", () => {
    it("should emit terminal:close and clear sessionId", async () => {
      const { result } = renderHook(() =>
        useTerminal({
          token: "test-token",
        })
      );

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

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

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

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

      await waitFor(() => {
        expect(result.current.sessionId).toBeNull();
      });
    });
  });

  // ==========================================
  // Cleanup
  // ==========================================

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

      unmount();

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

    it("should emit terminal:close for active session on unmount", () => {
      const { result, unmount } = renderHook(() =>
        useTerminal({
          token: "test-token",
        })
      );

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

      expect(result.current.sessionId).toBe("session-abc");

      unmount();

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