import { describe, it, expect } from 'vitest';
import { partsFromAssistantMessage, partsFromToolMessage } from '../inference/parts.js';
import type { ToolCall, ToolResult } from '../../types/api.js';

describe('partsFromAssistantMessage', () => {
  it('emits one text part for content-only assistant', () => {
    const parts = partsFromAssistantMessage({ content: 'hello world', tool_calls: null });
    expect(parts).toHaveLength(1);
    expect(parts[0]).toEqual({
      sequence: 0,
      kind: 'text',
      payload: { text: 'hello world' },
    });
  });

  it('emits one tool_call part for empty-content + single tool_call', () => {
    const tc: ToolCall = { id: 'call_1', name: 'view_file', args: { path: 'src/a.ts' } };
    const parts = partsFromAssistantMessage({ content: '', tool_calls: [tc] });
    expect(parts).toHaveLength(1);
    expect(parts[0]).toEqual({
      sequence: 0,
      kind: 'tool_call',
      payload: { id: 'call_1', name: 'view_file', args: { path: 'src/a.ts' } },
    });
  });

  it('emits text then tool_call parts in order when both present', () => {
    const tc: ToolCall = { id: 'call_2', name: 'grep', args: { pattern: 'foo' } };
    const parts = partsFromAssistantMessage({ content: 'let me search', tool_calls: [tc] });
    expect(parts.map((p) => [p.sequence, p.kind])).toEqual([
      [0, 'text'],
      [1, 'tool_call'],
    ]);
  });

  it('preserves tool_call order with multiple calls', () => {
    const calls: ToolCall[] = [
      { id: 'a', name: 'list_dir', args: { path: '.' } },
      { id: 'b', name: 'view_file', args: { path: 'x.ts' } },
      { id: 'c', name: 'grep', args: { pattern: 'y' } },
    ];
    const parts = partsFromAssistantMessage({ content: '', tool_calls: calls });
    expect(parts).toHaveLength(3);
    expect(parts.map((p) => p.payload)).toEqual([
      { id: 'a', name: 'list_dir', args: { path: '.' } },
      { id: 'b', name: 'view_file', args: { path: 'x.ts' } },
      { id: 'c', name: 'grep', args: { pattern: 'y' } },
    ]);
    expect(parts.map((p) => p.sequence)).toEqual([0, 1, 2]);
  });

  it('returns empty array for empty content + null tool_calls', () => {
    expect(partsFromAssistantMessage({ content: '', tool_calls: null })).toEqual([]);
  });

  it('v1.13.1-C: reasoning lands at sequence 0 before text + tool_calls', () => {
    const tc: ToolCall = { id: 'call_r', name: 'view_file', args: { path: 'x.ts' } };
    const parts = partsFromAssistantMessage({
      content: 'inspecting now',
      tool_calls: [tc],
      reasoning: 'user asked about x.ts; I should view it',
    });
    expect(parts.map((p) => [p.sequence, p.kind])).toEqual([
      [0, 'reasoning'],
      [1, 'text'],
      [2, 'tool_call'],
    ]);
    expect(parts[0]!.payload).toEqual({
      text: 'user asked about x.ts; I should view it',
    });
  });

  it('v1.13.1-C: reasoning + empty content + tool_calls preserves seq 0 reasoning', () => {
    const tc: ToolCall = { id: 'call_r2', name: 'grep', args: { pattern: 'foo' } };
    const parts = partsFromAssistantMessage({
      content: '',
      tool_calls: [tc],
      reasoning: 'jumping straight to grep',
    });
    expect(parts.map((p) => [p.sequence, p.kind])).toEqual([
      [0, 'reasoning'],
      [1, 'tool_call'],
    ]);
  });

  it('Phase 2: signed reasoning blocks become one reasoning part each, supersede the joined string', () => {
    const parts = partsFromAssistantMessage({
      content: 'done',
      tool_calls: null,
      reasoning: 'block1block2', // the joined fallback — must be ignored here
      reasoningBlocks: [
        { text: 'block1', signature: 'sig1' },
        { text: 'block2', signature: 'sig2' },
      ],
    });
    expect(parts.map((p) => [p.sequence, p.kind])).toEqual([
      [0, 'reasoning'],
      [1, 'reasoning'],
      [2, 'text'],
    ]);
    expect(parts[0]!.payload).toEqual({ text: 'block1', signature: 'sig1' });
    expect(parts[1]!.payload).toEqual({ text: 'block2', signature: 'sig2' });
  });

  it('Phase 2: an empty-text block with a signature is still persisted (display:omitted)', () => {
    const parts = partsFromAssistantMessage({
      content: '',
      tool_calls: null,
      reasoningBlocks: [{ text: '', signature: 'sig-only' }],
    });
    expect(parts.map((p) => [p.kind, p.payload])).toEqual([
      ['reasoning', { text: '', signature: 'sig-only' }],
    ]);
  });

  it('Phase 2: empty reasoningBlocks falls back to the joined reasoning string', () => {
    const parts = partsFromAssistantMessage({
      content: 'x',
      tool_calls: null,
      reasoning: 'plain reasoning',
      reasoningBlocks: [],
    });
    expect(parts[0]!.payload).toEqual({ text: 'plain reasoning' });
  });
});

describe('partsFromToolMessage', () => {
  it('emits a single tool_result part at sequence 0', () => {
    const tr: ToolResult = {
      tool_call_id: 'call_1',
      output: { contents: 'console.log(1)' },
      truncated: false,
    };
    const parts = partsFromToolMessage({ tool_results: tr });
    expect(parts).toHaveLength(1);
    expect(parts[0]).toEqual({
      sequence: 0,
      kind: 'tool_result',
      payload: {
        tool_call_id: 'call_1',
        output: { contents: 'console.log(1)' },
        truncated: false,
      },
    });
  });

  it('includes error in payload when present', () => {
    const tr: ToolResult = {
      tool_call_id: 'call_2',
      output: null,
      truncated: false,
      error: 'permission denied',
    };
    const parts = partsFromToolMessage({ tool_results: tr });
    expect(parts[0]!.payload).toMatchObject({ error: 'permission denied' });
  });

  it('returns empty array when tool_results is null', () => {
    expect(partsFromToolMessage({ tool_results: null })).toEqual([]);
  });
});
