import { describe, it, expect, beforeEach, vi } from "vitest";
import { Types } from "mongoose";
import { CodeSession } from "../src/lib/code-session";
import {
  IChatSession,
  IProject,
  IUser,
  IDroneRegistration,
  ChatTurnStatus,
} from "@gadget/api";
import SocketService from "../src/services/socket";
import ChatSessionService from "../src/services/chat-session";
import { ChatTurn } from "../src/models/chat-turn";
import ChatSession from "../src/models/chat-session";
import { nanoid } from "nanoid";

// Mock dependencies
vi.mock("../src/services/socket");
vi.mock("../src/services/chat-session");
vi.mock("../src/models/chat-turn");
vi.mock("../src/models/chat-session");

describe("CodeSession", () => {
  let mockSocket: any;
  let mockUser: IUser;
  let mockDrone: IDroneRegistration;
  let mockChatSession: IChatSession;
  let mockProject: IProject;
  let codeSession: CodeSession;

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

    mockSocket = {
      id: "test-socket-id",
      on: vi.fn(),
      emit: vi.fn(),
    };

    mockUser = {
      _id: nanoid(),
      email: "test@example.com",
      displayName: "Test User",
    } as IUser;

    mockDrone = {
      _id: nanoid(),
      hostname: "test-host",
      workspaceDir: "/test/workspace",
      status: "available",
    } as IDroneRegistration;

    mockProject = {
      _id: nanoid(),
      slug: "test-project",
      name: "Test Project",
    } as IProject;

    mockChatSession = {
      _id: nanoid(),
      name: "New Chat Session",
      mode: "build",
      provider: nanoid(),
      selectedModel: "llama3.1",
      user: mockUser,
      project: mockProject,
      stats: {
        turnCount: 0,
        toolCallCount: 0,
        inputTokens: 0,
        outputTokens: 0,
      },
    } as IChatSession;

    codeSession = new CodeSession(mockSocket, mockUser);
  });

  describe("setSelectedDrone", () => {
    it("should set the selected drone", () => {
      codeSession.setSelectedDrone(mockDrone);
      expect(() => codeSession.setSelectedDrone(mockDrone)).not.toThrow();
    });
  });

  describe("setChatSession", () => {
    it("should set the chat session and project", () => {
      codeSession.setChatSession(mockChatSession, mockProject);
      expect(() =>
        codeSession.setChatSession(mockChatSession, mockProject),
      ).not.toThrow();
    });
  });

  describe("onSubmitPrompt", () => {
    let mockTurn: any;
    let mockDroneSession: any;
    let cb: any;

    beforeEach(() => {
      mockTurn = {
        _id: nanoid(),
        status: ChatTurnStatus.Processing,
        errorMessage: "",
        save: vi.fn().mockResolvedValue(undefined),
      };
      vi.mocked(ChatTurn).mockImplementation(function () {
        return mockTurn as any;
      });
      (vi.mocked(ChatTurn) as any).populate = vi
        .fn()
        .mockResolvedValue(mockTurn);

      mockDroneSession = {
        socket: {
          emit: vi.fn(),
        },
        setChatSessionId: vi.fn(),
        setCurrentTurnId: vi.fn(),
      };
      vi.mocked(SocketService.getDroneSession).mockReturnValue(
        mockDroneSession as any,
      );

      vi.mocked(ChatSessionService.createTurn).mockResolvedValue(
        mockTurn as any,
      );
      vi.mocked(ChatSessionService.getById).mockResolvedValue(
        mockChatSession as any,
      );

      cb = vi.fn();

      codeSession.setSelectedDrone(mockDrone);
      codeSession.setChatSession(mockChatSession, mockProject);
    });

    it("should return error if no drone is selected", async () => {
      codeSession = new CodeSession(mockSocket, mockUser);
      codeSession.setChatSession(mockChatSession, mockProject);

      await codeSession.onSubmitPrompt("test prompt", cb);

      expect(cb).toHaveBeenCalledWith(false, { message: "No drone selected" });
    });

    it("should return error if no chat session is active", async () => {
      codeSession = new CodeSession(mockSocket, mockUser);
      codeSession.setSelectedDrone(mockDrone);

      await codeSession.onSubmitPrompt("test prompt", cb);

      expect(cb).toHaveBeenCalledWith(false, {
        message: "No chat session active",
      });
    });

    it("should return error if no project is selected", async () => {
      codeSession.setChatSession(mockChatSession, undefined as any);
      await codeSession.onSubmitPrompt("test prompt", cb);

      expect(cb).toHaveBeenCalledWith(false, { message: "No project selected" });
    });

    it("should create a ChatTurn, increment turnCount, and emit processWorkOrder", async () => {
      const updatedSession = {
        ...mockChatSession,
        stats: { ...mockChatSession.stats, turnCount: 1 },
      };
      vi.mocked(ChatSession.findOneAndUpdate).mockResolvedValue(
        updatedSession as any,
      );
      vi.mocked(
        ChatSessionService.generateSessionNameFromPrompt,
      ).mockResolvedValue(updatedSession as any);

      await codeSession.onSubmitPrompt("test prompt", cb);

      expect(ChatSessionService.createTurn).toHaveBeenCalledWith(
        mockChatSession,
        "test prompt",
      );
      expect(ChatSession.findOneAndUpdate).toHaveBeenCalledWith(
        { _id: mockChatSession._id },
        { $inc: { "stats.turnCount": 1 } },
        expect.objectContaining({ new: true }),
      );
      expect(mockDroneSession.socket.emit).toHaveBeenCalledWith(
        "processWorkOrder",
        mockDrone,
        mockTurn,
        expect.any(Function),
      );
    });

    it("should reload the latest chat session before creating a turn", async () => {
      const latestSession = {
        ...mockChatSession,
        provider: {
          _id: nanoid(),
          name: "Gab AI",
          apiType: "openai",
          baseUrl: "https://api.gabai.chat/v1",
          apiKey: "test-key",
        },
        selectedModel: "minimax-m2-7",
        reasoningEffort: "medium",
      } as any;
      const updatedSession = {
        ...latestSession,
        stats: { ...mockChatSession.stats, turnCount: 1 },
      };
      vi.mocked(ChatSessionService.getById).mockResolvedValue(latestSession);
      vi.mocked(ChatSession.findOneAndUpdate).mockResolvedValue(
        updatedSession as any,
      );

      await codeSession.onSubmitPrompt("test prompt", cb);

      expect(ChatSessionService.getById).toHaveBeenCalledWith(mockChatSession._id);
      expect(ChatSessionService.createTurn).toHaveBeenCalledWith(
        latestSession,
        "test prompt",
      );
    });

    it("should update ChatTurn to Error status if drone rejects work order", async () => {
      const updatedSession = {
        ...mockChatSession,
        stats: { ...mockChatSession.stats, turnCount: 1 },
      };
      vi.mocked(ChatSession.findOneAndUpdate).mockResolvedValue(
        updatedSession as any,
      );
      mockDroneSession.socket.emit = vi.fn((event: string, ...args: any[]) => {
        const callback = args[args.length - 1];
        callback(false, "Drone is busy");
      });

      await codeSession.onSubmitPrompt("test prompt", cb);

      expect(mockTurn.status).toBe(ChatTurnStatus.Error);
      expect(mockTurn.errorMessage).toBe("Drone is busy");
      expect(mockTurn.save).toHaveBeenCalled();
    });

    it("should reject prompt and delete turn if session was removed during increment", async () => {
      vi.mocked(ChatSession.findOneAndUpdate).mockResolvedValue(null);
      vi.mocked(ChatSessionService.delete).mockResolvedValue(undefined);

      await codeSession.onSubmitPrompt("test prompt", cb);

      expect(ChatSessionService.delete).toHaveBeenCalledWith(mockChatSession._id);
      expect(cb).toHaveBeenCalledWith(false, {});
    });

    it("should auto-generate session name and emit sessionUpdated on first prompt with default name", async () => {
      const updatedSession = {
        ...mockChatSession,
        stats: { ...mockChatSession.stats, turnCount: 1 },
      };
      vi.mocked(ChatSession.findOneAndUpdate).mockResolvedValue(
        updatedSession as any,
      );

      const namedSession = {
        ...updatedSession,
        name: "Build a REST API",
      };
      vi.mocked(
        ChatSessionService.generateSessionNameFromPrompt,
      ).mockResolvedValue(namedSession as any);

      await codeSession.onSubmitPrompt("build a rest api", cb);

      expect(
        ChatSessionService.generateSessionNameFromPrompt,
      ).toHaveBeenCalledWith(updatedSession, "build a rest api");
      expect(mockSocket.emit).toHaveBeenCalledWith("sessionUpdated", {
        name: "Build a REST API",
      });
    });

    it("should NOT auto-generate name if session already has a custom name", async () => {
      const customSession = {
        ...mockChatSession,
        name: "My Custom Session",
      };
      codeSession.setChatSession(customSession, mockProject);

      const updatedSession = {
        ...customSession,
        stats: { ...customSession.stats, turnCount: 1 },
      };
      vi.mocked(ChatSession.findOneAndUpdate).mockResolvedValue(
        updatedSession as any,
      );

      await codeSession.onSubmitPrompt("test prompt", cb);

      expect(
        ChatSessionService.generateSessionNameFromPrompt,
      ).not.toHaveBeenCalled();
      expect(mockSocket.emit).not.toHaveBeenCalledWith(
        "sessionUpdated",
        expect.anything(),
      );
    });

    it("should NOT auto-generate name on subsequent prompts (turnCount > 1)", async () => {
      const multiTurnSession = {
        ...mockChatSession,
        stats: { ...mockChatSession.stats, turnCount: 5 },
      };
      codeSession.setChatSession(multiTurnSession, mockProject);

      const updatedSession = {
        ...multiTurnSession,
        stats: { ...multiTurnSession.stats, turnCount: 6 },
      };
      vi.mocked(ChatSession.findOneAndUpdate).mockResolvedValue(
        updatedSession as any,
      );

      await codeSession.onSubmitPrompt("test prompt", cb);

      expect(
        ChatSessionService.generateSessionNameFromPrompt,
      ).not.toHaveBeenCalled();
      expect(mockSocket.emit).not.toHaveBeenCalledWith(
        "sessionUpdated",
        expect.anything(),
      );
    });
  });

  describe("onRequestSessionLock", () => {
    it("should set selected drone, chat session, and project on success", () => {
      const mockDroneSession = {
        socket: {
          emit: vi.fn((event: string, ...args: any[]) => {
            const callback = args[args.length - 1];
            callback(true, mockChatSession._id);
          }),
        },
        setChatSessionId: vi.fn(),
        setCurrentTurnId: vi.fn(),
      };
      vi.mocked(SocketService.getDroneSession).mockReturnValue(
        mockDroneSession as any,
      );

      const callback = vi.fn();
      codeSession.onRequestSessionLock(
        mockDrone,
        mockProject,
        mockChatSession,
        callback,
      );

      expect(callback).toHaveBeenCalledWith(true, mockChatSession._id);
    });

    it("should not set session data on failure", () => {
      const mockDroneSession = {
        socket: {
          emit: vi.fn((event: string, ...args: any[]) => {
            const callback = args[args.length - 1];
            callback(false, "");
          }),
        },
        setChatSessionId: vi.fn(),
        setCurrentTurnId: vi.fn(),
      };
      vi.mocked(SocketService.getDroneSession).mockReturnValue(
        mockDroneSession as any,
      );

      const callback = vi.fn();
      codeSession.onRequestSessionLock(
        mockDrone,
        mockProject,
        mockChatSession,
        callback,
      );

      expect(callback).toHaveBeenCalledWith(false, "");
    });
  });
});
