import { describe, it, expect, beforeEach, vi } from "vitest";
import { Test, TestingModule } from "@nestjs/testing";
import { TasksService } from "./tasks.service";
import { PrismaService } from "../prisma/prisma.service";
import { ActivityService } from "../activity/activity.service";
import { TaskStatus, TaskPriority } from "@prisma/client";
import { NotFoundException } from "@nestjs/common";
import { Prisma } from "@prisma/client";

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

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

  const mockActivityService = {
    logTaskCreated: vi.fn(),
    logTaskUpdated: vi.fn(),
    logTaskDeleted: vi.fn(),
    logTaskCompleted: vi.fn(),
    logTaskAssigned: vi.fn(),
  };

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

  const mockTask = {
    id: mockTaskId,
    workspaceId: mockWorkspaceId,
    title: "Test Task",
    description: "Test Description",
    status: TaskStatus.NOT_STARTED,
    priority: TaskPriority.MEDIUM,
    dueDate: new Date("2026-02-01T12:00:00Z"),
    assigneeId: null,
    creatorId: mockUserId,
    projectId: null,
    parentId: null,
    assignedAgent: null,
    sortOrder: 0,
    metadata: {},
    createdAt: new Date(),
    updatedAt: new Date(),
    completedAt: null,
  };

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

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

    // Clear all mocks before each test
    vi.clearAllMocks();
    mockPrismaService.withWorkspaceContext.mockImplementation(async (_userId, _workspaceId, fn) => {
      return fn(mockPrismaService as unknown as PrismaService);
    });
  });

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

  describe("create", () => {
    it("should create a task and log activity", async () => {
      const createDto = {
        title: "New Task",
        description: "Task description",
        priority: TaskPriority.HIGH,
      };

      mockPrismaService.task.create.mockResolvedValue(mockTask);
      mockActivityService.logTaskCreated.mockResolvedValue({});

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

      expect(result).toEqual(mockTask);
      expect(prisma.withWorkspaceContext).toHaveBeenCalledWith(
        mockUserId,
        mockWorkspaceId,
        expect.any(Function)
      );
      expect(prisma.task.create).toHaveBeenCalledWith({
        data: {
          title: createDto.title,
          description: createDto.description ?? null,
          dueDate: null,
          workspace: { connect: { id: mockWorkspaceId } },
          creator: { connect: { id: mockUserId } },
          status: TaskStatus.NOT_STARTED,
          priority: TaskPriority.HIGH,
          sortOrder: 0,
          metadata: {},
        },
        include: {
          assignee: {
            select: { id: true, name: true, email: true },
          },
          creator: {
            select: { id: true, name: true, email: true },
          },
          project: {
            select: { id: true, name: true, color: true },
          },
        },
      });
      expect(activityService.logTaskCreated).toHaveBeenCalledWith(
        mockWorkspaceId,
        mockUserId,
        mockTask.id,
        { title: mockTask.title }
      );
    });

    it("should set completedAt when status is COMPLETED", async () => {
      const createDto = {
        title: "Completed Task",
        status: TaskStatus.COMPLETED,
      };

      mockPrismaService.task.create.mockResolvedValue({
        ...mockTask,
        status: TaskStatus.COMPLETED,
        completedAt: new Date(),
      });

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

      expect(prisma.task.create).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({
            completedAt: expect.any(Date),
          }),
        })
      );
    });

    it("should include assignedAgent when provided", async () => {
      const createDto = {
        title: "Agent-owned Task",
        assignedAgent: "fleet-worker-1",
      };

      mockPrismaService.task.create.mockResolvedValue({
        ...mockTask,
        assignedAgent: createDto.assignedAgent,
      });

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

      expect(prisma.task.create).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({
            assignedAgent: createDto.assignedAgent,
          }),
        })
      );
    });
  });

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

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

      expect(result).toEqual({
        data: tasks,
        meta: {
          total: 1,
          page: 1,
          limit: 50,
          totalPages: 1,
        },
      });
      expect(prisma.task.findMany).toHaveBeenCalledWith({
        where: { workspaceId: mockWorkspaceId },
        include: expect.any(Object),
        orderBy: { createdAt: "desc" },
        skip: 0,
        take: 50,
      });
    });

    it("should use workspace context when userId is provided", async () => {
      mockPrismaService.task.findMany.mockResolvedValue([mockTask]);
      mockPrismaService.task.count.mockResolvedValue(1);

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

      expect(prisma.withWorkspaceContext).toHaveBeenCalledWith(
        mockUserId,
        mockWorkspaceId,
        expect.any(Function)
      );
    });

    it("should fallback to direct Prisma access when userId is missing", async () => {
      mockPrismaService.task.findMany.mockResolvedValue([mockTask]);
      mockPrismaService.task.count.mockResolvedValue(1);

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

      expect(prisma.withWorkspaceContext).not.toHaveBeenCalled();
      expect(prisma.task.findMany).toHaveBeenCalled();
    });

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

      await service.findAll({
        workspaceId: mockWorkspaceId,
        status: TaskStatus.IN_PROGRESS,
      });

      expect(prisma.task.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: {
            workspaceId: mockWorkspaceId,
            status: TaskStatus.IN_PROGRESS,
          },
        })
      );
    });

    it("should filter by priority", async () => {
      mockPrismaService.task.findMany.mockResolvedValue([mockTask]);
      mockPrismaService.task.count.mockResolvedValue(1);

      await service.findAll({
        workspaceId: mockWorkspaceId,
        priority: TaskPriority.HIGH,
      });

      expect(prisma.task.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: {
            workspaceId: mockWorkspaceId,
            priority: TaskPriority.HIGH,
          },
        })
      );
    });

    it("should filter by assigneeId", async () => {
      mockPrismaService.task.findMany.mockResolvedValue([mockTask]);
      mockPrismaService.task.count.mockResolvedValue(1);

      await service.findAll({
        workspaceId: mockWorkspaceId,
        assigneeId: mockUserId,
      });

      expect(prisma.task.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: {
            workspaceId: mockWorkspaceId,
            assigneeId: mockUserId,
          },
        })
      );
    });

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

      mockPrismaService.task.findMany.mockResolvedValue([mockTask]);
      mockPrismaService.task.count.mockResolvedValue(1);

      await service.findAll({
        workspaceId: mockWorkspaceId,
        dueDateFrom,
        dueDateTo,
      });

      expect(prisma.task.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: {
            workspaceId: mockWorkspaceId,
            dueDate: {
              gte: dueDateFrom,
              lte: dueDateTo,
            },
          },
        })
      );
    });

    it("should handle pagination correctly", async () => {
      mockPrismaService.task.findMany.mockResolvedValue([mockTask]);
      mockPrismaService.task.count.mockResolvedValue(100);

      const result = await service.findAll({
        workspaceId: mockWorkspaceId,
        page: 2,
        limit: 10,
      });

      expect(result.meta).toEqual({
        total: 100,
        page: 2,
        limit: 10,
        totalPages: 10,
      });
      expect(prisma.task.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          skip: 10,
          take: 10,
        })
      );
    });
  });

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

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

      expect(result).toEqual(mockTask);
      expect(prisma.task.findUnique).toHaveBeenCalledWith({
        where: {
          id: mockTaskId,
          workspaceId: mockWorkspaceId,
        },
        include: expect.any(Object),
      });
    });

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

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

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

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

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

  describe("update", () => {
    it("should update a task and log activity", async () => {
      const updateDto = {
        title: "Updated Task",
        status: TaskStatus.IN_PROGRESS,
      };

      mockPrismaService.task.findUnique.mockResolvedValue(mockTask);
      mockPrismaService.task.update.mockResolvedValue({
        ...mockTask,
        ...updateDto,
      });
      mockActivityService.logTaskUpdated.mockResolvedValue({});

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

      expect(result.title).toBe("Updated Task");
      expect(activityService.logTaskUpdated).toHaveBeenCalledWith(
        mockWorkspaceId,
        mockUserId,
        mockTaskId,
        { changes: updateDto }
      );
    });

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

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

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

    it("should set completedAt when status changes to COMPLETED", async () => {
      const updateDto = { status: TaskStatus.COMPLETED };

      mockPrismaService.task.findUnique.mockResolvedValue(mockTask);
      mockPrismaService.task.update.mockResolvedValue({
        ...mockTask,
        status: TaskStatus.COMPLETED,
        completedAt: new Date(),
      });

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

      expect(prisma.task.update).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({
            completedAt: expect.any(Date),
          }),
        })
      );
      expect(activityService.logTaskCompleted).toHaveBeenCalled();
    });

    it("should clear completedAt when status changes from COMPLETED", async () => {
      const completedTask = {
        ...mockTask,
        status: TaskStatus.COMPLETED,
        completedAt: new Date(),
      };
      const updateDto = { status: TaskStatus.IN_PROGRESS };

      mockPrismaService.task.findUnique.mockResolvedValue(completedTask);
      mockPrismaService.task.update.mockResolvedValue({
        ...completedTask,
        status: TaskStatus.IN_PROGRESS,
        completedAt: null,
      });

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

      expect(prisma.task.update).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({
            completedAt: null,
          }),
        })
      );
    });

    it("should log assignment when assigneeId changes", async () => {
      const updateDto = { assigneeId: "550e8400-e29b-41d4-a716-446655440099" };

      mockPrismaService.task.findUnique.mockResolvedValue(mockTask);
      mockPrismaService.task.update.mockResolvedValue({
        ...mockTask,
        assigneeId: updateDto.assigneeId,
      });

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

      expect(activityService.logTaskAssigned).toHaveBeenCalledWith(
        mockWorkspaceId,
        mockUserId,
        mockTaskId,
        updateDto.assigneeId
      );
    });

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

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

    it("should update assignedAgent when provided", async () => {
      const updateDto = { assignedAgent: "fleet-worker-2" };

      mockPrismaService.task.findUnique.mockResolvedValue(mockTask);
      mockPrismaService.task.update.mockResolvedValue({
        ...mockTask,
        assignedAgent: updateDto.assignedAgent,
      });

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

      expect(prisma.task.update).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({
            assignedAgent: updateDto.assignedAgent,
          }),
        })
      );
    });
  });

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

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

      expect(prisma.task.delete).toHaveBeenCalledWith({
        where: {
          id: mockTaskId,
          workspaceId: mockWorkspaceId,
        },
      });
      expect(activityService.logTaskDeleted).toHaveBeenCalledWith(
        mockWorkspaceId,
        mockUserId,
        mockTaskId,
        { title: mockTask.title }
      );
    });

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

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

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

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

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

  describe("database constraint violations", () => {
    it("should handle foreign key constraint violations on create", async () => {
      const createDto = {
        title: "Task with invalid assignee",
        assigneeId: "non-existent-user-id",
      };

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

      mockPrismaService.task.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 = {
        assigneeId: "non-existent-user-id",
      };

      mockPrismaService.task.findUnique.mockResolvedValue(mockTask);

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

      mockPrismaService.task.update.mockRejectedValue(prismaError);

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

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

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

      mockPrismaService.task.update.mockRejectedValue(prismaError);

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