/**
 * B2 tests — registry-backed MtlsVerifier (INV4/INV14, fail-closed).
 *
 * Two layers, mirroring relay-auth/test/mtls.test.ts:
 *  1. REAL X.509 path: a self-signed root → leaf chain (relay-auth's committed mTLS fixtures) fed to
 *     `verifyPeer` as DER bytes, exercising the production `defaultParseX509` chain walk + DER→PEM
 *     conversion end-to-end against a fake host registry.
 *  2. Deterministic seam: an injected `ParseCert` isolates the DER→PEM conversion + registry-gating +
 *     fail-closed mapping without depending on fixture contents.
 */
import { readFileSync } from 'node:fs'
import { X509Certificate } from 'node:crypto'
import { fileURLToPath } from 'node:url'
import { dirname, join } from 'node:path'
import { describe, it, expect } from 'vitest'
import { defaultParseX509, spiffeIdFor, type ParseCert, type ParsedCert } from 'relay-auth'
import type { HostRegistryPort } from 'relay-auth'
import { createMtlsVerifier, derToPem } from '../../src/wiring/mtls-verifier.js'
import { fakeHostRegistry, makeHostRecord } from '../../src/wiring/memory-stores.js'

// Reuse the committed real mTLS fixtures (leaf chains to ca-chain; SPIFFE account/acct-A/host/host-1).
const FIXTURES = join(dirname(fileURLToPath(import.meta.url)), '../../../relay-auth/test/fixtures/mtls')
const readFixture = (name: string): string => readFileSync(join(FIXTURES, name), 'utf8')

const leafPem = readFixture('leaf.pem')
const caChainPem = readFixture('ca-chain.pem')
const foreignLeafPem = readFixture('foreign-leaf.pem')

const leafDer = new Uint8Array(new X509Certificate(leafPem).raw)
const foreignDer = new Uint8Array(new X509Certificate(foreignLeafPem).raw)

// A time strictly inside the leaf's validity window (fixtures are long-lived; do not hardcode).
const parsedLeaf = defaultParseX509(leafPem, caChainPem)
const IN_WINDOW = parsedLeaf.notBefore + 60

const enrolledHosts = (): HostRegistryPort =>
  fakeHostRegistry([makeHostRecord('acct-A', 'host-1', 'sub-host-1')])

describe('derToPem (DER → PEM)', () => {
  it('wraps DER as a 64-column PEM CERTIFICATE block that round-trips back to the same DER', () => {
    const pem = derToPem(leafDer)
    expect(pem.startsWith('-----BEGIN CERTIFICATE-----\n')).toBe(true)
    expect(pem.trimEnd().endsWith('-----END CERTIFICATE-----')).toBe(true)
    // No base64 body line exceeds the PEM 64-column width.
    const body = pem.split('\n').slice(1, -2)
    for (const line of body) expect(line.length).toBeLessThanOrEqual(64)
    // Parsing the produced PEM yields byte-identical DER.
    expect(new Uint8Array(new X509Certificate(pem).raw)).toEqual(leafDer)
  })
})

