import { describe, it, expect, vi, beforeEach } from 'vitest';
import { RbacService } from '../src/services/rbac.service.js';
import type { IRbacDefinitionRepository } from '../src/repositories/rbac-definition.repository.js';
import type { RbacDefinition, PrismaClient } from '@prisma/client';

function makeDef(overrides: Partial<RbacDefinition> = {}): RbacDefinition {
  return {
    id: 'def-1',
    name: 'test-rbac',
    subjects: [{ kind: 'User', name: 'alice@example.com' }],
    roleBindings: [{ role: 'edit', resource: '*' }],
    version: 1,
    createdAt: new Date(),
    updatedAt: new Date(),
    ...overrides,
  };
}

function mockRepo(definitions: RbacDefinition[] = []): IRbacDefinitionRepository {
  return {
    findAll: vi.fn(async () => definitions),
    findById: vi.fn(async () => null),
    findByName: vi.fn(async () => null),
    create: vi.fn(async () => makeDef()),
    update: vi.fn(async () => makeDef()),
    delete: vi.fn(async () => {}),
  };
}

interface MockPrisma {
  user: { findUnique: ReturnType<typeof vi.fn> };
  groupMember: { findMany: ReturnType<typeof vi.fn> };
}

function mockPrisma(overrides?: Partial<MockPrisma>): PrismaClient {
  return {
    user: {
      findUnique: overrides?.user?.findUnique ?? vi.fn(async () => null),
    },
    groupMember: {
      findMany: overrides?.groupMember?.findMany ?? vi.fn(async () => []),
    },
  } as unknown as PrismaClient;
}

