/**
 * A6 (FIX H-host-3) — leaf RENEWAL upgraded off the DEV JSON-blob placeholder to REAL X.509. Proves
 * `createLeafRenewer` re-issues a tool-parseable, CA-verifiable X.509 v3 leaf (host + device) that
 * carries the SAME subdomain the registry holds (no smuggling), enforces the SAME-KEY rule (a
 * different-key CSR is rejected), and refuses a revoked identity — all by DELEGATING to the P-256
 * issuers (`frpclient-issue` / `device-issue`) that route through `assembleCertificate`. The final
 * `buildCaSigner` block (startup KMS key-policy fail-fast, INV9) is unrelated to rotation but colocated.
 */
import 'reflect-metadata'
import { describe, test, expect } from 'vitest'
import * as x509 from '@peculiar/x509'
import { webcrypto, generateKeyPairSync, randomUUID } from 'node:crypto'
import { parseSpiffeId } from 'relay-auth/src/agent/spiffe.js'
import { createMemoryStores } from '../src/store/memory.js'
import { createHostRegistry } from '../src/registry/hosts.js'
import { createDeviceRegistry, createMemoryDeviceStore } from '../src/registry/devices.js'
import { fingerprint } from '../src/ca/fingerprint.js'
import { assembleCertificate } from '../src/ca/x509-assembler.js'
import { buildCsrEc } from '../src/ca/csr-ec.js'
import { createFrpClientLeafSigner } from '../src/ca/frpclient-issue.js'
import { createDeviceLeafSigner } from '../src/ca/device-issue.js'
import { createLeafRenewer } from '../src/ca/rotate.js'
import { LeafSignError } from '../src/ca/sign.js'
import { DeviceLeafSignError } from '../src/ca/device-issue.js'
import {
  buildCaSigner,
  inProcessCaSigner,
  inProcessP256CaSigner,
  type CaSigner,
  type KmsResolver,
} from '../src/boot/ca-wiring.js'
import { loadEnv } from '../src/env.js'
import { bytesToBase64 } from '../src/util/bytes.js'

x509.cryptoProvider.set(webcrypto)

const DAY_MS = 24 * 60 * 60 * 1000
const TRUST_DOMAIN = 'terminal.yaojia.wang'
const DNS_ZONE = 'terminal.yaojia.wang'
const BASE_DOMAIN = 'terminal.yaojia.wang'

interface P256Ca {
  readonly caSigner: CaSigner
  readonly caCert: x509.X509Certificate
  readonly caDer: Uint8Array
}

/** Self-signed P-256 CA (subject key == signer key) so leaves chain to a real CA. */
async function makeP256Ca(cn: string): Promise<P256Ca> {
  const caKeys = generateKeyPairSync('ec', { namedCurve: 'P-256' })
  const caSpki = new Uint8Array(caKeys.publicKey.export({ format: 'der', type: 'spki' }))
  const caSigner = inProcessP256CaSigner(caKeys.privateKey)
  const now = Date.now()
  const caDer = await assembleCertificate({
    subjectPublicKey: caSpki,
    subject: `CN=${cn}`,
    issuer: `CN=${cn}`,
    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',
  })
  return { caSigner, caCert: new x509.X509Certificate(caDer), caDer }
}

async function importP256Public(spkiDer: Uint8Array): Promise<CryptoKey> {
  return webcrypto.subtle.importKey('spki', new Uint8Array(spkiDer), { name: 'ECDSA', namedCurve: 'P-256' }, true, ['verify'])
}

async function spkiOf(pub: CryptoKey): Promise<Uint8Array> {
  return new Uint8Array(await webcrypto.subtle.exportKey('spki', pub))
}

// ── Host renewal setup ─────────────────────────────────────────────────────────────────────────────
async function hostRenewFixture(subdomain = 'alice') {
  const ca = await makeP256Ca('frp-client-CA')
  const stores = createMemoryStores()
  const hosts = createHostRegistry({ hosts: stores.hosts })
  const { der: csr, keys } = await buildCsrEc(`CN=${subdomain}`)
  const spki = await spkiOf(keys.publicKey)
  const accountId = randomUUID()
  const host = await hosts.bindHost({ accountId, subdomain, agentPubkey: spki, enrollFpr: fingerprint(spki) })
  const hostSigner = createFrpClientLeafSigner({
    hosts: stores.hosts,
    signer: ca.caSigner,
    issuerName: ca.caCert.subjectName,
    caChainDer: [ca.caDer],
    trustDomain: TRUST_DOMAIN,
    dnsZone: DNS_ZONE,
  })
  // Device signer + expiry renewer are required by the renewer shape but unused in host tests — a
  // throwaway pair sharing one store.
  const throwawayDeviceStore = createMemoryDeviceStore()
  const deviceSigner = createDeviceLeafSigner({
    signer: (await makeP256Ca('device-CA')).caSigner,
    issuer: 'CN=device-CA',
    caChainDer: [],
    sanBaseDomain: BASE_DOMAIN,
    trustDomain: TRUST_DOMAIN,
    devices: throwawayDeviceStore,
  })
  const renewer = createLeafRenewer({
    hostSigner,
    deviceSigner,
    deviceExpiry: createDeviceRegistry({ devices: throwawayDeviceStore }),
  })
  return { ca, stores, hosts, host, csr, spki, keys, accountId, renewer }
}

