/**
 * Persona-matrix integration tests per ADR-0025 §"More Information"
 * phasing step 2: the three new guards exercised against every
 * persona provisioned in the `apfrd.onmicrosoft.com` test tenant on
 * 2026-05-20. Each test instantiates the guard with the persona's
 * resolved `Principal` (the shape `PrincipalBuilder` would have
 * produced at sign-in) and asserts the guard either passes or
 * denies — the matrix proves the contracts hold against the real
 * provisioning matrix, not just synthesised toy principals.
 *
 * Scopes are stubbed `unrestricted` for every persona in v1 per
 * ADR-0025 §331 (the per-persona scope values land with the
 * Prisma `user_scopes` table in a later PR). The scope guard is
 * therefore exercised here against synthesised varied principals,
 * not against the persona matrix.
 */

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

interface Persona {
  readonly label: string;
  readonly privileges: readonly Privilege[];
  readonly roles: readonly FunctionalRole[];
}

// Verbatim copy of the persona matrix from
// `principal-builder.spec.ts`. Kept here as a local constant so a
// reviewer sees the same 19-row table both files assert against;
// the two specs cover different layers (builder, then guards).
const PERSONAS: readonly Persona[] = [
  { label: 'admin', privileges: ['Portal.Admin'], roles: ['collaborateur', 'rh'] },
  {
    label: 'directeur-bordeaux',
    privileges: [],
    roles: ['collaborateur', 'directeur-etablissement'],
  },
  {
    label: 'directeur-complexe',
    privileges: [],
    roles: ['collaborateur', 'directeur-etablissement'],
  },
  { label: 'rh-aquitaine', privileges: [], roles: ['collaborateur', 'rh', 'formation'] },
  {
    label: 'rh-siege',
    privileges: [],
    roles: ['collaborateur', 'rh', 'responsable-paie', 'comptable'],
  },
  { label: 'collaborateur-simple', privileges: [], roles: ['collaborateur'] },
  { label: 'tresorier-bordeaux', privileges: [], roles: ['elu-cd', 'elu-cd-tresorier'] },
  {
    label: 'dpo',
    privileges: ['Portal.DPO', 'Portal.Auditor'],
    roles: ['collaborateur', 'dpo', 'qualite'],
  },
  { label: 'it', privileges: [], roles: ['collaborateur', 'it'] },
  {
    label: 'benevole-aquitaine',
    privileges: [],
    roles: ['delegue', 'benevole', 'benevole-responsable'],
  },
  { label: 'chef-equipe-bordeaux', privileges: [], roles: ['collaborateur', 'chef-equipe'] },
  { label: 'chef-service-bordeaux', privileges: [], roles: ['collaborateur', 'chef-service'] },
  {
    label: 'directeur-territorial-aquitaine',
    privileges: [],
    roles: ['collaborateur', 'directeur-territorial'],
  },
  { label: 'juriste-siege', privileges: [], roles: ['collaborateur', 'juriste'] },
  { label: 'rssi', privileges: ['Portal.SecurityOfficer'], roles: ['collaborateur', 'rssi'] },
  { label: 'communication-siege', privileges: [], roles: ['collaborateur', 'communication'] },
  { label: 'elu-ca-national', privileges: [], roles: ['elu-ca'] },
  { label: 'president-cd-aquitaine', privileges: [], roles: ['elu-cd', 'elu-cd-president'] },
  { label: 'secretaire-cd-aquitaine', privileges: [], roles: ['elu-cd', 'elu-cd-secretaire'] },
];

function principalOf(persona: Persona): Principal {
  return {
    user: {
      id: `oid-${persona.label}`,
      personId: `oid-${persona.label}`,
      entraOid: `oid-${persona.label}`,
      tenantId: 'tenant-1',
      displayName: persona.label,
    },
    privileges: persona.privileges,
    roles: persona.roles,
    scopes: [{ kind: 'unrestricted' }],
    amr: ['pwd', 'mfa'],
  };
}

function makeCtxFor(principal: Principal): ExecutionContext {
  const req = {
    session: { principal },
    method: 'GET',
    originalUrl: '/api/test',
  } as unknown as Request;
  return {
    switchToHttp: () => ({ getRequest: <T>() => req as T }),
    getHandler: () => 'handler',
    getClass: () => 'TestController',
  } as unknown as ExecutionContext;
}

function privilegeGuardFor(metadata: readonly Privilege[]) {
  const reflector = {
    getAllAndOverride: jest.fn().mockReturnValue(metadata),
  } as unknown as Reflector;
  const audit = jest.fn().mockResolvedValue(undefined);
  const writer = { authorizationDenied: audit } as unknown as AuditWriter;
  return { guard: new RequirePrivilegeGuard(reflector, writer), audit };
}

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

