/**
 * TerminalGateway Tests
 *
 * Unit tests for WebSocket terminal gateway:
 * - Authentication on connection
 * - terminal:create event handling
 * - terminal:input event handling
 * - terminal:resize event handling
 * - terminal:close event handling
 * - disconnect cleanup
 * - Error paths
 */

import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
import type { Socket } from "socket.io";
import { TerminalGateway } from "./terminal.gateway";
import { TerminalService } from "./terminal.service";
import { AuthService } from "../auth/auth.service";
import { PrismaService } from "../prisma/prisma.service";

// ==========================================
// Mocks
// ==========================================

// Mock node-pty globally so TerminalService doesn't fail to import
vi.mock("node-pty", () => ({
  spawn: vi.fn(() => ({
    onData: vi.fn(),
    onExit: vi.fn(),
    write: vi.fn(),
    resize: vi.fn(),
    kill: vi.fn(),
    pid: 1000,
  })),
}));

interface AuthenticatedSocket extends Socket {
  data: {
    userId?: string;
    workspaceId?: string;
  };
}

function createMockSocket(id = "test-socket-id"): AuthenticatedSocket {
  return {
    id,
    emit: vi.fn(),
    join: vi.fn(),
    leave: vi.fn(),
    disconnect: vi.fn(),
    data: {},
    handshake: {
      auth: { token: "valid-token" },
      query: {},
      headers: {},
    },
  } as unknown as AuthenticatedSocket;
}

function createMockAuthService() {
  return {
    verifySession: vi.fn().mockResolvedValue({
      user: { id: "user-123" },
      session: { id: "session-123" },
    }),
  };
}

function createMockPrismaService() {
  return {
    workspaceMember: {
      findFirst: vi.fn().mockResolvedValue({
        userId: "user-123",
        workspaceId: "workspace-456",
        role: "MEMBER",
      }),
    },
  };
}

function createMockTerminalService() {
  return {
    createSession: vi.fn().mockReturnValue({
      sessionId: "session-uuid-1",
      name: undefined,
      cols: 80,
      rows: 24,
    }),
    writeToSession: vi.fn(),
    resizeSession: vi.fn(),
    closeSession: vi.fn().mockReturnValue(true),
    closeWorkspaceSessions: vi.fn(),
    sessionBelongsToWorkspace: vi.fn().mockReturnValue(true),
    getWorkspaceSessionCount: vi.fn().mockReturnValue(0),
  };
}

// ==========================================
// Tests
// ==========================================

