import { mkdir, mkdtemp, readdir, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { afterAll, describe, expect, it, vi } from 'vitest'
import type { Context } from 'cordis'
import { agentEvents } from '@deepseek-ai/dsh-agent'
import { createUserMessage, CallId, type ContentBlock , createMessage, createToolResultMessage } from '@deepseek-ai/dsh-llm'
import type {} from '@deepseek-ai/dsh-llm-retry'
import { SessionId, type JsonValue, type Session } from '@deepseek-ai/dsh-session'
import SessionReferenceService from '@deepseek-ai/dsh-session-reference'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { type ToolDefinition, type ToolResultView } from '@deepseek-ai/dsh-tools'
import * as ToolCordis from '@deepseek-ai/dsh-tool-cordis'
import * as ToolWorkflow from '@deepseek-ai/dsh-tool-workflow'
import {
  appendAssistant,
  appendUser,
  createTuiTestHarness,
  disposeTuiTestHarness,
  type TuiHarness,
  type TuiHarnessOptions,
} from './harness.ts'
import { HeadlessTerminal, type TerminalSnapshotOptions } from './headless-terminal.ts'
import { TestSessionQueryService } from './session-query.ts'

const SNAPSHOTS_DIR = join(dirname(fileURLToPath(import.meta.url)), 'snapshots')
const REFRESHING = process.env.DSH_SNAPSHOT === 'refresh'

const CHECKPOINTS = [
  'conversation-streaming',
  'shell-prompt-multiline',
  'step-timing-completed',
  'retry-scheduled',
  'retry-recovered',
  'retry-cancelled',
  'retry-exhausted',
  'banner-gradient',
  'file-autocomplete',
  'session-title-autocomplete',
  'code-mode-pending',
  'dynamic-workflow-pending',
  'cordis-tools-pending',
  'advanced-cards-collapsed',
  'advanced-cards-expanded',
  'untrusted-controls',
  'question-dialog',
  'question-dialog-single-option',
  'question-dialog-validation',
  'surface-before-compaction',
  'surface-after-compaction-narrow',
  'surface-after-compaction-wide',
  'model-selector',
  'model-switching',
  'errors-and-help',
  'disposed-terminal',
  'resume-sessions',
  'status-diagnostics',
  'status-diagnostics-narrow',
  'todo-plan-cleared',
] as const

// Real-loop scenarios own their assertions in separate snapshot suites but
// share this directory, whose inventory remains exact.
const STANDALONE_CHECKPOINTS = ['session-reference'] as const

type Checkpoint = typeof CHECKPOINTS[number]
type SnapshotHarness = TuiHarness<HeadlessTerminal, (code: number) => void>

const observedCheckpoints = new Set<Checkpoint>()

async function checkpoint(
  name: Checkpoint,
  terminal: HeadlessTerminal,
  options: TerminalSnapshotOptions = {},
  bannerGradient = false,
): Promise<void> {
  observedCheckpoints.add(name)
  const violations = terminal.themeViolations()
  if (bannerGradient) {
    // The banner paints its product name in the DeepSeek brand gradient with
    // 24-bit foreground codes: the sole sanctioned truecolor. Require it to be
    // present and to never leak a background or extended-palette color into the
    // otherwise theme-agnostic UI.
    expect(violations, `${name} must render the banner gradient`).not.toEqual([])
    expect(
      violations.every(entry => entry.endsWith('rgb-fg')),
      `${name} must confine truecolor to the banner foreground`,
    ).toBe(true)
  } else {
    expect(violations, `${name} must remain theme-agnostic`).toEqual([])
  }
  const snapshot = await terminal.snapshot(options)
  const path = join(SNAPSHOTS_DIR, `${name}.expected.txt`)
  if (REFRESHING) {
    await mkdir(SNAPSHOTS_DIR, { recursive: true })
    await writeFile(path, snapshot)
  }
  await expect(snapshot).toMatchFileSnapshot(path)
}

async function setupSnapshot(
  options: TuiHarnessOptions = {},
  size: { columns?: number; rows?: number } = {},
): Promise<SnapshotHarness> {
  const terminal = new HeadlessTerminal(size.columns ?? 96, size.rows ?? 36)
  const before = terminal.frames
  const result = await createTuiTestHarness(terminal, () => {}, {
    ...options,
    cwd: options.cwd === undefined ? '/workspace/project' : options.cwd,
    config: Object.assign({
      welcome: 'Snapshot agent ready.',
      theme: { color: true },
      title: 'DSH snapshot',
    }, options.config),
  })
  await terminal.waitForFrame(before)
  return result
}

async function renderAfter(harness: SnapshotHarness, action: () => void): Promise<void> {
  const before = harness.terminal.frames
  action()
  await harness.terminal.waitForFrame(before)
}

async function disposeSnapshot(harness: SnapshotHarness): Promise<void> {
  await disposeTuiTestHarness(harness)
  await harness.terminal.dispose()
}

async function configureAdvancedTools(ctx: Context): Promise<void> {
  await ctx.plugin(SystemPrompt)
  await ctx.plugin(ToolRegistry, { mode: 'code' })
  ctx.provide('workflows', { start() {} } as never)
  await ctx.plugin(ToolWorkflow, { toolName: 'workflow', maxResultChars: 50_000 })
  await ctx.plugin(ToolCordis, { vmTimeoutMs: 5_000 })
}

interface ToolCallFixture {
  id: string
  name: string
  arguments: unknown
}

function appendToolCalls(session: Session, calls: readonly ToolCallFixture[]): void {
  appendAssistant(session, calls.map(call => ({
    type: 'tool-call',
    id: CallId(call.id),
    name: call.name,
    arguments: JSON.stringify(call.arguments),
  })))
  for (const call of calls) {
    session.append('tool/call', {
      turn: 1,
      step: 1,
      callId: CallId(call.id),
      name: call.name,
      arguments: JSON.stringify(call.arguments),
    })
  }
}

function appendToolResult(
  session: Session,
  id: string,
  content: ContentBlock[],
  options: { isError?: boolean; meta?: JsonValue } = {},
): void {
  session.append('tool/result', {
    turn: 1,
    step: 1,
    message: createToolResultMessage({
      callId: CallId(id),
      content,
      isError: options.isError ?? false,
    }),
    ...options.meta === undefined ? {} : { meta: options.meta },
  }, { surfaceOp: 'append' })
}

function visualTool(
  name: string,
  call: NonNullable<ToolDefinition['presentCall']>,
  result?: NonNullable<ToolDefinition['presentResult']>,
): ToolDefinition {
  return {
    name,
    description: `${name} snapshot fixture`,
    parameters: {},
    output: { schema: { type: 'null' }, render: () => [] },
    execute: () => Promise.resolve([]),
    presentCall: call,
    ...result === undefined ? {} : { presentResult: result },
  }
}

const ADVANCED_CARD_TOOLS: Record<string, ToolDefinition> = {
  bash: visualTool(
    'bash',
    () => ({ card: 'terminal', title: 'pnpm run test:coverage', description: 'Run the coverage gate', cwd: '/workspace/project' }),
    () => ({ card: 'terminal', output: 'packages/ui/tui 100%\n4016 tests passed\n1 test skipped\ncoverage complete', exitCode: 0 }),
  ),
  edit: visualTool(
    'edit',
    () => ({ card: 'diff', title: 'Edit src/view.ts', diffs: [{ path: 'src/view.ts', oldText: 'old line', newText: 'new line' }] }),
    // The real edit/write tools produce exactly one diff whose path the title
    // already names, so the card omits the redundant per-file header.
    (): ToolResultView => ({
      card: 'diff',
      diffs: [{ path: 'src/view.ts', oldText: 'old line\nkeep', newText: 'new line\nkeep' }],
    }),
  ),
  subagent: visualTool('subagent', args => ({
    card: 'generic',
    title: 'Delegate renderer audit',
    rawInput: (args as { prompt: string }).prompt,
  })),
  task_output: visualTool(
    'task_output',
    args => ({
      card: 'generic',
      kind: 'read',
      title: `Read output from background task ${(args as { task_id: string }).task_id}`,
      rawInput: (args as { task_id: string }).task_id,
    }),
    () => ({
      card: 'generic',
      content: [{ type: 'text', text: '```console\nstarted background task bash-5\n```' }],
    }),
  ),
  skill: visualTool('skill', args => ({
    card: 'generic',
    kind: 'read',
    title: `Load skill ${(args as { name: string }).name}`,
    rawInput: (args as { name: string }).name,
  })),
}

const CONTROL_PROBE = '\u001b]2;snapshot-controlled\u0007\t\u007f\u009b31m'
const DISPLAYED_CONTROL_PROBE = String.raw`\x1b]2;snapshot-controlled\x07\x09\x7f\x9b31m`

describe('TUI terminal-state snapshots', () => {
  it('pins an in-flight reasoning and Markdown stream', async () => {
    let clock = new Date(2026, 6, 21, 14, 30, 0).getTime()
    const nowSpy = vi.spyOn(Date, 'now').mockImplementation(() => clock)
    const harness = await setupSnapshot()
    await renderAfter(harness, () => {
      harness.agent.status = 'running'
      harness.ctx.emit('agent/status', harness.agent, 'running')
      appendUser(harness.session, 'Show the live update.')
      clock += 1_000
      harness.session.append('assistant/chunk', {
        turn: 1,
        step: 1,
        chunk: { type: 'block-start', index: 0, blockType: 'reasoning' },
      })
      harness.session.append('assistant/chunk', {
        turn: 1,
        step: 1,
        chunk: { type: 'reasoning-delta', index: 0, text: 'Inspecting width and styles.' },
      })
      clock += 2_000
      harness.session.append('assistant/chunk', {
        turn: 1,
        step: 1,
        chunk: { type: 'block-start', index: 1, blockType: 'text' },
      })
      harness.session.append('assistant/chunk', {
        turn: 1,
        step: 1,
        chunk: { type: 'text-delta', index: 1, text: 'Streaming **visible state**…\n\n```ts\nconst visible = true\n```' },
      })
    })
    await checkpoint('conversation-streaming', harness.terminal)
    await disposeSnapshot(harness)
    nowSpy.mockRestore()
  })

  it('pins a completed step timing summary', async () => {
    let clock = new Date(2026, 6, 21, 14, 32, 6).getTime()
    const nowSpy = vi.spyOn(Date, 'now').mockImplementation(() => clock)
    const harness = await setupSnapshot()
    await renderAfter(harness, () => {
      clock += 1_000
      harness.session.append('assistant/chunk', {
        turn: 1,
        step: 1,
        chunk: { type: 'reasoning-delta', index: 0, text: 'Checking the result.' },
      })
      clock += 2_000
      harness.session.append('assistant/chunk', {
        turn: 1,
        step: 1,
        chunk: { type: 'text-delta', index: 1, text: 'The result is ready.' },
      })
      clock += 3_000
      harness.session.append('step/end', { turn: 1, step: 1 })
    })
    await checkpoint('step-timing-completed', harness.terminal, { includeScrollback: true })
    nowSpy.mockRestore()
    await disposeSnapshot(harness)
  })

  it('clears the plan strip when the next turn starts', async () => {
    // Freeze Completed-at formatting: the first turn ends before the next starts,
    // so the assistant timing line still appears without a Plan strip below it.
    const nowSpy = vi.spyOn(Date, 'now').mockReturnValue(new Date(2026, 6, 21, 14, 45, 0).getTime())
    const harness = await setupSnapshot({
      beforeMount(session) {
        appendUser(session, 'Plan the work.')
        appendAssistant(session, [{ type: 'text', text: 'Tracking the steps.' }])
        session.append('todo/write', {
          todos: [
            { content: 'read code', status: 'completed' },
            { content: 'write tests', status: 'in_progress' },
          ],
        })
        session.append('step/end', { turn: 1, step: 1 })
        session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
        session.append('turn/start', {
          turn: 2,
          trigger: { kind: 'message', source: { kind: 'user' } },
        })
        appendUser(session, 'Next question.')
      },
    })
    await checkpoint('todo-plan-cleared', harness.terminal)
    nowSpy.mockRestore()
    await disposeSnapshot(harness)
  })

  it('pins failed-stream retraction, scheduled retry, and eventual success', async () => {
    const harness = await setupSnapshot()
    await renderAfter(harness, () => {
      appendUser(harness.session, 'Recover this request.')
      harness.session.append('assistant/chunk', {
        turn: 1,
        step: 1,
        chunk: { type: 'text-delta', index: 0, text: 'discarded partial output' },
      })
      harness.session.append('llm/retry', {
        turn: 1,
        step: 1,
        provider: 'mock',
        mode: 'normal',
        policyKey: '["normal",2,["RATE_LIMIT"],1,10000,0]',
        retry: 1,
        maxRetries: 2,
        delayMs: 500,
        failure: { message: 'provider rate limit', code: 'RATE_LIMIT', status: 429 },
      })
    })
    await checkpoint('retry-scheduled', harness.terminal, { includeScrollback: true })

    harness.session.append('assistant/message', {
      turn: 1,
      step: 2,
      message: createMessage({
        role: 'assistant',
        content: [{ type: 'text', text: 'Recovered on the next bounded attempt.' }],
        source: {
          kind: 'model',
          ...{ provider: 'mock', model: 'deepseek-v4-flash' },
        },
      }),
    }, { surfaceOp: 'append' })
    harness.session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
    await checkpoint('retry-recovered', harness.terminal, { includeScrollback: true })
    await disposeSnapshot(harness)
  })

  it('pins cancellation during a scheduled retry delay', async () => {
    const harness = await setupSnapshot()
    await renderAfter(harness, () => {
      appendUser(harness.session, 'Start then cancel.')
      harness.session.append('llm/retry', {
        turn: 1,
        step: 1,
        provider: 'mock',
        mode: 'always',
        policyKey: '["always",1,10000,0]',
        retry: 1,
        delayMs: 1_000,
        failure: { message: 'temporary transport failure', code: 'TRANSPORT' },
      })
      harness.session.append('turn/end', {
        turn: 1,
        reason: { kind: 'aborted' },
      })
    })
    await checkpoint('retry-cancelled', harness.terminal, { includeScrollback: true })
    await disposeSnapshot(harness)
  })

  it('pins terminal exhaustion after retracting a failed partial stream', async () => {
    const harness = await setupSnapshot()
    await renderAfter(harness, () => {
      appendUser(harness.session, 'Let the bounded policy exhaust.')
      harness.session.append('assistant/chunk', {
        turn: 1,
        step: 3,
        chunk: { type: 'text-delta', index: 0, text: 'discarded terminal partial output' },
      })
      harness.session.append('turn/end', {
        turn: 1,
        reason: {
          kind: 'error',
          step: 3,
          failure: { message: 'provider still unavailable', code: 'SERVER', status: 503 },
        },
      })
    })
    await checkpoint('retry-exhausted', harness.terminal, { includeScrollback: true })
    await disposeSnapshot(harness)
  })

  it('paints the startup banner product name in the DeepSeek brand gradient on truecolor terminals', async () => {
    const harness = await setupSnapshot({ config: { theme: { truecolor: true } } })
    await checkpoint('banner-gradient', harness.terminal, {}, true)
    await disposeSnapshot(harness)
  })

  it('pins fuzzy file candidates and the active path-only mention', async () => {
    const cwd = await mkdtemp(join(tmpdir(), 'dsh-tui-file-snapshot-'))
    await mkdir(join(cwd, 'src'), { recursive: true })
    await writeFile(join(cwd, 'src', 'terminal-special-case.ts'), 'export const marker = true\n')
    await writeFile(join(cwd, 'src', 'terminal-state.ts'), 'export const state = true\n')
    const harness = await setupSnapshot({ cwd, formatCwd: () => '/workspace/project' })
    try {
      harness.terminal.send('@tsc')
      await vi.waitFor(async () => {
        expect(await harness.terminal.snapshot()).toContain('File · terminal-special-case.t')
      })
      await checkpoint('file-autocomplete', harness.terminal)
    } finally {
      await disposeSnapshot(harness)
      await rm(cwd, { recursive: true, force: true })
    }
  })

  it('pins session autocomplete discovered through a log-backed title', async () => {
    const harness = await setupSnapshot({
      async configureContext(ctx) {
        ctx.provide('tools', { get: () => undefined } as never)
        await ctx.plugin(TestSessionQueryService)
        await ctx.plugin(SessionReferenceService)
        const source = ctx.sessions.create(SessionId('opaque-source-id'), {
          meta: { cwd: '/workspace/project', createdAt: 1 },
        })
        source.append('session/title', {
          title: 'Searchable design review',
          messageSeqs: [],
          source: { kind: 'fallback' },
        })
      },
    })
    harness.terminal.send('@design')
    await vi.waitFor(async () => {
      expect(await harness.terminal.snapshot()).toContain('Session · Searchable design re')
    })
    await checkpoint('session-title-autocomplete', harness.terminal)
    await disposeSnapshot(harness)
  })

  it('pins Code Mode run_code with its production presenter', async () => {
    const harness = await setupSnapshot({ configureContext: configureAdvancedTools })
    const call = {
      id: 'code-1',
      name: 'run_code',
      arguments: {
        code: "const first = await tools.bash({ command: 'echo CODE_ONE' })\nconst second = await tools.bash({ command: 'echo CODE_TWO' })\nconsole.log(first, second)\nreturn `${first}+${second}`",
        description: 'Echo two markers and combine them',
      },
    }
    await renderAfter(harness, () => { appendToolCalls(harness.session, [call]) })
    await checkpoint('code-mode-pending', harness.terminal, { includeScrollback: true })
    await disposeSnapshot(harness)
  })

  it('pins a dynamic workflow with phases, parallel agents, and structured output', async () => {
    const harness = await setupSnapshot({ configureContext: configureAdvancedTools })
    const call = {
      id: 'workflow-1',
      name: 'workflow',
      arguments: {
        meta: {
          name: 'tui-matrix',
          description: 'Audit terminal states from independent angles',
          phases: [
            { title: 'Inspect', detail: 'Map renderer branches' },
            { title: 'Verify', detail: 'Challenge missing states', provider: 'deepseek', model: 'deepseek-v4-flash' },
          ],
        },
        args: { packages: ['ui/tui', 'workflow/tool-workflow'] },
        script: "phase('Inspect')\nconst reports = await parallel([\n  () => agent('Audit layout', { label: 'layout', phase: 'Inspect' }),\n  () => agent('Audit lifecycle', { label: 'lifecycle', phase: 'Inspect' }),\n])\nphase('Verify')\nreturn { reports, verdict: 'covered' }",
      },
    }
    await renderAfter(harness, () => { appendToolCalls(harness.session, [call]) })
    await checkpoint('dynamic-workflow-pending', harness.terminal, { includeScrollback: true })
    await disposeSnapshot(harness)
  })

  it('pins cordis inspect, try, and stop cards with production presenters', async () => {
    const harness = await setupSnapshot({ configureContext: configureAdvancedTools })
    const calls = [
      { id: 'cordis-1', name: 'cordis_inspect', arguments: { what: 'tools' } },
      {
        id: 'cordis-2',
        name: 'cordis_mount',
        arguments: { code: "return { name: 'snapshot-marker', apply(ctx) { ctx.provide('snapshotMarker', { ready: true }) } }" },
      },
      { id: 'cordis-3', name: 'cordis_unmount', arguments: { id: 'dyn-1' } },
    ]
    await renderAfter(harness, () => { appendToolCalls(harness.session, calls) })
    await checkpoint('cordis-tools-pending', harness.terminal, { includeScrollback: true })
    await disposeSnapshot(harness)
  })

  it('pins terminal, diff, subagent, task, skill, collapsed, and expanded cards', async () => {
    const harness = await setupSnapshot({
      tools: ADVANCED_CARD_TOOLS,
      config: { maxToolOutputLines: 3 },
    }, { columns: 100, rows: 40 })
    const calls = [
      { id: 'advanced-1', name: 'bash', arguments: { command: 'pnpm run test:coverage' } },
      { id: 'advanced-2', name: 'edit', arguments: { file_path: 'src/view.ts' } },
      { id: 'advanced-3', name: 'subagent', arguments: { prompt: 'Review renderer ownership and report only gaps.' } },
      { id: 'advanced-4', name: 'task_output', arguments: { task_id: 'subagent-7', wait: true } },
      { id: 'advanced-5', name: 'skill', arguments: { name: 'dsh-code-review' } },
    ]
    await renderAfter(harness, () => {
      appendToolCalls(harness.session, calls)
      appendToolResult(harness.session, 'advanced-1', [{ type: 'text', text: 'raw process output' }])
      appendToolResult(harness.session, 'advanced-2', [{ type: 'text', text: 'edit complete' }])
      appendToolResult(harness.session, 'advanced-3', [{ type: 'text', text: 'The renderer has explicit lifecycle ownership.' }])
      appendToolResult(harness.session, 'advanced-4', [{ type: 'text', text: 'audit complete\n[status: completed]' }])
      appendToolResult(harness.session, 'advanced-5', [{ type: 'text', text: 'Loaded review instructions.' }])
    })
    await checkpoint('advanced-cards-collapsed', harness.terminal, { includeScrollback: true })

    await renderAfter(harness, () => { harness.terminal.send('\x0f') })
    await checkpoint('advanced-cards-expanded', harness.terminal, { includeScrollback: true })
    await disposeSnapshot(harness)
  })

  it('renders terminal controls as inert text across transcripts, tools, dialogs, diagnostics, and title', async () => {
    const nowSpy = vi.spyOn(Date, 'now').mockReturnValue(new Date(2026, 6, 21, 15, 0, 0).getTime())
    const tools = {
      unsafe: visualTool(
        'unsafe',
        () => ({
          card: 'terminal',
          title: `Unsafe title ${CONTROL_PROBE}`,
          description: `Unsafe description ${CONTROL_PROBE}`,
          cwd: `/unsafe/${CONTROL_PROBE}`,
        }),
        () => ({
          card: 'terminal',
          output: `Unsafe output ${CONTROL_PROBE}`,
          signal: `SIG${CONTROL_PROBE}`,
        }),
      ),
    }
    const harness = await setupSnapshot({
      tools,
      config: {
        welcome: `Unsafe welcome ${CONTROL_PROBE}`,
        title: `Unsafe terminal title ${CONTROL_PROBE}`,
      },
      beforeMount(session) {
        appendUser(session, `Unsafe user ${CONTROL_PROBE}`)
        appendAssistant(session, [
          { type: 'reasoning', text: `Unsafe reasoning ${CONTROL_PROBE}` },
          { type: 'text', text: `Unsafe assistant ${CONTROL_PROBE}` },
        ])
        appendToolCalls(session, [{ id: 'unsafe-1', name: 'unsafe', arguments: { value: CONTROL_PROBE } }])
        appendToolResult(session, 'unsafe-1', [{ type: 'text', text: `Unsafe fallback ${CONTROL_PROBE}` }])
        session.append('todo/write', {
          todos: [{ content: `Unsafe todo ${CONTROL_PROBE}`, status: 'in_progress' }],
        })
        session.append('user/message', createUserMessage({
          content: [{ type: 'text', text: `Unsafe context ${CONTROL_PROBE}` }],
          source: { kind: 'plugin', plugin: `unsafe-${CONTROL_PROBE}` },
        }), { surfaceOp: 'append' })
        session.append('step/end', { turn: 1, step: 1 })
        session.append('turn/end', {
          turn: 1,
          reason: { kind: 'error', step: 1, message: `Unsafe turn error ${CONTROL_PROBE}` },
        })
      },
    }, { columns: 100, rows: 34 })
    expect(harness.terminal.title).toContain(DISPLAYED_CONTROL_PROBE)
    expect(harness.terminal.title).not.toContain('\u001b')
    expect(harness.terminal.title).not.toContain('\u009b')

    const controller = new AbortController()
    const beforeQuestion = harness.terminal.frames
    const answer = harness.ctx.userInteraction.ask({
      questions: [{
        id: 'unsafe-question',
        header: `Unsafe header ${CONTROL_PROBE}`,
        question: `Unsafe question ${CONTROL_PROBE}`,
        options: [{ label: `Unsafe option ${CONTROL_PROBE}`, description: `Unsafe detail ${CONTROL_PROBE}` }],
      }],
      signal: controller.signal,
    })
    const rejected = expect(answer).rejects.toMatchObject({ code: 'ASK_ABORTED' })
    await harness.terminal.waitForFrame(beforeQuestion)
    agentEvents(harness.ctx, harness.agent).emit('agent/error', 8, 3, new Error(`Unsafe live error ${CONTROL_PROBE}`))
    await checkpoint('untrusted-controls', harness.terminal, { includeScrollback: true })

    controller.abort()
    await rejected
    await disposeSnapshot(harness)
    nowSpy.mockRestore()
  })

  it('pins a constrained multi-select question and its validation state', async () => {
    const harness = await setupSnapshot({
      config: {
        maxQuestionOptions: 3,
        questionDialogWidth: 200,
        questionDialogMaxHeight: 16,
      },
    }, { columns: 56, rows: 20 })
    const controller = new AbortController()
    const beforeQuestion = harness.terminal.frames
    const answer = harness.ctx.userInteraction.ask({
      questions: [
        {
          id: 'coverage',
          header: 'Coverage',
          question: 'Which advanced TUI states belong in the required matrix?',
          multiSelect: true,
          options: [
            { label: 'Code Mode', description: 'run_code programs and captured output' },
            { label: 'Workflows', description: 'phases and parallel agents' },
            { label: 'Cordis tools', description: 'inspect, mount, and unmount' },
            { label: 'Compaction', description: 'surface replacement and reflow' },
          ],
        },
        { id: 'priority', question: 'Which state should be implemented first?' },
        { id: 'notes', question: 'Any additional constraints?' },
      ],
      signal: controller.signal,
    })
    const rejected = expect(answer).rejects.toMatchObject({ code: 'ASK_ABORTED' })
    await harness.terminal.waitForFrame(beforeQuestion)
    await checkpoint('question-dialog', harness.terminal)

    await renderAfter(harness, () => { harness.terminal.send('\r') })
    await checkpoint('question-dialog-validation', harness.terminal)
    controller.abort()
    await rejected
    await disposeSnapshot(harness)
  })

  it('pins a single-option question', async () => {
    const harness = await setupSnapshot({
      config: {
        questionDialogWidth: 200,
        questionDialogMaxHeight: 16,
      },
    }, { columns: 56, rows: 20 })
    const controller = new AbortController()
    const beforeQuestion = harness.terminal.frames
    const answer = harness.ctx.userInteraction.ask({
      questions: [{
        id: 'confirm',
        header: 'Confirm',
        question: 'Continue with this change?',
        options: [{ label: 'Proceed', description: 'Apply the proposed change' }],
      }],
      signal: controller.signal,
    })
    const rejected = expect(answer).rejects.toMatchObject({ code: 'ASK_ABORTED' })
    await harness.terminal.waitForFrame(beforeQuestion)
    await checkpoint('question-dialog-single-option', harness.terminal)
    controller.abort()
    await rejected
    await disposeSnapshot(harness)
  })

  it('pins compaction surface replacement and narrow-to-wide reflow', async () => {
    // Freeze the clock: the timing header hides zero-duration buckets, so a
    // real-clock millisecond tick between the fixture appends and the render
    // would flip `Tools 0.0s` in and out of the pinned header.
    const nowSpy = vi.spyOn(Date, 'now').mockReturnValue(new Date(2026, 6, 21, 14, 40, 0).getTime())
    let replacementStart = 0
    let replacementEnd = 0
    let replacementSources: number[] = []
    const harness = await setupSnapshot({
      tools: ADVANCED_CARD_TOOLS,
      beforeMount(session) {
        const user = session.append('user/message', createUserMessage({
          content: [{ type: 'text', text: 'Old prompt with a long line that exercises wrapping before compaction.' }],
          source: { kind: 'user' },
        }), { surfaceOp: 'append' })
        const assistant = session.append('assistant/message', {
          turn: 1,
          step: 1,
          message: createMessage({
            role: 'assistant',
            content: [{ type: 'tool-call', id: CallId('old-tool'), name: 'bash', arguments: '{}' }],
            source: {
              kind: 'model',
              ...{ provider: 'mock', model: 'deepseek-v4-flash' },
            },
          }),
        }, { surfaceOp: 'append' })
        session.append('tool/call', { turn: 1, step: 1, callId: CallId('old-tool'), name: 'bash', arguments: '{}' })
        const result = session.append('tool/result', {
          turn: 1,
          step: 1,
          message: createToolResultMessage({
            callId: CallId('old-tool'),
            content: [{ type: 'text', text: 'obsolete output that must disappear' }],
            isError: false,
          }),
        }, { surfaceOp: 'append' })
        replacementStart = user.seq
        replacementEnd = result.seq
        replacementSources = [user.seq, assistant.seq, result.seq]
      },
    }, { columns: 80, rows: 24 })
    await checkpoint('surface-before-compaction', harness.terminal, { includeScrollback: true })

    await renderAfter(harness, () => {
      harness.session.append('user/message', createUserMessage({
        content: [{
          type: 'text',
          text: '<system-reminder>\nAdditional instructions from: nested/AGENTS.md\n\nRender workspace context XML clearly.\n</system-reminder>',
        }],
        source: { kind: 'plugin', plugin: 'workspace-context' },
      }), {
        surfaceOp: { op: 'replace', start: replacementStart, end: replacementEnd },
        sourceEventSeqs: replacementSources,
      })
      harness.terminal.resize(44, 18)
    })
    await checkpoint('surface-after-compaction-narrow', harness.terminal, { includeScrollback: true })

    await renderAfter(harness, () => { harness.terminal.resize(104, 30) })
    await checkpoint('surface-after-compaction-wide', harness.terminal, { includeScrollback: true })
    await disposeSnapshot(harness)
    nowSpy.mockRestore()
  })

  it('pins wrapped and explicit multiline shell-prompt input', async () => {
    const harness = await setupSnapshot({}, { columns: 44, rows: 18 })
    await renderAfter(harness, () => {
      harness.terminal.send('Explain this implementation with enough detail to wrap across multiple full-width continuation rows without leaving a prompt-sized gap at the right edge.')
      harness.terminal.send('\x1b[13;2u')
      harness.terminal.send('Then suggest a simpler version.')
    })
    await checkpoint('shell-prompt-multiline', harness.terminal)
    await disposeSnapshot(harness)
  })

  it('pins help, unknown commands, live errors, turn failures, and terminal restoration', async () => {
    const nowSpy = vi.spyOn(Date, 'now').mockReturnValue(new Date(2026, 6, 21, 15, 5, 0).getTime())
    const harness = await setupSnapshot({}, { columns: 92, rows: 32 })
    await renderAfter(harness, () => {
      harness.terminal.send('/help')
      harness.terminal.send('\r')
      harness.terminal.send('/unknown-advanced-command')
      harness.terminal.send('\r')
      agentEvents(harness.ctx, harness.agent).emit('agent/error', 1, 1, new Error('provider stream failed after partial output'))
      harness.session.append('step/end', { turn: 1, step: 1 })
      harness.session.append('turn/end', {
        turn: 1,
        reason: { kind: 'error', step: 1, message: 'provider stream failed after partial output' },
      })
      harness.session.append('turn/start', { turn: 2, trigger: { kind: 'message', source: { kind: 'user' } } })
      harness.session.append('turn/end', {
        turn: 2,
        reason: { kind: 'interrupted' },
      })
      harness.session.append('turn/start', { turn: 3, trigger: { kind: 'message', source: { kind: 'user' } } })
      harness.session.append('turn/end', { turn: 3, reason: { kind: 'disposed' } })
      harness.session.append('turn/start', { turn: 4, trigger: { kind: 'message', source: { kind: 'user' } } })
      // A merge-extensible turn-end kind unknown to the TUI still surfaces its
      // name so the agent never stops without a visible reason.
      harness.session.append('turn/end', { turn: 4, reason: { kind: 'plugin-policy' } as never })
    })
    await checkpoint('errors-and-help', harness.terminal, { includeScrollback: true })

    await harness.controller.dispose()
    await harness.terminal.flush()
    await checkpoint('disposed-terminal', harness.terminal, { includeScrollback: true })
    await harness.ctx.fiber.dispose()
    await harness.terminal.dispose()
    nowSpy.mockRestore()
  })

  it('pins the model selector and selection notice', async () => {
    const harness = await setupSnapshot({}, { columns: 92, rows: 32 })
    await renderAfter(harness, () => {
      harness.terminal.send('/model')
      harness.terminal.send('\r')
    })
    await checkpoint('model-selector', harness.terminal, { includeScrollback: true })
    await renderAfter(harness, () => {
      harness.terminal.send('\x1b[B')
      harness.terminal.send('\r')
    })
    await checkpoint('model-switching', harness.terminal, { includeScrollback: true })
    await disposeSnapshot(harness)
  })

  it('opens the searchable resume selector with log-backed session summaries', async () => {
    const dateNow = vi.spyOn(Date, 'now').mockReturnValue(Date.parse('2026-07-23T08:00:00.000Z'))
    const earlier = { version: 0, id: SessionId('earlier-session'), createdAt: Date.parse('2024-01-01T00:00:00Z'), cwd: '/workspace/project' }
    const harness = await setupSnapshot({
      config: { resumeCommand: 'dsh --resume {session}' },
      sessionPersistence: {
        list: async () => [earlier],
        load: async () => ({
          meta: earlier,
          events: [
            { type: 'turn/start', seq: 0, time: Date.parse('2024-01-01T00:00:01Z'), data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } } },
            { type: 'user/message', seq: 1, time: Date.parse('2024-01-01T00:00:02Z'), data: createUserMessage({
              content: [{ type: 'text', text: 'restore the selector' }], source: { kind: 'user' },
            }), surfaceOp: 'append' },
            { type: 'step/start', seq: 2, time: Date.parse('2024-01-01T00:00:03Z'), data: { turn: 1, step: 1 } },
            { type: 'request/header', seq: 3, time: Date.parse('2024-01-01T00:00:04Z'), data: { header: { config: { provider: 'deepseek', model: 'deepseek-v4-pro' } }, reason: 'initial' } },
            { type: 'assistant/message', seq: 4, time: Date.parse('2024-01-01T00:00:05Z'), data: {
              turn: 1, step: 1,
              message: createMessage({
                role: 'assistant',
                content: [{ type: 'text', text: 'ready' }],
                source: {
                  kind: 'model',
                  ...{ provider: 'deepseek', model: 'deepseek-v4-pro' },
                },
              }),
            }, surfaceOp: 'append' },
            { type: 'step/end', seq: 5, time: Date.parse('2024-01-01T00:00:06Z'), data: { turn: 1, step: 1 } },
            { type: 'turn/end', seq: 6, time: Date.parse('2024-01-01T00:00:07Z'), data: { turn: 1, reason: { kind: 'completed' } } },
            { type: 'session/title', seq: 7, time: Date.parse('2024-01-01T00:00:08Z'), data: { title: 'Resume selector design', messageSeqs: [1], source: { kind: 'fallback' } } },
          ],
        }),
      },
    }, { columns: 92, rows: 32 })
    harness.terminal.send('/resume')
    harness.terminal.send('\r')
    // `/resume` scans persistence asynchronously, so the listing renders a tick
    // after submit (the unit suite waits the same way); settle, then flush.
    await new Promise(resolve => setTimeout(resolve, 60))
    await harness.terminal.flush()
    await checkpoint('resume-sessions', harness.terminal, { includeScrollback: true })
    await disposeSnapshot(harness)
    dateNow.mockRestore()
  })

  it('pins the detailed session diagnostics card', async () => {
    const dateNow = vi.spyOn(Date, 'now').mockReturnValue(Date.parse('2026-07-22T09:10:11.000Z'))
    const harness = await setupSnapshot({
      contextWindow: 128_000,
      contextTokens: 42_000,
      agentOptions: { provider: 'deepseek', model: 'deepseek-v4-pro' },
      tools: {
        read: {
          name: 'read',
          description: 'Read a file',
          parameters: {},
          output: { schema: { type: 'null' }, render: () => [] },
          execute: async () => null,
        },
        write: {
          name: 'write',
          description: 'Write a file',
          parameters: {},
          output: { schema: { type: 'null' }, render: () => [] },
          execute: async () => null,
        },
      },
      beforeMount(session) {
        appendUser(session, 'inspect this session')
        appendAssistant(session, [{ type: 'text', text: 'Session inspected.' }], {
          inputTokens: 1_250,
          outputTokens: 340,
          cacheReadTokens: 3_000,
          cacheWriteTokens: 250,
        })
        session.append('tool/call', {
          turn: 1,
          step: 1,
          callId: CallId('status-call'),
          name: 'read',
          arguments: '{"path":"README.md"}',
        })
        session.append('session/title', {
          title: 'Inspect session diagnostics',
          messageSeqs: [1],
          source: { kind: 'fallback' },
        })
      },
    }, { columns: 92, rows: 32 })
    await renderAfter(harness, () => {
      harness.terminal.send('/status')
      harness.terminal.send('\r')
    })
    await checkpoint('status-diagnostics', harness.terminal, { includeScrollback: true })
    await renderAfter(harness, () => { harness.terminal.resize(56, 36) })
    await checkpoint('status-diagnostics-narrow', harness.terminal, { includeScrollback: true })
    await disposeSnapshot(harness)
    dateNow.mockRestore()
  })
})

afterAll(async () => {
  expect([...observedCheckpoints].sort()).toEqual([...CHECKPOINTS].sort())
  const files = (await readdir(SNAPSHOTS_DIR))
    .filter(file => file.endsWith('.expected.txt'))
    .sort()
  expect(files).toEqual([...CHECKPOINTS, ...STANDALONE_CHECKPOINTS].map(name => `${name}.expected.txt`).sort())
})