describe('A6 renewHostLeaf → REAL X.509 (FIX H-host-3)', () => {
  test('active host + same-key CSR → a re-parseable, CA-verifiable X.509 leaf carrying the SAME subdomain', async () => {
    const { ca, host, csr, accountId, renewer } = await hostRenewFixture('alice')
    const { cert, caChain, notAfter } = await renewer.renewHostLeaf(host.hostId, host.agentPubkey, csr)

    // (1) it is a REAL X.509 cert — not a JSON blob — that re-parses.
    expect(() => new x509.X509Certificate(cert)).not.toThrow()
    const leaf = new x509.X509Certificate(cert)
    // (2) it chains to the frp-client-CA (signature verifies under the CA key).
    const caPub = await importP256Public(ca.caSigner.publicKeyRaw)
    expect(await leaf.verify({ publicKey: caPub, signatureOnly: true })).toBe(true)
    // (3) SAN carries the registry subdomain as the dNSName enforcement key + the host SPIFFE URI.
    const san = leaf.getExtension(x509.SubjectAlternativeNameExtension)
    const names = san!.names.toJSON()
    expect(names).toContainEqual({ type: 'dns', value: 'alice.terminal.yaojia.wang' })
    const uri = names.find((n) => n.type === 'url')!.value
    expect(parseSpiffeId(uri)).toEqual({ accountId, id: 'alice', kind: 'host' })
    // (4) renewal surfaces a real future expiry + the CA chain.
    expect(notAfter.getTime()).toBeGreaterThan(Date.now())
    expect(caChain).toEqual([ca.caDer])
  })

  test('a CSR with a DIFFERENT key (key-swap attempt) → rejected (MVP same-key)', async () => {
    const { host, renewer } = await hostRenewFixture('alice')
    const { der: otherCsr } = await buildCsrEc('CN=alice') // fresh, different P-256 key
    await expect(renewer.renewHostLeaf(host.hostId, host.agentPubkey, otherCsr)).rejects.toBeInstanceOf(LeafSignError)
  })

  test('a revoked host cannot renew (INV12+INV14 loop)', async () => {
    const { hosts, host, csr, renewer } = await hostRenewFixture('alice')
    await hosts.setHostStatus(host.hostId, 'revoked')
    await expect(renewer.renewHostLeaf(host.hostId, host.agentPubkey, csr)).rejects.toBeInstanceOf(LeafSignError)
  })

  test('the re-issued subdomain comes from the REGISTRY, never the CSR subject (no smuggling)', async () => {
    const { host, keys, renewer } = await hostRenewFixture('alice')
    // A malicious renewal CSR over the SAME key but with a foreign subject label.
    const evilCsr = new Uint8Array(
      (
        await x509.Pkcs10CertificateRequestGenerator.create({
          name: 'CN=bob.terminal.yaojia.wang',
          keys,
          signingAlgorithm: { name: 'ECDSA', hash: 'SHA-256' },
        })
      ).rawData,
    )
    const { cert } = await renewer.renewHostLeaf(host.hostId, host.agentPubkey, evilCsr)
    const names = new x509.X509Certificate(cert).getExtension(x509.SubjectAlternativeNameExtension)!.names.toJSON()
    expect(names).toContainEqual({ type: 'dns', value: 'alice.terminal.yaojia.wang' }) // still alice
    expect(names).not.toContainEqual({ type: 'dns', value: 'bob.terminal.yaojia.wang' })
  })
})

// ── Device renewal setup ────────────────────────────────────────────────────────────────────────────
async function deviceRenewFixture(subdomainScope = 'alice', now?: () => number) {
  const ca = await makeP256Ca('device-CA')
  const store = createMemoryDeviceStore()
  const registry = createDeviceRegistry({ devices: store, ...(now ? { now } : {}) })
  const { der: csr, keys } = await buildCsrEc('CN=web-terminal-device')
  const spki = await spkiOf(keys.publicKey)
  const record = await registry.registerDevice({
    accountId: randomUUID(),
    subdomainScope,
    ecPubkeySpki: spki,
    attestationLevel: 'none',
  })
  const deviceSigner = createDeviceLeafSigner({
    signer: ca.caSigner,
    issuer: ca.caCert.subjectName,
    caChainDer: [ca.caDer],
    sanBaseDomain: BASE_DOMAIN,
    trustDomain: TRUST_DOMAIN,
    devices: store,
  })
  // Host signer is required by the renewer shape but unused in device tests — a throwaway.
  const hostSigner = createFrpClientLeafSigner({
    hosts: createMemoryStores().hosts,
    signer: (await makeP256Ca('frp-client-CA')).caSigner,
    issuerName: 'CN=frp-client-CA',
    caChainDer: [],
    trustDomain: TRUST_DOMAIN,
    dnsZone: DNS_ZONE,
  })
  const renewer = createLeafRenewer({ hostSigner, deviceSigner, deviceExpiry: registry })
  return { ca, store, registry, record, csr, spki, keys, renewer }
}

