/**
 * Test helpers for generating real X.509 PEM certificates in unit tests.
 *
 * PR #501 (FED-M2-11) introduced strict `new X509Certificate(certPem)` parsing
 * in both EnrollmentService.extractCertNotAfter and CaService.issueCert — dummy
 * cert strings now throw `error:0680007B:asn1 encoding routines::header too long`.
 *
 * These helpers produce minimal but cryptographically valid self-signed EC P-256
 * certificates via @peculiar/x509 + Node.js webcrypto, suitable for test mocks.
 *
 * Two variants:
 *  - makeSelfSignedCert()          Plain cert — satisfies node:crypto X509Certificate parse.
 *  - makeMosaicIssuedCert(opts)    Cert with custom Mosaic OID extensions — satisfies the
 *                                  CRIT-1 OID presence + value checks in CaService.issueCert.
 */

import { webcrypto } from 'node:crypto';
import {
  X509CertificateGenerator,
  Extension,
  KeyUsagesExtension,
  KeyUsageFlags,
  BasicConstraintsExtension,
  cryptoProvider,
} from '@peculiar/x509';

// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------

/**
 * Encode a string as an ASN.1 UTF8String TLV:
 *   0x0C (tag) + 1-byte length (for strings ≤ 127 bytes) + UTF-8 bytes.
 *
 * CaService.issueCert reads the extension value as:
 *   decoder.decode(grantIdExt.value.slice(2))
 * i.e. it skips the tag + length byte and decodes the remainder as UTF-8.
 * So we must produce exactly this encoding as the OCTET STRING content.
 */
function encodeUtf8String(value: string): Uint8Array {
  const utf8 = new TextEncoder().encode(value);
  if (utf8.length > 127) {
    throw new Error('encodeUtf8String: value too long for single-byte length encoding');
  }
  const buf = new Uint8Array(2 + utf8.length);
  buf[0] = 0x0c; // ASN.1 UTF8String tag
  buf[1] = utf8.length;
  buf.set(utf8, 2);
  return buf;
}

// ---------------------------------------------------------------------------
// Mosaic OID constants (must match production CaService)
// ---------------------------------------------------------------------------

const OID_MOSAIC_GRANT_ID = '1.3.6.1.4.1.99999.1';
const OID_MOSAIC_SUBJECT_USER_ID = '1.3.6.1.4.1.99999.2';

// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------

/**
 * Generate a minimal self-signed EC P-256 certificate valid for 1 day.
 * CN=harness-test, no custom extensions.
 *
 * Suitable for:
 *  - EnrollmentService.extractCertNotAfter (just needs parseable PEM)
 *  - Any mock that returns certPem / certChainPem without OID checks
 */
export async function makeSelfSignedCert(): Promise<string> {
  // Ensure @peculiar/x509 uses Node.js webcrypto (available as globalThis.crypto in Node 19+,
  // but we set it explicitly here to be safe on all Node 18+ versions).
  cryptoProvider.set(webcrypto as unknown as Parameters<typeof cryptoProvider.set>[0]);

  const alg = { name: 'ECDSA', namedCurve: 'P-256', hash: 'SHA-256' } as const;
  const keys = await webcrypto.subtle.generateKey(alg, false, ['sign', 'verify']);

  const now = new Date();
  const tomorrow = new Date(now.getTime() + 86_400_000);

  const cert = await X509CertificateGenerator.createSelfSigned({
    serialNumber: '01',
    name: 'CN=harness-test',
    notBefore: now,
    notAfter: tomorrow,
    signingAlgorithm: alg,
    keys,
    extensions: [
      new BasicConstraintsExtension(false),
      new KeyUsagesExtension(KeyUsageFlags.digitalSignature),
    ],
  });

  return cert.toString('pem');
}

/**
 * Generate a self-signed EC P-256 certificate that contains the two custom
 * Mosaic OID extensions required by CaService.issueCert's CRIT-1 check:
 *   OID 1.3.6.1.4.1.99999.1  → mosaic_grant_id   (value = grantId)
 *   OID 1.3.6.1.4.1.99999.2  → mosaic_subject_user_id (value = subjectUserId)
 *
 * The extension value encoding matches the production parser's `.slice(2)` assumption:
 * each extension value is an OCTET STRING wrapping an ASN.1 UTF8String TLV.
 */
export async function makeMosaicIssuedCert(opts: {
  grantId: string;
  subjectUserId: string;
}): Promise<string> {
  // Ensure @peculiar/x509 uses Node.js webcrypto.
  cryptoProvider.set(webcrypto as unknown as Parameters<typeof cryptoProvider.set>[0]);

  const alg = { name: 'ECDSA', namedCurve: 'P-256', hash: 'SHA-256' } as const;
  const keys = await webcrypto.subtle.generateKey(alg, false, ['sign', 'verify']);

  const now = new Date();
  const tomorrow = new Date(now.getTime() + 86_400_000);

  const cert = await X509CertificateGenerator.createSelfSigned({
    serialNumber: '01',
    name: 'CN=mosaic-issued-test',
    notBefore: now,
    notAfter: tomorrow,
    signingAlgorithm: alg,
    keys,
    extensions: [
      new BasicConstraintsExtension(false),
      new KeyUsagesExtension(KeyUsageFlags.digitalSignature),
      // mosaic_grant_id — OID 1.3.6.1.4.1.99999.1
      new Extension(OID_MOSAIC_GRANT_ID, false, encodeUtf8String(opts.grantId)),
      // mosaic_subject_user_id — OID 1.3.6.1.4.1.99999.2
      new Extension(OID_MOSAIC_SUBJECT_USER_ID, false, encodeUtf8String(opts.subjectUserId)),
    ],
  });

  return cert.toString('pem');
}