describe('RbacService', () => {
  describe('canAccess — edit:* (wildcard resource)', () => {
    let service: RbacService;

    beforeEach(() => {
      const repo = mockRepo([
        makeDef({
          subjects: [{ kind: 'User', name: 'alice@example.com' }],
          roleBindings: [{ role: 'edit', resource: '*' }],
        }),
      ]);
      const prisma = mockPrisma({
        user: { findUnique: vi.fn(async () => ({ email: 'alice@example.com' })) },
        groupMember: { findMany: vi.fn(async () => []) },
      });
      service = new RbacService(repo, prisma);
    });

    it('can view servers', async () => {
      expect(await service.canAccess('user-1', 'view', 'servers')).toBe(true);
    });

    it('can edit users', async () => {
      expect(await service.canAccess('user-1', 'edit', 'users')).toBe(true);
    });

    it('can create resources (edit includes create)', async () => {
      expect(await service.canAccess('user-1', 'create', 'servers')).toBe(true);
    });

    it('can delete resources (edit includes delete)', async () => {
      expect(await service.canAccess('user-1', 'delete', 'secrets')).toBe(true);
    });

    it('cannot run resources (edit does not include run)', async () => {
      expect(await service.canAccess('user-1', 'run', 'projects')).toBe(false);
    });

    it('can edit any resource (wildcard)', async () => {
      expect(await service.canAccess('user-1', 'edit', 'secrets')).toBe(true);
      expect(await service.canAccess('user-1', 'edit', 'projects')).toBe(true);
      expect(await service.canAccess('user-1', 'edit', 'instances')).toBe(true);
    });
  });

  describe('canAccess — edit:servers', () => {
    let service: RbacService;

    beforeEach(() => {
      const repo = mockRepo([
        makeDef({
          subjects: [{ kind: 'User', name: 'bob@example.com' }],
          roleBindings: [{ role: 'edit', resource: 'servers' }],
        }),
      ]);
      const prisma = mockPrisma({
        user: { findUnique: vi.fn(async () => ({ email: 'bob@example.com' })) },
        groupMember: { findMany: vi.fn(async () => []) },
      });
      service = new RbacService(repo, prisma);
    });

    it('can view servers', async () => {
      expect(await service.canAccess('user-2', 'view', 'servers')).toBe(true);
    });

    it('can edit servers', async () => {
      expect(await service.canAccess('user-2', 'edit', 'servers')).toBe(true);
    });

    it('can create servers (edit includes create)', async () => {
      expect(await service.canAccess('user-2', 'create', 'servers')).toBe(true);
    });

    it('can delete servers (edit includes delete)', async () => {
      expect(await service.canAccess('user-2', 'delete', 'servers')).toBe(true);
    });

    it('cannot edit users (wrong resource)', async () => {
      expect(await service.canAccess('user-2', 'edit', 'users')).toBe(false);
    });
  });

  describe('canAccess — view:servers', () => {
    let service: RbacService;

    beforeEach(() => {
      const repo = mockRepo([
        makeDef({
          subjects: [{ kind: 'User', name: 'carol@example.com' }],
          roleBindings: [{ role: 'view', resource: 'servers' }],
        }),
      ]);
      const prisma = mockPrisma({
        user: { findUnique: vi.fn(async () => ({ email: 'carol@example.com' })) },
        groupMember: { findMany: vi.fn(async () => []) },
      });
      service = new RbacService(repo, prisma);
    });

    it('can view servers', async () => {
      expect(await service.canAccess('user-3', 'view', 'servers')).toBe(true);
    });

    it('cannot edit servers', async () => {
      expect(await service.canAccess('user-3', 'edit', 'servers')).toBe(false);
    });

    it('cannot create servers', async () => {
      expect(await service.canAccess('user-3', 'create', 'servers')).toBe(false);
    });

    it('cannot delete servers', async () => {
      expect(await service.canAccess('user-3', 'delete', 'servers')).toBe(false);
    });
  });

  describe('canAccess — create role', () => {
    let service: RbacService;

    beforeEach(() => {
      const repo = mockRepo([
        makeDef({
          subjects: [{ kind: 'User', name: 'dan@example.com' }],
          roleBindings: [{ role: 'create', resource: 'servers' }],
        }),
      ]);
      const prisma = mockPrisma({
        user: { findUnique: vi.fn(async () => ({ email: 'dan@example.com' })) },
        groupMember: { findMany: vi.fn(async () => []) },
      });
      service = new RbacService(repo, prisma);
    });

    it('can create servers', async () => {
      expect(await service.canAccess('user-d', 'create', 'servers')).toBe(true);
    });

    it('cannot view servers', async () => {
      expect(await service.canAccess('user-d', 'view', 'servers')).toBe(false);
    });

    it('cannot delete servers', async () => {
      expect(await service.canAccess('user-d', 'delete', 'servers')).toBe(false);
    });

    it('cannot edit servers', async () => {
      expect(await service.canAccess('user-d', 'edit', 'servers')).toBe(false);
    });
  });

  describe('canAccess — delete role', () => {
    let service: RbacService;

    beforeEach(() => {
      const repo = mockRepo([
        makeDef({
          subjects: [{ kind: 'User', name: 'eve@example.com' }],
          roleBindings: [{ role: 'delete', resource: 'secrets' }],
        }),
      ]);
      const prisma = mockPrisma({
        user: { findUnique: vi.fn(async () => ({ email: 'eve@example.com' })) },
        groupMember: { findMany: vi.fn(async () => []) },
      });
      service = new RbacService(repo, prisma);
    });

    it('can delete secrets', async () => {
      expect(await service.canAccess('user-e', 'delete', 'secrets')).toBe(true);
    });

    it('cannot create secrets', async () => {
      expect(await service.canAccess('user-e', 'create', 'secrets')).toBe(false);
    });

    it('cannot view secrets', async () => {
      expect(await service.canAccess('user-e', 'view', 'secrets')).toBe(false);
    });
  });

  describe('canAccess — run role on resource', () => {
    let service: RbacService;

    beforeEach(() => {
      const repo = mockRepo([
        makeDef({
          subjects: [{ kind: 'User', name: 'frank@example.com' }],
          roleBindings: [{ role: 'run', resource: 'projects' }],
        }),
      ]);
      const prisma = mockPrisma({
        user: { findUnique: vi.fn(async () => ({ email: 'frank@example.com' })) },
        groupMember: { findMany: vi.fn(async () => []) },
      });
      service = new RbacService(repo, prisma);
    });

    it('can run projects', async () => {
      expect(await service.canAccess('user-f', 'run', 'projects')).toBe(true);
    });

    it('cannot view projects (run does not include view)', async () => {
      expect(await service.canAccess('user-f', 'view', 'projects')).toBe(false);
    });

    it('cannot run servers (wrong resource)', async () => {
      expect(await service.canAccess('user-f', 'run', 'servers')).toBe(false);
    });
  });

  describe('canAccess — no matching binding', () => {
    it('returns false when user has no matching definitions', async () => {
      const repo = mockRepo([
        makeDef({
          subjects: [{ kind: 'User', name: 'other@example.com' }],
          roleBindings: [{ role: 'edit', resource: '*' }],
        }),
      ]);
      const prisma = mockPrisma({
        user: { findUnique: vi.fn(async () => ({ email: 'nobody@example.com' })) },
        groupMember: { findMany: vi.fn(async () => []) },
      });
      const service = new RbacService(repo, prisma);

      expect(await service.canAccess('user-x', 'view', 'servers')).toBe(false);
    });

    it('returns false when user does not exist', async () => {
      const repo = mockRepo([makeDef()]);
      const prisma = mockPrisma(); // user.findUnique returns null
      const service = new RbacService(repo, prisma);

      expect(await service.canAccess('nonexistent', 'view', 'servers')).toBe(false);
    });
  });

  describe('canAccess — empty subjects', () => {
    it('matches nobody when subjects is empty', async () => {
      const repo = mockRepo([
        makeDef({
          subjects: [],
          roleBindings: [{ role: 'edit', resource: '*' }],
        }),
      ]);
      const prisma = mockPrisma({
        user: { findUnique: vi.fn(async () => ({ email: 'alice@example.com' })) },
        groupMember: { findMany: vi.fn(async () => []) },
      });
      const service = new RbacService(repo, prisma);

      expect(await service.canAccess('user-1', 'view', 'servers')).toBe(false);
    });
  });

  describe('canAccess — group membership', () => {
    it('grants access through group subject', async () => {
      const repo = mockRepo([
        makeDef({
          subjects: [{ kind: 'Group', name: 'devs' }],
          roleBindings: [{ role: 'edit', resource: 'servers' }],
        }),
      ]);
      const prisma = mockPrisma({
        user: { findUnique: vi.fn(async () => ({ email: 'dave@example.com' })) },
        groupMember: {
          findMany: vi.fn(async () => [{ group: { name: 'devs' } }]),
        },
      });
      const service = new RbacService(repo, prisma);

      expect(await service.canAccess('user-4', 'view', 'servers')).toBe(true);
      expect(await service.canAccess('user-4', 'edit', 'servers')).toBe(true);
      expect(await service.canAccess('user-4', 'run', 'servers')).toBe(false);
    });

    it('denies access when user is not in the group', async () => {
      const repo = mockRepo([
        makeDef({
          subjects: [{ kind: 'Group', name: 'devs' }],
          roleBindings: [{ role: 'edit', resource: 'servers' }],
        }),
      ]);
      const prisma = mockPrisma({
        user: { findUnique: vi.fn(async () => ({ email: 'eve@example.com' })) },
        groupMember: {
          findMany: vi.fn(async () => [{ group: { name: 'ops' } }]),
        },
      });
      const service = new RbacService(repo, prisma);

      expect(await service.canAccess('user-5', 'view', 'servers')).toBe(false);
    });
  });

  describe('canAccess — multiple definitions (union)', () => {
    it('unions permissions from multiple matching definitions', async () => {
      const repo = mockRepo([
        makeDef({
          id: 'def-1',
          name: 'rbac-viewers',
          subjects: [{ kind: 'User', name: 'frank@example.com' }],
          roleBindings: [{ role: 'view', resource: 'servers' }],
        }),
        makeDef({
          id: 'def-2',
          name: 'rbac-editors',
          subjects: [{ kind: 'User', name: 'frank@example.com' }],
          roleBindings: [{ role: 'edit', resource: 'secrets' }],
        }),
      ]);
      const prisma = mockPrisma({
        user: { findUnique: vi.fn(async () => ({ email: 'frank@example.com' })) },
        groupMember: { findMany: vi.fn(async () => []) },
      });
      const service = new RbacService(repo, prisma);

      // From def-1: view on servers
      expect(await service.canAccess('user-6', 'view', 'servers')).toBe(true);
      expect(await service.canAccess('user-6', 'edit', 'servers')).toBe(false);

      // From def-2: edit on secrets (includes view, create, delete)
      expect(await service.canAccess('user-6', 'view', 'secrets')).toBe(true);
      expect(await service.canAccess('user-6', 'edit', 'secrets')).toBe(true);
      expect(await service.canAccess('user-6', 'create', 'secrets')).toBe(true);

      // No permission on other resources
      expect(await service.canAccess('user-6', 'view', 'users')).toBe(false);
    });
  });

  describe('canAccess — mixed user and group subjects', () => {
    it('matches on either user or group subject', async () => {
      const repo = mockRepo([
        makeDef({
          subjects: [
            { kind: 'User', name: 'grace@example.com' },
            { kind: 'Group', name: 'admins' },
          ],
          roleBindings: [{ role: 'edit', resource: '*' }],
        }),
      ]);

      // Test user match (not in group)
      const prismaUser = mockPrisma({
        user: { findUnique: vi.fn(async () => ({ email: 'grace@example.com' })) },
        groupMember: { findMany: vi.fn(async () => []) },
      });
      const serviceUser = new RbacService(repo, prismaUser);
      expect(await serviceUser.canAccess('user-7', 'edit', 'servers')).toBe(true);

      // Test group match (different email)
      const prismaGroup = mockPrisma({
        user: { findUnique: vi.fn(async () => ({ email: 'hank@example.com' })) },
        groupMember: { findMany: vi.fn(async () => [{ group: { name: 'admins' } }]) },
      });
      const serviceGroup = new RbacService(repo, prismaGroup);
      expect(await serviceGroup.canAccess('user-8', 'edit', 'servers')).toBe(true);
    });
  });

  describe('canAccess — singular resource names', () => {
    it('normalizes singular resource in binding to match plural check', async () => {
      const repo = mockRepo([
        makeDef({
          subjects: [{ kind: 'User', name: 'alice@example.com' }],
          roleBindings: [{ role: 'edit', resource: 'server' }],
        }),
      ]);
      const prisma = mockPrisma({
        user: { findUnique: vi.fn(async () => ({ email: 'alice@example.com' })) },
        groupMember: { findMany: vi.fn(async () => []) },
      });
      const service = new RbacService(repo, prisma);

      expect(await service.canAccess('user-1', 'edit', 'servers')).toBe(true);
      expect(await service.canAccess('user-1', 'view', 'servers')).toBe(true);
    });

    it('normalizes singular resource in check to match plural binding', async () => {
      const repo = mockRepo([
        makeDef({
          subjects: [{ kind: 'User', name: 'alice@example.com' }],
          roleBindings: [{ role: 'edit', resource: 'servers' }],
        }),
      ]);
      const prisma = mockPrisma({
        user: { findUnique: vi.fn(async () => ({ email: 'alice@example.com' })) },
        groupMember: { findMany: vi.fn(async () => []) },
      });
      const service = new RbacService(repo, prisma);

      expect(await service.canAccess('user-1', 'edit', 'server')).toBe(true);
      expect(await service.canAccess('user-1', 'view', 'instance')).toBe(false);
    });
  });

  describe('canAccess — name-scoped resource bindings', () => {
    let service: RbacService;

    beforeEach(() => {
      const repo = mockRepo([
        makeDef({
          subjects: [{ kind: 'User', name: 'alice@example.com' }],
          roleBindings: [{ role: 'view', resource: 'servers', name: 'my-ha' }],
        }),
      ]);
      const prisma = mockPrisma({
        user: { findUnique: vi.fn(async () => ({ email: 'alice@example.com' })) },
        groupMember: { findMany: vi.fn(async () => []) },
      });
      service = new RbacService(repo, prisma);
    });

    it('allows access to the named resource', async () => {
      expect(await service.canAccess('user-1', 'view', 'servers', 'my-ha')).toBe(true);
    });

    it('denies access to a different named resource', async () => {
      expect(await service.canAccess('user-1', 'view', 'servers', 'other-server')).toBe(false);
    });

    it('allows listing (no resourceName specified)', async () => {
      expect(await service.canAccess('user-1', 'view', 'servers')).toBe(true);
    });
  });

  describe('canAccess — unnamed binding matches any resourceName', () => {
    let service: RbacService;

    beforeEach(() => {
      const repo = mockRepo([
        makeDef({
          subjects: [{ kind: 'User', name: 'alice@example.com' }],
          roleBindings: [{ role: 'view', resource: 'servers' }],
        }),
      ]);
      const prisma = mockPrisma({
        user: { findUnique: vi.fn(async () => ({ email: 'alice@example.com' })) },
        groupMember: { findMany: vi.fn(async () => []) },
      });
      service = new RbacService(repo, prisma);
    });

    it('allows access to any named resource', async () => {
      expect(await service.canAccess('user-1', 'view', 'servers', 'any-server')).toBe(true);
    });

    it('allows listing', async () => {
      expect(await service.canAccess('user-1', 'view', 'servers')).toBe(true);
    });
  });

  describe('canRunOperation', () => {
    it('grants operation when run:action binding matches', async () => {
      const repo = mockRepo([
        makeDef({
          subjects: [{ kind: 'User', name: 'alice@example.com' }],
          roleBindings: [{ role: 'run', action: 'logs' }],
        }),
      ]);
      const prisma = mockPrisma({
        user: { findUnique: vi.fn(async () => ({ email: 'alice@example.com' })) },
        groupMember: { findMany: vi.fn(async () => []) },
      });
      const service = new RbacService(repo, prisma);

      expect(await service.canRunOperation('user-1', 'logs')).toBe(true);
    });

    it('denies operation when action does not match', async () => {
      const repo = mockRepo([
        makeDef({
          subjects: [{ kind: 'User', name: 'alice@example.com' }],
          roleBindings: [{ role: 'run', action: 'logs' }],
        }),
      ]);
      const prisma = mockPrisma({
        user: { findUnique: vi.fn(async () => ({ email: 'alice@example.com' })) },
        groupMember: { findMany: vi.fn(async () => []) },
      });
      const service = new RbacService(repo, prisma);

      expect(await service.canRunOperation('user-1', 'backup')).toBe(false);
    });

    it('ignores resource bindings (only checks operation bindings)', async () => {
      const repo = mockRepo([
        makeDef({
          subjects: [{ kind: 'User', name: 'alice@example.com' }],
          roleBindings: [{ role: 'edit', resource: '*' }],
        }),
      ]);
      const prisma = mockPrisma({
        user: { findUnique: vi.fn(async () => ({ email: 'alice@example.com' })) },
        groupMember: { findMany: vi.fn(async () => []) },
      });
      const service = new RbacService(repo, prisma);

      expect(await service.canRunOperation('user-1', 'logs')).toBe(false);
    });
  });

  describe('mixed resource + operation bindings', () => {
    let service: RbacService;

    beforeEach(() => {
      const repo = mockRepo([
        makeDef({
          subjects: [{ kind: 'Group', name: 'admin' }],
          roleBindings: [
            { role: 'edit', resource: '*' },
            { role: 'run', resource: '*' },
            { role: 'run', action: 'impersonate' },
            { role: 'run', action: 'logs' },
            { role: 'run', action: 'backup' },
          ],
        }),
      ]);
      const prisma = mockPrisma({
        user: { findUnique: vi.fn(async () => ({ email: 'admin@example.com' })) },
        groupMember: { findMany: vi.fn(async () => [{ group: { name: 'admin' } }]) },
      });
      service = new RbacService(repo, prisma);
    });

    it('can access resources', async () => {
      expect(await service.canAccess('user-1', 'edit', 'servers')).toBe(true);
      expect(await service.canAccess('user-1', 'view', 'users')).toBe(true);
      expect(await service.canAccess('user-1', 'run', 'projects')).toBe(true);
    });

    it('can run operations', async () => {
      expect(await service.canRunOperation('user-1', 'impersonate')).toBe(true);
      expect(await service.canRunOperation('user-1', 'logs')).toBe(true);
      expect(await service.canRunOperation('user-1', 'backup')).toBe(true);
    });

    it('cannot run undefined operations', async () => {
      expect(await service.canRunOperation('user-1', 'destroy-all')).toBe(false);
    });
  });

  describe('getPermissions', () => {
    it('returns all permissions for a user', async () => {
      const repo = mockRepo([
        makeDef({
          subjects: [{ kind: 'User', name: 'alice@example.com' }],
          roleBindings: [
            { role: 'edit', resource: '*' },
            { role: 'view', resource: 'secrets' },
          ],
        }),
      ]);
      const prisma = mockPrisma({
        user: { findUnique: vi.fn(async () => ({ email: 'alice@example.com' })) },
        groupMember: { findMany: vi.fn(async () => []) },
      });
      const service = new RbacService(repo, prisma);

      const perms = await service.getPermissions('user-1');
      expect(perms).toEqual([
        { role: 'edit', resource: '*' },
        { role: 'view', resource: 'secrets' },
      ]);
    });

    it('returns mixed resource and operation permissions', async () => {
      const repo = mockRepo([
        makeDef({
          subjects: [{ kind: 'User', name: 'alice@example.com' }],
          roleBindings: [
            { role: 'edit', resource: 'servers' },
            { role: 'run', action: 'logs' },
          ],
        }),
      ]);
      const prisma = mockPrisma({
        user: { findUnique: vi.fn(async () => ({ email: 'alice@example.com' })) },
        groupMember: { findMany: vi.fn(async () => []) },
      });
      const service = new RbacService(repo, prisma);

      const perms = await service.getPermissions('user-1');
      expect(perms).toEqual([
        { role: 'edit', resource: 'servers' },
        { role: 'run', action: 'logs' },
      ]);
    });

    it('includes name field in name-scoped permissions', async () => {
      const repo = mockRepo([
        makeDef({
          subjects: [{ kind: 'User', name: 'alice@example.com' }],
          roleBindings: [
            { role: 'view', resource: 'servers', name: 'my-ha' },
          ],
        }),
      ]);
      const prisma = mockPrisma({
        user: { findUnique: vi.fn(async () => ({ email: 'alice@example.com' })) },
        groupMember: { findMany: vi.fn(async () => []) },
      });
      const service = new RbacService(repo, prisma);

      const perms = await service.getPermissions('user-1');
      expect(perms).toEqual([
        { role: 'view', resource: 'servers', name: 'my-ha' },
      ]);
    });

    it('returns empty for unknown user', async () => {
      const repo = mockRepo([makeDef()]);
      const prisma = mockPrisma();
      const service = new RbacService(repo, prisma);

      const perms = await service.getPermissions('nonexistent');
      expect(perms).toEqual([]);
    });

    it('returns empty when no definitions match', async () => {
      const repo = mockRepo([
        makeDef({
          subjects: [{ kind: 'User', name: 'other@example.com' }],
          roleBindings: [{ role: 'edit', resource: '*' }],
        }),
      ]);
      const prisma = mockPrisma({
        user: { findUnique: vi.fn(async () => ({ email: 'alice@example.com' })) },
        groupMember: { findMany: vi.fn(async () => []) },
      });
      const service = new RbacService(repo, prisma);

      const perms = await service.getPermissions('user-1');
      expect(perms).toEqual([]);
    });
  });

  describe('getAllowedScope', () => {
    describe('unscoped binding → wildcard', () => {
      it('returns wildcard:true for matching resource', async () => {
        const repo = mockRepo([
          makeDef({
            subjects: [{ kind: 'User', name: 'alice@example.com' }],
            roleBindings: [{ role: 'view', resource: 'servers' }],
          }),
        ]);
        const prisma = mockPrisma({
          user: { findUnique: vi.fn(async () => ({ email: 'alice@example.com' })) },
          groupMember: { findMany: vi.fn(async () => []) },
        });
        const service = new RbacService(repo, prisma);

        const scope = await service.getAllowedScope('user-1', 'view', 'servers');
        expect(scope.wildcard).toBe(true);
        expect(scope.names.size).toBe(0);
      });

      it('returns wildcard:true with wildcard resource binding', async () => {
        const repo = mockRepo([
          makeDef({
            subjects: [{ kind: 'User', name: 'alice@example.com' }],
            roleBindings: [{ role: 'edit', resource: '*' }],
          }),
        ]);
        const prisma = mockPrisma({
          user: { findUnique: vi.fn(async () => ({ email: 'alice@example.com' })) },
          groupMember: { findMany: vi.fn(async () => []) },
        });
        const service = new RbacService(repo, prisma);

        const scope = await service.getAllowedScope('user-1', 'view', 'servers');
        expect(scope.wildcard).toBe(true);
      });
    });

    describe('name-scoped binding → restricted', () => {
      let service: RbacService;

      beforeEach(() => {
        const repo = mockRepo([
          makeDef({
            subjects: [{ kind: 'User', name: 'alice@example.com' }],
            roleBindings: [{ role: 'view', resource: 'servers', name: 'my-ha' }],
          }),
        ]);
        const prisma = mockPrisma({
          user: { findUnique: vi.fn(async () => ({ email: 'alice@example.com' })) },
          groupMember: { findMany: vi.fn(async () => []) },
        });
        service = new RbacService(repo, prisma);
      });

      it('returns names containing the scoped name', async () => {
        const scope = await service.getAllowedScope('user-1', 'view', 'servers');
        expect(scope.wildcard).toBe(false);
        expect(scope.names).toEqual(new Set(['my-ha']));
      });

      it('returns empty names for wrong resource', async () => {
        const scope = await service.getAllowedScope('user-1', 'view', 'secrets');
        expect(scope.wildcard).toBe(false);
        expect(scope.names.size).toBe(0);
      });

      it('returns empty names for wrong action', async () => {
        const scope = await service.getAllowedScope('user-1', 'edit', 'servers');
        expect(scope.wildcard).toBe(false);
        expect(scope.names.size).toBe(0);
      });
    });

    describe('multiple name-scoped bindings → union of names', () => {
      it('collects names from multiple bindings', async () => {
        const repo = mockRepo([
          makeDef({
            id: 'def-1',
            name: 'rbac-a',
            subjects: [{ kind: 'User', name: 'alice@example.com' }],
            roleBindings: [{ role: 'view', resource: 'servers', name: 'server-a' }],
          }),
          makeDef({
            id: 'def-2',
            name: 'rbac-b',
            subjects: [{ kind: 'User', name: 'alice@example.com' }],
            roleBindings: [{ role: 'view', resource: 'servers', name: 'server-b' }],
          }),
        ]);
        const prisma = mockPrisma({
          user: { findUnique: vi.fn(async () => ({ email: 'alice@example.com' })) },
          groupMember: { findMany: vi.fn(async () => []) },
        });
        const service = new RbacService(repo, prisma);

        const scope = await service.getAllowedScope('user-1', 'view', 'servers');
        expect(scope.wildcard).toBe(false);
        expect(scope.names).toEqual(new Set(['server-a', 'server-b']));
      });
    });

    describe('mixed scoped + unscoped → wildcard wins', () => {
      it('returns wildcard:true when any binding is unscoped', async () => {
        const repo = mockRepo([
          makeDef({
            subjects: [{ kind: 'User', name: 'alice@example.com' }],
            roleBindings: [
              { role: 'view', resource: 'servers', name: 'my-ha' },
              { role: 'view', resource: 'servers' },
            ],
          }),
        ]);
        const prisma = mockPrisma({
          user: { findUnique: vi.fn(async () => ({ email: 'alice@example.com' })) },
          groupMember: { findMany: vi.fn(async () => []) },
        });
        const service = new RbacService(repo, prisma);

        const scope = await service.getAllowedScope('user-1', 'view', 'servers');
        expect(scope.wildcard).toBe(true);
      });
    });

    describe('no matching permissions → empty', () => {
      it('returns wildcard:false with empty names', async () => {
        const repo = mockRepo([]);
        const prisma = mockPrisma();
        const service = new RbacService(repo, prisma);

        const scope = await service.getAllowedScope('unknown', 'view', 'servers');
        expect(scope.wildcard).toBe(false);
        expect(scope.names.size).toBe(0);
      });
    });

    describe('edit role grants view scope', () => {
      let service: RbacService;

      beforeEach(() => {
        const repo = mockRepo([
          makeDef({
            subjects: [{ kind: 'User', name: 'alice@example.com' }],
            roleBindings: [{ role: 'edit', resource: 'servers', name: 'my-ha' }],
          }),
        ]);
        const prisma = mockPrisma({
          user: { findUnique: vi.fn(async () => ({ email: 'alice@example.com' })) },
          groupMember: { findMany: vi.fn(async () => []) },
        });
        service = new RbacService(repo, prisma);
      });

      it('returns names for view action', async () => {
        const scope = await service.getAllowedScope('user-1', 'view', 'servers');
        expect(scope.wildcard).toBe(false);
        expect(scope.names).toEqual(new Set(['my-ha']));
      });

      it('returns names for create action', async () => {
        const scope = await service.getAllowedScope('user-1', 'create', 'servers');
        expect(scope.wildcard).toBe(false);
        expect(scope.names).toEqual(new Set(['my-ha']));
      });

      it('returns names for delete action', async () => {
        const scope = await service.getAllowedScope('user-1', 'delete', 'servers');
        expect(scope.wildcard).toBe(false);
        expect(scope.names).toEqual(new Set(['my-ha']));
      });
    });

    describe('operation bindings are ignored', () => {
      it('returns empty names when only operation bindings exist', async () => {
        const repo = mockRepo([
          makeDef({
            subjects: [{ kind: 'User', name: 'alice@example.com' }],
            roleBindings: [{ role: 'run', action: 'logs' }],
          }),
        ]);
        const prisma = mockPrisma({
          user: { findUnique: vi.fn(async () => ({ email: 'alice@example.com' })) },
          groupMember: { findMany: vi.fn(async () => []) },
        });
        const service = new RbacService(repo, prisma);

        const scope = await service.getAllowedScope('user-1', 'view', 'servers');
        expect(scope.wildcard).toBe(false);
        expect(scope.names.size).toBe(0);
      });
    });
  });

  describe('unknown/legacy roles are denied', () => {
    let service: RbacService;

    beforeEach(() => {
      const repo = mockRepo([
        makeDef({
          roleBindings: [{ role: 'admin', resource: '*' }],
        }),
      ]);
      const prisma = mockPrisma({
        user: { findUnique: vi.fn(async () => ({ email: 'alice@example.com' })) },
        groupMember: { findMany: vi.fn(async () => []) },
      });
      service = new RbacService(repo, prisma);
    });

    it('denies view when only legacy admin role exists', async () => {
      expect(await service.canAccess('user-1', 'view', 'servers')).toBe(false);
    });

    it('denies create when only legacy admin role exists', async () => {
      expect(await service.canAccess('user-1', 'create', 'servers')).toBe(false);
    });

    it('denies edit when only legacy admin role exists', async () => {
      expect(await service.canAccess('user-1', 'edit', 'servers')).toBe(false);
    });

    it('denies delete when only legacy admin role exists', async () => {
      expect(await service.canAccess('user-1', 'delete', 'servers')).toBe(false);
    });

    it('denies any made-up role', async () => {
      const repo = mockRepo([
        makeDef({
          roleBindings: [{ role: 'superuser', resource: 'servers' }],
        }),
      ]);
      const prisma = mockPrisma({
        user: { findUnique: vi.fn(async () => ({ email: 'alice@example.com' })) },
        groupMember: { findMany: vi.fn(async () => []) },
      });
      const svc = new RbacService(repo, prisma);
      expect(await svc.canAccess('user-1', 'view', 'servers')).toBe(false);
      expect(await svc.canAccess('user-1', 'edit', 'servers')).toBe(false);
    });
  });

  describe('expose role', () => {
    it('grants expose access with expose role binding', async () => {
      const repo = mockRepo([
        makeDef({
          roleBindings: [{ role: 'expose', resource: 'projects' }],
        }),
      ]);
      const prisma = mockPrisma({
        user: { findUnique: vi.fn(async () => ({ email: 'alice@example.com' })) },
        groupMember: { findMany: vi.fn(async () => []) },
      });
      const service = new RbacService(repo, prisma);
      expect(await service.canAccess('user-1', 'expose', 'projects')).toBe(true);
    });

    it('grants expose access with edit role binding (edit includes expose)', async () => {
      const repo = mockRepo([
        makeDef({
          roleBindings: [{ role: 'edit', resource: 'projects' }],
        }),
      ]);
      const prisma = mockPrisma({
        user: { findUnique: vi.fn(async () => ({ email: 'alice@example.com' })) },
        groupMember: { findMany: vi.fn(async () => []) },
      });
      const service = new RbacService(repo, prisma);
      expect(await service.canAccess('user-1', 'expose', 'projects')).toBe(true);
    });

    it('denies expose access with view role binding', async () => {
      const repo = mockRepo([
        makeDef({
          roleBindings: [{ role: 'view', resource: 'projects' }],
        }),
      ]);
      const prisma = mockPrisma({
        user: { findUnique: vi.fn(async () => ({ email: 'alice@example.com' })) },
        groupMember: { findMany: vi.fn(async () => []) },
      });
      const service = new RbacService(repo, prisma);
      expect(await service.canAccess('user-1', 'expose', 'projects')).toBe(false);
    });

    it('expose role also grants view access', async () => {
      const repo = mockRepo([
        makeDef({
          roleBindings: [{ role: 'expose', resource: 'projects' }],
        }),
      ]);
      const prisma = mockPrisma({
        user: { findUnique: vi.fn(async () => ({ email: 'alice@example.com' })) },
        groupMember: { findMany: vi.fn(async () => []) },
      });
      const service = new RbacService(repo, prisma);
      expect(await service.canAccess('user-1', 'view', 'projects')).toBe(true);
    });

    it('expose role with name-scoped binding', async () => {
      const repo = mockRepo([
        makeDef({
          roleBindings: [{ role: 'expose', resource: 'projects', name: 'my-project' }],
        }),
      ]);
      const prisma = mockPrisma({
        user: { findUnique: vi.fn(async () => ({ email: 'alice@example.com' })) },
        groupMember: { findMany: vi.fn(async () => []) },
      });
      const service = new RbacService(repo, prisma);
      expect(await service.canAccess('user-1', 'expose', 'projects', 'my-project')).toBe(true);
      expect(await service.canAccess('user-1', 'expose', 'projects', 'other-project')).toBe(false);
    });

    it('getAllowedScope with expose role grants view scope', async () => {
      const repo = mockRepo([
        makeDef({
          roleBindings: [{ role: 'expose', resource: 'projects' }],
        }),
      ]);
      const prisma = mockPrisma({
        user: { findUnique: vi.fn(async () => ({ email: 'alice@example.com' })) },
        groupMember: { findMany: vi.fn(async () => []) },
      });
      const service = new RbacService(repo, prisma);
      const scope = await service.getAllowedScope('user-1', 'view', 'projects');
      expect(scope.wildcard).toBe(true);
    });
  });
});
