import { describe, test, expect } from 'vitest'
import { createMemoryStores } from '../src/store/memory.js'
import { createHostRegistry } from '../src/registry/hosts.js'
import { createSubdomainAssigner } from '../src/subdomain/assign.js'
import { createPairingIssuer } from '../src/pairing/issue.js'
import { createPairingRedeemer, RedeemError } from '../src/pairing/redeem.js'
import { createLeafSigner } from '../src/ca/sign.js'
import { inProcessCaSigner, type CaSigner } from '../src/boot/ca-wiring.js'
import { buildCsr } from '../src/ca/csr.js'
import { generatePairingCode, formatPairingCode, normalizePairingCode, PAIRING_CODE_BITS } from '../src/pairing/code.js'
import { sha256Hex } from '../src/util/hash.js'
import { generateEd25519 } from '../src/util/crypto.js'

const ACCOUNT = '11111111-1111-4111-8111-111111111111'

function harness(opts?: { ttlSec?: number; maxAttempts?: number; signer?: CaSigner }) {
  const stores = createMemoryStores()
  const hosts = createHostRegistry({ hosts: stores.hosts })
  const subdomains = createSubdomainAssigner({ subdomains: stores.subdomains })
  const signer = opts?.signer ?? inProcessCaSigner()
  const leafSigner = createLeafSigner({ hosts: stores.hosts, signer, caChainDer: [new Uint8Array([1, 2, 3])] })
  const issuer = createPairingIssuer({ pairing: stores.pairing, pairingTtlSec: opts?.ttlSec ?? 600 })
  const redeemer = createPairingRedeemer({
    pairing: stores.pairing,
    hosts,
    subdomains,
    leafSigner,
    pairingMaxRedeemAttempts: opts?.maxAttempts ?? 5,
  })
  return { stores, issuer, redeemer }
}

function agentEnroll() {
  const { publicKeyRaw, privateKey } = generateEd25519()
  return { agentPubkey: publicKeyRaw, csr: buildCsr(privateKey, publicKeyRaw) }
}

describe('T7 pairing issuance (INV5, entropy)', () => {
  test('code is >= 128-bit entropy, display round-trips to canonical', () => {
    expect(PAIRING_CODE_BITS).toBeGreaterThanOrEqual(128)
    const canonical = generatePairingCode()
    expect(canonical.length).toBe(26)
    expect(normalizePairingCode(formatPairingCode(canonical))).toBe(canonical)
  })

  test('stored row holds code_hash only — no raw code at rest (INV5)', async () => {
    const { stores, issuer } = harness()
    const { code } = await issuer.issuePairingCode(ACCOUNT)
    const codeHash = sha256Hex(normalizePairingCode(code))
    const row = await stores.pairing.get(codeHash)
    expect(row).not.toBeNull()
    expect(JSON.stringify(row)).not.toContain(normalizePairingCode(code))
    expect(row?.redeemAttempts).toBe(0)
  })

  test('two issues → two distinct codes', async () => {
    const { issuer } = harness()
    const a = await issuer.issuePairingCode(ACCOUNT)
    const b = await issuer.issuePairingCode(ACCOUNT)
    expect(a.code).not.toBe(b.code)
  })
})