/**
 * Each row asserts which personas should pass a given guard. The
 * complement (everyone NOT in `allowed`) must be denied — the
 * `expect denied` block at the bottom of each describe catches a
 * regression that would silently let extra personas through.
 */
interface Matrix {
  readonly title: string;
  readonly allowed: ReadonlyArray<string>;
}

const PRIVILEGE_MATRIX: ReadonlyArray<{
  metadata: readonly Privilege[];
  matrix: Matrix;
}> = [
  {
    metadata: ['Portal.Admin'],
    matrix: { title: '@RequirePrivilege(Portal.Admin)', allowed: ['admin'] },
  },
  {
    metadata: ['Portal.DPO'],
    matrix: { title: '@RequirePrivilege(Portal.DPO)', allowed: ['dpo'] },
  },
  {
    metadata: ['Portal.Auditor'],
    matrix: { title: '@RequirePrivilege(Portal.Auditor)', allowed: ['dpo'] },
  },
  {
    metadata: ['Portal.SecurityOfficer'],
    matrix: { title: '@RequirePrivilege(Portal.SecurityOfficer)', allowed: ['rssi'] },
  },
  {
    metadata: ['Portal.Admin', 'Portal.DPO'],
    matrix: {
      title: '@RequirePrivilege(Portal.Admin, Portal.DPO) — OR composition',
      allowed: ['admin', 'dpo'],
    },
  },
];

const ROLE_MATRIX: ReadonlyArray<{
  metadata: readonly FunctionalRole[];
  matrix: Matrix;
}> = [
  {
    metadata: ['rh'],
    matrix: { title: '@RequireRole(rh)', allowed: ['admin', 'rh-aquitaine', 'rh-siege'] },
  },
  {
    metadata: ['directeur-etablissement'],
    matrix: {
      title: '@RequireRole(directeur-etablissement)',
      allowed: ['directeur-bordeaux', 'directeur-complexe'],
    },
  },
  {
    metadata: ['rh', 'comptable', 'responsable-paie'],
    matrix: {
      title: '@RequireRole(rh, comptable, responsable-paie) — OR composition',
      allowed: ['admin', 'rh-aquitaine', 'rh-siege'],
    },
  },
  {
    metadata: ['elu-cd'],
    matrix: {
      title: '@RequireRole(elu-cd) — every CD member',
      allowed: ['tresorier-bordeaux', 'president-cd-aquitaine', 'secretaire-cd-aquitaine'],
    },
  },
  {
    metadata: ['benevole-responsable'],
    matrix: {
      title: '@RequireRole(benevole-responsable)',
      allowed: ['benevole-aquitaine'],
    },
  },
  {
    metadata: ['collaborateur'],
    matrix: {
      title: '@RequireRole(collaborateur) — every workforce persona',
      allowed: [
        'admin',
        'directeur-bordeaux',
        'directeur-complexe',
        'rh-aquitaine',
        'rh-siege',
        'collaborateur-simple',
        'dpo',
        'it',
        'chef-equipe-bordeaux',
        'chef-service-bordeaux',
        'directeur-territorial-aquitaine',
        'juriste-siege',
        'rssi',
        'communication-siege',
      ],
    },
  },
];

describe('Privilege guard — persona matrix', () => {
  for (const { metadata, matrix } of PRIVILEGE_MATRIX) {
    describe(matrix.title, () => {
      for (const persona of PERSONAS) {
        const shouldPass = matrix.allowed.includes(persona.label);
        const verb = shouldPass ? 'allows' : 'denies';
        it(`${verb} ${persona.label}`, async () => {
          const { guard } = privilegeGuardFor(metadata);
          const ctx = makeCtxFor(principalOf(persona));
          if (shouldPass) {
            await expect(guard.canActivate(ctx)).resolves.toBe(true);
          } else {
            await expect(guard.canActivate(ctx)).rejects.toBeInstanceOf(ForbiddenException);
          }
        });
      }
    });
  }
});

describe('Role guard — persona matrix', () => {
  for (const { metadata, matrix } of ROLE_MATRIX) {
    describe(matrix.title, () => {
      for (const persona of PERSONAS) {
        const shouldPass = matrix.allowed.includes(persona.label);
        const verb = shouldPass ? 'allows' : 'denies';
        it(`${verb} ${persona.label}`, async () => {
          const { guard } = roleGuardFor(metadata);
          const ctx = makeCtxFor(principalOf(persona));
          if (shouldPass) {
            await expect(guard.canActivate(ctx)).resolves.toBe(true);
          } else {
            await expect(guard.canActivate(ctx)).rejects.toBeInstanceOf(ForbiddenException);
          }
        });
      }
    });
  }
});
