import { describe, expect, it } from 'vitest'
import { encodeMessage, MessageDecoder } from '@deepseek-ai/dsh-lsp-stdio'

/** Frame a message the way a server would, for decoder round-trips. */
function frame(body: string): Buffer {
  return Buffer.concat([Buffer.from(`Content-Length: ${Buffer.byteLength(body)}\r\n\r\n`, 'ascii'), Buffer.from(body, 'utf8')])
}

describe('encodeMessage', () => {
  it('prefixes a Content-Length header with the utf-8 byte length', () => {
    const buffer = encodeMessage({ jsonrpc: '2.0', method: 'x', params: { s: 'é' } })
    const text = buffer.toString('utf8')
    const body = '{"jsonrpc":"2.0","method":"x","params":{"s":"é"}}'
    expect(text).toBe(`Content-Length: ${Buffer.byteLength(body)}\r\n\r\n${body}`)
  })
})

describe('MessageDecoder', () => {
  it('decodes a single framed message', () => {
    const decoder = new MessageDecoder(1_000)
    expect(decoder.push(frame('{"id":1,"result":42}'))).toEqual([{ id: 1, result: 42 }])
  })

  it('decodes multiple messages arriving in one chunk', () => {
    const decoder = new MessageDecoder(1_000)
    const chunk = Buffer.concat([frame('{"a":1}'), frame('{"b":2}')])
    expect(decoder.push(chunk)).toEqual([{ a: 1 }, { b: 2 }])
  })

  it('reassembles a message split across chunks', () => {
    const decoder = new MessageDecoder(1_000)
    const full = frame('{"hello":"world"}')
    expect(decoder.push(full.subarray(0, 10))).toEqual([])
    expect(decoder.push(full.subarray(10))).toEqual([{ hello: 'world' }])
  })

  it('handles a header split from its body', () => {
    const decoder = new MessageDecoder(1_000)
    const body = '{"x":1}'
    expect(decoder.push(Buffer.from(`Content-Length: ${body.length}\r\n\r\n`, 'ascii'))).toEqual([])
    expect(decoder.push(Buffer.from(body, 'utf8'))).toEqual([{ x: 1 }])
  })

  it('reads a case-insensitive header and ignores other headers', () => {
    const decoder = new MessageDecoder(1_000)
    const body = '{"ok":true}'
    const chunk = Buffer.from(`content-length: ${body.length}\r\nContent-Type: x\r\n\r\n${body}`, 'utf8')
    expect(decoder.push(chunk)).toEqual([{ ok: true }])
  })

  it('rejects a body over the size limit', () => {
    const decoder = new MessageDecoder(4)
    expect(() => decoder.push(frame('{"big":true}'))).toThrow(/exceeds the 4-byte limit/)
  })

  it('rejects a missing Content-Length header', () => {
    const decoder = new MessageDecoder(1_000)
    expect(() => decoder.push(Buffer.from('X: 1\r\n\r\n{}', 'utf8'))).toThrow(/missing Content-Length/)
  })

  it('rejects a non-numeric Content-Length', () => {
    const decoder = new MessageDecoder(1_000)
    expect(() => decoder.push(Buffer.from('Content-Length: abc\r\n\r\n{}', 'utf8'))).toThrow(/invalid Content-Length/)
  })

  it('rejects a header block that never terminates', () => {
    const decoder = new MessageDecoder(1_000)
    const huge = Buffer.alloc((1 << 16) + 1, 0x41)
    expect(() => decoder.push(huge)).toThrow(/exceeded .* bytes without a terminator/)
  })

  it('rejects an oversized header block that includes its terminator', () => {
    const decoder = new MessageDecoder(1_000)
    const huge = Buffer.from(`Content-Length: 2\r\nX-Fill: ${'a'.repeat(70_000)}\r\n\r\n{}`, 'ascii')
    expect(() => decoder.push(huge)).toThrow(/header exceeded .* bytes/)
  })

  it('rejects a non-JSON body', () => {
    const decoder = new MessageDecoder(1_000)
    expect(() => decoder.push(frame('not json'))).toThrow(/not valid JSON/)
  })
})