describe('T8 redemption + BIND + cert + content-secret', () => {
  test('happy path → EnrollResult with frozen fields, subject pubkey == agentPubkey', async () => {
    const { issuer, redeemer } = harness()
    const { code } = await issuer.issuePairingCode(ACCOUNT)
    const { agentPubkey, csr } = agentEnroll()
    const result = await redeemer.redeemPairingCode({ code, agentPubkey, csr })
    expect(result.hostId).toMatch(/[0-9a-f-]{36}/)
    expect(result.subdomain.length).toBeGreaterThan(0)
    expect(result.cert).toContain('BEGIN CERTIFICATE')
    expect(result.caChain).toContain('BEGIN CERTIFICATE')
    expect(result.hostContentSecret.length).toBeGreaterThan(0)
    // subject pubkey inside the cert == agentPubkey
    const certJson = JSON.parse(Buffer.from(result.cert.split('\n').slice(1, -2).join(''), 'base64').toString())
    const tbs = JSON.parse(Buffer.from(certJson.tbs, 'base64').toString())
    expect(tbs.subjectSpki).toBe(Buffer.from(agentPubkey).toString('base64'))
  })

  test('hostContentSecret: wrapped, raw secret absent, distinct per enrollment (FIX 3)', async () => {
    const { issuer, redeemer } = harness()
    const c1 = (await issuer.issuePairingCode(ACCOUNT)).code
    const c2 = (await issuer.issuePairingCode(ACCOUNT)).code
    const r1 = await redeemer.redeemPairingCode({ code: c1, ...agentEnroll() })
    const r2 = await redeemer.redeemPairingCode({ code: c2, ...agentEnroll() })
    expect(Buffer.from(r1.hostContentSecret).equals(Buffer.from(r2.hostContentSecret))).toBe(false)
  })

  test('double-spend: two concurrent redeems → exactly one wins (CAS)', async () => {
    const { issuer, redeemer } = harness()
    const { code } = await issuer.issuePairingCode(ACCOUNT)
    const results = await Promise.allSettled([
      redeemer.redeemPairingCode({ code, ...agentEnroll() }),
      redeemer.redeemPairingCode({ code, ...agentEnroll() }),
    ])
    const ok = results.filter((r) => r.status === 'fulfilled')
    const rejected = results.filter((r) => r.status === 'rejected')
    expect(ok.length).toBe(1)
    expect(rejected.length).toBe(1)
    expect((rejected[0] as PromiseRejectedResult).reason).toBeInstanceOf(RedeemError)
    expect(((rejected[0] as PromiseRejectedResult).reason as RedeemError).code).toBe('already_redeemed')
  })

  test('expired / unknown / CSR-substitution rejected', async () => {
    const expiredH = harness({ ttlSec: -10 })
    const { code } = await expiredH.issuer.issuePairingCode(ACCOUNT)
    await expect(expiredH.redeemer.redeemPairingCode({ code, ...agentEnroll() })).rejects.toMatchObject({ code: 'expired' })

    const h = harness()
    await expect(h.redeemer.redeemPairingCode({ code: 'ZZZZZ-ZZZZZ-ZZZZZ-ZZZZZ-ZZZZZ-Z', ...agentEnroll() })).rejects.toMatchObject({ code: 'unknown' })

    // CSR embeds a DIFFERENT pubkey than the presented agentPubkey → bad_csr
    const good = (await h.issuer.issuePairingCode(ACCOUNT)).code
    const a = generateEd25519()
    const b = generateEd25519()
    const substitutedCsr = buildCsr(b.privateKey, b.publicKeyRaw)
    await expect(h.redeemer.redeemPairingCode({ code: good, agentPubkey: a.publicKeyRaw, csr: substitutedCsr })).rejects.toMatchObject({ code: 'bad_csr' })
  })

  test('code-scoped lockout after maxAttempts — correct code then refused (Finding-4)', async () => {
    const h = harness({ maxAttempts: 3 })
    const { code } = await h.issuer.issuePairingCode(ACCOUNT)
    const bad = generateEd25519()
    const badCsr = buildCsr(bad.privateKey, bad.publicKeyRaw)
    const victim = agentEnroll()
    for (let i = 0; i < 3; i++) {
      // substitution failure against the SAME code_hash
      await expect(h.redeemer.redeemPairingCode({ code, agentPubkey: victim.agentPubkey, csr: badCsr })).rejects.toMatchObject({ code: 'bad_csr' })
    }
    // now even a correct redemption is locked out
    await expect(h.redeemer.redeemPairingCode({ code, ...victim })).rejects.toMatchObject({ code: 'too_many_attempts' })
  })
})
