/**
 * A1 acceptance (FIX C-1) — the single X.509 issuance primitive. Proves that a leaf assembled by
 * DER-encoding the TBS ourselves and signing the SERIALIZED TBS via `CaSigner.sign` (the KMS
 * boundary) is a REAL, tool-parseable, signature-verifiable X.509 v3 certificate for BOTH the
 * Ed25519 and ECDSA-P256 CA families — and that the dNSName+URI SAN the A3 nginx njs will parse
 * round-trips byte-correct.
 */
import 'reflect-metadata'
import { describe, test, expect } from 'vitest'
import * as x509 from '@peculiar/x509'
import { AsnConvert } from '@peculiar/asn1-schema'
import { Certificate } from '@peculiar/asn1-x509'
import { webcrypto, generateKeyPairSync, randomBytes } from 'node:crypto'
import { execFileSync } from 'node:child_process'
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import {
  assembleCertificate,
  normalizeEcdsaSignatureToDer,
  type AssembleCertificateInput,
} from '../src/ca/x509-assembler.js'
import { inProcessCaSigner, inProcessP256CaSigner, type CaSigner } from '../src/boot/ca-wiring.js'

x509.cryptoProvider.set(webcrypto)

const DAY_MS = 24 * 60 * 60 * 1000
const SAN_DNS = 'alice.terminal.yaojia.wang'
const SAN_URI = 'spiffe://relay.example.com/account/a1/host/alice'

/** WebCrypto key pair (the `CryptoKeyPair` global is not in this project's TS lib set). */
type KeyPair = { readonly publicKey: CryptoKey; readonly privateKey: CryptoKey }

async function genP256(): Promise<KeyPair> {
  return (await webcrypto.subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify'])) as unknown as KeyPair
}

const ED25519_SPKI_PREFIX = Uint8Array.from([
  0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00,
])

function derToPem(der: Uint8Array, label = 'CERTIFICATE'): string {
  const b64 = Buffer.from(der).toString('base64').match(/.{1,64}/g)?.join('\n') ?? ''
  return `-----BEGIN ${label}-----\n${b64}\n-----END ${label}-----\n`
}

function leafExtensions(): x509.Extension[] {
  return [
    new x509.SubjectAlternativeNameExtension([
      { type: 'dns', value: SAN_DNS },
      { type: 'url', value: SAN_URI },
    ]),
    new x509.BasicConstraintsExtension(false, undefined, true),
    new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature, true),
    new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage.clientAuth]),
  ]
}

async function importEd25519Public(raw: Uint8Array): Promise<CryptoKey> {
  const spki = new Uint8Array(ED25519_SPKI_PREFIX.length + raw.length)
  spki.set(ED25519_SPKI_PREFIX, 0)
  spki.set(raw, ED25519_SPKI_PREFIX.length)
  return webcrypto.subtle.importKey('spki', spki, { name: 'Ed25519' }, true, ['verify'])
}

async function importP256Public(spkiDer: Uint8Array): Promise<CryptoKey> {
  // Copy into a fresh ArrayBuffer-backed view so it satisfies BufferSource (not ArrayBufferLike).
  return webcrypto.subtle.importKey('spki', new Uint8Array(spkiDer), { name: 'ECDSA', namedCurve: 'P-256' }, true, ['verify'])
}

async function ed25519SubjectSpki(): Promise<Uint8Array> {
  const { publicKey } = generateKeyPairSync('ed25519')
  return new Uint8Array(publicKey.export({ format: 'der', type: 'spki' }))
}

function baseInput(overrides: Partial<AssembleCertificateInput>): AssembleCertificateInput {
  const now = Date.now()
  return {
    subjectPublicKey: new Uint8Array(0),
    subject: 'CN=alice',
    issuer: 'CN=test-CA',
    serialNumber: randomBytes(16),
    notBefore: new Date(now - 60_000),
    notAfter: new Date(now + DAY_MS),
    extensions: leafExtensions(),
    signer: inProcessCaSigner(),
    sigAlg: 'ed25519',
    ...overrides,
  }
}

