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

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

  const mockPrismaService = {
    user: {
      findMany: vi.fn(),
      findUnique: vi.fn(),
      count: vi.fn(),
      create: vi.fn(),
      update: vi.fn(),
    },
    workspace: {
      findUnique: vi.fn(),
      create: vi.fn(),
      update: vi.fn(),
    },
    workspaceMember: {
      create: vi.fn(),
    },
    session: {
      deleteMany: vi.fn(),
    },
    $transaction: vi.fn(async (ops) => {
      if (typeof ops === "function") {
        return ops(mockPrismaService);
      }
      return Promise.all(ops);
    }),
  };

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

  const mockUser = {
    id: mockUserId,
    name: "Test User",
    email: "test@example.com",
    emailVerified: false,
    image: null,
    createdAt: new Date("2026-01-01"),
    updatedAt: new Date("2026-01-01"),
    deactivatedAt: null,
    isLocalAuth: false,
    passwordHash: null,
    invitedBy: null,
    invitationToken: null,
    invitedAt: null,
    authProviderId: null,
    preferences: {},
    workspaceMemberships: [
      {
        workspaceId: mockWorkspaceId,
        userId: mockUserId,
        role: WorkspaceMemberRole.MEMBER,
        joinedAt: new Date("2026-01-01"),
        workspace: { id: mockWorkspaceId, name: "Test Workspace" },
      },
    ],
  };

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

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

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

    vi.clearAllMocks();
  });

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

  describe("listUsers", () => {
    it("should return paginated users with memberships", async () => {
      mockPrismaService.user.findMany.mockResolvedValue([mockUser]);
      mockPrismaService.user.count.mockResolvedValue(1);

      const result = await service.listUsers(1, 50);

      expect(result.data).toHaveLength(1);
      expect(result.data[0]?.id).toBe(mockUserId);
      expect(result.data[0]?.workspaceMemberships).toHaveLength(1);
      expect(result.meta).toEqual({
        total: 1,
        page: 1,
        limit: 50,
        totalPages: 1,
      });
    });

    it("should use default pagination when not provided", async () => {
      mockPrismaService.user.findMany.mockResolvedValue([]);
      mockPrismaService.user.count.mockResolvedValue(0);

      await service.listUsers();

      expect(mockPrismaService.user.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          skip: 0,
          take: 50,
        })
      );
    });

    it("should calculate pagination correctly", async () => {
      mockPrismaService.user.findMany.mockResolvedValue([]);
      mockPrismaService.user.count.mockResolvedValue(150);

      const result = await service.listUsers(3, 25);

      expect(mockPrismaService.user.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          skip: 50,
          take: 25,
        })
      );
      expect(result.meta.totalPages).toBe(6);
    });
  });

  describe("inviteUser", () => {
    it("should create a user with invitation token", async () => {
      mockPrismaService.user.findUnique.mockResolvedValue(null);
      const createdUser = {
        id: "new-user-id",
        email: "new@example.com",
        name: "new",
        invitationToken: "some-token",
      };
      mockPrismaService.user.create.mockResolvedValue(createdUser);

      const result = await service.inviteUser({ email: "new@example.com" }, mockAdminId);

      expect(result.email).toBe("new@example.com");
      expect(result.invitationToken).toBeDefined();
      expect(result.userId).toBe("new-user-id");
      expect(mockPrismaService.user.create).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({
            email: "new@example.com",
            invitedBy: mockAdminId,
            invitationToken: expect.any(String),
          }),
        })
      );
    });

    it("should add user to workspace when workspaceId provided", async () => {
      mockPrismaService.user.findUnique.mockResolvedValue(null);
      mockPrismaService.workspace.findUnique.mockResolvedValue(mockWorkspace);
      const createdUser = { id: "new-user-id", email: "new@example.com", name: "new" };
      mockPrismaService.user.create.mockResolvedValue(createdUser);

      await service.inviteUser(
        {
          email: "new@example.com",
          workspaceId: mockWorkspaceId,
          role: WorkspaceMemberRole.ADMIN,
        },
        mockAdminId
      );

      expect(mockPrismaService.workspaceMember.create).toHaveBeenCalledWith({
        data: {
          workspaceId: mockWorkspaceId,
          userId: "new-user-id",
          role: WorkspaceMemberRole.ADMIN,
        },
      });
    });

    it("should throw ConflictException if email already exists", async () => {
      mockPrismaService.user.findUnique.mockResolvedValue(mockUser);

      await expect(service.inviteUser({ email: "test@example.com" }, mockAdminId)).rejects.toThrow(
        ConflictException
      );
    });

    it("should throw NotFoundException if workspace does not exist", async () => {
      mockPrismaService.user.findUnique.mockResolvedValue(null);
      mockPrismaService.workspace.findUnique.mockResolvedValue(null);

      await expect(
        service.inviteUser({ email: "new@example.com", workspaceId: "non-existent" }, mockAdminId)
      ).rejects.toThrow(NotFoundException);
    });

    it("should use email prefix as default name", async () => {
      mockPrismaService.user.findUnique.mockResolvedValue(null);
      const createdUser = { id: "new-user-id", email: "jane.doe@example.com", name: "jane.doe" };
      mockPrismaService.user.create.mockResolvedValue(createdUser);

      await service.inviteUser({ email: "jane.doe@example.com" }, mockAdminId);

      expect(mockPrismaService.user.create).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({
            name: "jane.doe",
          }),
        })
      );
    });

    it("should use provided name when given", async () => {
      mockPrismaService.user.findUnique.mockResolvedValue(null);
      const createdUser = { id: "new-user-id", email: "j@example.com", name: "Jane Doe" };
      mockPrismaService.user.create.mockResolvedValue(createdUser);

      await service.inviteUser({ email: "j@example.com", name: "Jane Doe" }, mockAdminId);

      expect(mockPrismaService.user.create).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({
            name: "Jane Doe",
          }),
        })
      );
    });
  });

  describe("updateUser", () => {
    it("should update user fields", async () => {
      mockPrismaService.user.findUnique.mockResolvedValue(mockUser);
      mockPrismaService.user.update.mockResolvedValue({
        ...mockUser,
        name: "Updated Name",
      });

      const result = await service.updateUser(mockUserId, { name: "Updated Name" });

      expect(result.name).toBe("Updated Name");
      expect(mockPrismaService.user.update).toHaveBeenCalledWith(
        expect.objectContaining({
          where: { id: mockUserId },
          data: { name: "Updated Name" },
        })
      );
    });

    it("should set deactivatedAt when provided", async () => {
      const deactivatedAt = "2026-02-28T00:00:00.000Z";
      mockPrismaService.user.findUnique.mockResolvedValue(mockUser);
      mockPrismaService.user.update.mockResolvedValue({
        ...mockUser,
        deactivatedAt: new Date(deactivatedAt),
      });

      const result = await service.updateUser(mockUserId, { deactivatedAt });

      expect(result.deactivatedAt).toEqual(new Date(deactivatedAt));
    });

    it("should clear deactivatedAt when set to null", async () => {
      const deactivatedUser = { ...mockUser, deactivatedAt: new Date() };
      mockPrismaService.user.findUnique.mockResolvedValue(deactivatedUser);
      mockPrismaService.user.update.mockResolvedValue({
        ...deactivatedUser,
        deactivatedAt: null,
      });

      const result = await service.updateUser(mockUserId, { deactivatedAt: null });

      expect(result.deactivatedAt).toBeNull();
    });

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

      await expect(service.updateUser("non-existent", { name: "Test" })).rejects.toThrow(
        NotFoundException
      );
    });

    it("should update emailVerified", async () => {
      mockPrismaService.user.findUnique.mockResolvedValue(mockUser);
      mockPrismaService.user.update.mockResolvedValue({
        ...mockUser,
        emailVerified: true,
      });

      const result = await service.updateUser(mockUserId, { emailVerified: true });

      expect(result.emailVerified).toBe(true);
    });

    it("should update preferences", async () => {
      const prefs = { theme: "dark" };
      mockPrismaService.user.findUnique.mockResolvedValue(mockUser);
      mockPrismaService.user.update.mockResolvedValue({
        ...mockUser,
        preferences: prefs,
      });

      await service.updateUser(mockUserId, { preferences: prefs });

      expect(mockPrismaService.user.update).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({ preferences: prefs }),
        })
      );
    });
  });

  describe("deactivateUser", () => {
    it("should set deactivatedAt and invalidate sessions", async () => {
      mockPrismaService.user.findUnique.mockResolvedValue(mockUser);
      mockPrismaService.user.update.mockResolvedValue({
        ...mockUser,
        deactivatedAt: new Date(),
      });
      mockPrismaService.session.deleteMany.mockResolvedValue({ count: 3 });

      const result = await service.deactivateUser(mockUserId);

      expect(result.deactivatedAt).toBeDefined();
      expect(mockPrismaService.user.update).toHaveBeenCalledWith(
        expect.objectContaining({
          where: { id: mockUserId },
          data: { deactivatedAt: expect.any(Date) },
        })
      );
      expect(mockPrismaService.session.deleteMany).toHaveBeenCalledWith({
        where: { userId: mockUserId },
      });
    });

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

      await expect(service.deactivateUser("non-existent")).rejects.toThrow(NotFoundException);
    });

    it("should throw BadRequestException if user is already deactivated", async () => {
      mockPrismaService.user.findUnique.mockResolvedValue({
        ...mockUser,
        deactivatedAt: new Date(),
      });

      await expect(service.deactivateUser(mockUserId)).rejects.toThrow(BadRequestException);
    });
  });

  describe("createWorkspace", () => {
    it("should create a workspace with owner membership", async () => {
      mockPrismaService.user.findUnique.mockResolvedValue(mockUser);
      mockPrismaService.workspace.create.mockResolvedValue(mockWorkspace);

      const result = await service.createWorkspace({
        name: "New Workspace",
        ownerId: mockAdminId,
      });

      expect(result.name).toBe("Test Workspace");
      expect(result.memberCount).toBe(1);
      expect(mockPrismaService.workspace.create).toHaveBeenCalled();
      expect(mockPrismaService.workspaceMember.create).toHaveBeenCalledWith({
        data: {
          workspaceId: mockWorkspace.id,
          userId: mockAdminId,
          role: WorkspaceMemberRole.OWNER,
        },
      });
    });

    it("should throw NotFoundException if owner does not exist", async () => {
      mockPrismaService.user.findUnique.mockResolvedValue(null);

      await expect(
        service.createWorkspace({ name: "New Workspace", ownerId: "non-existent" })
      ).rejects.toThrow(NotFoundException);
    });

    it("should pass settings when provided", async () => {
      const settings = { theme: "dark", features: ["chat"] };
      mockPrismaService.user.findUnique.mockResolvedValue(mockUser);
      mockPrismaService.workspace.create.mockResolvedValue({
        ...mockWorkspace,
        settings,
      });

      await service.createWorkspace({
        name: "New Workspace",
        ownerId: mockAdminId,
        settings,
      });

      expect(mockPrismaService.workspace.create).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({ settings }),
        })
      );
    });
  });

  describe("updateWorkspace", () => {
    it("should update workspace name", async () => {
      mockPrismaService.workspace.findUnique.mockResolvedValue(mockWorkspace);
      mockPrismaService.workspace.update.mockResolvedValue({
        ...mockWorkspace,
        name: "Updated Workspace",
        _count: { members: 3 },
      });

      const result = await service.updateWorkspace(mockWorkspaceId, {
        name: "Updated Workspace",
      });

      expect(result.name).toBe("Updated Workspace");
      expect(result.memberCount).toBe(3);
    });

    it("should update workspace settings", async () => {
      const newSettings = { notifications: true };
      mockPrismaService.workspace.findUnique.mockResolvedValue(mockWorkspace);
      mockPrismaService.workspace.update.mockResolvedValue({
        ...mockWorkspace,
        settings: newSettings,
        _count: { members: 1 },
      });

      const result = await service.updateWorkspace(mockWorkspaceId, {
        settings: newSettings,
      });

      expect(result.settings).toEqual(newSettings);
    });

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

      await expect(service.updateWorkspace("non-existent", { name: "Test" })).rejects.toThrow(
        NotFoundException
      );
    });

    it("should only update provided fields", async () => {
      mockPrismaService.workspace.findUnique.mockResolvedValue(mockWorkspace);
      mockPrismaService.workspace.update.mockResolvedValue({
        ...mockWorkspace,
        _count: { members: 1 },
      });

      await service.updateWorkspace(mockWorkspaceId, { name: "Only Name" });

      expect(mockPrismaService.workspace.update).toHaveBeenCalledWith(
        expect.objectContaining({
          data: { name: "Only Name" },
        })
      );
    });
  });
});
