/**
 * Happy-path full flow, end-to-end across P5 (auth) + P4 (E2E crypto) + P2 (replay), asserting each
 * stage. Everything runs through the REAL package exports; only registries/buckets/sockets are faked.
 */
import { describe, it, expect, beforeEach } from 'vitest'
import { buildRelayWorld, DEFAULT_NOW, type RelayWorld } from '../harness/world.js'

const utf8 = (s: string): Uint8Array => new TextEncoder().encode(s)
const fromUtf8 = (b: Uint8Array): string => new TextDecoder().decode(b)
const NOW = DEFAULT_NOW

describe('relay flow (happy path)', () => {
  let world: RelayWorld
  beforeEach(async () => {
    world = await buildRelayWorld(NOW)
  })

  it('issueCap → upgrade allows (origin ok, DPoP bound, deny-by-default satisfied)', async () => {
    const { hostA } = world
    const cap = await world.issueCap({ accountId: hostA.accountId, host: hostA.hostId, aud: hostA.aud })
    const out = await world.upgrade({
      raw: cap.raw,
      dpop: cap.dpop,
      host: hostA.hostId,
      aud: hostA.aud,
      origin: hostA.origin,
      now: NOW,
    })
    expect(out.ok).toBe(true)
    expect(world.audit.events.at(-1)).toMatchObject({ outcome: 'allow', action: 'attach' })
  })

  it('handshake establishes MATCHING session keys on both sides (client↔host round-trip)', async () => {
    const { client, host } = await world.establishSession()
    // Matching keys ⇒ bidirectional plaintext round-trips through the direction split.
    const c2h = host.open(client.seal(utf8('ls -la')))
    expect(fromUtf8(c2h)).toBe('ls -la')
    const h2c = client.open(host.seal(utf8('total 0')))
    expect(fromUtf8(h2c)).toBe('total 0')
  })

  it('seals client→host and host→client through the RelaySpy; the RelaySpy never sees plaintext (INV2)', async () => {
    const { client, host, spy } = await world.establishSession()
    const marker = `PLAINTEXT_${crypto.randomUUID()}`

    const up = spy.forward(client.seal(utf8(marker)))
    expect(fromUtf8(host.open(up))).toBe(marker)
    const down = spy.forward(host.seal(utf8(`reply_${marker}`)))
    expect(fromUtf8(client.open(down))).toBe(`reply_${marker}`)

    expect(spy.contains(marker)).toBe(false)
    expect(spy.captured.length).toBe(2)
  })

  it('reattach to an own-account session is allowed', async () => {
    const { hostA } = world
    const sessionId = crypto.randomUUID()
    world.addSession({ sessionId, hostId: hostA.hostId, accountId: hostA.accountId })
    const cap = await world.issueCap({ accountId: hostA.accountId, host: hostA.hostId, aud: hostA.aud })
    const out = await world.reattach({
      raw: cap.raw,
      dpop: cap.dpop,
      host: hostA.hostId,
      aud: hostA.aud,
      origin: hostA.origin,
      sessionId,
      now: NOW,
    })
    expect(out.ok).toBe(true)
    expect(world.audit.events.at(-1)).toMatchObject({ outcome: 'allow', action: 'reattach' })
  })

  it('revokeToken then re-presenting the same jti (fresh DPoP) is denied', async () => {
    const { hostA } = world
    const cap = await world.issueCap({ accountId: hostA.accountId, host: hostA.hostId, aud: hostA.aud })
    const first = await world.upgrade({
      raw: cap.raw,
      dpop: cap.dpop,
      host: hostA.hostId,
      aud: hostA.aud,
      origin: hostA.origin,
      now: NOW,
    })
    expect(first.ok).toBe(true)
    if (!first.ok) return

    await world.revokeToken(first.jti)

    const second = await world.upgrade({
      raw: cap.raw,
      dpop: await cap.newDpop(NOW), // fresh DPoP jti so we reach the revocation gate, not the DPoP cache
      host: hostA.hostId,
      aud: hostA.aud,
      origin: hostA.origin,
      now: NOW,
    })
    expect(second).toMatchObject({ ok: false, status: 403, reason: 'token_revoked' })
  })
})
