import type { Request } from 'express';
import type { AuditWriter } from '../audit/audit.service';
import { UserScopesController } from './user-scopes.controller';
import type { UserScopesService } from './user-scopes.service';

function makeFixture(): {
  controller: UserScopesController;
  service: { list: jest.Mock; grant: jest.Mock; revoke: jest.Mock };
  audit: { adminScopeGranted: jest.Mock; adminScopeRevoked: jest.Mock };
} {
  const service = {
    list: jest.fn(),
    grant: jest.fn(),
    revoke: jest.fn(),
  };
  const audit = {
    adminScopeGranted: jest.fn().mockResolvedValue(undefined),
    adminScopeRevoked: jest.fn().mockResolvedValue(undefined),
  };
  const controller = new UserScopesController(
    service as unknown as UserScopesService,
    audit as unknown as AuditWriter,
  );
  return { controller, service, audit };
}

function makeReq(actorOid: string | undefined): Request {
  return {
    session: actorOid === undefined ? {} : { user: { oid: actorOid } },
  } as unknown as Request;
}

describe('UserScopesController.list', () => {
  it('returns the service payload verbatim — no audit on reads', async () => {
    const { controller, service, audit } = makeFixture();
    service.list.mockResolvedValue({
      user: { oid: 'target-oid', userId: 'u', displayName: 'Jane', email: 'j@e' },
      scopes: [],
    });
    const page = await controller.list('target-oid');
    expect(service.list).toHaveBeenCalledWith('target-oid');
    expect(page.user.oid).toBe('target-oid');
    expect(audit.adminScopeGranted).not.toHaveBeenCalled();
    expect(audit.adminScopeRevoked).not.toHaveBeenCalled();
  });
});

describe('UserScopesController.grant', () => {
  it('creates the scope and emits admin.scope_granted blocking audit', async () => {
    const { controller, service, audit } = makeFixture();
    service.grant.mockResolvedValue({
      user: { oid: 'target-oid', userId: 'u', displayName: 'Jane', email: 'j@e' },
      scope: {
        id: 'new-scope',
        kind: 'etablissement',
        value: '0330800013',
        source: 'admin-ui',
        createdAt: '2026-05-26T10:00:00.000Z',
        expiresAt: null,
      },
    });
    const result = await controller.grant(makeReq('admin-oid'), 'target-oid', {
      kind: 'etablissement',
      value: '0330800013',
    });
    expect(service.grant).toHaveBeenCalledWith('target-oid', {
      kind: 'etablissement',
      value: '0330800013',
    });
    expect(audit.adminScopeGranted).toHaveBeenCalledWith({
      actor: { oid: 'admin-oid' },
      target: { oid: 'target-oid' },
      scopeId: 'new-scope',
      kind: 'etablissement',
      value: '0330800013',
      expiresAt: null,
    });
    expect(result.id).toBe('new-scope');
  });

  it('propagates service errors without writing the audit row', async () => {
    const { controller, service, audit } = makeFixture();
    service.grant.mockRejectedValue(new Error('boom'));
    await expect(
      controller.grant(makeReq('admin-oid'), 'target-oid', { kind: 'self' }),
    ).rejects.toThrow('boom');
    expect(audit.adminScopeGranted).not.toHaveBeenCalled();
  });

  it('skips audit when the session has no actor (defensive — guard guarantees it)', async () => {
    const { controller, service, audit } = makeFixture();
    service.grant.mockResolvedValue({
      user: { oid: 'target-oid', userId: 'u', displayName: 'Jane', email: 'j@e' },
      scope: {
        id: 'new-scope',
        kind: 'self',
        value: '',
        source: 'admin-ui',
        createdAt: '2026-05-26T10:00:00.000Z',
        expiresAt: null,
      },
    });
    await controller.grant(makeReq(undefined), 'target-oid', { kind: 'self' });
    expect(audit.adminScopeGranted).not.toHaveBeenCalled();
  });
});

describe('UserScopesController.revoke', () => {
  it('deletes and emits admin.scope_revoked', async () => {
    const { controller, service, audit } = makeFixture();
    service.revoke.mockResolvedValue({
      user: { oid: 'target-oid', userId: 'u', displayName: 'Jane', email: 'j@e' },
      revoked: { kind: 'delegation', value: '33' },
    });
    await controller.revoke(makeReq('admin-oid'), 'target-oid', 'scope-1');
    expect(service.revoke).toHaveBeenCalledWith('target-oid', 'scope-1');
    expect(audit.adminScopeRevoked).toHaveBeenCalledWith({
      actor: { oid: 'admin-oid' },
      target: { oid: 'target-oid' },
      scopeId: 'scope-1',
      kind: 'delegation',
      value: '33',
    });
  });

  it('propagates service NotFound without writing the audit row', async () => {
    const { controller, service, audit } = makeFixture();
    service.revoke.mockRejectedValue(new Error('not found'));
    await expect(controller.revoke(makeReq('admin-oid'), 'target-oid', 'ghost')).rejects.toThrow();
    expect(audit.adminScopeRevoked).not.toHaveBeenCalled();
  });
});