describe('createMtlsVerifier — real X.509 path (INV14)', () => {
  it('accepts an enrolled, in-date leaf chaining to the pinned CA → {hostId, accountId}', async () => {
    const v = createMtlsVerifier({ caChainPem, hosts: enrolledHosts(), now: () => IN_WINDOW })
    expect(await v.verifyPeer(leafDer)).toEqual({ hostId: 'host-1', accountId: 'acct-A' })
  })

  it('refuses a leaf signed by a foreign CA not in the pinned bundle → null', async () => {
    const v = createMtlsVerifier({ caChainPem, hosts: enrolledHosts(), now: () => IN_WINDOW })
    expect(await v.verifyPeer(foreignDer)).toBeNull()
  })

  it('refuses a valid leaf whose host is NOT in the registry (INV4) → null', async () => {
    const v = createMtlsVerifier({ caChainPem, hosts: fakeHostRegistry([]), now: () => IN_WINDOW })
    expect(await v.verifyPeer(leafDer)).toBeNull()
  })

  it('refuses when the registry account ≠ the cert SPIFFE account → null', async () => {
    const hosts = fakeHostRegistry([makeHostRecord('acct-B', 'host-1', 'sub-host-1')])
    const v = createMtlsVerifier({ caChainPem, hosts, now: () => IN_WINDOW })
    expect(await v.verifyPeer(leafDer)).toBeNull()
  })

  it('refuses a revoked host (INV12/registry gate) → null', async () => {
    const hosts = fakeHostRegistry([makeHostRecord('acct-A', 'host-1', 'sub-host-1', 'revoked')])
    const v = createMtlsVerifier({ caChainPem, hosts, now: () => IN_WINDOW })
    expect(await v.verifyPeer(leafDer)).toBeNull()
  })

  it('refuses an expired leaf → null', async () => {
    const v = createMtlsVerifier({ caChainPem, hosts: enrolledHosts(), now: () => parsedLeaf.notAfter + 100 })
    expect(await v.verifyPeer(leafDer)).toBeNull()
  })

  it('refuses a not-yet-valid leaf → null', async () => {
    const v = createMtlsVerifier({ caChainPem, hosts: enrolledHosts(), now: () => parsedLeaf.notBefore - 100 })
    expect(await v.verifyPeer(leafDer)).toBeNull()
  })
})

describe('createMtlsVerifier — fail-closed on malformed / absent input', () => {
  it('returns null for an empty DER (no client cert presented)', async () => {
    const v = createMtlsVerifier({ caChainPem, hosts: enrolledHosts(), now: () => IN_WINDOW })
    expect(await v.verifyPeer(new Uint8Array(0))).toBeNull()
  })

  it('returns null for garbage DER bytes that are not a certificate', async () => {
    const v = createMtlsVerifier({ caChainPem, hosts: enrolledHosts(), now: () => IN_WINDOW })
    expect(await v.verifyPeer(new Uint8Array([1, 2, 3, 4, 5]))).toBeNull()
  })
})

describe('createMtlsVerifier — construction validation (INV14)', () => {
  it('throws when the CA bundle is empty', () => {
    expect(() => createMtlsVerifier({ caChainPem: '', hosts: enrolledHosts(), now: () => IN_WINDOW })).toThrow()
  })

  it('throws when the CA bundle has no PEM CERTIFICATE block', () => {
    expect(() =>
      createMtlsVerifier({ caChainPem: 'not a certificate', hosts: enrolledHosts(), now: () => IN_WINDOW }),
    ).toThrow()
  })
})

// ── Deterministic seam: isolate DER→PEM + registry gating from fixture contents ──────────────────
const okParsed = (spiffeUri: string): ParsedCert => ({
  spiffeUri,
  notBefore: 0,
  notAfter: 4_000_000_000,
  chainValid: true,
})

describe('createMtlsVerifier — ParseCert seam', () => {
  it('feeds verifyAgentCert the exact PEM produced by derToPem from the input DER', async () => {
    let seenLeafPem: string | null = null
    const capturingParse: ParseCert = (leaf) => {
      seenLeafPem = leaf
      return okParsed(spiffeIdFor('acct-A', 'host-1', 'example.com'))
    }
    const der = new Uint8Array([9, 8, 7, 6, 5, 4, 3, 2, 1])
    const v = createMtlsVerifier({
      caChainPem,
      hosts: enrolledHosts(),
      now: () => 1_000_000,
      parse: capturingParse,
    })
    expect(await v.verifyPeer(der)).toEqual({ hostId: 'host-1', accountId: 'acct-A' })
    expect(seenLeafPem).toBe(derToPem(der))
  })

  it('fails closed (null) and reports to onError when the registry lookup throws', async () => {
    const errors: unknown[] = []
    const throwingHosts: HostRegistryPort = {
      getById: async () => {
        throw new Error('registry unavailable')
      },
    }
    const v = createMtlsVerifier({
      caChainPem,
      hosts: throwingHosts,
      now: () => 1_000_000,
      onError: (e) => errors.push(e),
      parse: () => okParsed(spiffeIdFor('acct-A', 'host-1', 'example.com')),
    })
    expect(await v.verifyPeer(new Uint8Array([1, 2, 3]))).toBeNull()
    expect(errors).toHaveLength(1)
  })
})
