import { ExecutionContext, ForbiddenException, UnauthorizedException } from '@nestjs/common';
import type { Reflector } from '@nestjs/core';
import type { Request } from 'express';
import type { FunctionalRole, Principal } from 'shared-auth';
import type { AuditWriter } from '../audit/audit.service';
import { RequireRoleGuard } from './require-role.guard';

function principalFor(roles: readonly FunctionalRole[]): Principal {
  return {
    user: {
      id: 'user-1',
      personId: 'user-1',
      entraOid: 'oid-1',
      tenantId: 'tenant-1',
      displayName: 'Alice',
    },
    privileges: [],
    roles,
    scopes: [{ kind: 'unrestricted' }],
    amr: ['pwd', 'mfa'],
  };
}

function makeContext(opts: {
  session?: { principal?: Principal };
  method?: string;
  url?: string;
}): { ctx: ExecutionContext; req: Request } {
  const req = {
    session: opts.session,
    method: opts.method ?? 'GET',
    originalUrl: opts.url ?? '/api/hr/payroll',
  } as unknown as Request;
  const ctx = {
    switchToHttp: () => ({ getRequest: <T>() => req as T }),
    getHandler: () => 'handler',
    getClass: () => 'HrController',
  } as unknown as ExecutionContext;
  return { ctx, req };
}

function makeGuard(opts: { metadata: readonly FunctionalRole[] | undefined }): {
  guard: RequireRoleGuard;
  audit: jest.Mock;
} {
  const audit = jest.fn().mockResolvedValue(undefined);
  const reflector = {
    getAllAndOverride: jest.fn().mockReturnValue(opts.metadata),
  } as unknown as Reflector;
  const writer = { authorizationDenied: audit } as unknown as AuditWriter;
  return { guard: new RequireRoleGuard(reflector, writer), audit };
}

describe('RequireRoleGuard', () => {
  it('passes when no metadata is set', async () => {
    const { guard } = makeGuard({ metadata: undefined });
    const { ctx } = makeContext({});
    await expect(guard.canActivate(ctx)).resolves.toBe(true);
  });

  it('throws 401 when no session is present', async () => {
    const { guard, audit } = makeGuard({ metadata: ['rh'] });
    const { ctx } = makeContext({ session: undefined });
    await expect(guard.canActivate(ctx)).rejects.toBeInstanceOf(UnauthorizedException);
    expect(audit).not.toHaveBeenCalled();
  });

  it('passes when the principal carries the required role', async () => {
    const { guard } = makeGuard({ metadata: ['rh'] });
    const { ctx } = makeContext({ session: { principal: principalFor(['rh']) } });
    await expect(guard.canActivate(ctx)).resolves.toBe(true);
  });

  it('OR-combines multiple required roles', async () => {
    const { guard } = makeGuard({ metadata: ['rh', 'responsable-paie', 'comptable'] });
    const { ctx } = makeContext({
      session: { principal: principalFor(['comptable']) },
    });
    await expect(guard.canActivate(ctx)).resolves.toBe(true);
  });

  it('throws 403 + audits when the principal lacks every required role', async () => {
    const { guard, audit } = makeGuard({ metadata: ['rh'] });
    const { ctx } = makeContext({
      session: { principal: principalFor(['collaborateur']) },
      method: 'GET',
      url: '/api/hr/payroll',
    });
    await expect(guard.canActivate(ctx)).rejects.toBeInstanceOf(ForbiddenException);
    expect(audit).toHaveBeenCalledWith({
      actor: { oid: 'oid-1' },
      attemptedRoute: 'GET /api/hr/payroll',
      kind: 'role',
      required: ['rh'],
      held: ['collaborateur'],
    });
  });

  it('returns a generic forbidden body (no role hint leaks)', async () => {
    const { guard } = makeGuard({ metadata: ['rh'] });
    const { ctx } = makeContext({ session: { principal: principalFor([]) } });
    try {
      await guard.canActivate(ctx);
      throw new Error('guard did not throw');
    } catch (err) {
      const body = (err as ForbiddenException).getResponse() as Record<string, unknown>;
      expect(body['code']).toBe('forbidden');
      expect(JSON.stringify(body)).not.toContain('rh');
    }
  });
});