describe('x509-assembler — gate (a): assembled leaves re-parse as X.509', () => {
  test('Ed25519 leaf re-parses via new x509.X509Certificate without throwing', async () => {
    const der = await assembleCertificate(baseInput({ subjectPublicKey: await ed25519SubjectSpki() }))
    expect(() => new x509.X509Certificate(der)).not.toThrow()
  })

  test('ECDSA-P256 leaf re-parses via new x509.X509Certificate without throwing', async () => {
    const subject = await genP256()
    const der = await assembleCertificate(baseInput({ subjectPublicKey: subject.publicKey, signer: inProcessP256CaSigner(), sigAlg: 'ecdsa-p256' }))
    expect(() => new x509.X509Certificate(der)).not.toThrow()
    expect(new x509.X509Certificate(der).subject).toBe('CN=alice')
  })
})

describe('x509-assembler — gate (b): re-parsed leaf signature verifies against the CA public key', () => {
  test('Ed25519 leaf verifies against the Ed25519 CA public key', async () => {
    const ca = inProcessCaSigner()
    const der = await assembleCertificate(baseInput({ subjectPublicKey: await ed25519SubjectSpki(), signer: ca, sigAlg: 'ed25519' }))
    const leaf = new x509.X509Certificate(der)
    const caPub = await importEd25519Public(ca.publicKeyRaw)
    expect(await leaf.verify({ publicKey: caPub, signatureOnly: true })).toBe(true)
  })

  test('ECDSA-P256 leaf verifies against the P-256 CA public key', async () => {
    const ca = inProcessP256CaSigner()
    const subject = await genP256()
    const der = await assembleCertificate(baseInput({ subjectPublicKey: subject.publicKey, signer: ca, sigAlg: 'ecdsa-p256' }))
    const leaf = new x509.X509Certificate(der)
    const caPub = await importP256Public(ca.publicKeyRaw)
    expect(await leaf.verify({ publicKey: caPub, signatureOnly: true })).toBe(true)
  })

  test('a leaf does NOT verify against a different CA key (negative)', async () => {
    const ca = inProcessP256CaSigner()
    const subject = await genP256()
    const der = await assembleCertificate(baseInput({ subjectPublicKey: subject.publicKey, signer: ca, sigAlg: 'ecdsa-p256' }))
    const leaf = new x509.X509Certificate(der)
    const otherCaPub = await importP256Public(inProcessP256CaSigner().publicKeyRaw)
    expect(await leaf.verify({ publicKey: otherCaPub, signatureOnly: true })).toBe(false)
  })
})

describe('x509-assembler — gate (c): dNSName + URI SAN round-trips byte-correct', () => {
  test('Ed25519 leaf SAN carries both the dNSName and the URI', async () => {
    const der = await assembleCertificate(baseInput({ subjectPublicKey: await ed25519SubjectSpki() }))
    const san = new x509.X509Certificate(der).getExtension(x509.SubjectAlternativeNameExtension)
    const names = san!.names.toJSON()
    expect(names).toContainEqual({ type: 'dns', value: SAN_DNS })
    expect(names).toContainEqual({ type: 'url', value: SAN_URI })
  })

  test('ECDSA-P256 leaf SAN carries both the dNSName and the URI', async () => {
    const subject = await genP256()
    const der = await assembleCertificate(baseInput({ subjectPublicKey: subject.publicKey, signer: inProcessP256CaSigner(), sigAlg: 'ecdsa-p256' }))
    const san = new x509.X509Certificate(der).getExtension(x509.SubjectAlternativeNameExtension)
    const names = san!.names.toJSON()
    expect(names).toContainEqual({ type: 'dns', value: SAN_DNS })
    expect(names).toContainEqual({ type: 'url', value: SAN_URI })
  })
})

