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

function principalFor(privileges: readonly Privilege[]): 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: {
  metadata: readonly Privilege[] | undefined;
  session?: { principal?: Principal };
  method?: string;
  url?: string;
}): { ctx: ExecutionContext; req: Request } {
  const req = {
    session: opts.session,
    method: opts.method ?? 'GET',
    originalUrl: opts.url ?? '/api/audit',
  } as unknown as Request;
  const ctx = {
    switchToHttp: () => ({ getRequest: <T>() => req as T }),
    getHandler: () => 'handler',
    getClass: () => 'AuditController',
  } as unknown as ExecutionContext;
  return { ctx, req };
}

function makeGuard(opts: { metadata: readonly Privilege[] | undefined; audit?: jest.Mock }): {
  guard: RequirePrivilegeGuard;
  audit: jest.Mock;
} {
  const audit = opts.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 RequirePrivilegeGuard(reflector, writer), audit };
}

describe('RequirePrivilegeGuard', () => {
  it('passes when no metadata is set (decorator absent — guard wired but route unprotected)', async () => {
    const { guard } = makeGuard({ metadata: undefined });
    const { ctx } = makeContext({ metadata: undefined });
    await expect(guard.canActivate(ctx)).resolves.toBe(true);
  });

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

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

  it('OR-combines multiple required privileges (matches any one)', async () => {
    const { guard } = makeGuard({
      metadata: ['Portal.Auditor', 'Portal.Admin'],
    });
    const { ctx } = makeContext({
      metadata: ['Portal.Auditor', 'Portal.Admin'],
      session: { principal: principalFor(['Portal.Auditor']) },
    });
    await expect(guard.canActivate(ctx)).resolves.toBe(true);
  });

  it('throws 403 + audits when the principal lacks every required privilege', async () => {
    const { guard, audit } = makeGuard({ metadata: ['Portal.Admin'] });
    const { ctx } = makeContext({
      metadata: ['Portal.Admin'],
      session: { principal: principalFor(['Portal.Auditor']) },
      method: 'POST',
      url: '/api/admin/cms/pages',
    });
    await expect(guard.canActivate(ctx)).rejects.toBeInstanceOf(ForbiddenException);
    expect(audit).toHaveBeenCalledWith({
      actor: { oid: 'oid-1' },
      attemptedRoute: 'POST /api/admin/cms/pages',
      kind: 'privilege',
      required: ['Portal.Admin'],
      held: ['Portal.Auditor'],
    });
  });

  it('audits the held privileges as the principal saw them, not the legacy roles claim', async () => {
    const { guard, audit } = makeGuard({ metadata: ['Portal.Admin'] });
    const { ctx } = makeContext({
      metadata: ['Portal.Admin'],
      session: { principal: principalFor([]) },
    });
    await expect(guard.canActivate(ctx)).rejects.toBeInstanceOf(ForbiddenException);
    expect(audit).toHaveBeenCalledWith(expect.objectContaining({ held: [] }));
  });

  it('returns a generic forbidden body (no privilege hint leaks)', async () => {
    const { guard } = makeGuard({ metadata: ['Portal.Admin'] });
    const { ctx } = makeContext({
      metadata: ['Portal.Admin'],
      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(body['message']).toBe('Forbidden');
      expect(JSON.stringify(body)).not.toContain('Portal.Admin');
    }
  });
});
