/**
 * TerminalService Tests
 *
 * Unit tests for PTY session management: create, write, resize, close,
 * workspace cleanup, and access control.
 */

import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
import type { Socket } from "socket.io";
import { TerminalService, MAX_SESSIONS_PER_WORKSPACE } from "./terminal.service";

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

// Mock node-pty before importing service
const mockPtyProcess = {
  onData: vi.fn(),
  onExit: vi.fn(),
  write: vi.fn(),
  resize: vi.fn(),
  kill: vi.fn(),
  pid: 12345,
};

vi.mock("node-pty", () => ({
  spawn: vi.fn(() => mockPtyProcess),
}));

function createMockSocket(id = "socket-1"): Socket {
  return {
    id,
    emit: vi.fn(),
    join: vi.fn(),
    leave: vi.fn(),
    disconnect: vi.fn(),
    data: {},
  } as unknown as Socket;
}

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

describe("TerminalService", () => {
  let service: TerminalService;
  let mockSocket: Socket;

  beforeEach(async () => {
    vi.clearAllMocks();
    // Reset mock implementations
    mockPtyProcess.onData.mockImplementation((_cb: (data: string) => void) => {});
    mockPtyProcess.onExit.mockImplementation(
      (_cb: (e: { exitCode: number; signal?: number }) => void) => {}
    );
    service = new TerminalService();
    // Trigger lazy import of node-pty (uses dynamic import(), intercepted by vi.mock)
    await service.onModuleInit();
    mockSocket = createMockSocket();
  });

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

  // ==========================================
  // createSession
  // ==========================================
  describe("createSession", () => {
    it("should create a PTY session and return sessionId", () => {
      const result = service.createSession(mockSocket, {
        workspaceId: "ws-1",
        socketId: "socket-1",
      });

      expect(result.sessionId).toBeDefined();
      expect(typeof result.sessionId).toBe("string");
      expect(result.cols).toBe(80);
      expect(result.rows).toBe(24);
    });

    it("should use provided cols and rows", () => {
      const result = service.createSession(mockSocket, {
        workspaceId: "ws-1",
        socketId: "socket-1",
        cols: 120,
        rows: 40,
      });

      expect(result.cols).toBe(120);
      expect(result.rows).toBe(40);
    });

    it("should return the provided session name", () => {
      const result = service.createSession(mockSocket, {
        workspaceId: "ws-1",
        socketId: "socket-1",
        name: "my-terminal",
      });

      expect(result.name).toBe("my-terminal");
    });

    it("should wire PTY onData to emit terminal:output", () => {
      let dataCallback: ((data: string) => void) | undefined;
      mockPtyProcess.onData.mockImplementation((cb: (data: string) => void) => {
        dataCallback = cb;
      });

      const result = service.createSession(mockSocket, {
        workspaceId: "ws-1",
        socketId: "socket-1",
      });

      expect(dataCallback).toBeDefined();
      dataCallback!("hello world");

      expect(mockSocket.emit).toHaveBeenCalledWith("terminal:output", {
        sessionId: result.sessionId,
        data: "hello world",
      });
    });

    it("should wire PTY onExit to emit terminal:exit and cleanup", () => {
      let exitCallback: ((e: { exitCode: number; signal?: number }) => void) | undefined;
      mockPtyProcess.onExit.mockImplementation(
        (cb: (e: { exitCode: number; signal?: number }) => void) => {
          exitCallback = cb;
        }
      );

      const result = service.createSession(mockSocket, {
        workspaceId: "ws-1",
        socketId: "socket-1",
      });

      expect(exitCallback).toBeDefined();
      exitCallback!({ exitCode: 0 });

      expect(mockSocket.emit).toHaveBeenCalledWith("terminal:exit", {
        sessionId: result.sessionId,
        exitCode: 0,
        signal: undefined,
      });

      // Session should be cleaned up
      expect(service.sessionBelongsToWorkspace(result.sessionId, "ws-1")).toBe(false);
      expect(service.getWorkspaceSessionCount("ws-1")).toBe(0);
    });

    it("should throw when workspace session limit is reached", () => {
      const limit = MAX_SESSIONS_PER_WORKSPACE;

      for (let i = 0; i < limit; i++) {
        service.createSession(createMockSocket(`socket-${String(i)}`), {
          workspaceId: "ws-limit",
          socketId: `socket-${String(i)}`,
        });
      }

      expect(() =>
        service.createSession(createMockSocket("socket-overflow"), {
          workspaceId: "ws-limit",
          socketId: "socket-overflow",
        })
      ).toThrow(/maximum/i);
    });

    it("should allow sessions in different workspaces independently", () => {
      service.createSession(mockSocket, { workspaceId: "ws-a", socketId: "s1" });
      service.createSession(createMockSocket("s2"), { workspaceId: "ws-b", socketId: "s2" });

      expect(service.getWorkspaceSessionCount("ws-a")).toBe(1);
      expect(service.getWorkspaceSessionCount("ws-b")).toBe(1);
    });
  });

  // ==========================================
  // writeToSession
  // ==========================================
  describe("writeToSession", () => {
    it("should write data to PTY", () => {
      const result = service.createSession(mockSocket, {
        workspaceId: "ws-1",
        socketId: "socket-1",
      });

      service.writeToSession(result.sessionId, "ls -la\n");

      expect(mockPtyProcess.write).toHaveBeenCalledWith("ls -la\n");
    });

    it("should throw for unknown sessionId", () => {
      expect(() => service.writeToSession("nonexistent-id", "data")).toThrow(/not found/i);
    });
  });

  // ==========================================
  // resizeSession
  // ==========================================
  describe("resizeSession", () => {
    it("should resize PTY dimensions", () => {
      const result = service.createSession(mockSocket, {
        workspaceId: "ws-1",
        socketId: "socket-1",
      });

      service.resizeSession(result.sessionId, 132, 50);

      expect(mockPtyProcess.resize).toHaveBeenCalledWith(132, 50);
    });

    it("should throw for unknown sessionId", () => {
      expect(() => service.resizeSession("nonexistent-id", 80, 24)).toThrow(/not found/i);
    });
  });

  // ==========================================
  // closeSession
  // ==========================================
  describe("closeSession", () => {
    it("should kill PTY and return true for existing session", () => {
      const result = service.createSession(mockSocket, {
        workspaceId: "ws-1",
        socketId: "socket-1",
      });

      const closed = service.closeSession(result.sessionId);

      expect(closed).toBe(true);
      expect(mockPtyProcess.kill).toHaveBeenCalled();
      expect(service.sessionBelongsToWorkspace(result.sessionId, "ws-1")).toBe(false);
    });

    it("should return false for nonexistent sessionId", () => {
      const closed = service.closeSession("does-not-exist");
      expect(closed).toBe(false);
    });

    it("should clean up workspace tracking after close", () => {
      const result = service.createSession(mockSocket, {
        workspaceId: "ws-1",
        socketId: "socket-1",
      });

      expect(service.getWorkspaceSessionCount("ws-1")).toBe(1);
      service.closeSession(result.sessionId);
      expect(service.getWorkspaceSessionCount("ws-1")).toBe(0);
    });

    it("should not throw if PTY kill throws", () => {
      mockPtyProcess.kill.mockImplementationOnce(() => {
        throw new Error("PTY already dead");
      });

      const result = service.createSession(mockSocket, {
        workspaceId: "ws-1",
        socketId: "socket-1",
      });

      expect(() => service.closeSession(result.sessionId)).not.toThrow();
    });
  });

  // ==========================================
  // closeWorkspaceSessions
  // ==========================================
  describe("closeWorkspaceSessions", () => {
    it("should kill all sessions for a workspace", () => {
      service.createSession(mockSocket, { workspaceId: "ws-1", socketId: "s1" });
      service.createSession(createMockSocket("s2"), { workspaceId: "ws-1", socketId: "s2" });

      expect(service.getWorkspaceSessionCount("ws-1")).toBe(2);

      service.closeWorkspaceSessions("ws-1");

      expect(service.getWorkspaceSessionCount("ws-1")).toBe(0);
      expect(mockPtyProcess.kill).toHaveBeenCalledTimes(2);
    });

    it("should not affect sessions in other workspaces", () => {
      service.createSession(mockSocket, { workspaceId: "ws-1", socketId: "s1" });
      service.createSession(createMockSocket("s2"), { workspaceId: "ws-2", socketId: "s2" });

      service.closeWorkspaceSessions("ws-1");

      expect(service.getWorkspaceSessionCount("ws-1")).toBe(0);
      expect(service.getWorkspaceSessionCount("ws-2")).toBe(1);
    });

    it("should not throw for workspaces with no sessions", () => {
      expect(() => service.closeWorkspaceSessions("ws-nonexistent")).not.toThrow();
    });
  });

  // ==========================================
  // sessionBelongsToWorkspace
  // ==========================================
  describe("sessionBelongsToWorkspace", () => {
    it("should return true for a session belonging to the workspace", () => {
      const result = service.createSession(mockSocket, {
        workspaceId: "ws-1",
        socketId: "socket-1",
      });

      expect(service.sessionBelongsToWorkspace(result.sessionId, "ws-1")).toBe(true);
    });

    it("should return false for a session in a different workspace", () => {
      const result = service.createSession(mockSocket, {
        workspaceId: "ws-1",
        socketId: "socket-1",
      });

      expect(service.sessionBelongsToWorkspace(result.sessionId, "ws-2")).toBe(false);
    });

    it("should return false for a nonexistent sessionId", () => {
      expect(service.sessionBelongsToWorkspace("no-such-id", "ws-1")).toBe(false);
    });
  });

  // ==========================================
  // getWorkspaceSessionCount
  // ==========================================
  describe("getWorkspaceSessionCount", () => {
    it("should return 0 for workspace with no sessions", () => {
      expect(service.getWorkspaceSessionCount("empty-ws")).toBe(0);
    });

    it("should track session count accurately", () => {
      service.createSession(mockSocket, { workspaceId: "ws-count", socketId: "s1" });
      expect(service.getWorkspaceSessionCount("ws-count")).toBe(1);

      service.createSession(createMockSocket("s2"), { workspaceId: "ws-count", socketId: "s2" });
      expect(service.getWorkspaceSessionCount("ws-count")).toBe(2);
    });
  });
});
