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

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

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

  const mockActivityService = {
    logEventCreated: vi.fn(),
    logEventUpdated: vi.fn(),
    logEventDeleted: vi.fn(),
  };

  const mockWebSocketGateway = {
    emitEventCreated: vi.fn(),
    emitEventUpdated: vi.fn(),
    emitEventDeleted: vi.fn(),
  };

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

  const mockEvent = {
    id: mockEventId,
    workspaceId: mockWorkspaceId,
    title: "Test Event",
    description: "Test Description",
    startTime: new Date("2026-02-01T10:00:00Z"),
    endTime: new Date("2026-02-01T11:00:00Z"),
    allDay: false,
    location: "Conference Room A",
    recurrence: null,
    creatorId: mockUserId,
    projectId: null,
    metadata: {},
    createdAt: new Date(),
    updatedAt: new Date(),
  };

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

    service = module.get<EventsService>(EventsService);
    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 an event and log activity", async () => {
      const createDto = {
        title: "New Event",
        description: "Event description",
        startTime: new Date("2026-02-01T10:00:00Z"),
        allDay: false,
      };

      mockPrismaService.event.create.mockResolvedValue(mockEvent);
      mockActivityService.logEventCreated.mockResolvedValue({});

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

      expect(result).toEqual(mockEvent);
      expect(prisma.event.create).toHaveBeenCalledWith({
        data: {
          title: createDto.title,
          description: createDto.description ?? null,
          startTime: createDto.startTime,
          endTime: null,
          location: null,
          workspace: { connect: { id: mockWorkspaceId } },
          creator: { connect: { id: mockUserId } },
          allDay: false,
          metadata: {},
        },
        include: {
          creator: {
            select: { id: true, name: true, email: true },
          },
          project: {
            select: { id: true, name: true, color: true },
          },
        },
      });
      expect(activityService.logEventCreated).toHaveBeenCalledWith(
        mockWorkspaceId,
        mockUserId,
        mockEvent.id,
        { title: mockEvent.title }
      );
    });
  });

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

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

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

    it("should filter by date range", async () => {
      const startFrom = new Date("2026-02-01");
      const startTo = new Date("2026-02-28");

      mockPrismaService.event.findMany.mockResolvedValue([mockEvent]);
      mockPrismaService.event.count.mockResolvedValue(1);

      await service.findAll({
        workspaceId: mockWorkspaceId,
        startFrom,
        startTo,
      });

      expect(prisma.event.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: {
            workspaceId: mockWorkspaceId,
            startTime: {
              gte: startFrom,
              lte: startTo,
            },
          },
        })
      );
    });
  });

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

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

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

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

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

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

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

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

  describe("update", () => {
    it("should update an event and log activity", async () => {
      const updateDto = {
        title: "Updated Event",
        location: "New Location",
      };

      mockPrismaService.event.findUnique.mockResolvedValue(mockEvent);
      mockPrismaService.event.update.mockResolvedValue({
        ...mockEvent,
        ...updateDto,
      });
      mockActivityService.logEventUpdated.mockResolvedValue({});

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

      expect(result.title).toBe("Updated Event");
      expect(activityService.logEventUpdated).toHaveBeenCalled();
    });

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

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

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

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

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

  describe("remove", () => {
    it("should delete an event and log activity", async () => {
      mockPrismaService.event.findUnique.mockResolvedValue(mockEvent);
      mockPrismaService.event.delete.mockResolvedValue(mockEvent);
      mockActivityService.logEventDeleted.mockResolvedValue({});

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

      expect(prisma.event.delete).toHaveBeenCalled();
      expect(activityService.logEventDeleted).toHaveBeenCalled();
    });

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

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

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

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

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

  describe("database constraint violations", () => {
    it("should handle foreign key constraint violations on create", async () => {
      const createDto = {
        title: "Event with invalid project",
        startTime: new Date("2026-02-01T10:00:00Z"),
        projectId: "non-existent-project-id",
      };

      const prismaError = new Prisma.PrismaClientKnownRequestError(
        "Foreign key constraint failed",
        {
          code: "P2003",
          clientVersion: "5.0.0",
        }
      );

      mockPrismaService.event.create.mockRejectedValue(prismaError);

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

    it("should handle foreign key constraint violations on update", async () => {
      const updateDto = {
        projectId: "non-existent-project-id",
      };

      mockPrismaService.event.findUnique.mockResolvedValue(mockEvent);

      const prismaError = new Prisma.PrismaClientKnownRequestError(
        "Foreign key constraint failed",
        {
          code: "P2003",
          clientVersion: "5.0.0",
        }
      );

      mockPrismaService.event.update.mockRejectedValue(prismaError);

      await expect(
        service.update(mockEventId, mockWorkspaceId, mockUserId, updateDto)
      ).rejects.toThrow(Prisma.PrismaClientKnownRequestError);
    });
  });
});
