import { describe, expect, it } from 'vitest'
import { webcrypto } from 'node:crypto'
import { decodeBase64UrlBytes, decodeBase64UrlString } from 'relay-contracts'
import {
  createDpopKey,
  encodeDpopSubprotocol,
  extractDpopFromSubprotocols,
  htuFor,
  DPOP_HTM,
  DPOP_SUBPROTOCOL_PREFIX,
} from '../src/dpop'

// jsdom's global crypto lacks a full SubtleCrypto/Ed25519; inject Node's webcrypto (DI seam).
const subtle = webcrypto.subtle as unknown as SubtleCrypto
/** Copy into a fresh ArrayBuffer-backed view (WebCrypto's BufferSource wants Uint8Array<ArrayBuffer>). */
const ab = (u: Uint8Array): Uint8Array<ArrayBuffer> => {
  const out = new Uint8Array(u.byteLength)
  out.set(u)
  return out
}
const utf8 = (s: string): Uint8Array<ArrayBuffer> => ab(new TextEncoder().encode(s))
const bytes = (b64u: string): Uint8Array<ArrayBuffer> => ab(decodeBase64UrlBytes(b64u))
const decodeJson = (b64u: string): unknown =>
  JSON.parse(new TextDecoder().decode(decodeBase64UrlBytes(b64u)))

describe('createDpopKey (B6) — browser DPoP proof-of-possession', () => {
  it('produces a 43-char base64url jkt (matches the server JKT_RE)', async () => {
    const key = await createDpopKey({ subtle })
    expect(key.jkt).toMatch(/^[A-Za-z0-9_-]{43}$/)
  })

  it("jkt equals base64url(SHA-256(canonical JWK)) with member order crv,kty,x", async () => {
    let jti = 0
    const key = await createDpopKey({ subtle, randomJti: () => `jti-${jti++}` })
    const proof = await key.proof('https://alice/ws', DPOP_HTM, 1000)
    const [h] = proof.split('.') as [string, string, string]
    const header = decodeJson(h) as { jwk: { crv: string; kty: string; x: string } }
    // Recompute the thumbprint from the proof's embedded JWK exactly as relay-auth does.
    const canonical = JSON.stringify({ crv: 'Ed25519', kty: 'OKP', x: header.jwk.x })
    const digest = new Uint8Array(await subtle.digest('SHA-256', utf8(canonical)))
    const recomputed = Buffer.from(digest).toString('base64url')
    expect(recomputed).toBe(key.jkt)
  })

  it('builds a 3-part DPoP JWS whose header/payload match relay-auth buildDpopProof', async () => {
    const key = await createDpopKey({ subtle, randomJti: () => 'fixed-jti' })
    const proof = await key.proof('https://alice/ws', 'GET', 1234)
    const parts = proof.split('.')
    expect(parts).toHaveLength(3)
    const [h, p] = parts as [string, string, string]
    const header = decodeJson(h) as { typ: string; jwk: { crv: string; kty: string; x: string } }
    const payload = decodeJson(p) as Record<string, unknown>
    expect(header.typ).toBe('dpop+ed25519')
    expect(header.jwk.kty).toBe('OKP')
    expect(header.jwk.crv).toBe('Ed25519')
    expect(payload).toEqual({ htu: 'https://alice/ws', htm: 'GET', jti: 'fixed-jti', iat: 1234 })
  })

  it('the DPoP signature verifies under the embedded public JWK (self-consistent proof)', async () => {
    const key = await createDpopKey({ subtle })
    const proof = await key.proof('https://alice/ws', 'GET', 2000)
    const [h, p, s] = proof.split('.') as [string, string, string]
    const header = decodeJson(h) as { jwk: { x: string } }
    const pub = await subtle.importKey('raw', bytes(header.jwk.x), { name: 'Ed25519' }, true, ['verify'])
    const ok = await subtle.verify({ name: 'Ed25519' }, pub, bytes(s), utf8(`${h}.${p}`))
    expect(ok).toBe(true)
  })

  it('mints a FRESH jti per proof (no DPoP replay across upgrades)', async () => {
    const key = await createDpopKey({ subtle })
    const a = await key.proof('https://alice/ws', 'GET', 3000)
    const b = await key.proof('https://alice/ws', 'GET', 3000)
    const jtiOf = (proof: string): unknown =>
      (decodeJson(proof.split('.')[1] as string) as { jti: unknown }).jti
    expect(jtiOf(a)).not.toEqual(jtiOf(b))
  })

  it('fails fast (typed RelayWebError, no key material leaked) when Ed25519 keygen is unsupported', async () => {
    const brokenSubtle = {
      generateKey: async () => {
        throw new Error('Ed25519 not supported')
      },
    } as unknown as SubtleCrypto
    await expect(createDpopKey({ subtle: brokenSubtle })).rejects.toThrow(
      /failed to generate an Ed25519 DPoP key/,
    )
  })

  it('htuFor mirrors relay-run: https://<aud>/ws', () => {
    expect(htuFor('alice')).toBe('https://alice/ws')
  })
})

describe('DPoP subprotocol carrier (encode/extract round-trip)', () => {
  it('encodes term.dpop.<base64url(proofJws)> and extracts it back verbatim', () => {
    const proof = 'aGVhZGVy.cGF5bG9hZA.c2ln'
    const entry = encodeDpopSubprotocol(proof)
    expect(entry.startsWith(DPOP_SUBPROTOCOL_PREFIX)).toBe(true)
    expect(decodeBase64UrlString(entry.slice(DPOP_SUBPROTOCOL_PREFIX.length))).toBe(proof)
    expect(extractDpopFromSubprotocols(['term.relay.v1', entry])).toBe(proof)
  })

  it('extract returns null when no DPoP entry is present', () => {
    expect(extractDpopFromSubprotocols(['term.relay.v1', 'term.token.QUJD'])).toBeNull()
  })
})
