import { describe, it, expect, beforeEach, vi } from "vitest";
import { Test, TestingModule } from "@nestjs/testing";
import { UserAgentService } from "./user-agent.service";
import { PrismaService } from "../prisma/prisma.service";
import { NotFoundException, ConflictException, ForbiddenException } from "@nestjs/common";

describe("UserAgentService", () => {
  let service: UserAgentService;
  let prisma: PrismaService;

  const mockPrismaService = {
    userAgent: {
      findMany: vi.fn(),
      findUnique: vi.fn(),
      create: vi.fn(),
      update: vi.fn(),
      delete: vi.fn(),
    },
    agentTemplate: {
      findUnique: vi.fn(),
    },
  };

  const mockUserId = "550e8400-e29b-41d4-a716-446655440001";
  const mockAgentId = "550e8400-e29b-41d4-a716-446655440002";
  const mockTemplateId = "550e8400-e29b-41d4-a716-446655440003";

  const mockAgent = {
    id: mockAgentId,
    userId: mockUserId,
    templateId: null,
    name: "jarvis",
    displayName: "Jarvis",
    role: "orchestrator",
    personality: "Capable, direct, proactive.",
    primaryModel: "opus",
    fallbackModels: ["sonnet"],
    toolPermissions: ["all"],
    discordChannel: "jarvis",
    isActive: true,
    createdAt: new Date(),
    updatedAt: new Date(),
  };

  const mockTemplate = {
    id: mockTemplateId,
    name: "builder",
    displayName: "Builder",
    role: "coding",
    personality: "Focused, thorough.",
    primaryModel: "codex",
    fallbackModels: ["sonnet"],
    toolPermissions: ["exec", "read", "write"],
    discordChannel: "builder",
    isActive: true,
    createdAt: new Date(),
    updatedAt: new Date(),
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UserAgentService,
        {
          provide: PrismaService,
          useValue: mockPrismaService,
        },
      ],
    }).compile();

    service = module.get<UserAgentService>(UserAgentService);
    prisma = module.get<PrismaService>(PrismaService);

    vi.clearAllMocks();
  });

  it("should be defined", () => {
    expect(service).toBeDefined();
  });

  describe("findAll", () => {
    it("should return all agents for a user", async () => {
      mockPrismaService.userAgent.findMany.mockResolvedValue([mockAgent]);

      const result = await service.findAll(mockUserId);

      expect(result).toEqual([mockAgent]);
      expect(mockPrismaService.userAgent.findMany).toHaveBeenCalledWith({
        where: { userId: mockUserId },
        orderBy: { createdAt: "asc" },
      });
    });

    it("should return empty array if no agents", async () => {
      mockPrismaService.userAgent.findMany.mockResolvedValue([]);

      const result = await service.findAll(mockUserId);

      expect(result).toEqual([]);
    });
  });

  describe("findOne", () => {
    it("should return an agent by id", async () => {
      mockPrismaService.userAgent.findUnique.mockResolvedValue(mockAgent);

      const result = await service.findOne(mockUserId, mockAgentId);

      expect(result).toEqual(mockAgent);
    });

    it("should throw NotFoundException if agent not found", async () => {
      mockPrismaService.userAgent.findUnique.mockResolvedValue(null);

      await expect(service.findOne(mockUserId, mockAgentId)).rejects.toThrow(NotFoundException);
    });

    it("should throw ForbiddenException if agent belongs to different user", async () => {
      mockPrismaService.userAgent.findUnique.mockResolvedValue({
        ...mockAgent,
        userId: "different-user-id",
      });

      await expect(service.findOne(mockUserId, mockAgentId)).rejects.toThrow(ForbiddenException);
    });
  });

  describe("findByName", () => {
    it("should return an agent by name", async () => {
      mockPrismaService.userAgent.findUnique.mockResolvedValue(mockAgent);

      const result = await service.findByName(mockUserId, "jarvis");

      expect(result).toEqual(mockAgent);
      expect(mockPrismaService.userAgent.findUnique).toHaveBeenCalledWith({
        where: { userId_name: { userId: mockUserId, name: "jarvis" } },
      });
    });

    it("should throw NotFoundException if agent not found", async () => {
      mockPrismaService.userAgent.findUnique.mockResolvedValue(null);

      await expect(service.findByName(mockUserId, "nonexistent")).rejects.toThrow(
        NotFoundException
      );
    });
  });

  describe("create", () => {
    it("should create a new agent", async () => {
      const createDto = {
        name: "jarvis",
        displayName: "Jarvis",
        role: "orchestrator",
        personality: "Capable, direct, proactive.",
      };

      mockPrismaService.userAgent.findUnique.mockResolvedValue(null);
      mockPrismaService.userAgent.create.mockResolvedValue(mockAgent);

      const result = await service.create(mockUserId, createDto);

      expect(result).toEqual(mockAgent);
    });

    it("should throw ConflictException if agent name already exists", async () => {
      const createDto = {
        name: "jarvis",
        displayName: "Jarvis",
        role: "orchestrator",
        personality: "Capable, direct, proactive.",
      };

      mockPrismaService.userAgent.findUnique.mockResolvedValue(mockAgent);

      await expect(service.create(mockUserId, createDto)).rejects.toThrow(ConflictException);
    });

    it("should throw NotFoundException if templateId is invalid", async () => {
      const createDto = {
        name: "custom",
        displayName: "Custom",
        role: "custom",
        personality: "Custom agent",
        templateId: "nonexistent-template",
      };

      mockPrismaService.userAgent.findUnique.mockResolvedValue(null);
      mockPrismaService.agentTemplate.findUnique.mockResolvedValue(null);

      await expect(service.create(mockUserId, createDto)).rejects.toThrow(NotFoundException);
    });
  });

  describe("createFromTemplate", () => {
    it("should create an agent from a template", async () => {
      mockPrismaService.agentTemplate.findUnique.mockResolvedValue(mockTemplate);
      mockPrismaService.userAgent.findUnique.mockResolvedValue(null);
      mockPrismaService.userAgent.create.mockResolvedValue({
        ...mockAgent,
        templateId: mockTemplateId,
        name: mockTemplate.name,
        displayName: mockTemplate.displayName,
        role: mockTemplate.role,
      });

      const result = await service.createFromTemplate(mockUserId, mockTemplateId);

      expect(result.name).toBe(mockTemplate.name);
      expect(result.displayName).toBe(mockTemplate.displayName);
    });

    it("should throw NotFoundException if template not found", async () => {
      mockPrismaService.agentTemplate.findUnique.mockResolvedValue(null);

      await expect(service.createFromTemplate(mockUserId, mockTemplateId)).rejects.toThrow(
        NotFoundException
      );
    });

    it("should throw ConflictException if agent name already exists", async () => {
      mockPrismaService.agentTemplate.findUnique.mockResolvedValue(mockTemplate);
      mockPrismaService.userAgent.findUnique.mockResolvedValue(mockAgent);

      await expect(service.createFromTemplate(mockUserId, mockTemplateId)).rejects.toThrow(
        ConflictException
      );
    });
  });

  describe("update", () => {
    it("should update an agent", async () => {
      const updateDto = { displayName: "Updated Jarvis" };
      const updatedAgent = { ...mockAgent, ...updateDto };

      mockPrismaService.userAgent.findUnique.mockResolvedValue(mockAgent);
      mockPrismaService.userAgent.update.mockResolvedValue(updatedAgent);

      const result = await service.update(mockUserId, mockAgentId, updateDto);

      expect(result.displayName).toBe("Updated Jarvis");
    });

    it("should throw ConflictException if new name already exists", async () => {
      const updateDto = { name: "existing-name" };

      mockPrismaService.userAgent.findUnique.mockResolvedValue(mockAgent);
      // Second call checks for existing name
      mockPrismaService.userAgent.findUnique.mockResolvedValue({ ...mockAgent, id: "other-id" });

      await expect(service.update(mockUserId, mockAgentId, updateDto)).rejects.toThrow(
        ConflictException
      );
    });
  });

  describe("remove", () => {
    it("should delete an agent", async () => {
      mockPrismaService.userAgent.findUnique.mockResolvedValue(mockAgent);
      mockPrismaService.userAgent.delete.mockResolvedValue(mockAgent);

      const result = await service.remove(mockUserId, mockAgentId);

      expect(result).toEqual(mockAgent);
    });
  });

  describe("getStatus", () => {
    it("should return agent status", async () => {
      mockPrismaService.userAgent.findUnique.mockResolvedValue(mockAgent);

      const result = await service.getStatus(mockUserId, mockAgentId);

      expect(result).toEqual({
        id: mockAgentId,
        name: "jarvis",
        displayName: "Jarvis",
        role: "orchestrator",
        isActive: true,
      });
    });
  });

  describe("getAllStatuses", () => {
    it("should return all agent statuses", async () => {
      mockPrismaService.userAgent.findMany.mockResolvedValue([mockAgent]);

      const result = await service.getAllStatuses(mockUserId);

      expect(result).toHaveLength(1);
      expect(result[0]).toEqual({
        id: mockAgentId,
        name: "jarvis",
        displayName: "Jarvis",
        role: "orchestrator",
        isActive: true,
      });
    });
  });
});
