import { BadRequestException } from '@nestjs/common';
import { AuditService } from '../../common/audit/audit.service';
import { RbacService } from '../../common/rbac/rbac.service';
import { ProjectMemberService } from './project-member.service';

describe('ProjectMemberService', () => {
  const makeService = () => {
    const prisma = {
      projectMember: {
        count: jest.fn(),
        create: jest.fn(),
        delete: jest.fn(),
        findMany: jest.fn(),
        findUnique: jest.fn(),
        update: jest.fn(),
      },
    };
    const rbac = {
      assertProjectRole: jest.fn().mockResolvedValue({ role: 'owner' }),
    } as unknown as RbacService;
    const audit = {
      record: jest.fn(),
    } as unknown as AuditService;
    return {
      prisma,
      rbac,
      audit,
      service: new ProjectMemberService(prisma as any, rbac, audit),
    };
  };

  it('lists project members with their user profile', async () => {
    const { prisma, service } = makeService();
    prisma.projectMember.findMany.mockResolvedValue([{ id: 'pm-1', role: 'owner' }]);

    await expect(service.list('project-1')).resolves.toEqual([{ id: 'pm-1', role: 'owner' }]);

    expect(prisma.projectMember.findMany).toHaveBeenCalledWith({
      where: { projectId: 'project-1' },
      include: { user: { select: { id: true, name: true, email: true } } },
      orderBy: [{ role: 'asc' }, { createdAt: 'asc' }],
    });
  });

  it('creates a project member after admin-or-owner authorization and writes audit', async () => {
    const { prisma, rbac, audit, service } = makeService();
    prisma.projectMember.create.mockResolvedValue({ id: 'pm-2', role: 'member' });

    await service.add({ actorId: 'm-owner', projectId: 'project-1', userId: 'm-dev', role: 'member' });

    expect((rbac.assertProjectRole as jest.Mock)).toHaveBeenCalledWith({
      actorId: 'm-owner',
      projectId: 'project-1',
      allowedRoles: ['admin'],
    });
    expect(prisma.projectMember.create).toHaveBeenCalledWith({
      data: { projectId: 'project-1', userId: 'm-dev', role: 'member' },
    });
    expect((audit.record as jest.Mock)).toHaveBeenCalledWith(expect.objectContaining({
      actorId: 'm-owner',
      action: 'project_member.created',
      resourceType: 'project_member',
      projectId: 'project-1',
    }));
  });

  it('rejects demoting the last project owner', async () => {
    const { prisma, service } = makeService();
    prisma.projectMember.findUnique.mockResolvedValue({
      id: 'pm-owner',
      projectId: 'project-1',
      userId: 'm-owner',
      role: 'owner',
    });
    prisma.projectMember.count.mockResolvedValue(1);

    await expect(service.updateRole({
      actorId: 'm-owner',
      projectId: 'project-1',
      userId: 'm-owner',
      role: 'admin',
    })).rejects.toBeInstanceOf(BadRequestException);

    expect(prisma.projectMember.update).not.toHaveBeenCalled();
  });

  it('rejects removing the last project owner', async () => {
    const { prisma, service } = makeService();
    prisma.projectMember.findUnique.mockResolvedValue({
      id: 'pm-owner',
      projectId: 'project-1',
      userId: 'm-owner',
      role: 'owner',
    });
    prisma.projectMember.count.mockResolvedValue(1);

    await expect(service.remove({
      actorId: 'm-owner',
      projectId: 'project-1',
      userId: 'm-owner',
    })).rejects.toBeInstanceOf(BadRequestException);

    expect(prisma.projectMember.delete).not.toHaveBeenCalled();
  });

  it('updates roles and writes an audit diff', async () => {
    const { prisma, audit, service } = makeService();
    prisma.projectMember.findUnique.mockResolvedValue({
      id: 'pm-dev',
      projectId: 'project-1',
      userId: 'm-dev',
      role: 'member',
    });
    prisma.projectMember.update.mockResolvedValue({
      id: 'pm-dev',
      projectId: 'project-1',
      userId: 'm-dev',
      role: 'admin',
    });

    await service.updateRole({
      actorId: 'm-owner',
      projectId: 'project-1',
      userId: 'm-dev',
      role: 'admin',
    });

    expect(prisma.projectMember.update).toHaveBeenCalledWith({
      where: { projectId_userId: { projectId: 'project-1', userId: 'm-dev' } },
      data: { role: 'admin' },
    });
    expect((audit.record as jest.Mock)).toHaveBeenCalledWith(expect.objectContaining({
      action: 'project_member.role_changed',
      before: expect.objectContaining({ role: 'member' }),
      after: expect.objectContaining({ role: 'admin' }),
    }));
  });
});
