import { ValidationPipe } from '@nestjs/common';
import { RbacService } from './rbac.service';
import { CreateRoleDto, CreateUserDto, UpdateUserDto } from './dto/rbac.dto';

function makeService(overrides?: {
  permRepo?: Record<string, jest.Mock>;
  roleRepo?: Record<string, jest.Mock>;
  userRepo?: Record<string, jest.Mock>;
}) {
  const permRepo = {
    findByIds: jest.fn().mockResolvedValue([]),
    ...(overrides?.permRepo ?? {}),
  };
  const roleRepo = {
    create: jest.fn((value) => ({ ...value })),
    save: jest.fn(async (value) => value),
    findByIds: jest.fn().mockResolvedValue([]),
    findOneOrFail: jest.fn(),
    ...(overrides?.roleRepo ?? {}),
  };
  const userRepo = {
    create: jest.fn((value) => ({ ...value })),
    save: jest.fn(async (value) => value),
    findOne: jest.fn().mockResolvedValue(null),
    ...(overrides?.userRepo ?? {}),
  };
  return {
    service: new RbacService(
      permRepo as never,
      roleRepo as never,
      userRepo as never,
      {} as never,
      {} as never,
      {} as never,
      {} as never,
      {} as never,
    ),
    permRepo,
    roleRepo,
    userRepo,
  };
}

describe('RBAC mutation boundaries', () => {
  it('rejects a role when any requested permission id does not exist', async () => {
    const { service, roleRepo } = makeService({
      permRepo: { findByIds: jest.fn().mockResolvedValue([{ id: 1, code: 'student:view' }]) },
    });

    await expect(service.createRole({ name: 'partial', permissionIds: [1, 999] })).rejects.toThrow(
      '权限不存在: 999',
    );
    expect(roleRepo.save).not.toHaveBeenCalled();
  });

  it('rejects a user when any requested role id does not exist', async () => {
    const { service, userRepo } = makeService({
      roleRepo: { findByIds: jest.fn().mockResolvedValue([{ id: 2, name: '老师' }]) },
    });

    await expect(
      service.createUser({
        username: 'alice',
        password: 'secret',
        name: 'Alice',
        roleIds: [2, 404],
      }),
    ).rejects.toThrow('角色不存在: 404');
    expect(userRepo.save).not.toHaveBeenCalled();
  });

  it('allows explicitly clearing all roles from an existing user', async () => {
    const user = { id: 7, username: 'alice', name: 'Alice', roles: [{ id: 2 }] };
    const { service, userRepo } = makeService({
      userRepo: { findOne: jest.fn().mockResolvedValue(user) },
    });

    await expect(service.updateUser(7, { roleIds: [] })).resolves.toEqual({ message: '更新成功' });
    expect(user.roles).toEqual([]);
    expect(userRepo.save).toHaveBeenCalledWith(user);
  });
});

describe('RBAC DTO id arrays', () => {
  const pipe = new ValidationPipe({ transform: true, whitelist: true });

  it.each([
    [CreateRoleDto, { name: 'role', permissionIds: [1, '2'] }],
    [CreateRoleDto, { name: 'role', permissionIds: [1, 1] }],
    [CreateUserDto, { username: 'alice', password: 'secret', name: 'Alice', roleIds: [0] }],
    [UpdateUserDto, { roleIds: [1.5] }],
  ])('rejects invalid, duplicate, or non-positive ids for %p', async (metatype, value) => {
    await expect(pipe.transform(value, { type: 'body', metatype })).rejects.toBeDefined();
  });
});
