import { describe, expect, it } from 'vitest'
import {
  ContractDecodeError,
  decodeCborMap,
  decodeGoAwayReason,
  decodeGoaway,
  decodeOpen,
  decodeWindowUpdate,
  encodeCborMap,
  encodeGoaway,
  encodeOpen,
  encodeWindowUpdate,
  type MuxOpen,
} from '../src/index.js'

const sampleOpen = (): MuxOpen => ({
  streamId: 7,
  subdomain: 'alice',
  requestPath: '/term?join=abc',
  originHeader: 'https://alice.term.example.com',
  remoteAddrHash: 'deadbeef',
  capabilityTokenRef: 'jti-123',
})

describe('§4.1 CBOR primitive', () => {
  it('KAT: decodes a hand-written CBOR map {"a":"b"} from fixed bytes', () => {
    const bytes = Uint8Array.of(0xa1, 0x61, 0x61, 0x61, 0x62)
    expect(decodeCborMap(bytes)).toEqual({ a: 'b' })
  })

  it('KAT: encodes {a:1} to fixed bytes (uint value)', () => {
    const bytes = encodeCborMap({ a: 1 }, ['a'])
    expect([...bytes]).toEqual([0xa1, 0x61, 0x61, 0x01])
  })

  it('rejects trailing bytes after the map', () => {
    expect(() => decodeCborMap(Uint8Array.of(0xa1, 0x61, 0x61, 0x61, 0x62, 0x00))).toThrow(
      /trailing bytes/,
    )
  })

  it('rejects a duplicate key', () => {
    // map(2) with key "a" twice
    const bytes = Uint8Array.of(0xa2, 0x61, 0x61, 0x01, 0x61, 0x61, 0x02)
    expect(() => decodeCborMap(bytes)).toThrow(/duplicate/)
  })
})

describe('§4.1 OPEN payload (CBOR of MuxOpen)', () => {
  it('round-trips MuxOpen through encode/decode (identity)', () => {
    const open = sampleOpen()
    expect(decodeOpen(encodeOpen(open))).toEqual(open)
  })

  it('is deterministic — same input yields identical bytes', () => {
    expect([...encodeOpen(sampleOpen())]).toEqual([...encodeOpen(sampleOpen())])
  })

  it('rejects a MuxOpen with streamId 0 (reserved for connection-level control)', () => {
    expect(() => encodeOpen({ ...sampleOpen(), streamId: 0 })).toThrow()
  })

  it('rejects a decoded payload with a missing field', () => {
    const bad = encodeCborMap({ subdomain: 'x' }, ['subdomain'])
    expect(() => decodeOpen(bad)).toThrow(ContractDecodeError)
  })
})

describe('§4.1 WINDOW_UPDATE codec', () => {
  it('KAT: encodes credit=255 to 4 big-endian bytes', () => {
    expect([...encodeWindowUpdate(255)]).toEqual([0x00, 0x00, 0x00, 0xff])
  })

  it('round-trips a credit value', () => {
    expect(decodeWindowUpdate(encodeWindowUpdate(0x0abbccdd))).toBe(0x0abbccdd)
  })

  it('rejects a wrong-length payload', () => {
    expect(() => decodeWindowUpdate(Uint8Array.of(1, 2, 3))).toThrow(ContractDecodeError)
  })
})

describe('§4.1 GOAWAY codec', () => {
  it('KAT: encodes lastStreamId=10, reason=revoked to fixed bytes', () => {
    expect([...encodeGoaway(10, 'revoked')]).toEqual([
      0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x02,
    ])
  })

  it.each([
    ['operatorDrain', 1],
    ['revoked', 2],
    ['shutdown', 3],
  ] as const)('round-trips reason=%s (code %i)', (reason, code) => {
    const bytes = encodeGoaway(1, reason)
    expect([...bytes.subarray(4)]).toEqual([0x00, 0x00, 0x00, code])
    expect(decodeGoaway(bytes)).toEqual({ lastStreamId: 1, reason })
  })

  it('decodeGoAwayReason maps frozen wire codes', () => {
    expect(decodeGoAwayReason(1)).toBe('operatorDrain')
    expect(decodeGoAwayReason(2)).toBe('revoked')
    expect(decodeGoAwayReason(3)).toBe('shutdown')
  })

  it('decodeGoAwayReason throws on an unknown code', () => {
    expect(() => decodeGoAwayReason(99)).toThrow(ContractDecodeError)
  })

  it('rejects a wrong-length GOAWAY payload', () => {
    expect(() => decodeGoaway(Uint8Array.of(0, 0, 0, 1))).toThrow(ContractDecodeError)
  })
})
