/**
 * Transport (P4) attack matrix, run from the untrusted-relay / attacker vantage (the RelaySpy).
 * Each case ASSERTS the defense holds. Wires the REAL relay-e2e handshake/session + agent replay
 * sealer through the harness. Pass an explicit `now` everywhere.
 */
import { describe, it, expect, beforeEach } from 'vitest'
import { FingerprintMismatchError } from 'relay-e2e'
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

function flipByte(b: Uint8Array, i = 0): Uint8Array {
  const c = b.slice()
  c[i] = (c[i]! ^ 0xff) & 0xff
  return c
}

describe('adversarial — transport (P4)', () => {
  let world: RelayWorld
  beforeEach(async () => {
    world = await buildRelayWorld(NOW)
  })

  describe('MITM: host_hello must verify against the registry key', () => {
    it('rejects when the client verifies host_hello against a WRONG agentPubkey (no keys derived)', async () => {
      // The malicious relay swaps in a key it controls; the client checks against hostB's real key.
      await expect(
        world.establishSession({ verifyAgainstPubkey: world.hostB.agentPubkey }),
      ).rejects.toBeInstanceOf(FingerprintMismatchError)
    })

    it('rejects a tampered host_hello (flipped hostEphPub breaks the transcript signature)', async () => {
      await expect(
        world.establishSession({
          tamperHostHello: (h) => ({ ...h, hostEphPub: flipByte(h.hostEphPub) }),
        }),
      ).rejects.toBeInstanceOf(FingerprintMismatchError)
    })
  })

  it('reflection: a c2h frame fed back into the client’s own open is rejected (direction split)', async () => {
    const { client } = await world.establishSession()
    const c2h = client.seal(utf8('sudo rm -rf /'))
    // Reflecting the client's own ciphertext back to it must fail: the client opens with the h2c
    // read key + h2c aad label, so the tag over the c2h direction never verifies.
    expect(() => client.open(c2h)).toThrow()
  })

  it('replay: delivering the same valid frame twice → the second open throws (SequenceGuard)', async () => {
    const { client, host, spy } = await world.establishSession()
    const f0 = spy.forward(client.seal(utf8('whoami')))
    expect(fromUtf8(host.open(f0))).toBe('whoami')
    expect(() => host.open(f0)).toThrow() // seq 0 already consumed → strict-successor guard
  })

  it('reorder: delivering seq 1 before seq 0 → open throws (strict successor)', async () => {
    const { client, host } = await world.establishSession()
    const f0 = client.seal(utf8('cmd-0'))
    const f1 = client.seal(utf8('cmd-1'))
    expect(() => host.open(f1)).toThrow() // expected seq 0, got 1
    // and the in-order pair still works on a fresh pair (sanity)
    expect(fromUtf8(host.open(f0))).toBe('cmd-0')
    expect(fromUtf8(host.open(f1))).toBe('cmd-1')
  })

  it('INV2: the RelaySpy ciphertext never contains the plaintext marker', async () => {
    const { client, host, spy } = await world.establishSession()
    const marker = `TOP_SECRET_${crypto.randomUUID()}`
    for (let i = 0; i < 3; i++) {
      const wire = spy.forward(client.seal(utf8(`${marker}#${i}`)))
      expect(fromUtf8(host.open(wire))).toBe(`${marker}#${i}`)
    }
    expect(spy.contains(marker)).toBe(false)
    expect(spy.captured.length).toBe(3)
  })

  describe('F6 (dynamic): recoverable K_content must not reuse (key, nonce) across sealer generations', () => {
    it('two generations for the same (secret, sessionId) get different epochs → different keys at seq 0', () => {
      const sessionId = 'sess-restart-1'
      const gen1 = world.newReplaySealer(sessionId)
      const gen2 = world.newReplaySealer(sessionId) // simulates an agent restart / re-attach

      expect(gen2.epoch).not.toBe(gen1.epoch)

      const pt = utf8('IDENTICAL PLAINTEXT AT SEQ 0')
      const e1 = gen1.seal(pt)
      const e2 = gen2.seal(pt)

      // Same deterministic nonce (seq 0) …
      expect(e1.seq).toBe(0n)
      expect(e2.seq).toBe(0n)
      expect(Buffer.from(e1.nonce).equals(Buffer.from(e2.nonce))).toBe(true)
      // … yet a FRESH key per generation ⇒ ciphertext + tag differ (no (key, nonce) reuse).
      expect(Buffer.from(e1.ciphertext).equals(Buffer.from(e2.ciphertext))).toBe(false)
      expect(Buffer.from(e1.tag).equals(Buffer.from(e2.tag))).toBe(false)

      // Cross-generation open fails: gen1's epoch-derived key cannot open gen2's frame.
      expect(() => world.openReplay(sessionId, gen1.epoch, e2)).toThrow()

      // Recoverability WITHIN a generation is preserved (same epoch ⇒ same key).
      expect(fromUtf8(world.openReplay(sessionId, gen1.epoch, e1))).toBe('IDENTICAL PLAINTEXT AT SEQ 0')
      expect(fromUtf8(world.openReplay(sessionId, gen2.epoch, e2))).toBe('IDENTICAL PLAINTEXT AT SEQ 0')
    })

    it('the replay ciphertext itself never contains the plaintext marker (INV2 on the replay path)', () => {
      const sealer = world.newReplaySealer('sess-inv2')
      const marker = 'REPLAY_MARKER_ABC'
      const env = sealer.seal(utf8(marker))
      expect(Buffer.from(env.ciphertext).toString('latin1')).not.toContain(marker)
      expect(Buffer.from(env.ciphertext).toString('utf8')).not.toContain(marker)
    })
  })
})
