import { describe, it, expect, beforeEach, vi } from "vitest";
import { Test, TestingModule } from "@nestjs/testing";
import { ProjectsService } from "./projects.service";
import { PrismaService } from "../prisma/prisma.service";
import { ActivityService } from "../activity/activity.service";
import { WebSocketGateway } from "../websocket/websocket.gateway";
import { ProjectStatus, Prisma } from "@prisma/client";
import { NotFoundException } from "@nestjs/common";

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

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

  const mockActivityService = {
    logProjectCreated: vi.fn(),
    logProjectUpdated: vi.fn(),
    logProjectDeleted: vi.fn(),
  };

  const mockWebSocketGateway = {
    emitProjectUpdated: vi.fn(),
  };

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

  const mockProject = {
    id: mockProjectId,
    workspaceId: mockWorkspaceId,
    name: "Test Project",
    description: "Test Description",
    status: ProjectStatus.PLANNING,
    startDate: new Date("2026-02-01"),
    endDate: new Date("2026-03-01"),
    creatorId: mockUserId,
    color: "#FF5733",
    metadata: {},
    createdAt: new Date(),
    updatedAt: new Date(),
  };

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

    service = module.get<ProjectsService>(ProjectsService);
    prisma = module.get<PrismaService>(PrismaService);
    activityService = module.get<ActivityService>(ActivityService);
    wsGateway = module.get<WebSocketGateway>(WebSocketGateway);

    vi.clearAllMocks();
  });

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

  describe("create", () => {
    it("should create a project and log activity", async () => {
      const createDto = {
        name: "New Project",
        description: "Project description",
        color: "#FF5733",
      };

      mockPrismaService.project.create.mockResolvedValue(mockProject);
      mockActivityService.logProjectCreated.mockResolvedValue({});

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

      expect(result).toEqual(mockProject);
      expect(prisma.project.create).toHaveBeenCalledWith({
        data: {
          name: createDto.name,
          description: createDto.description ?? null,
          color: createDto.color,
          startDate: null,
          endDate: null,
          workspace: { connect: { id: mockWorkspaceId } },
          creator: { connect: { id: mockUserId } },
          status: ProjectStatus.PLANNING,
          metadata: {},
        },
        include: {
          creator: {
            select: { id: true, name: true, email: true },
          },
          _count: {
            select: { tasks: true, events: true },
          },
        },
      });
      expect(activityService.logProjectCreated).toHaveBeenCalledWith(
        mockWorkspaceId,
        mockUserId,
        mockProject.id,
        { name: mockProject.name }
      );
    });
  });

  describe("findAll", () => {
    it("should return paginated projects with default pagination", async () => {
      const projects = [mockProject];
      mockPrismaService.project.findMany.mockResolvedValue(projects);
      mockPrismaService.project.count.mockResolvedValue(1);

      const result = await service.findAll({ workspaceId: mockWorkspaceId });

      expect(result).toEqual({
        data: projects,
        meta: {
          total: 1,
          page: 1,
          limit: 50,
          totalPages: 1,
        },
      });
    });

    it("should filter by status", async () => {
      mockPrismaService.project.findMany.mockResolvedValue([mockProject]);
      mockPrismaService.project.count.mockResolvedValue(1);

      await service.findAll({
        workspaceId: mockWorkspaceId,
        status: ProjectStatus.ACTIVE,
      });

      expect(prisma.project.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: {
            workspaceId: mockWorkspaceId,
            status: ProjectStatus.ACTIVE,
          },
        })
      );
    });
  });

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

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

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

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

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

    it("should enforce workspace isolation when finding project", async () => {
      const otherWorkspaceId = "550e8400-e29b-41d4-a716-446655440099";
      mockPrismaService.project.findUnique.mockResolvedValue(null);

      await expect(service.findOne(mockProjectId, otherWorkspaceId)).rejects.toThrow(
        NotFoundException
      );

      expect(prisma.project.findUnique).toHaveBeenCalledWith({
        where: {
          id: mockProjectId,
          workspaceId: otherWorkspaceId,
        },
        include: expect.any(Object),
      });
    });
  });

  describe("update", () => {
    it("should update a project and log activity", async () => {
      const updateDto = {
        name: "Updated Project",
        status: ProjectStatus.ACTIVE,
      };

      mockPrismaService.project.findUnique.mockResolvedValue(mockProject);
      mockPrismaService.project.update.mockResolvedValue({
        ...mockProject,
        ...updateDto,
      });
      mockActivityService.logProjectUpdated.mockResolvedValue({});

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

      expect(result.name).toBe("Updated Project");
      expect(activityService.logProjectUpdated).toHaveBeenCalled();
    });

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

      await expect(
        service.update(mockProjectId, mockWorkspaceId, mockUserId, { name: "Test" })
      ).rejects.toThrow(NotFoundException);
    });

    it("should enforce workspace isolation when updating project", async () => {
      const otherWorkspaceId = "550e8400-e29b-41d4-a716-446655440099";
      mockPrismaService.project.findUnique.mockResolvedValue(null);

      await expect(
        service.update(mockProjectId, otherWorkspaceId, mockUserId, { name: "Hacked" })
      ).rejects.toThrow(NotFoundException);

      expect(prisma.project.findUnique).toHaveBeenCalledWith({
        where: { id: mockProjectId, workspaceId: otherWorkspaceId },
      });
    });
  });

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

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

      expect(prisma.project.delete).toHaveBeenCalled();
      expect(activityService.logProjectDeleted).toHaveBeenCalled();
    });

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

      await expect(service.remove(mockProjectId, mockWorkspaceId, mockUserId)).rejects.toThrow(
        NotFoundException
      );
    });

    it("should enforce workspace isolation when deleting project", async () => {
      const otherWorkspaceId = "550e8400-e29b-41d4-a716-446655440099";
      mockPrismaService.project.findUnique.mockResolvedValue(null);

      await expect(service.remove(mockProjectId, otherWorkspaceId, mockUserId)).rejects.toThrow(
        NotFoundException
      );

      expect(prisma.project.findUnique).toHaveBeenCalledWith({
        where: { id: mockProjectId, workspaceId: otherWorkspaceId },
      });
    });
  });

  describe("database constraint violations", () => {
    it("should handle unique constraint violations on create", async () => {
      const createDto = {
        name: "Duplicate Project",
        description: "Project description",
      };

      const prismaError = new Prisma.PrismaClientKnownRequestError("Unique constraint failed", {
        code: "P2002",
        clientVersion: "5.0.0",
        meta: {
          target: ["workspaceId", "name"],
        },
      });

      mockPrismaService.project.create.mockRejectedValue(prismaError);

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

    it("should handle record not found on update (P2025)", async () => {
      mockPrismaService.project.findUnique.mockResolvedValue(mockProject);

      const prismaError = new Prisma.PrismaClientKnownRequestError("Record to update not found", {
        code: "P2025",
        clientVersion: "5.0.0",
      });

      mockPrismaService.project.update.mockRejectedValue(prismaError);

      await expect(
        service.update(mockProjectId, mockWorkspaceId, mockUserId, { name: "Updated" })
      ).rejects.toThrow(Prisma.PrismaClientKnownRequestError);
    });
  });
});