describe("TerminalGateway", () => {
  let gateway: TerminalGateway;
  let mockAuthService: ReturnType<typeof createMockAuthService>;
  let mockPrismaService: ReturnType<typeof createMockPrismaService>;
  let mockTerminalService: ReturnType<typeof createMockTerminalService>;
  let mockClient: AuthenticatedSocket;

  beforeEach(() => {
    mockAuthService = createMockAuthService();
    mockPrismaService = createMockPrismaService();
    mockTerminalService = createMockTerminalService();
    mockClient = createMockSocket();

    gateway = new TerminalGateway(
      mockAuthService as unknown as AuthService,
      mockPrismaService as unknown as PrismaService,
      mockTerminalService as unknown as TerminalService
    );

    vi.clearAllMocks();
  });

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

  // ==========================================
  // handleConnection (authentication)
  // ==========================================
  describe("handleConnection", () => {
    it("should authenticate client and join workspace room on valid token", async () => {
      mockAuthService.verifySession.mockResolvedValue({
        user: { id: "user-123" },
      });
      mockPrismaService.workspaceMember.findFirst.mockResolvedValue({
        userId: "user-123",
        workspaceId: "workspace-456",
        role: "MEMBER",
      });

      await gateway.handleConnection(mockClient);

      expect(mockAuthService.verifySession).toHaveBeenCalledWith("valid-token");
      expect(mockClient.data.userId).toBe("user-123");
      expect(mockClient.data.workspaceId).toBe("workspace-456");
      expect(mockClient.join).toHaveBeenCalledWith("terminal:workspace-456");
    });

    it("should disconnect and emit error if no token provided", async () => {
      const clientNoToken = createMockSocket("no-token");
      clientNoToken.handshake = {
        auth: {},
        query: {},
        headers: {},
      } as typeof clientNoToken.handshake;

      await gateway.handleConnection(clientNoToken);

      expect(clientNoToken.disconnect).toHaveBeenCalled();
      expect(clientNoToken.emit).toHaveBeenCalledWith(
        "terminal:error",
        expect.objectContaining({ message: expect.stringContaining("no token") })
      );
    });

    it("should disconnect and emit error if token is invalid", async () => {
      mockAuthService.verifySession.mockResolvedValue(null);

      await gateway.handleConnection(mockClient);

      expect(mockClient.disconnect).toHaveBeenCalled();
      expect(mockClient.emit).toHaveBeenCalledWith(
        "terminal:error",
        expect.objectContaining({ message: expect.stringContaining("invalid") })
      );
    });

    it("should disconnect and emit error if no workspace access", async () => {
      mockAuthService.verifySession.mockResolvedValue({ user: { id: "user-123" } });
      mockPrismaService.workspaceMember.findFirst.mockResolvedValue(null);

      await gateway.handleConnection(mockClient);

      expect(mockClient.disconnect).toHaveBeenCalled();
      expect(mockClient.emit).toHaveBeenCalledWith(
        "terminal:error",
        expect.objectContaining({ message: expect.stringContaining("workspace") })
      );
    });

    it("should disconnect and emit error if auth throws", async () => {
      mockAuthService.verifySession.mockRejectedValue(new Error("Auth service down"));

      await gateway.handleConnection(mockClient);

      expect(mockClient.disconnect).toHaveBeenCalled();
      expect(mockClient.emit).toHaveBeenCalledWith(
        "terminal:error",
        expect.objectContaining({ message: expect.any(String) })
      );
    });

    it("should extract token from handshake.query as fallback", async () => {
      const clientQueryToken = createMockSocket("query-token-client");
      clientQueryToken.handshake = {
        auth: {},
        query: { token: "query-token" },
        headers: {},
      } as typeof clientQueryToken.handshake;

      mockAuthService.verifySession.mockResolvedValue({ user: { id: "user-123" } });
      mockPrismaService.workspaceMember.findFirst.mockResolvedValue({
        userId: "user-123",
        workspaceId: "workspace-456",
        role: "MEMBER",
      });

      await gateway.handleConnection(clientQueryToken);

      expect(mockAuthService.verifySession).toHaveBeenCalledWith("query-token");
    });

    it("should extract token from Authorization header as last fallback", async () => {
      const clientHeaderToken = createMockSocket("header-token-client");
      clientHeaderToken.handshake = {
        auth: {},
        query: {},
        headers: { authorization: "Bearer header-token" },
      } as typeof clientHeaderToken.handshake;

      mockAuthService.verifySession.mockResolvedValue({ user: { id: "user-123" } });
      mockPrismaService.workspaceMember.findFirst.mockResolvedValue({
        userId: "user-123",
        workspaceId: "workspace-456",
        role: "MEMBER",
      });

      await gateway.handleConnection(clientHeaderToken);

      expect(mockAuthService.verifySession).toHaveBeenCalledWith("header-token");
    });
  });

  // ==========================================
  // handleDisconnect
  // ==========================================
  describe("handleDisconnect", () => {
    it("should close all workspace sessions on disconnect", async () => {
      await gateway.handleConnection(mockClient);
      vi.clearAllMocks();

      gateway.handleDisconnect(mockClient);

      expect(mockTerminalService.closeWorkspaceSessions).toHaveBeenCalledWith("workspace-456");
    });

    it("should not throw for unauthenticated client disconnect", () => {
      const unauthClient = createMockSocket("unauth-disconnect");

      expect(() => gateway.handleDisconnect(unauthClient)).not.toThrow();
      expect(mockTerminalService.closeWorkspaceSessions).not.toHaveBeenCalled();
    });
  });

  // ==========================================
  // handleCreate (terminal:create)
  // ==========================================
  describe("handleCreate", () => {
    beforeEach(async () => {
      mockAuthService.verifySession.mockResolvedValue({ user: { id: "user-123" } });
      mockPrismaService.workspaceMember.findFirst.mockResolvedValue({
        userId: "user-123",
        workspaceId: "workspace-456",
        role: "MEMBER",
      });
      await gateway.handleConnection(mockClient);
      vi.clearAllMocks();
    });

    it("should create a PTY session and emit terminal:created", async () => {
      mockTerminalService.createSession.mockReturnValue({
        sessionId: "new-session-id",
        cols: 80,
        rows: 24,
      });

      await gateway.handleCreate(mockClient, {});

      expect(mockTerminalService.createSession).toHaveBeenCalled();
      expect(mockClient.emit).toHaveBeenCalledWith(
        "terminal:created",
        expect.objectContaining({ sessionId: "new-session-id" })
      );
    });

    it("should pass cols, rows, cwd, name to service", async () => {
      await gateway.handleCreate(mockClient, {
        cols: 132,
        rows: 50,
        cwd: "/home/user",
        name: "my-shell",
      });

      expect(mockTerminalService.createSession).toHaveBeenCalledWith(
        expect.anything(),
        expect.objectContaining({ cols: 132, rows: 50, cwd: "/home/user", name: "my-shell" })
      );
    });

    it("should emit terminal:error if not authenticated", async () => {
      const unauthClient = createMockSocket("unauth");

      await gateway.handleCreate(unauthClient, {});

      expect(unauthClient.emit).toHaveBeenCalledWith(
        "terminal:error",
        expect.objectContaining({ message: expect.stringContaining("authenticated") })
      );
    });

    it("should emit terminal:error if service throws (session limit)", async () => {
      mockTerminalService.createSession.mockImplementation(() => {
        throw new Error("Workspace has reached the maximum of 10 concurrent terminal sessions");
      });

      await gateway.handleCreate(mockClient, {});

      expect(mockClient.emit).toHaveBeenCalledWith(
        "terminal:error",
        expect.objectContaining({ message: expect.stringContaining("maximum") })
      );
    });

    it("should emit terminal:error for invalid payload (negative cols)", async () => {
      await gateway.handleCreate(mockClient, { cols: -1 });

      expect(mockClient.emit).toHaveBeenCalledWith(
        "terminal:error",
        expect.objectContaining({ message: expect.stringContaining("Invalid payload") })
      );
    });
  });

  // ==========================================
  // handleInput (terminal:input)
  // ==========================================
  describe("handleInput", () => {
    beforeEach(async () => {
      mockAuthService.verifySession.mockResolvedValue({ user: { id: "user-123" } });
      mockPrismaService.workspaceMember.findFirst.mockResolvedValue({
        userId: "user-123",
        workspaceId: "workspace-456",
        role: "MEMBER",
      });
      await gateway.handleConnection(mockClient);
      vi.clearAllMocks();
    });

    it("should write data to the PTY session", async () => {
      mockTerminalService.sessionBelongsToWorkspace.mockReturnValue(true);

      await gateway.handleInput(mockClient, { sessionId: "sess-1", data: "ls\n" });

      expect(mockTerminalService.writeToSession).toHaveBeenCalledWith("sess-1", "ls\n");
    });

    it("should emit terminal:error if session does not belong to workspace", async () => {
      mockTerminalService.sessionBelongsToWorkspace.mockReturnValue(false);

      await gateway.handleInput(mockClient, { sessionId: "alien-sess", data: "data" });

      expect(mockClient.emit).toHaveBeenCalledWith(
        "terminal:error",
        expect.objectContaining({ message: expect.stringContaining("not found") })
      );
      expect(mockTerminalService.writeToSession).not.toHaveBeenCalled();
    });

    it("should emit terminal:error if not authenticated", async () => {
      const unauthClient = createMockSocket("unauth");

      await gateway.handleInput(unauthClient, { sessionId: "sess-1", data: "x" });

      expect(unauthClient.emit).toHaveBeenCalledWith(
        "terminal:error",
        expect.objectContaining({ message: expect.stringContaining("authenticated") })
      );
    });

    it("should emit terminal:error for invalid payload (missing sessionId)", async () => {
      await gateway.handleInput(mockClient, { data: "some input" });

      expect(mockClient.emit).toHaveBeenCalledWith(
        "terminal:error",
        expect.objectContaining({ message: expect.stringContaining("Invalid payload") })
      );
    });
  });

  // ==========================================
  // handleResize (terminal:resize)
  // ==========================================
  describe("handleResize", () => {
    beforeEach(async () => {
      mockAuthService.verifySession.mockResolvedValue({ user: { id: "user-123" } });
      mockPrismaService.workspaceMember.findFirst.mockResolvedValue({
        userId: "user-123",
        workspaceId: "workspace-456",
        role: "MEMBER",
      });
      await gateway.handleConnection(mockClient);
      vi.clearAllMocks();
    });

    it("should resize the PTY session", async () => {
      mockTerminalService.sessionBelongsToWorkspace.mockReturnValue(true);

      await gateway.handleResize(mockClient, { sessionId: "sess-1", cols: 120, rows: 40 });

      expect(mockTerminalService.resizeSession).toHaveBeenCalledWith("sess-1", 120, 40);
    });

    it("should emit terminal:error if session does not belong to workspace", async () => {
      mockTerminalService.sessionBelongsToWorkspace.mockReturnValue(false);

      await gateway.handleResize(mockClient, { sessionId: "alien-sess", cols: 80, rows: 24 });

      expect(mockClient.emit).toHaveBeenCalledWith(
        "terminal:error",
        expect.objectContaining({ message: expect.stringContaining("not found") })
      );
    });

    it("should emit terminal:error for invalid payload (cols too large)", async () => {
      await gateway.handleResize(mockClient, { sessionId: "sess-1", cols: 9999, rows: 24 });

      expect(mockClient.emit).toHaveBeenCalledWith(
        "terminal:error",
        expect.objectContaining({ message: expect.stringContaining("Invalid payload") })
      );
    });
  });

  // ==========================================
  // handleClose (terminal:close)
  // ==========================================
  describe("handleClose", () => {
    beforeEach(async () => {
      mockAuthService.verifySession.mockResolvedValue({ user: { id: "user-123" } });
      mockPrismaService.workspaceMember.findFirst.mockResolvedValue({
        userId: "user-123",
        workspaceId: "workspace-456",
        role: "MEMBER",
      });
      await gateway.handleConnection(mockClient);
      vi.clearAllMocks();
    });

    it("should close an existing PTY session", async () => {
      mockTerminalService.sessionBelongsToWorkspace.mockReturnValue(true);
      mockTerminalService.closeSession.mockReturnValue(true);

      await gateway.handleClose(mockClient, { sessionId: "sess-1" });

      expect(mockTerminalService.closeSession).toHaveBeenCalledWith("sess-1");
    });

    it("should emit terminal:error if session does not belong to workspace", async () => {
      mockTerminalService.sessionBelongsToWorkspace.mockReturnValue(false);

      await gateway.handleClose(mockClient, { sessionId: "alien-sess" });

      expect(mockClient.emit).toHaveBeenCalledWith(
        "terminal:error",
        expect.objectContaining({ message: expect.stringContaining("not found") })
      );
    });

    it("should emit terminal:error if closeSession returns false (session gone)", async () => {
      mockTerminalService.sessionBelongsToWorkspace.mockReturnValue(true);
      mockTerminalService.closeSession.mockReturnValue(false);

      await gateway.handleClose(mockClient, { sessionId: "gone-sess" });

      expect(mockClient.emit).toHaveBeenCalledWith(
        "terminal:error",
        expect.objectContaining({ message: expect.stringContaining("not found") })
      );
    });

    it("should emit terminal:error for invalid payload (missing sessionId)", async () => {
      await gateway.handleClose(mockClient, {});

      expect(mockClient.emit).toHaveBeenCalledWith(
        "terminal:error",
        expect.objectContaining({ message: expect.stringContaining("Invalid payload") })
      );
    });
  });
});
