import { describe, it, expect, beforeEach, vi } from "vitest";
import { Test, TestingModule } from "@nestjs/testing";
import { WorkspacesService } from "./workspaces.service";
import { PrismaService } from "../prisma/prisma.service";
import { WorkspaceMemberRole } from "@prisma/client";
import {
  BadRequestException,
  ConflictException,
  ForbiddenException,
  NotFoundException,
} from "@nestjs/common";

describe("WorkspacesService", () => {
  let service: WorkspacesService;

  const mockUserId = "550e8400-e29b-41d4-a716-446655440001";
  const mockAdminUserId = "550e8400-e29b-41d4-a716-446655440010";
  const mockMemberUserId = "550e8400-e29b-41d4-a716-446655440011";
  const mockWorkspaceId = "550e8400-e29b-41d4-a716-446655440002";

  const mockWorkspace = {
    id: mockWorkspaceId,
    name: "Test Workspace",
    ownerId: mockUserId,
    settings: {},
    matrixRoomId: null,
    createdAt: new Date("2026-01-01"),
    updatedAt: new Date("2026-01-01"),
  };

  const mockMembership = {
    workspaceId: mockWorkspaceId,
    userId: mockUserId,
    role: WorkspaceMemberRole.OWNER,
    joinedAt: new Date("2026-01-01"),
    workspace: {
      id: mockWorkspaceId,
      name: "Test Workspace",
      ownerId: mockUserId,
      createdAt: new Date("2026-01-01"),
    },
  };

  const mockPrismaService = {
    workspaceMember: {
      findMany: vi.fn(),
      findUnique: vi.fn(),
      count: vi.fn(),
      create: vi.fn(),
      update: vi.fn(),
      delete: vi.fn(),
    },
    workspace: {
      create: vi.fn(),
    },
    user: {
      findUnique: vi.fn(),
    },
    $transaction: vi.fn(),
  };

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

    service = module.get<WorkspacesService>(WorkspacesService);

    vi.clearAllMocks();

    mockPrismaService.$transaction.mockImplementation(
      async (fn: (tx: typeof mockPrismaService) => Promise<unknown>) =>
        fn(mockPrismaService as unknown as typeof mockPrismaService)
    );
  });

  describe("getUserWorkspaces", () => {
    it("should return all workspaces user is a member of", async () => {
      mockPrismaService.workspaceMember.findMany.mockResolvedValueOnce([mockMembership]);

      const result = await service.getUserWorkspaces(mockUserId);

      expect(result).toEqual([
        {
          id: mockWorkspaceId,
          name: "Test Workspace",
          ownerId: mockUserId,
          role: WorkspaceMemberRole.OWNER,
          createdAt: mockMembership.workspace.createdAt,
        },
      ]);
      expect(mockPrismaService.workspaceMember.findMany).toHaveBeenCalledWith({
        where: { userId: mockUserId },
        include: {
          workspace: {
            select: { id: true, name: true, ownerId: true, createdAt: true },
          },
        },
        orderBy: { joinedAt: "asc" },
      });
    });

    it("should return multiple workspaces ordered by joinedAt", async () => {
      const secondWorkspace = {
        ...mockMembership,
        workspaceId: "ws-2",
        role: WorkspaceMemberRole.MEMBER,
        joinedAt: new Date("2026-02-01"),
        workspace: {
          id: "ws-2",
          name: "Second Workspace",
          ownerId: "other-user",
          createdAt: new Date("2026-02-01"),
        },
      };

      mockPrismaService.workspaceMember.findMany.mockResolvedValueOnce([
        mockMembership,
        secondWorkspace,
      ]);

      const result = await service.getUserWorkspaces(mockUserId);

      expect(result).toHaveLength(2);
      expect(result[0].id).toBe(mockWorkspaceId);
      expect(result[1].id).toBe("ws-2");
      expect(result[1].role).toBe(WorkspaceMemberRole.MEMBER);
    });

    it("should auto-provision a default workspace when user has no memberships", async () => {
      mockPrismaService.workspaceMember.findMany.mockResolvedValueOnce([]);
      mockPrismaService.$transaction.mockImplementationOnce(
        async (fn: (tx: typeof mockPrismaService) => Promise<unknown>) => {
          const txMock = {
            workspaceMember: {
              findFirst: vi.fn().mockResolvedValueOnce(null),
              create: vi.fn().mockResolvedValueOnce({}),
            },
            workspace: {
              create: vi.fn().mockResolvedValueOnce(mockWorkspace),
            },
          };
          return fn(txMock as unknown as typeof mockPrismaService);
        }
      );

      const result = await service.getUserWorkspaces(mockUserId);

      expect(result).toHaveLength(1);
      expect(result[0].name).toBe("Test Workspace");
      expect(result[0].role).toBe(WorkspaceMemberRole.OWNER);
      expect(mockPrismaService.$transaction).toHaveBeenCalledTimes(1);
    });

    it("should return existing workspace if one was created between initial check and transaction", async () => {
      // Simulates a race condition: initial findMany returns [], but inside the
      // transaction another request already created a workspace.
      mockPrismaService.workspaceMember.findMany.mockResolvedValueOnce([]);
      mockPrismaService.$transaction.mockImplementationOnce(
        async (fn: (tx: typeof mockPrismaService) => Promise<unknown>) => {
          const txMock = {
            workspaceMember: {
              findFirst: vi.fn().mockResolvedValueOnce(mockMembership),
            },
            workspace: {
              create: vi.fn(),
            },
          };
          return fn(txMock as unknown as typeof mockPrismaService);
        }
      );

      const result = await service.getUserWorkspaces(mockUserId);

      expect(result).toHaveLength(1);
      expect(result[0].id).toBe(mockWorkspaceId);
      expect(result[0].name).toBe("Test Workspace");
    });

    it("should create workspace with correct data during auto-provisioning", async () => {
      mockPrismaService.workspaceMember.findMany.mockResolvedValueOnce([]);

      let capturedWorkspaceData: unknown;
      let capturedMemberData: unknown;

      mockPrismaService.$transaction.mockImplementationOnce(
        async (fn: (tx: typeof mockPrismaService) => Promise<unknown>) => {
          const txMock = {
            workspaceMember: {
              findFirst: vi.fn().mockResolvedValueOnce(null),
              create: vi.fn().mockImplementation((args: unknown) => {
                capturedMemberData = args;
                return {};
              }),
            },
            workspace: {
              create: vi.fn().mockImplementation((args: unknown) => {
                capturedWorkspaceData = args;
                return mockWorkspace;
              }),
            },
          };
          return fn(txMock as unknown as typeof mockPrismaService);
        }
      );

      await service.getUserWorkspaces(mockUserId);

      expect(capturedWorkspaceData).toEqual({
        data: {
          name: "My Workspace",
          ownerId: mockUserId,
          settings: {},
        },
      });
      expect(capturedMemberData).toEqual({
        data: {
          workspaceId: mockWorkspaceId,
          userId: mockUserId,
          role: WorkspaceMemberRole.OWNER,
        },
      });
    });

    it("should not auto-provision when user already has workspaces", async () => {
      mockPrismaService.workspaceMember.findMany.mockResolvedValueOnce([mockMembership]);

      await service.getUserWorkspaces(mockUserId);

      expect(mockPrismaService.$transaction).not.toHaveBeenCalled();
    });

    it("should propagate database errors", async () => {
      mockPrismaService.workspaceMember.findMany.mockRejectedValueOnce(
        new Error("Database connection failed")
      );

      await expect(service.getUserWorkspaces(mockUserId)).rejects.toThrow(
        "Database connection failed"
      );
    });
  });

  describe("addMember", () => {
    const addMemberDto = {
      userId: mockMemberUserId,
      role: WorkspaceMemberRole.MEMBER,
    };

    it("should add a new member to the workspace", async () => {
      const createdMembership = {
        workspaceId: mockWorkspaceId,
        userId: mockMemberUserId,
        role: WorkspaceMemberRole.MEMBER,
        joinedAt: new Date("2026-02-02"),
      };
      mockPrismaService.user.findUnique.mockResolvedValueOnce({ id: mockMemberUserId });
      mockPrismaService.workspaceMember.findUnique
        .mockResolvedValueOnce({
          workspaceId: mockWorkspaceId,
          userId: mockAdminUserId,
          role: WorkspaceMemberRole.ADMIN,
          joinedAt: new Date("2026-01-01"),
        })
        .mockResolvedValueOnce(null);
      mockPrismaService.workspaceMember.create.mockResolvedValueOnce(createdMembership);

      const result = await service.addMember(mockWorkspaceId, mockAdminUserId, addMemberDto);

      expect(result).toEqual(createdMembership);
      expect(mockPrismaService.workspaceMember.create).toHaveBeenCalledWith({
        data: {
          workspaceId: mockWorkspaceId,
          userId: mockMemberUserId,
          role: WorkspaceMemberRole.MEMBER,
        },
      });
    });

    it("should throw NotFoundException when user does not exist", async () => {
      mockPrismaService.workspaceMember.findUnique.mockResolvedValueOnce({
        workspaceId: mockWorkspaceId,
        userId: mockAdminUserId,
        role: WorkspaceMemberRole.ADMIN,
        joinedAt: new Date("2026-01-01"),
      });
      mockPrismaService.user.findUnique.mockResolvedValueOnce(null);

      await expect(
        service.addMember(mockWorkspaceId, mockAdminUserId, addMemberDto)
      ).rejects.toThrow(NotFoundException);
    });

    it("should throw ConflictException when user is already a member", async () => {
      mockPrismaService.workspaceMember.findUnique.mockResolvedValueOnce({
        workspaceId: mockWorkspaceId,
        userId: mockAdminUserId,
        role: WorkspaceMemberRole.ADMIN,
        joinedAt: new Date("2026-01-01"),
      });
      mockPrismaService.user.findUnique.mockResolvedValueOnce({ id: mockMemberUserId });
      mockPrismaService.workspaceMember.findUnique.mockResolvedValueOnce({
        workspaceId: mockWorkspaceId,
        userId: mockMemberUserId,
        role: WorkspaceMemberRole.MEMBER,
        joinedAt: new Date("2026-01-02"),
      });

      await expect(
        service.addMember(mockWorkspaceId, mockAdminUserId, addMemberDto)
      ).rejects.toThrow(ConflictException);
    });

    it("should throw ForbiddenException when admin tries to assign OWNER role", async () => {
      mockPrismaService.workspaceMember.findUnique.mockResolvedValueOnce({
        workspaceId: mockWorkspaceId,
        userId: mockAdminUserId,
        role: WorkspaceMemberRole.ADMIN,
        joinedAt: new Date("2026-01-01"),
      });

      await expect(
        service.addMember(mockWorkspaceId, mockAdminUserId, {
          userId: mockMemberUserId,
          role: WorkspaceMemberRole.OWNER,
        })
      ).rejects.toThrow(ForbiddenException);
    });
  });

  describe("updateMemberRole", () => {
    it("should update a member role", async () => {
      const updatedMembership = {
        workspaceId: mockWorkspaceId,
        userId: mockMemberUserId,
        role: WorkspaceMemberRole.ADMIN,
        joinedAt: new Date("2026-01-02"),
      };
      mockPrismaService.workspaceMember.findUnique
        .mockResolvedValueOnce({
          workspaceId: mockWorkspaceId,
          userId: mockUserId,
          role: WorkspaceMemberRole.OWNER,
          joinedAt: new Date("2026-01-01"),
        })
        .mockResolvedValueOnce({
          workspaceId: mockWorkspaceId,
          userId: mockMemberUserId,
          role: WorkspaceMemberRole.MEMBER,
          joinedAt: new Date("2026-01-02"),
        });
      mockPrismaService.workspaceMember.update.mockResolvedValueOnce(updatedMembership);

      const result = await service.updateMemberRole(mockWorkspaceId, mockUserId, mockMemberUserId, {
        role: WorkspaceMemberRole.ADMIN,
      });

      expect(result).toEqual(updatedMembership);
      expect(mockPrismaService.workspaceMember.update).toHaveBeenCalledWith({
        where: {
          workspaceId_userId: {
            workspaceId: mockWorkspaceId,
            userId: mockMemberUserId,
          },
        },
        data: {
          role: WorkspaceMemberRole.ADMIN,
        },
      });
    });

    it("should throw NotFoundException when target member does not exist", async () => {
      mockPrismaService.workspaceMember.findUnique
        .mockResolvedValueOnce({
          workspaceId: mockWorkspaceId,
          userId: mockUserId,
          role: WorkspaceMemberRole.OWNER,
          joinedAt: new Date("2026-01-01"),
        })
        .mockResolvedValueOnce(null);

      await expect(
        service.updateMemberRole(mockWorkspaceId, mockUserId, mockMemberUserId, {
          role: WorkspaceMemberRole.ADMIN,
        })
      ).rejects.toThrow(NotFoundException);
    });

    it("should throw BadRequestException when sole owner attempts self-demotion", async () => {
      mockPrismaService.workspaceMember.findUnique
        .mockResolvedValueOnce({
          workspaceId: mockWorkspaceId,
          userId: mockUserId,
          role: WorkspaceMemberRole.OWNER,
          joinedAt: new Date("2026-01-01"),
        })
        .mockResolvedValueOnce({
          workspaceId: mockWorkspaceId,
          userId: mockUserId,
          role: WorkspaceMemberRole.OWNER,
          joinedAt: new Date("2026-01-01"),
        });
      mockPrismaService.workspaceMember.count.mockResolvedValueOnce(1);

      await expect(
        service.updateMemberRole(mockWorkspaceId, mockUserId, mockUserId, {
          role: WorkspaceMemberRole.ADMIN,
        })
      ).rejects.toThrow(BadRequestException);
    });

    it("should throw ForbiddenException when actor tries to change role of higher-ranked member", async () => {
      mockPrismaService.workspaceMember.findUnique
        .mockResolvedValueOnce({
          workspaceId: mockWorkspaceId,
          userId: mockAdminUserId,
          role: WorkspaceMemberRole.ADMIN,
          joinedAt: new Date("2026-01-01"),
        })
        .mockResolvedValueOnce({
          workspaceId: mockWorkspaceId,
          userId: mockUserId,
          role: WorkspaceMemberRole.OWNER,
          joinedAt: new Date("2026-01-01"),
        });

      await expect(
        service.updateMemberRole(mockWorkspaceId, mockAdminUserId, mockUserId, {
          role: WorkspaceMemberRole.MEMBER,
        })
      ).rejects.toThrow(ForbiddenException);
    });
  });

  describe("removeMember", () => {
    it("should remove a workspace member", async () => {
      mockPrismaService.workspaceMember.findUnique
        .mockResolvedValueOnce({
          workspaceId: mockWorkspaceId,
          userId: mockUserId,
          role: WorkspaceMemberRole.OWNER,
          joinedAt: new Date("2026-01-01"),
        })
        .mockResolvedValueOnce({
          workspaceId: mockWorkspaceId,
          userId: mockMemberUserId,
          role: WorkspaceMemberRole.MEMBER,
          joinedAt: new Date("2026-01-02"),
        });
      mockPrismaService.workspaceMember.delete.mockResolvedValueOnce({
        workspaceId: mockWorkspaceId,
        userId: mockMemberUserId,
        role: WorkspaceMemberRole.MEMBER,
        joinedAt: new Date("2026-01-02"),
      });

      await service.removeMember(mockWorkspaceId, mockUserId, mockMemberUserId);

      expect(mockPrismaService.workspaceMember.delete).toHaveBeenCalledWith({
        where: {
          workspaceId_userId: {
            workspaceId: mockWorkspaceId,
            userId: mockMemberUserId,
          },
        },
      });
    });

    it("should throw BadRequestException when trying to remove the last owner", async () => {
      mockPrismaService.workspaceMember.findUnique
        .mockResolvedValueOnce({
          workspaceId: mockWorkspaceId,
          userId: mockUserId,
          role: WorkspaceMemberRole.OWNER,
          joinedAt: new Date("2026-01-01"),
        })
        .mockResolvedValueOnce({
          workspaceId: mockWorkspaceId,
          userId: mockUserId,
          role: WorkspaceMemberRole.OWNER,
          joinedAt: new Date("2026-01-01"),
        });
      mockPrismaService.workspaceMember.count.mockResolvedValueOnce(1);

      await expect(service.removeMember(mockWorkspaceId, mockUserId, mockUserId)).rejects.toThrow(
        BadRequestException
      );
    });

    it("should throw ForbiddenException when admin attempts to remove an owner", async () => {
      mockPrismaService.workspaceMember.findUnique
        .mockResolvedValueOnce({
          workspaceId: mockWorkspaceId,
          userId: mockAdminUserId,
          role: WorkspaceMemberRole.ADMIN,
          joinedAt: new Date("2026-01-01"),
        })
        .mockResolvedValueOnce({
          workspaceId: mockWorkspaceId,
          userId: mockUserId,
          role: WorkspaceMemberRole.OWNER,
          joinedAt: new Date("2026-01-01"),
        });

      await expect(
        service.removeMember(mockWorkspaceId, mockAdminUserId, mockUserId)
      ).rejects.toThrow(ForbiddenException);
    });
  });
});