describe('x509-assembler — X.509 structural invariants', () => {
  test('tbsCertificate.signature OID equals certificate.signatureAlgorithm OID (identical)', async () => {
    const der = await assembleCertificate(baseInput({ subjectPublicKey: await ed25519SubjectSpki() }))
    const cert = AsnConvert.parse(der.buffer.slice(der.byteOffset, der.byteOffset + der.byteLength) as ArrayBuffer, Certificate)
    expect(cert.tbsCertificate.signature.algorithm).toBe(cert.signatureAlgorithm.algorithm)
    expect(cert.signatureAlgorithm.algorithm).toBe('1.3.101.112')
  })

  test('ECDSA-P256 signatureAlgorithm OID is ecdsa-with-SHA256', async () => {
    const subject = await genP256()
    const der = await assembleCertificate(baseInput({ subjectPublicKey: subject.publicKey, signer: inProcessP256CaSigner(), sigAlg: 'ecdsa-p256' }))
    const cert = AsnConvert.parse(der.buffer.slice(der.byteOffset, der.byteOffset + der.byteLength) as ArrayBuffer, Certificate)
    expect(cert.tbsCertificate.signature.algorithm).toBe('1.2.840.10045.4.3.2')
    expect(cert.signatureAlgorithm.algorithm).toBe('1.2.840.10045.4.3.2')
  })

  test('a high-bit serial is emitted as a positive INTEGER (0x00-prefixed content octets)', async () => {
    const highBit = Uint8Array.from([0x80, 0x01, 0x02, 0x03])
    const der = await assembleCertificate(baseInput({ subjectPublicKey: await ed25519SubjectSpki(), serialNumber: highBit }))
    // Inspect the raw INTEGER content octets: the assembler MUST prepend 0x00 so a leading 0x80 is
    // never read as a negative integer. (@peculiar's serialNumber getter strips it for display.)
    const cert = AsnConvert.parse(der.buffer.slice(der.byteOffset, der.byteOffset + der.byteLength) as ArrayBuffer, Certificate)
    const serialBytes = new Uint8Array(cert.tbsCertificate.serialNumber)
    expect(Array.from(serialBytes)).toEqual([0x00, 0x80, 0x01, 0x02, 0x03])
  })

  test('subject public key accepted as BOTH a CryptoKey and as SPKI DER', async () => {
    const kp = await genP256()
    const spkiDer = new Uint8Array(await webcrypto.subtle.exportKey('spki', kp.publicKey))
    const ca = inProcessP256CaSigner()
    const fromKey = await assembleCertificate(baseInput({ subjectPublicKey: kp.publicKey, signer: ca, sigAlg: 'ecdsa-p256' }))
    const fromDer = await assembleCertificate(baseInput({ subjectPublicKey: spkiDer, signer: ca, sigAlg: 'ecdsa-p256' }))
    // Both embed the SAME subjectPublicKeyInfo.
    expect(new x509.X509Certificate(fromKey).publicKey.rawData.byteLength).toBe(new x509.X509Certificate(fromDer).publicKey.rawData.byteLength)
    expect(Buffer.from(new x509.X509Certificate(fromKey).publicKey.rawData).equals(Buffer.from(new x509.X509Certificate(fromDer).publicKey.rawData))).toBe(true)
  })
})

describe('normalizeEcdsaSignatureToDer — both signer output shapes', () => {
  test('raw P1363 (64 bytes) is converted to a valid DER ECDSA-Sig-Value', async () => {
    const kp = generateKeyPairSync('ec', { namedCurve: 'P-256' })
    const { sign } = await import('node:crypto')
    const raw = new Uint8Array(sign('sha256', Buffer.from('hello'), { key: kp.privateKey, dsaEncoding: 'ieee-p1363' }))
    expect(raw.length).toBe(64)
    const der = normalizeEcdsaSignatureToDer(raw)
    expect(der[0]).toBe(0x30) // SEQUENCE
    // openssl-shape signature verifies against the same key (proves the r,s survived intact).
    const { verify } = await import('node:crypto')
    expect(verify('sha256', Buffer.from('hello'), { key: kp.publicKey, dsaEncoding: 'der' }, Buffer.from(der))).toBe(true)
  })

  test('an already-DER ECDSA-Sig-Value passes through unchanged', async () => {
    const kp = generateKeyPairSync('ec', { namedCurve: 'P-256' })
    const { sign } = await import('node:crypto')
    const der = new Uint8Array(sign('sha256', Buffer.from('world'), { key: kp.privateKey, dsaEncoding: 'der' }))
    const out = normalizeEcdsaSignatureToDer(der)
    expect(Buffer.from(out).equals(Buffer.from(der))).toBe(true)
  })

  test('garbage that is neither P1363 nor DER throws (fail loud at issuance)', () => {
    expect(() => normalizeEcdsaSignatureToDer(Uint8Array.from([1, 2, 3, 4, 5]))).toThrow()
  })
})

