import { ExecutionContext, ForbiddenException, UnauthorizedException } from '@nestjs/common';
import type { Reflector } from '@nestjs/core';
import type { Request } from 'express';
import type { Principal, ScopableResource, Scope } from 'shared-auth';
import type { AuditWriter } from '../audit/audit.service';
import type { RequireScopeExtractor } from './require-scope.decorator';
import { RequireScopeGuard } from './require-scope.guard';

function principalFor(scopes: readonly Scope[]): Principal {
  return {
    user: {
      id: 'user-1',
      personId: 'person-1',
      entraOid: 'oid-1',
      tenantId: 'tenant-1',
      displayName: 'Alice',
    },
    privileges: [],
    roles: [],
    scopes,
    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/etablissements/0330800013',
  } as unknown as Request;
  const ctx = {
    switchToHttp: () => ({ getRequest: <T>() => req as T }),
    getHandler: () => 'handler',
    getClass: () => 'EtablissementController',
  } as unknown as ExecutionContext;
  return { ctx, req };
}

function makeGuard(opts: { extractor: RequireScopeExtractor | undefined }): {
  guard: RequireScopeGuard;
  audit: jest.Mock;
} {
  const audit = jest.fn().mockResolvedValue(undefined);
  const reflector = {
    getAllAndOverride: jest.fn().mockReturnValue(opts.extractor),
  } as unknown as Reflector;
  const writer = { authorizationDenied: audit } as unknown as AuditWriter;
  return { guard: new RequireScopeGuard(reflector, writer), audit };
}

describe('RequireScopeGuard', () => {
  it('passes when no extractor metadata is set', async () => {
    const { guard } = makeGuard({ extractor: 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({
      extractor: () => ({ etablissementFiness: '0330800013' }),
    });
    const { ctx } = makeContext({ session: undefined });
    await expect(guard.canActivate(ctx)).rejects.toBeInstanceOf(UnauthorizedException);
    expect(audit).not.toHaveBeenCalled();
  });

  it('passes when the principal scope covers the resource', async () => {
    const { guard } = makeGuard({
      extractor: () => ({ etablissementFiness: '0330800013' }),
    });
    const { ctx } = makeContext({
      session: {
        principal: principalFor([{ kind: 'etablissement', value: '0330800013' }]),
      },
    });
    await expect(guard.canActivate(ctx)).resolves.toBe(true);
  });

  it('honours async extractors (Prisma lookup pattern)', async () => {
    const { guard } = makeGuard({
      extractor: async () => Promise.resolve({ delegationCode: '33' }),
    });
    const { ctx } = makeContext({
      session: { principal: principalFor([{ kind: 'delegation', value: '33' }]) },
    });
    await expect(guard.canActivate(ctx)).resolves.toBe(true);
  });

  it('throws 403 + audits when the resource is outside the principal scopes', async () => {
    const { guard, audit } = makeGuard({
      extractor: () => ({ etablissementFiness: '0330800021' }),
    });
    const { ctx } = makeContext({
      session: {
        principal: principalFor([{ kind: 'etablissement', value: '0330800013' }]),
      },
    });
    await expect(guard.canActivate(ctx)).rejects.toBeInstanceOf(ForbiddenException);
    expect(audit).toHaveBeenCalledWith({
      actor: { oid: 'oid-1' },
      attemptedRoute: 'GET /api/etablissements/0330800013',
      kind: 'scope',
      required: ['etablissement:0330800021'],
      held: ['etablissement:0330800013'],
    });
  });

  it('throws 403 + audits with empty required when the extractor returns null', async () => {
    const { guard, audit } = makeGuard({ extractor: () => null });
    const { ctx } = makeContext({
      session: { principal: principalFor([{ kind: 'unrestricted' }]) },
    });
    await expect(guard.canActivate(ctx)).rejects.toBeInstanceOf(ForbiddenException);
    expect(audit).toHaveBeenCalledWith(expect.objectContaining({ kind: 'scope', required: [] }));
  });

  it('describes the resource correctly when it carries the full parentage chain', async () => {
    const resource: ScopableResource = {
      etablissementFiness: '0330800021',
      delegationCode: '33',
      regionCode: '75',
    };
    const { guard, audit } = makeGuard({ extractor: () => resource });
    const { ctx } = makeContext({
      session: { principal: principalFor([{ kind: 'region', value: '11' }]) },
    });
    await expect(guard.canActivate(ctx)).rejects.toBeInstanceOf(ForbiddenException);
    expect(audit).toHaveBeenCalledWith(
      expect.objectContaining({
        required: ['etablissement:0330800021', 'delegation:33', 'region:75'],
        held: ['region:11'],
      }),
    );
  });

  it('returns a generic forbidden body (no resource fingerprint leaks)', async () => {
    const { guard } = makeGuard({
      extractor: () => ({ etablissementFiness: '0330800021' }),
    });
    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('0330800021');
    }
  });
});
