/**
 * PartialAccumulator: six-variant chunk folding, sparse-index compaction, and
 * the block/snapshot reference discipline (a delta swaps only that block).
 */

import { describe, expect, it } from 'vitest'
import type { StreamChunk } from '@deepseek-ai/dsh-client-connection/client'
import { PartialAccumulator } from '../src/client/sessions/partial.ts'

const chunk = (c: Record<string, unknown>): StreamChunk => c as unknown as StreamChunk

describe('PartialAccumulator', () => {
  it('builds empty blocks per block-start type, unknown type falls to other', () => {
    const acc = new PartialAccumulator(1, 0)
    acc.push(chunk({ type: 'block-start', index: 0, blockType: 'text' }))
    acc.push(chunk({ type: 'block-start', index: 1, blockType: 'reasoning' }))
    acc.push(chunk({ type: 'block-start', index: 2, blockType: 'tool-call' }))
    acc.push(chunk({ type: 'block-start', index: 3, blockType: 'no-such' }))
    expect(acc.toPartial().blocks).toEqual([
      { kind: 'text', text: '' },
      { kind: 'reasoning', text: '' },
      { kind: 'tool-call', callId: '', name: '', argsRaw: '' },
      { kind: 'other', block: null },
    ])
  })

  it('accumulates text deltas, starting from empty when prev is missing or another kind', () => {
    const acc = new PartialAccumulator(1, 0)
    acc.push(chunk({ type: 'text-delta', index: 0, text: '无 start ' })) // prev missing
    acc.push(chunk({ type: 'text-delta', index: 0, text: '也累积' }))
    expect(acc.toPartial().blocks).toEqual([{ kind: 'text', text: '无 start 也累积' }])
    acc.push(chunk({ type: 'reasoning-delta', index: 0, text: '换型重起' })) // prev is text → restart
    expect(acc.toPartial().blocks).toEqual([{ kind: 'reasoning', text: '换型重起' }])
  })

  it('accumulates reasoning deltas on the reasoning lane', () => {
    const acc = new PartialAccumulator(1, 0)
    acc.push(chunk({ type: 'block-start', index: 0, blockType: 'reasoning' }))
    acc.push(chunk({ type: 'reasoning-delta', index: 0, text: '思' }))
    acc.push(chunk({ type: 'reasoning-delta', index: 0, text: '考' }))
    expect(acc.toPartial().blocks).toEqual([{ kind: 'reasoning', text: '思考' }])
  })

  it('folds tool-call deltas: first id pins callId, late name overrides, argsRaw concatenates', () => {
    const acc = new PartialAccumulator(1, 0)
    acc.push(chunk({ type: 'tool-call-delta', index: 0, id: 'c1', argumentsDelta: '{"a"' }))
    acc.push(chunk({ type: 'tool-call-delta', index: 0, id: 'c2-late', name: 'echo', argumentsDelta: ':1}' }))
    expect(acc.toPartial().blocks).toEqual([
      { kind: 'tool-call', callId: 'c1', name: 'echo', argsRaw: '{"a":1}' },
    ])
  })

  it('replaces the accumulated block wholesale on block-end', () => {
    const acc = new PartialAccumulator(1, 0)
    acc.push(chunk({ type: 'text-delta', index: 0, text: '中间态' }))
    acc.push(chunk({ type: 'block-end', index: 0, block: { type: 'text', text: '定稿全文' } }))
    expect(acc.toPartial().blocks).toEqual([{ kind: 'text', text: '定稿全文' }])
  })

  it('returns false (no notification) for usage/finish/unknown variants and keeps blocks', () => {
    const acc = new PartialAccumulator(1, 0)
    acc.push(chunk({ type: 'text-delta', index: 0, text: 'x' }))
    const before = acc.toPartial()
    expect(acc.push(chunk({ type: 'usage', usage: {} }))).toBe(false)
    expect(acc.push(chunk({ type: 'finish', reason: 'stop' }))).toBe(false)
    expect(acc.push(chunk({ type: 'future-variant' }))).toBe(false)
    expect(acc.toPartial()).toBe(before) // unchanged: same snapshot reference
  })

  it('compacts sparse indexes into a dense render-order array', () => {
    const acc = new PartialAccumulator(1, 0)
    acc.push(chunk({ type: 'block-start', index: 2, blockType: 'text' }))
    acc.push(chunk({ type: 'text-delta', index: 2, text: '先到的高位' }))
    acc.push(chunk({ type: 'block-start', index: 0, blockType: 'reasoning' }))
    const { blocks } = acc.toPartial()
    expect(blocks).toHaveLength(2) // no undefined holes
    expect(blocks[0]).toEqual({ kind: 'reasoning', text: '' })
    expect(blocks[1]).toEqual({ kind: 'text', text: '先到的高位' })
  })

  it('keeps the snapshot reference stable without changes and swaps it once per mutation', () => {
    const acc = new PartialAccumulator(3, 1)
    const first = acc.toPartial()
    expect(first).toMatchObject({ turn: 3, step: 1, blocks: [] })
    expect(acc.toPartial()).toBe(first)
    acc.push(chunk({ type: 'text-delta', index: 0, text: 'a' }))
    const second = acc.toPartial()
    expect(second).not.toBe(first)
    expect(acc.toPartial()).toBe(second)
  })
})