describe('x509-assembler — Ed25519 signatureValue length guard (CP2)', () => {
  /** A CaSigner whose `sign` returns a wrong-length "Ed25519" signature (truncated / malformed). */
  function fixedLenEd25519Signer(len: number): CaSigner {
    return { publicKeyRaw: new Uint8Array(32), async sign() { return new Uint8Array(len) } }
  }

  test('a signer returning fewer than 64 bytes is rejected at issuance (not embedded)', async () => {
    await expect(
      assembleCertificate(
        baseInput({ subjectPublicKey: await ed25519SubjectSpki(), signer: fixedLenEd25519Signer(63), sigAlg: 'ed25519' }),
      ),
    ).rejects.toThrow(/64 bytes/)
  })

  test('a signer returning more than 64 bytes is rejected too', async () => {
    await expect(
      assembleCertificate(
        baseInput({ subjectPublicKey: await ed25519SubjectSpki(), signer: fixedLenEd25519Signer(65), sigAlg: 'ed25519' }),
      ),
    ).rejects.toThrow(/64 bytes/)
  })

  test('a real 64-byte Ed25519 signature still issues fine (no false positive)', async () => {
    const der = await assembleCertificate(baseInput({ subjectPublicKey: await ed25519SubjectSpki() }))
    expect(() => new x509.X509Certificate(der)).not.toThrow()
  })
})

describe('x509-assembler — gate (e): openssl parses & verifies the chain', () => {
  function hasOpenssl(): boolean {
    try {
      execFileSync('openssl', ['version'], { stdio: 'ignore' })
      return true
    } catch {
      return false
    }
  }

  test('openssl x509 -text parses the P-256 leaf and openssl verify OKs it against the CA', async () => {
    if (!hasOpenssl()) {
      // openssl absent — gates (a)-(c) already prove real X.509; note and skip the tool check.
      expect(true).toBe(true)
      return
    }
    // Self-signed P-256 CA (subject key == signer key) so openssl can build a full chain.
    const caKeys = generateKeyPairSync('ec', { namedCurve: 'P-256' })
    const caSpki = new Uint8Array(caKeys.publicKey.export({ format: 'der', type: 'spki' }))
    const caSigner: CaSigner = inProcessP256CaSigner(caKeys.privateKey)
    const now = Date.now()
    const caDer = await assembleCertificate({
      subjectPublicKey: caSpki,
      subject: 'CN=Test P256 CA',
      issuer: 'CN=Test P256 CA',
      serialNumber: Uint8Array.from([0x01]),
      notBefore: new Date(now - DAY_MS),
      notAfter: new Date(now + 365 * DAY_MS),
      extensions: [
        new x509.BasicConstraintsExtension(true, undefined, true),
        new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true),
      ],
      signer: caSigner,
      sigAlg: 'ecdsa-p256',
    })

    const subject = await genP256()
    const leafDer = await assembleCertificate(baseInput({ subjectPublicKey: subject.publicKey, issuer: 'CN=Test P256 CA', signer: caSigner, sigAlg: 'ecdsa-p256' }))

    const dir = mkdtempSync(join(tmpdir(), 'x509asm-'))
    try {
      const caPem = join(dir, 'ca.pem')
      const leafPem = join(dir, 'leaf.pem')
      writeFileSync(caPem, derToPem(caDer))
      writeFileSync(leafPem, derToPem(leafDer))

      const text = execFileSync('openssl', ['x509', '-in', leafPem, '-noout', '-text'], { encoding: 'utf8' })
      expect(text).toContain('Signature Algorithm: ecdsa-with-SHA256')
      expect(text).toContain(SAN_DNS)
      expect(text).toContain(SAN_URI)

      const verifyOut = execFileSync('openssl', ['verify', '-CAfile', caPem, leafPem], { encoding: 'utf8' })
      expect(verifyOut).toContain('OK')
    } finally {
      rmSync(dir, { recursive: true, force: true })
    }
  })
})