describe('A6 renewDeviceLeaf → REAL X.509 (FIX H-host-3)', () => {
  test('active device + same-key CSR → a CA-verifiable X.509 leaf carrying the SAME subdomainScope', async () => {
    const { ca, record, csr, renewer } = await deviceRenewFixture('alice')
    const { cert, caChain, notAfter } = await renewer.renewDeviceLeaf(record.deviceId, csr)

    const leaf = new x509.X509Certificate(cert)
    const caPub = await importP256Public(ca.caSigner.publicKeyRaw)
    expect(await leaf.verify({ publicKey: caPub, signatureOnly: true })).toBe(true)
    const names = leaf.getExtension(x509.SubjectAlternativeNameExtension)!.names.toJSON()
    expect(names).toContainEqual({ type: 'dns', value: 'alice.terminal.yaojia.wang' })
    const uri = names.find((n) => n.type === 'url')!.value
    expect(parseSpiffeId(uri)).toEqual({ accountId: record.accountId, id: record.deviceId, kind: 'device' })
    expect(notAfter.getTime()).toBeGreaterThan(Date.now())
    expect(caChain).toEqual([ca.caDer])
  })

  test('a CSR with a DIFFERENT key → rejected (MVP same-key)', async () => {
    const { record, renewer } = await deviceRenewFixture('alice')
    const { der: otherCsr } = await buildCsrEc('CN=web-terminal-device')
    await expect(renewer.renewDeviceLeaf(record.deviceId, otherCsr)).rejects.toBeInstanceOf(DeviceLeafSignError)
  })

  test('a revoked device cannot renew', async () => {
    const { registry, record, csr, renewer } = await deviceRenewFixture('alice')
    await registry.setDeviceStatus(record.deviceId, 'revoked')
    await expect(renewer.renewDeviceLeaf(record.deviceId, csr)).rejects.toBeInstanceOf(DeviceLeafSignError)
  })

  test('renewing TWICE across an advanced clock EXTENDS validity — the 2nd notAfter is later than the 1st', async () => {
    // Regression for the H finding: the device signer stamps record.notAfter (set once at enroll), so
    // without a per-renewal bump both renewals returned a byte-identical expiry — eventually in the
    // PAST. Advance the registry clock a comfortable margin (X.509 notAfter is second-resolution) so
    // the increase is unambiguous in the DER, then assert the second expiry strictly exceeds the first.
    let clock = Date.now()
    const { record, csr, renewer } = await deviceRenewFixture('alice', () => clock)
    const first = await renewer.renewDeviceLeaf(record.deviceId, csr)
    clock += 2 * DAY_MS
    const second = await renewer.renewDeviceLeaf(record.deviceId, csr)
    expect(second.notAfter.getTime()).toBeGreaterThan(first.notAfter.getTime())
    // And the extension is REAL (roughly the clock advance), not a rounding artefact.
    expect(second.notAfter.getTime() - first.notAfter.getTime()).toBeGreaterThanOrEqual(DAY_MS)
  })
})

describe('T1/T15 buildCaSigner startup key-policy check (INV9, §3.1)', () => {
  const env = () =>
    loadEnv({
      PG_URL: 'postgres://u:p@localhost:5432/cp',
      REDIS_URL: 'redis://localhost:6379',
      CAPABILITY_SIGN_PUBKEY_B64: bytesToBase64(new Uint8Array(32).fill(1)),
      CA_INTERMEDIATE_KMS_KEY_REF: 'kms://key/intermediate',
      CA_INTERMEDIATE_CERT_PATH: '/etc/cp/int.pem',
      NODE_MTLS_TRUST_BUNDLE_PATH: '/etc/cp/node-ca.pem',
      BASE_DOMAIN: 'term.example.com',
    })

  test('fails fast when the KMS key ref cannot be resolved', async () => {
    const resolver: KmsResolver = { async resolve() { throw new Error('no such key') } }
    await expect(buildCaSigner(env(), resolver)).rejects.toThrow(/could not be resolved/)
  })

  test('fails fast when the key policy is broader than the service principal', async () => {
    const resolver: KmsResolver = {
      async resolve() {
        return { signer: inProcessCaSigner(), policyRestrictedToServicePrincipal: false }
      },
    }
    await expect(buildCaSigner(env(), resolver)).rejects.toThrow(/policy is broader/)
  })

  test('succeeds when policy is restricted', async () => {
    const resolver: KmsResolver = {
      async resolve() {
        return { signer: inProcessCaSigner(), policyRestrictedToServicePrincipal: true }
      },
    }
    await expect(buildCaSigner(env(), resolver)).resolves.toBeDefined()
  })
})
