import { mkdtempSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { generateKeyPairSync } from 'node:crypto';
import { assertJwksConfig } from './check-jwks-config';

const tmpDir = mkdtempSync(join(tmpdir(), 'apf-jwks-spec-'));

function writeKey(
  filename: string,
  type: 'rsa-3072' | 'rsa-1024' | 'ec-p256' | 'ec-p384' | 'ed25519' | 'garbage',
): string {
  const path = join(tmpDir, filename);
  let pem: string;
  switch (type) {
    case 'rsa-3072':
      pem = generateKeyPairSync('rsa', { modulusLength: 3072 }).privateKey.export({
        type: 'pkcs8',
        format: 'pem',
      }) as string;
      break;
    case 'rsa-1024':
      pem = generateKeyPairSync('rsa', { modulusLength: 1024 }).privateKey.export({
        type: 'pkcs8',
        format: 'pem',
      }) as string;
      break;
    case 'ec-p256':
      pem = generateKeyPairSync('ec', { namedCurve: 'prime256v1' }).privateKey.export({
        type: 'pkcs8',
        format: 'pem',
      }) as string;
      break;
    case 'ec-p384':
      pem = generateKeyPairSync('ec', { namedCurve: 'secp384r1' }).privateKey.export({
        type: 'pkcs8',
        format: 'pem',
      }) as string;
      break;
    case 'ed25519':
      pem = generateKeyPairSync('ed25519').privateKey.export({
        type: 'pkcs8',
        format: 'pem',
      }) as string;
      break;
    case 'garbage':
      pem = '-----BEGIN PRIVATE KEY-----\nbm90LWEta2V5\n-----END PRIVATE KEY-----\n';
      break;
  }
  writeFileSync(path, pem);
  return path;
}

describe('assertJwksConfig', () => {
  const originalPath = process.env['BFF_JWKS_PRIVATE_KEY_PATH'];
  const originalKid = process.env['BFF_JWKS_KID'];

  beforeEach(() => {
    process.env['BFF_JWKS_KID'] = 'bff-2026-05';
  });

  afterEach(() => {
    restore('BFF_JWKS_PRIVATE_KEY_PATH', originalPath);
    restore('BFF_JWKS_KID', originalKid);
  });

  it('returns the parsed key + kid + alg=RS256 for a well-formed RSA-3072 key', () => {
    process.env['BFF_JWKS_PRIVATE_KEY_PATH'] = writeKey('rsa.pem', 'rsa-3072');
    const config = assertJwksConfig();
    expect(config.kid).toBe('bff-2026-05');
    expect(config.alg).toBe('RS256');
    expect(config.privateKey.asymmetricKeyType).toBe('rsa');
  });

  it('returns alg=ES256 for an EC P-256 key', () => {
    process.env['BFF_JWKS_PRIVATE_KEY_PATH'] = writeKey('ec256.pem', 'ec-p256');
    expect(assertJwksConfig().alg).toBe('ES256');
  });

  it('returns alg=ES384 for an EC P-384 key', () => {
    process.env['BFF_JWKS_PRIVATE_KEY_PATH'] = writeKey('ec384.pem', 'ec-p384');
    expect(assertJwksConfig().alg).toBe('ES384');
  });

  it('throws when BFF_JWKS_PRIVATE_KEY_PATH is unset', () => {
    delete process.env['BFF_JWKS_PRIVATE_KEY_PATH'];
    expect(() => assertJwksConfig()).toThrow(/BFF_JWKS_PRIVATE_KEY_PATH is not set/);
  });

  it('throws when the file does not exist', () => {
    process.env['BFF_JWKS_PRIVATE_KEY_PATH'] = join(tmpDir, 'missing.pem');
    expect(() => assertJwksConfig()).toThrow(/could not be read/);
  });

  it('throws when the file is not a valid PEM private key', () => {
    process.env['BFF_JWKS_PRIVATE_KEY_PATH'] = writeKey('garbage.pem', 'garbage');
    expect(() => assertJwksConfig()).toThrow(/not a valid PEM private key/);
  });

  it('refuses RSA keys weaker than 2048 bits', () => {
    process.env['BFF_JWKS_PRIVATE_KEY_PATH'] = writeKey('weak.pem', 'rsa-1024');
    expect(() => assertJwksConfig()).toThrow(/unsupported key type/);
  });

  it('refuses Ed25519 keys (v1 supports RSA + EC P-256/P-384 only)', () => {
    process.env['BFF_JWKS_PRIVATE_KEY_PATH'] = writeKey('ed25519.pem', 'ed25519');
    expect(() => assertJwksConfig()).toThrow(/unsupported key type/);
  });

  it('throws when BFF_JWKS_KID is unset', () => {
    process.env['BFF_JWKS_PRIVATE_KEY_PATH'] = writeKey('rsa.pem', 'rsa-3072');
    delete process.env['BFF_JWKS_KID'];
    expect(() => assertJwksConfig()).toThrow(/BFF_JWKS_KID is not set/);
  });

  it('throws when BFF_JWKS_KID has illegal characters', () => {
    process.env['BFF_JWKS_PRIVATE_KEY_PATH'] = writeKey('rsa.pem', 'rsa-3072');
    process.env['BFF_JWKS_KID'] = 'has spaces and !';
    expect(() => assertJwksConfig()).toThrow(/must match/);
  });

  it('throws when BFF_JWKS_KID is too short', () => {
    process.env['BFF_JWKS_PRIVATE_KEY_PATH'] = writeKey('rsa.pem', 'rsa-3072');
    process.env['BFF_JWKS_KID'] = 'ab';
    expect(() => assertJwksConfig()).toThrow(/must match/);
  });
});

function restore(name: string, original: string | undefined): void {
  if (original === undefined) {
    delete process.env[name];
  } else {
    process.env[name] = original;
  }
}
