import { describe, it, expect, beforeEach, vi } from "vitest";
import { Test, TestingModule } from "@nestjs/testing";
import { PersonalitiesService } from "./personalities.service";
import { PrismaService } from "../prisma/prisma.service";
import { CreatePersonalityDto, UpdatePersonalityDto } from "./dto";
import { NotFoundException, ConflictException } from "@nestjs/common";

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

  const mockWorkspaceId = "workspace-123";
  const mockPersonalityId = "personality-123";
  const mockProviderId = "provider-123";

  const mockPersonality = {
    id: mockPersonalityId,
    workspaceId: mockWorkspaceId,
    name: "professional-assistant",
    displayName: "Professional Assistant",
    description: "A professional communication assistant",
    systemPrompt: "You are a professional assistant who helps with tasks.",
    temperature: 0.7,
    maxTokens: 2000,
    llmProviderInstanceId: mockProviderId,
    isDefault: true,
    isEnabled: true,
    createdAt: new Date(),
    updatedAt: new Date(),
  };

  const mockPrismaService = {
    personality: {
      findMany: vi.fn(),
      findUnique: vi.fn(),
      findFirst: vi.fn(),
      create: vi.fn(),
      update: vi.fn(),
      delete: vi.fn(),
      count: vi.fn(),
    },
    $transaction: vi.fn((callback) => callback(mockPrismaService)),
  };

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

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

    // Reset mocks
    vi.clearAllMocks();
  });

  describe("create", () => {
    const createDto: CreatePersonalityDto = {
      name: "casual-helper",
      displayName: "Casual Helper",
      description: "A casual communication helper",
      systemPrompt: "You are a casual assistant.",
      temperature: 0.8,
      maxTokens: 1500,
      llmProviderInstanceId: mockProviderId,
    };

    it("should create a new personality", async () => {
      mockPrismaService.personality.findFirst.mockResolvedValue(null);
      mockPrismaService.personality.create.mockResolvedValue({
        ...mockPersonality,
        ...createDto,
        id: "new-personality-id",
        isDefault: false,
        isEnabled: true,
      });

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

      expect(result).toMatchObject(createDto);
      expect(prisma.personality.create).toHaveBeenCalledWith({
        data: {
          workspaceId: mockWorkspaceId,
          name: createDto.name,
          displayName: createDto.displayName,
          description: createDto.description ?? null,
          systemPrompt: createDto.systemPrompt,
          temperature: createDto.temperature ?? null,
          maxTokens: createDto.maxTokens ?? null,
          llmProviderInstanceId: createDto.llmProviderInstanceId ?? null,
          isDefault: false,
          isEnabled: true,
        },
      });
    });

    it("should throw ConflictException when name already exists", async () => {
      mockPrismaService.personality.findFirst.mockResolvedValue(mockPersonality);

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

    it("should unset other defaults when creating a new default personality", async () => {
      const createDefaultDto = { ...createDto, isDefault: true };
      // First call to findFirst checks for name conflict (should be null)
      // Second call to findFirst finds the existing default personality
      mockPrismaService.personality.findFirst
        .mockResolvedValueOnce(null) // No name conflict
        .mockResolvedValueOnce(mockPersonality); // Existing default
      mockPrismaService.personality.update.mockResolvedValue({
        ...mockPersonality,
        isDefault: false,
      });
      mockPrismaService.personality.create.mockResolvedValue({
        ...mockPersonality,
        ...createDefaultDto,
      });

      await service.create(mockWorkspaceId, createDefaultDto);

      expect(prisma.personality.update).toHaveBeenCalledWith({
        where: { id: mockPersonalityId },
        data: { isDefault: false },
      });
    });
  });

  describe("findAll", () => {
    it("should return all personalities for a workspace", async () => {
      const mockPersonalities = [mockPersonality];
      mockPrismaService.personality.findMany.mockResolvedValue(mockPersonalities);

      const result = await service.findAll(mockWorkspaceId);

      expect(result).toEqual(mockPersonalities);
      expect(prisma.personality.findMany).toHaveBeenCalledWith({
        where: { workspaceId: mockWorkspaceId },
        orderBy: [{ isDefault: "desc" }, { name: "asc" }],
      });
    });
  });

  describe("findOne", () => {
    it("should return a personality by id", async () => {
      mockPrismaService.personality.findUnique.mockResolvedValue(mockPersonality);

      const result = await service.findOne(mockWorkspaceId, mockPersonalityId);

      expect(result).toEqual(mockPersonality);
      expect(prisma.personality.findUnique).toHaveBeenCalledWith({
        where: {
          id: mockPersonalityId,
          workspaceId: mockWorkspaceId,
        },
      });
    });

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

      await expect(service.findOne(mockWorkspaceId, mockPersonalityId)).rejects.toThrow(
        NotFoundException
      );
    });
  });

  describe("findByName", () => {
    it("should return a personality by name", async () => {
      mockPrismaService.personality.findFirst.mockResolvedValue(mockPersonality);

      const result = await service.findByName(mockWorkspaceId, "professional-assistant");

      expect(result).toEqual(mockPersonality);
      expect(prisma.personality.findFirst).toHaveBeenCalledWith({
        where: {
          workspaceId: mockWorkspaceId,
          name: "professional-assistant",
        },
      });
    });

    it("should throw NotFoundException when personality not found", async () => {
      mockPrismaService.personality.findFirst.mockResolvedValue(null);

      await expect(service.findByName(mockWorkspaceId, "non-existent")).rejects.toThrow(
        NotFoundException
      );
    });
  });

  describe("findDefault", () => {
    it("should return the default personality", async () => {
      mockPrismaService.personality.findFirst.mockResolvedValue(mockPersonality);

      const result = await service.findDefault(mockWorkspaceId);

      expect(result).toEqual(mockPersonality);
      expect(prisma.personality.findFirst).toHaveBeenCalledWith({
        where: { workspaceId: mockWorkspaceId, isDefault: true, isEnabled: true },
      });
    });

    it("should throw NotFoundException when no default personality exists", async () => {
      mockPrismaService.personality.findFirst.mockResolvedValue(null);

      await expect(service.findDefault(mockWorkspaceId)).rejects.toThrow(NotFoundException);
    });
  });

  describe("update", () => {
    const updateDto: UpdatePersonalityDto = {
      description: "Updated description",
      temperature: 0.9,
    };

    it("should update a personality", async () => {
      mockPrismaService.personality.findUnique.mockResolvedValue(mockPersonality);
      mockPrismaService.personality.findFirst.mockResolvedValue(null);
      mockPrismaService.personality.update.mockResolvedValue({
        ...mockPersonality,
        ...updateDto,
      });

      const result = await service.update(mockWorkspaceId, mockPersonalityId, updateDto);

      expect(result).toMatchObject(updateDto);
      expect(prisma.personality.update).toHaveBeenCalledWith({
        where: { id: mockPersonalityId },
        data: updateDto,
      });
    });

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

      await expect(service.update(mockWorkspaceId, mockPersonalityId, updateDto)).rejects.toThrow(
        NotFoundException
      );
    });

    it("should throw ConflictException when updating to existing name", async () => {
      const updateNameDto = { name: "existing-name" };
      mockPrismaService.personality.findUnique.mockResolvedValue(mockPersonality);
      mockPrismaService.personality.findFirst.mockResolvedValue({
        ...mockPersonality,
        id: "different-id",
      });

      await expect(
        service.update(mockWorkspaceId, mockPersonalityId, updateNameDto)
      ).rejects.toThrow(ConflictException);
    });

    it("should unset other defaults when setting as default", async () => {
      const updateDefaultDto = { isDefault: true };
      const otherPersonality = { ...mockPersonality, id: "other-id", isDefault: true };

      mockPrismaService.personality.findUnique.mockResolvedValue(mockPersonality);
      mockPrismaService.personality.findFirst.mockResolvedValue(otherPersonality); // Existing default from unsetOtherDefaults
      mockPrismaService.personality.update
        .mockResolvedValueOnce({ ...otherPersonality, isDefault: false }) // Unset old default
        .mockResolvedValueOnce({ ...mockPersonality, isDefault: true }); // Set new default

      await service.update(mockWorkspaceId, mockPersonalityId, updateDefaultDto);

      expect(prisma.personality.update).toHaveBeenNthCalledWith(1, {
        where: { id: "other-id" },
        data: { isDefault: false },
      });
      expect(prisma.personality.update).toHaveBeenNthCalledWith(2, {
        where: { id: mockPersonalityId },
        data: updateDefaultDto,
      });
    });
  });

  describe("delete", () => {
    it("should delete a personality", async () => {
      mockPrismaService.personality.findUnique.mockResolvedValue(mockPersonality);
      mockPrismaService.personality.delete.mockResolvedValue(undefined);

      await service.delete(mockWorkspaceId, mockPersonalityId);

      expect(prisma.personality.delete).toHaveBeenCalledWith({
        where: { id: mockPersonalityId },
      });
    });

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

      await expect(service.delete(mockWorkspaceId, mockPersonalityId)).rejects.toThrow(
        NotFoundException
      );
    });
  });

  describe("setDefault", () => {
    it("should set a personality as default", async () => {
      const otherPersonality = { ...mockPersonality, id: "other-id", isDefault: true };
      const updatedPersonality = { ...mockPersonality, isDefault: true };

      mockPrismaService.personality.findUnique.mockResolvedValue(mockPersonality);
      mockPrismaService.personality.findFirst.mockResolvedValue(otherPersonality);
      mockPrismaService.personality.update
        .mockResolvedValueOnce({ ...otherPersonality, isDefault: false }) // Unset old default
        .mockResolvedValueOnce(updatedPersonality); // Set new default

      const result = await service.setDefault(mockWorkspaceId, mockPersonalityId);

      expect(result).toMatchObject({ isDefault: true });
      expect(prisma.personality.update).toHaveBeenNthCalledWith(1, {
        where: { id: "other-id" },
        data: { isDefault: false },
      });
      expect(prisma.personality.update).toHaveBeenNthCalledWith(2, {
        where: { id: mockPersonalityId },
        data: { isDefault: true },
      });
    });

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

      await expect(service.setDefault(mockWorkspaceId, mockPersonalityId)).rejects.toThrow(
        NotFoundException
      );
    });
  });
});
