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 type { CreatePersonalityDto } from "./dto/create-personality.dto";
import type { UpdatePersonalityDto } from "./dto/update-personality.dto";
import { NotFoundException, ConflictException } from "@nestjs/common";
import { FormalityLevel } from "@prisma/client";

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

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

  /** Raw Prisma record shape (uses Prisma field names) */
  const mockPrismaRecord = {
    id: mockPersonalityId,
    workspaceId: mockWorkspaceId,
    name: "professional-assistant",
    displayName: "Professional Assistant",
    description: "A professional communication assistant",
    tone: "professional",
    formalityLevel: FormalityLevel.FORMAL,
    systemPrompt: "You are a professional assistant who helps with tasks.",
    temperature: 0.7,
    maxTokens: 2000,
    llmProviderInstanceId: "provider-123",
    isDefault: true,
    isEnabled: true,
    createdAt: new Date("2026-01-01"),
    updatedAt: new Date("2026-01-01"),
  };

  /** Expected API response shape (uses frontend field names) */
  const mockResponse = {
    id: mockPersonalityId,
    workspaceId: mockWorkspaceId,
    name: "professional-assistant",
    description: "A professional communication assistant",
    tone: "professional",
    formalityLevel: FormalityLevel.FORMAL,
    systemPromptTemplate: "You are a professional assistant who helps with tasks.",
    isDefault: true,
    isActive: true,
    createdAt: new Date("2026-01-01"),
    updatedAt: new Date("2026-01-01"),
  };

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

  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);

    vi.clearAllMocks();
  });

  describe("create", () => {
    const createDto: CreatePersonalityDto = {
      name: "casual-helper",
      description: "A casual communication helper",
      tone: "casual",
      formalityLevel: FormalityLevel.CASUAL,
      systemPromptTemplate: "You are a casual assistant.",
      isDefault: false,
      isActive: true,
    };

    const createdRecord = {
      ...mockPrismaRecord,
      name: createDto.name,
      description: createDto.description,
      tone: createDto.tone,
      formalityLevel: createDto.formalityLevel,
      systemPrompt: createDto.systemPromptTemplate,
      isDefault: false,
      isEnabled: true,
      id: "new-personality-id",
    };

    it("should create a new personality and return API response shape", async () => {
      mockPrismaService.personality.findFirst.mockResolvedValue(null);
      mockPrismaService.personality.create.mockResolvedValue(createdRecord);

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

      expect(result.name).toBe(createDto.name);
      expect(result.tone).toBe(createDto.tone);
      expect(result.formalityLevel).toBe(createDto.formalityLevel);
      expect(result.systemPromptTemplate).toBe(createDto.systemPromptTemplate);
      expect(result.isActive).toBe(true);
      expect(result.isDefault).toBe(false);

      expect(prisma.personality.create).toHaveBeenCalledWith({
        data: {
          workspaceId: mockWorkspaceId,
          name: createDto.name,
          displayName: createDto.name,
          description: createDto.description ?? null,
          tone: createDto.tone,
          formalityLevel: createDto.formalityLevel,
          systemPrompt: createDto.systemPromptTemplate,
          isDefault: false,
          isEnabled: true,
        },
      });
    });

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

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

    it("should unset other defaults when creating a new default personality", async () => {
      const createDefaultDto: CreatePersonalityDto = { ...createDto, isDefault: true };
      const otherDefault = { ...mockPrismaRecord, id: "other-id" };

      mockPrismaService.personality.findFirst
        .mockResolvedValueOnce(null) // name conflict check
        .mockResolvedValueOnce(otherDefault); // existing default lookup
      mockPrismaService.personality.update.mockResolvedValue({ ...otherDefault, isDefault: false });
      mockPrismaService.personality.create.mockResolvedValue({
        ...createdRecord,
        isDefault: true,
      });

      await service.create(mockWorkspaceId, createDefaultDto);

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

  describe("findAll", () => {
    it("should return mapped response list for a workspace", async () => {
      mockPrismaService.personality.findMany.mockResolvedValue([mockPrismaRecord]);

      const result = await service.findAll(mockWorkspaceId);

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

    it("should filter by isActive when provided", async () => {
      mockPrismaService.personality.findMany.mockResolvedValue([mockPrismaRecord]);

      await service.findAll(mockWorkspaceId, { isActive: true });

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

  describe("findOne", () => {
    it("should return a mapped personality response by id", async () => {
      mockPrismaService.personality.findFirst.mockResolvedValue(mockPrismaRecord);

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

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

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

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

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

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

      expect(result).toEqual(mockResponse);
      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(mockPrismaRecord);

      const result = await service.findDefault(mockWorkspaceId);

      expect(result).toEqual(mockResponse);
      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",
      tone: "formal",
      isActive: false,
    };

    it("should update a personality and return mapped response", async () => {
      const updatedRecord = {
        ...mockPrismaRecord,
        description: updateDto.description,
        tone: updateDto.tone,
        isEnabled: false,
      };

      mockPrismaService.personality.findFirst
        .mockResolvedValueOnce(mockPrismaRecord) // findOne check
        .mockResolvedValueOnce(null); // name conflict check (no dto.name here)
      mockPrismaService.personality.update.mockResolvedValue(updatedRecord);

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

      expect(result.description).toBe(updateDto.description);
      expect(result.tone).toBe(updateDto.tone);
      expect(result.isActive).toBe(false);
    });

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

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

    it("should throw ConflictException when updating to an existing name", async () => {
      const updateNameDto: UpdatePersonalityDto = { name: "existing-name" };
      const conflictRecord = { ...mockPrismaRecord, id: "different-id" };

      mockPrismaService.personality.findFirst
        .mockResolvedValueOnce(mockPrismaRecord) // findOne check
        .mockResolvedValueOnce(conflictRecord); // name conflict

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

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

      mockPrismaService.personality.findFirst
        .mockResolvedValueOnce(mockPrismaRecord) // findOne check
        .mockResolvedValueOnce(otherPersonality); // unsetOtherDefaults lookup
      mockPrismaService.personality.update
        .mockResolvedValueOnce({ ...otherPersonality, isDefault: false })
        .mockResolvedValueOnce(updatedRecord);

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

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

  describe("delete", () => {
    it("should delete a personality", async () => {
      mockPrismaService.personality.findFirst.mockResolvedValue(mockPrismaRecord);
      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.findFirst.mockResolvedValue(null);

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

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

      mockPrismaService.personality.findFirst
        .mockResolvedValueOnce(mockPrismaRecord) // findOne check
        .mockResolvedValueOnce(otherPersonality); // unsetOtherDefaults lookup
      mockPrismaService.personality.update
        .mockResolvedValueOnce({ ...otherPersonality, isDefault: false })
        .mockResolvedValueOnce(updatedRecord);

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

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

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

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