import { describe, test, expect } from 'vitest'
import {
  MUX_HEADER_BYTES,
  encodeMuxFrame,
  decodeHeader,
  decodeMuxFrame,
  encodeOpen,
  decodeOpen,
  encodeWindowUpdate,
  decodeWindowUpdate,
  encodeGoaway,
  decodeGoaway,
  TYPE_TO_BYTE,
  BYTE_TO_TYPE,
  type MuxFrameHeader,
  type MuxFrameType,
  type MuxOpen,
} from '../mux/frame-codec.js'

const ALL_TYPES: readonly MuxFrameType[] = [
  'open',
  'data',
  'close',
  'ping',
  'pong',
  'windowUpdate',
  'goaway',
]

function header(over: Partial<MuxFrameHeader>): MuxFrameHeader {
  return { version: 1, type: 'data', fin: false, rst: false, streamId: 7, payloadLen: 0, ...over }
}

describe('frame codec (T2, §4.1) — re-exported from the frozen relay-contracts codec', () => {
  test('round-trips every frame type incl. fin/rst bit packing', () => {
    for (const type of ALL_TYPES) {
      const payload = new Uint8Array([1, 2, 3])
      const h = header({ type, fin: true, rst: false, streamId: 42, payloadLen: payload.length })
      const decoded = decodeMuxFrame(encodeMuxFrame(h, payload))
      expect(decoded.header.type).toBe(type)
      expect(decoded.header.fin).toBe(true)
      expect(decoded.header.rst).toBe(false)
      expect(decoded.header.streamId).toBe(42)
      expect([...decoded.payload]).toEqual([1, 2, 3])
    }
  })

  test('streamId=0 link-level frames decode with streamId===0', () => {
    const h = header({ type: 'ping', streamId: 0, payloadLen: 0 })
    expect(decodeHeader(encodeMuxFrame(h, new Uint8Array(0))).streamId).toBe(0)
  })

  test('rst bit round-trips independently of fin', () => {
    const h = header({ type: 'close', fin: false, rst: true, payloadLen: 0 })
    const d = decodeHeader(encodeMuxFrame(h, new Uint8Array(0)))
    expect(d.rst).toBe(true)
    expect(d.fin).toBe(false)
  })

  test('type ⇄ byte maps agree with §4.1 (0x01 OPEN … 0x07 GOAWAY)', () => {
    expect(TYPE_TO_BYTE.open).toBe(0x01)
    expect(TYPE_TO_BYTE.goaway).toBe(0x07)
    expect(BYTE_TO_TYPE[0x02]).toBe('data')
  })

  test('negative: truncated buffer (< 15B) throws', () => {
    expect(() => decodeHeader(new Uint8Array(MUX_HEADER_BYTES - 1))).toThrow()
  })

  test('negative: payloadLen larger than actual payload throws', () => {
    const buf = encodeMuxFrame(header({ payloadLen: 2 }), new Uint8Array([9, 9]))
    // Claim a longer payload than present.
    buf[14] = 5
    expect(() => decodeMuxFrame(buf)).toThrow()
  })

  test('negative: unknown type byte throws', () => {
    const buf = encodeMuxFrame(header({ type: 'data', payloadLen: 0 }), new Uint8Array(0))
    buf[1] = 0x08 // unknown type code
    expect(() => decodeHeader(buf)).toThrow()
    buf[1] = 0x00
    expect(() => decodeHeader(buf)).toThrow()
  })

  test('negative: version ≠ 0x01 throws', () => {
    const buf = encodeMuxFrame(header({ payloadLen: 0 }), new Uint8Array(0))
    buf[0] = 0x02
    expect(() => decodeHeader(buf)).toThrow()
  })

  test('OPEN payload round-trips via CBOR + Zod guard', () => {
    const open: MuxOpen = {
      streamId: 3,
      subdomain: 'alice',
      requestPath: '/term?join=x',
      originHeader: 'https://alice.term.example.com',
      remoteAddrHash: 'deadbeef',
      capabilityTokenRef: 'jti-1',
    }
    expect(decodeOpen(encodeOpen(open))).toEqual(open)
  })

  test('decodeOpen rejects a malformed/partial CBOR payload (never returns partial)', () => {
    expect(() => decodeOpen(new Uint8Array([0x00, 0x01, 0x02]))).toThrow()
  })

  test('WINDOW_UPDATE + GOAWAY int codecs round-trip', () => {
    expect(decodeWindowUpdate(encodeWindowUpdate(65_536))).toBe(65_536)
    const g = decodeGoaway(encodeGoaway(9, 'revoked'))
    expect(g).toEqual({ lastStreamId: 9, reason: 'revoked' })
  })
})
