import { createPrivateKey, createPublicKey, generateKeyPairSync } from 'node:crypto';
import { jwtVerify } from 'jose';
import { buildBffSigningKey, type BffSigningKey } from '../bff-signing-key';
import { SignedAssertionStrategy } from './signed-assertion.strategy';

async function makeStrategy(): Promise<{ strategy: SignedAssertionStrategy; key: BffSigningKey }> {
  const { privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 });
  const key = await buildBffSigningKey({
    privateKey: createPrivateKey(privateKey.export({ type: 'pkcs8', format: 'pem' })),
    kid: 'bff-2026-05',
    alg: 'RS256',
  });
  return { strategy: new SignedAssertionStrategy(key), key };
}

const INPUT = {
  actorIdHash: 'hash(jane)',
  audience: 'workforce' as const,
  downstreamName: 'svc-cms',
  claims: { tenant: 't', roles: ['editor'] },
  traceId: 'abc123def456',
};

describe('SignedAssertionStrategy.sign', () => {
  it('mints a JWT verifiable against the BFF public key with the expected claim set', async () => {
    const { strategy, key } = await makeStrategy();
    const jwt = await strategy.sign(INPUT);

    const { payload, protectedHeader } = await jwtVerify(
      jwt,
      createPublicKey(key.config.privateKey),
      { issuer: 'portal-bff', audience: 'svc-cms' },
    );

    expect(protectedHeader.alg).toBe('RS256');
    expect(protectedHeader.kid).toBe('bff-2026-05');
    expect(payload.iss).toBe('portal-bff');
    expect(payload.sub).toBe('hash(jane)');
    expect(payload.aud).toBe('svc-cms');
    // Non-standard claims per ADR-0014 §"Service strategy":
    expect(payload['audience']).toBe('workforce');
    expect(payload['claims']).toEqual({ tenant: 't', roles: ['editor'] });
    expect(payload['trace_id']).toBe('abc123def456');
  });

  it('sets exp = iat + 60 seconds (matches ADR-0014 §"Service strategy")', async () => {
    const { strategy, key } = await makeStrategy();
    const jwt = await strategy.sign(INPUT);
    const { payload } = await jwtVerify(jwt, createPublicKey(key.config.privateKey), {
      issuer: 'portal-bff',
      audience: 'svc-cms',
    });
    expect(typeof payload.iat).toBe('number');
    expect(typeof payload.exp).toBe('number');
    expect((payload.exp as number) - (payload.iat as number)).toBe(60);
  });

  it('rejects with audience mismatch when the downstream verifies against the wrong aud', async () => {
    const { strategy, key } = await makeStrategy();
    const jwt = await strategy.sign(INPUT);
    await expect(
      jwtVerify(jwt, createPublicKey(key.config.privateKey), {
        issuer: 'portal-bff',
        audience: 'svc-other', // wrong target
      }),
    ).rejects.toThrow(/"aud"/i);
  });

  it('rejects when verified against a different public key (signature mismatch)', async () => {
    const { strategy } = await makeStrategy();
    const jwt = await strategy.sign(INPUT);
    // Forge a different RSA keypair — its public half cannot
    // verify the JWT signed under the original private key.
    const { privateKey: otherPriv } = generateKeyPairSync('rsa', { modulusLength: 2048 });
    const otherPub = createPublicKey(otherPriv.export({ type: 'pkcs8', format: 'pem' }));
    await expect(
      jwtVerify(jwt, otherPub, { issuer: 'portal-bff', audience: 'svc-cms' }),
    ).rejects.toThrow(/signature/i);
  });

  it('mints a different JWT for each call (no caching, fresh iat each time)', async () => {
    const { strategy } = await makeStrategy();
    const a = await strategy.sign(INPUT);
    // jose's setIssuedAt uses second-resolution; wait > 1s to make
    // sure iat differs deterministically. Faking time keeps the
    // test fast.
    jest.useFakeTimers().setSystemTime(Date.now() + 2000);
    try {
      const b = await strategy.sign(INPUT);
      expect(a).not.toBe(b);
    } finally {
      jest.useRealTimers();
    }
  });

  it('signs against the EC P-256 key with alg=ES256', async () => {
    const { privateKey } = generateKeyPairSync('ec', { namedCurve: 'prime256v1' });
    const key = await buildBffSigningKey({
      privateKey: createPrivateKey(privateKey.export({ type: 'pkcs8', format: 'pem' })),
      kid: 'ec-kid',
      alg: 'ES256',
    });
    const strategy = new SignedAssertionStrategy(key);
    const jwt = await strategy.sign(INPUT);
    const { protectedHeader } = await jwtVerify(jwt, createPublicKey(key.config.privateKey), {
      issuer: 'portal-bff',
      audience: 'svc-cms',
    });
    expect(protectedHeader.alg).toBe('ES256');
    expect(protectedHeader.kid).toBe('ec-kid');
  });
});
