import { randomBytes } from 'node:crypto';
import { describe, it, expect, beforeEach, afterAll } from '@jest/globals';
import { HashUserIdService } from '../../audit/hash-user-id.service';
import { PrincipalMapper } from './principal.mapper';

/**
 * Locks the Principal-mapping contract per ADR-0024 §"Sub-decision 4":
 *
 *   - subject  = HashUserIdService(oid) — same salt, same algo as the
 *                portal's audit and Pino log streams. This is the
 *                join key between portal and apf-ai-service audit
 *                trails.
 *   - roles    = pass-through (inclusive expansion lands in a future
 *                ADR; the wire contract on the AI side does not
 *                change).
 *   - attrs    = tenantId is reserved; consumers can layer extra
 *                key/value pairs but tenantId wins on collision.
 */

const STRONG_SALT = randomBytes(32).toString('base64url');
const ORIGINAL_SALT = process.env['LOG_USER_ID_SALT'];

beforeEach(() => {
  process.env['LOG_USER_ID_SALT'] = STRONG_SALT;
});

afterAll(() => {
  if (ORIGINAL_SALT === undefined) {
    delete process.env['LOG_USER_ID_SALT'];
  } else {
    process.env['LOG_USER_ID_SALT'] = ORIGINAL_SALT;
  }
});

function makeMapper(): { mapper: PrincipalMapper; hashService: HashUserIdService } {
  const hashService = new HashUserIdService();
  return { mapper: new PrincipalMapper(hashService), hashService };
}

describe('PrincipalMapper', () => {
  it('hashes the Entra oid into the subject via HashUserIdService', () => {
    const { mapper, hashService } = makeMapper();
    const principal = mapper.fromInputs({
      oid: 'user-oid-42',
      tid: 'tenant-1',
      roles: ['admin'],
    });
    expect(principal.subject).toBe(hashService.hash('user-oid-42'));
    expect(principal.subject).toMatch(/^[0-9a-f]{16}$/);
  });

  it('passes roles through verbatim (no expansion in v1)', () => {
    const { mapper } = makeMapper();
    const principal = mapper.fromInputs({
      oid: 'user-oid-42',
      tid: 'tenant-1',
      roles: ['directeur', 'rh'],
    });
    expect(principal.roles).toEqual(['directeur', 'rh']);
  });

  it('copies roles so caller mutations do not leak into the proto', () => {
    const { mapper } = makeMapper();
    const roles: string[] = ['admin'];
    const principal = mapper.fromInputs({
      oid: 'user-oid-42',
      tid: 'tenant-1',
      roles,
    });
    roles.push('mutated');
    expect(principal.roles).toEqual(['admin']);
  });

  it('always emits tenantId in attributes', () => {
    const { mapper } = makeMapper();
    const principal = mapper.fromInputs({
      oid: 'user-oid-42',
      tid: 'tenant-1',
      roles: [],
    });
    expect(principal.attributes).toEqual({ tenantId: 'tenant-1' });
  });

  it('merges extraAttributes into the attribute map', () => {
    const { mapper } = makeMapper();
    const principal = mapper.fromInputs({
      oid: 'user-oid-42',
      tid: 'tenant-1',
      roles: [],
      extraAttributes: { delegation: 'aquitaine', region: 'fr-sw' },
    });
    expect(principal.attributes).toEqual({
      tenantId: 'tenant-1',
      delegation: 'aquitaine',
      region: 'fr-sw',
    });
  });

  it('tenantId from `tid` wins over a colliding extraAttributes entry', () => {
    const { mapper } = makeMapper();
    const principal = mapper.fromInputs({
      oid: 'user-oid-42',
      tid: 'tenant-1',
      roles: [],
      extraAttributes: { tenantId: 'tenant-spoofed' },
    });
    expect(principal.attributes['tenantId']).toBe('tenant-1');
  });

  it('produces the same subject for the same oid + salt across instances', () => {
    const { mapper: m1 } = makeMapper();
    const { mapper: m2 } = makeMapper();
    expect(m1.fromInputs({ oid: 'x', tid: 't', roles: [] }).subject).toBe(
      m2.fromInputs({ oid: 'x', tid: 't', roles: [] }).subject,
    );
  });
});
