// Per-turn latency/throughput fold and the footer figure formatters.

import { describe, expect, it } from 'vitest'
import type { AssistantMessageNode, ConversationNode, UserMessageNode } from '@deepseek-ai/dsh-client-runtime/client'
import { assistantStepReading, deriveTurnMetrics } from '../src/client/chat/turn-metrics.ts'
import { formatLatencySeconds, formatTokensPerSecond } from '../src/client/chat/message-chrome.ts'

interface StepSpec {
  seq: number
  turn: number
  step: number
  timing?: AssistantMessageNode['timing']
  usage?: unknown
}

const assistant = ({ seq, turn, step, timing, usage }: StepSpec): AssistantMessageNode => ({
  kind: 'assistant', seq, time: seq * 1_000, turn, step, blocks: [{ kind: 'text', text: `t${seq}` }],
  ...(timing === undefined ? {} : { timing }),
  ...(usage === undefined ? {} : { usage }),
})

const user = (seq: number): UserMessageNode => ({
  kind: 'user', seq, time: seq * 1_000, content: [{ type: 'text', text: 'hi' }] as never, source: null,
})

describe('assistantStepReading', () => {
  it('derives ttft, decode time, and output tokens from a fully recorded step', () => {
    const reading = assistantStepReading(assistant({
      seq: 2, turn: 1, step: 1,
      timing: { stepStartTime: 1_000, firstTokenTime: 1_800, completedTime: 6_800 },
      usage: { outputTokens: 200 },
    }))
    expect(reading).toEqual({ ttftMs: 800, decodeMs: 5_000, outputTokens: 200 })
  })

  it('returns nulls when timing is absent', () => {
    const reading = assistantStepReading(assistant({ seq: 2, turn: 1, step: 1, usage: { outputTokens: 5 } }))
    expect(reading).toEqual({ ttftMs: null, decodeMs: null, outputTokens: 5 })
  })

  it('needs both boundaries for ttft and clamps negative spans to zero', () => {
    expect(assistantStepReading(assistant({
      seq: 2, turn: 1, step: 1,
      timing: { stepStartTime: null, firstTokenTime: 1_800, completedTime: 6_800 },
    }))).toEqual({ ttftMs: null, decodeMs: 5_000, outputTokens: null })
    expect(assistantStepReading(assistant({
      seq: 2, turn: 1, step: 1,
      timing: { stepStartTime: 1_000, firstTokenTime: null, completedTime: 6_800 },
    }))).toEqual({ ttftMs: null, decodeMs: null, outputTokens: null })
    expect(assistantStepReading(assistant({
      seq: 2, turn: 1, step: 1,
      timing: { stepStartTime: 2_000, firstTokenTime: 1_500, completedTime: 1_200 },
    }))).toEqual({ ttftMs: 0, decodeMs: 0, outputTokens: null })
  })

  it('rejects non-object, missing, and non-finite usage token counts', () => {
    const timing = { stepStartTime: 1_000, firstTokenTime: 1_500, completedTime: 2_000 }
    expect(assistantStepReading(assistant({ seq: 2, turn: 1, step: 1, timing, usage: 'weird' })).outputTokens).toBeNull()
    expect(assistantStepReading(assistant({ seq: 2, turn: 1, step: 1, timing, usage: {} })).outputTokens).toBeNull()
    const nan = assistant({ seq: 2, turn: 1, step: 1, timing, usage: { outputTokens: Number.NaN } })
    expect(assistantStepReading(nan).outputTokens).toBeNull()
    expect(assistantStepReading(assistant({ seq: 2, turn: 1, step: 1, timing, usage: { outputTokens: -3 } })).outputTokens).toBeNull()
  })
})

describe('deriveTurnMetrics', () => {
  it('takes ttft from the lowest step and throughput over all sampled steps', () => {
    const nodes: ConversationNode[] = [
      user(1),
      // Out of step order on purpose: the lowest step owns the ttft slot.
      assistant({
        seq: 4, turn: 1, step: 2,
        timing: { stepStartTime: 10_000, firstTokenTime: 10_200, completedTime: 12_200 },
        usage: { outputTokens: 60 },
      }),
      assistant({
        seq: 2, turn: 1, step: 1,
        timing: { stepStartTime: 1_000, firstTokenTime: 2_200, completedTime: 5_200 },
        usage: { outputTokens: 40 },
      }),
    ]
    // 100 tokens over 5s of decode.
    expect(deriveTurnMetrics(nodes).get(1)).toEqual({ ttftMs: 1_200, tokensPerSecond: 20 })
  })

  it('emits ttft without throughput when no step carries usage', () => {
    const nodes = [assistant({
      seq: 2, turn: 1, step: 1,
      timing: { stepStartTime: 1_000, firstTokenTime: 1_900, completedTime: 3_000 },
    })]
    expect(deriveTurnMetrics(nodes).get(1)).toEqual({ ttftMs: 900 })
  })

  it('emits throughput without ttft when only a later step is recorded', () => {
    const nodes = [
      assistant({ seq: 2, turn: 1, step: 1 }),
      assistant({
        seq: 4, turn: 1, step: 2,
        timing: { stepStartTime: 10_000, firstTokenTime: 10_500, completedTime: 12_500 },
        usage: { outputTokens: 30 },
      }),
    ]
    expect(deriveTurnMetrics(nodes).get(1)).toEqual({ tokensPerSecond: 15 })
  })

  it('omits turns with no readings and zero-decode throughput', () => {
    const nodes = [
      assistant({ seq: 2, turn: 1, step: 1 }),
      assistant({
        seq: 4, turn: 2, step: 1,
        timing: { stepStartTime: null, firstTokenTime: 5_000, completedTime: 5_000 },
        usage: { outputTokens: 10 },
      }),
    ]
    expect(deriveTurnMetrics(nodes).size).toBe(0)
  })

  it('keeps turns independent and ignores non-assistant nodes', () => {
    const nodes: ConversationNode[] = [
      user(1),
      assistant({
        seq: 2, turn: 1, step: 1,
        timing: { stepStartTime: 1_000, firstTokenTime: 1_400, completedTime: 2_400 },
        usage: { outputTokens: 10 },
      }),
      user(3),
      assistant({
        seq: 4, turn: 2, step: 1,
        timing: { stepStartTime: 4_000, firstTokenTime: 4_100, completedTime: 6_100 },
        usage: { outputTokens: 100 },
      }),
    ]
    const metrics = deriveTurnMetrics(nodes)
    expect(metrics.get(1)).toEqual({ ttftMs: 400, tokensPerSecond: 10 })
    expect(metrics.get(2)).toEqual({ ttftMs: 100, tokensPerSecond: 50 })
  })
})

describe('footer figure formatters', () => {
  it('formats latency with one decimal under ten seconds and whole seconds beyond', () => {
    expect(formatLatencySeconds(840)).toBe('0.8')
    expect(formatLatencySeconds(1_000)).toBe('1')
    expect(formatLatencySeconds(9_949)).toBe('9.9')
    expect(formatLatencySeconds(12_400)).toBe('12')
    expect(formatLatencySeconds(-5)).toBe('0')
  })

  it('formats throughput with whole tokens from ten up and one decimal below', () => {
    expect(formatTokensPerSecond(34.4)).toBe('34')
    expect(formatTokensPerSecond(9.96)).toBe('10')
    expect(formatTokensPerSecond(3.14)).toBe('3.1')
    expect(formatTokensPerSecond(-1)).toBe('0')
  })
})
