import { describe, it, expect, beforeEach, vi } from "vitest";
import { Test, TestingModule } from "@nestjs/testing";
import { DomainsService } from "./domains.service";
import { PrismaService } from "../prisma/prisma.service";
import { ActivityService } from "../activity/activity.service";
import { NotFoundException, ConflictException } from "@nestjs/common";

describe("DomainsService", () => {
  let service: DomainsService;
  let prisma: PrismaService;
  let activityService: ActivityService;

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

  const mockActivityService = {
    logDomainCreated: vi.fn(),
    logDomainUpdated: vi.fn(),
    logDomainDeleted: vi.fn(),
  };

  const mockWorkspaceId = "550e8400-e29b-41d4-a716-446655440001";
  const mockUserId = "550e8400-e29b-41d4-a716-446655440002";
  const mockDomainId = "550e8400-e29b-41d4-a716-446655440003";

  const mockDomain = {
    id: mockDomainId,
    workspaceId: mockWorkspaceId,
    name: "Work",
    slug: "work",
    description: "Work-related tasks and projects",
    color: "#3B82F6",
    icon: "briefcase",
    sortOrder: 0,
    metadata: {},
    createdAt: new Date(),
    updatedAt: new Date(),
  };

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

    service = module.get<DomainsService>(DomainsService);
    prisma = module.get<PrismaService>(PrismaService);
    activityService = module.get<ActivityService>(ActivityService);

    // Clear all mocks before each test
    vi.clearAllMocks();
  });

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

  describe("create", () => {
    it("should create a domain and log activity", async () => {
      const createDto = {
        name: "Work",
        slug: "work",
        description: "Work-related tasks",
        color: "#3B82F6",
        icon: "briefcase",
      };

      mockPrismaService.domain.create.mockResolvedValue(mockDomain);
      mockActivityService.logDomainCreated.mockResolvedValue({});

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

      expect(result).toEqual(mockDomain);
      expect(prisma.domain.create).toHaveBeenCalledWith({
        data: {
          name: createDto.name,
          slug: createDto.slug,
          description: createDto.description,
          color: createDto.color,
          icon: createDto.icon,
          workspace: {
            connect: { id: mockWorkspaceId },
          },
          sortOrder: 0,
          metadata: {},
        },
        include: {
          _count: {
            select: { tasks: true, events: true, projects: true, ideas: true },
          },
        },
      });
      expect(activityService.logDomainCreated).toHaveBeenCalledWith(
        mockWorkspaceId,
        mockUserId,
        mockDomainId,
        { name: mockDomain.name }
      );
    });

    it("should throw ConflictException if slug already exists", async () => {
      const createDto = {
        name: "Work",
        slug: "work",
      };

      // Mock Prisma throwing unique constraint error
      const prismaError = new Error("Unique constraint failed") as any;
      prismaError.code = "P2002";
      mockPrismaService.domain.create.mockRejectedValue(prismaError);

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

    it("should use default values for optional fields", async () => {
      const createDto = {
        name: "Work",
        slug: "work",
      };

      mockPrismaService.domain.create.mockResolvedValue(mockDomain);
      mockActivityService.logDomainCreated.mockResolvedValue({});

      await service.create(mockWorkspaceId, mockUserId, createDto);

      expect(prisma.domain.create).toHaveBeenCalledWith({
        data: {
          name: "Work",
          slug: "work",
          description: null,
          color: null,
          icon: null,
          workspace: {
            connect: { id: mockWorkspaceId },
          },
          sortOrder: 0,
          metadata: {},
        },
        include: {
          _count: {
            select: { tasks: true, events: true, projects: true, ideas: true },
          },
        },
      });
    });
  });

  describe("findAll", () => {
    it("should return paginated domains", async () => {
      const query = { workspaceId: mockWorkspaceId, page: 1, limit: 10 };
      const mockDomains = [mockDomain];

      mockPrismaService.domain.findMany.mockResolvedValue(mockDomains);
      mockPrismaService.domain.count.mockResolvedValue(1);

      const result = await service.findAll(query);

      expect(result).toEqual({
        data: mockDomains,
        meta: {
          total: 1,
          page: 1,
          limit: 10,
          totalPages: 1,
        },
      });
      expect(prisma.domain.findMany).toHaveBeenCalled();
      expect(prisma.domain.count).toHaveBeenCalled();
    });

    it("should filter by search term", async () => {
      const query = {
        workspaceId: mockWorkspaceId,
        page: 1,
        limit: 10,
        search: "work",
      };

      mockPrismaService.domain.findMany.mockResolvedValue([mockDomain]);
      mockPrismaService.domain.count.mockResolvedValue(1);

      await service.findAll(query);

      expect(prisma.domain.findMany).toHaveBeenCalled();
    });

    it("should use default pagination values", async () => {
      const query = { workspaceId: mockWorkspaceId };

      mockPrismaService.domain.findMany.mockResolvedValue([]);
      mockPrismaService.domain.count.mockResolvedValue(0);

      await service.findAll(query);

      expect(prisma.domain.findMany).toHaveBeenCalled();
    });

    it("should calculate pagination correctly", async () => {
      const query = { workspaceId: mockWorkspaceId, page: 3, limit: 20 };

      mockPrismaService.domain.findMany.mockResolvedValue([]);
      mockPrismaService.domain.count.mockResolvedValue(55);

      const result = await service.findAll(query);

      expect(result.meta).toEqual({
        total: 55,
        page: 3,
        limit: 20,
        totalPages: 3,
      });
      expect(prisma.domain.findMany).toHaveBeenCalled();
    });
  });

  describe("findOne", () => {
    it("should return a domain by ID", async () => {
      mockPrismaService.domain.findUnique.mockResolvedValue(mockDomain);

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

      expect(result).toEqual(mockDomain);
      expect(prisma.domain.findUnique).toHaveBeenCalledWith({
        where: {
          id: mockDomainId,
          workspaceId: mockWorkspaceId,
        },
        include: {
          _count: {
            select: {
              tasks: true,
              projects: true,
              events: true,
              ideas: true,
            },
          },
        },
      });
    });

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

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

  describe("update", () => {
    it("should update a domain and log activity", async () => {
      const updateDto = {
        name: "Updated Work",
        color: "#10B981",
      };

      const updatedDomain = { ...mockDomain, ...updateDto };

      mockPrismaService.domain.findUnique.mockResolvedValue(mockDomain);
      mockPrismaService.domain.update.mockResolvedValue(updatedDomain);
      mockActivityService.logDomainUpdated.mockResolvedValue({});

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

      expect(result).toEqual(updatedDomain);
      expect(prisma.domain.update).toHaveBeenCalledWith({
        where: {
          id: mockDomainId,
          workspaceId: mockWorkspaceId,
        },
        data: updateDto,
        include: {
          _count: {
            select: { tasks: true, events: true, projects: true, ideas: true },
          },
        },
      });
      expect(activityService.logDomainUpdated).toHaveBeenCalledWith(
        mockWorkspaceId,
        mockUserId,
        mockDomainId,
        { changes: updateDto }
      );
    });

    it("should throw NotFoundException if domain not found", async () => {
      const updateDto = { name: "Updated Work" };

      mockPrismaService.domain.findUnique.mockResolvedValue(null);

      await expect(
        service.update(mockDomainId, mockWorkspaceId, mockUserId, updateDto)
      ).rejects.toThrow(NotFoundException);
      expect(prisma.domain.update).not.toHaveBeenCalled();
    });

    it("should throw ConflictException if slug already exists for another domain", async () => {
      const updateDto = { slug: "existing-slug" };

      mockPrismaService.domain.findUnique.mockResolvedValue(mockDomain);
      // Mock Prisma throwing unique constraint error
      const prismaError = new Error("Unique constraint failed") as any;
      prismaError.code = "P2002";
      mockPrismaService.domain.update.mockRejectedValue(prismaError);

      await expect(
        service.update(mockDomainId, mockWorkspaceId, mockUserId, updateDto)
      ).rejects.toThrow();
    });

    it("should allow updating to the same slug", async () => {
      const updateDto = { slug: "work", name: "Updated Work" };

      mockPrismaService.domain.findUnique.mockResolvedValue(mockDomain);
      mockPrismaService.domain.update.mockResolvedValue({ ...mockDomain, ...updateDto });
      mockActivityService.logDomainUpdated.mockResolvedValue({});

      await service.update(mockDomainId, mockWorkspaceId, mockUserId, updateDto);

      expect(prisma.domain.update).toHaveBeenCalled();
    });
  });

  describe("remove", () => {
    it("should delete a domain and log activity", async () => {
      mockPrismaService.domain.findUnique.mockResolvedValue(mockDomain);
      mockPrismaService.domain.delete.mockResolvedValue(mockDomain);
      mockActivityService.logDomainDeleted.mockResolvedValue({});

      await service.remove(mockDomainId, mockWorkspaceId, mockUserId);

      expect(prisma.domain.delete).toHaveBeenCalledWith({
        where: {
          id: mockDomainId,
          workspaceId: mockWorkspaceId,
        },
      });
      expect(activityService.logDomainDeleted).toHaveBeenCalledWith(
        mockWorkspaceId,
        mockUserId,
        mockDomainId,
        { name: mockDomain.name }
      );
    });

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

      await expect(service.remove(mockDomainId, mockWorkspaceId, mockUserId)).rejects.toThrow(
        NotFoundException
      );
      expect(prisma.domain.delete).not.toHaveBeenCalled();
    });
  });
});
