import { describe, expect, test } from 'bun:test';
import type { ChatMessage } from '../types';
import { buildRows, foldLabel } from './turn-rows';

const user = (text: string): ChatMessage => ({ role: 'user', text });
const say = (text: string): ChatMessage => ({ role: 'assistant', id: text, text });
const tool = (id: string, isError?: boolean): ChatMessage => ({
  role: 'tool',
  toolName: 'Bash',
  toolInput: {},
  toolCallId: id,
  output: 'ok',
  ...(isError ? { isError: true } : {}),
});

/**
 * The shape under test is the row sequence, not the keys.
 *
 * `historicalCount` defaults to the whole list — a reload, where everything came from the server. That is
 * the case most of these describe; the live case sets it to 0 explicitly.
 */
const shape = (messages: ChatMessage[], isGenerating = false, historicalCount = messages.length) =>
  buildRows(messages, isGenerating, historicalCount).map((r) => (r.kind === 'message' ? r.message.role : r.kind));

describe('buildRows', () => {
  test('the live turn is untouched — every row stays where it was', () => {
    expect(shape([user('go'), say('one'), tool('a'), say('two')], true)).toEqual([
      'user',
      'assistant',
      'tool',
      'assistant',
    ]);
  });

  test('an older turn folds to question, summary, answer', () => {
    const messages = [user('first'), say('thinking'), tool('a'), tool('b'), say('answer'), user('second')];
    expect(shape(messages)).toEqual(['user', 'fold', 'assistant', 'user']);
  });

  test('the fold counts what it hid, failures included', () => {
    const messages = [user('first'), say('note'), tool('a'), tool('b', true), say('answer'), user('second')];
    const fold = buildRows(messages, false, messages.length).find((r) => r.kind === 'fold');
    expect(fold?.kind === 'fold' && fold.counts).toEqual({ tools: 2, notes: 1, tasks: 0, failed: 1 });
  });

  test('a fold opens onto exactly the messages it replaced, in order', () => {
    const messages = [user('first'), say('note'), tool('a'), say('answer'), user('second')];
    const fold = buildRows(messages, false, messages.length).find((r) => r.kind === 'fold');
    expect(fold?.kind === 'fold' && fold.messages).toEqual([say('note'), tool('a')]);
  });

  test('a divider is never swallowed by a fold — it splits it', () => {
    const messages = [
      user('first'),
      tool('a'),
      { role: 'divider', sessionId: 's1' } as ChatMessage,
      tool('b'),
      say('answer'),
      user('second'),
    ];
    expect(shape(messages)).toEqual(['user', 'fold', 'divider', 'fold', 'assistant', 'user']);
  });

  test('a compaction seam survives folding too', () => {
    const messages = [
      user('first'),
      tool('a'),
      { role: 'compact', trigger: 'auto', done: true } as ChatMessage,
      say('answer'),
      user('second'),
    ];
    expect(shape(messages)).toEqual(['user', 'fold', 'compact', 'assistant', 'user']);
  });

  // The marker tests describe a turn from this sitting, so they pass 0: whether a finished turn also
  // folds is a separate question, pinned in its own tests below.
  test('a turn that ends on a tool call is marked finished rather than left hanging', () => {
    expect(shape([user('go'), tool('a')], false, 0)).toEqual(['user', 'tool', 'turn-end']);
  });

  test('a turn still running is not marked finished', () => {
    expect(shape([user('go'), tool('a')], true)).toEqual(['user', 'tool']);
  });

  test('a turn that failed keeps its own row and gets no marker', () => {
    expect(shape([user('go'), tool('a'), { role: 'error', text: 'boom' }], false, 0)).toEqual([
      'user',
      'tool',
      'error',
    ]);
  });

  test('an interrupted turn gets no marker either', () => {
    expect(shape([user('go'), tool('a'), { role: 'interrupted' }], false, 0)).toEqual(['user', 'tool', 'interrupted']);
  });

  test('a cost row alone is not an answer — the marker still appears, above it', () => {
    const messages: ChatMessage[] = [
      user('go'),
      tool('a'),
      { role: 'result', cost: { inputTokens: 1, outputTokens: 1, totalUSD: 0 } },
    ];
    expect(shape(messages, false, 0)).toEqual(['user', 'tool', 'turn-end', 'result']);
  });

  test('an empty assistant block does not count as having answered', () => {
    expect(shape([user('go'), tool('a'), say('   ')], false, 0)).toEqual(['user', 'tool', 'turn-end', 'assistant']);
  });

  test('your own message is never folded', () => {
    const rows = buildRows([user('first'), tool('a'), say('answer'), user('second')], false, 4);
    expect(rows[0]?.kind === 'message' && rows[0].message).toEqual(user('first'));
  });

  test('a window with no user message still folds when it came from the server', () => {
    // It used to stay open because there was no turn boundary to fold against. Under the history rule
    // there is nothing to work out: it was loaded, so it is history.
    expect(shape([say('note'), tool('a'), say('answer')], false)).toEqual(['fold', 'assistant']);
  });

  test('nothing folds while you are watching, however many turns you send', () => {
    // The behaviour this whole rule exists for. Three complete turns, none of them loaded from the
    // server: under the old rule the first two collapsed the instant the next message was sent.
    const messages = [
      user('1'),
      tool('a'),
      say('one'),
      user('2'),
      tool('b'),
      say('two'),
      user('3'),
      tool('c'),
      say('three'),
    ];
    expect(shape(messages, false, 0)).toEqual([
      'user',
      'tool',
      'assistant',
      'user',
      'tool',
      'assistant',
      'user',
      'tool',
      'assistant',
    ]);
  });

  test('the boundary is where the server stopped and this sitting began', () => {
    // First turn loaded (6 messages), second typed here. The loaded one folds; the new one does not.
    const messages = [user('1'), tool('a'), say('one'), user('2'), tool('b'), say('two')];
    expect(shape(messages, false, 3)).toEqual(['user', 'fold', 'assistant', 'user', 'tool', 'assistant']);
  });

  test('a turn still generating is never folded, even reloaded onto mid-turn', () => {
    // Reload while a turn is in flight: every message is history, but hiding the work as it arrives is
    // the exact thing being undone. The finished turn above it folds as usual.
    const messages = [user('1'), tool('a'), say('one'), user('2'), tool('b')];
    expect(shape(messages, true, messages.length)).toEqual(['user', 'fold', 'assistant', 'user', 'tool']);
  });

  test('every older turn folds, not just the previous one', () => {
    const messages = [
      user('1'),
      tool('a'),
      say('one'),
      user('2'),
      tool('b'),
      say('two'),
      user('3'),
      tool('c'),
      say('three'),
    ];
    // Including the last one, which used to be exempt for being the live turn. Reloaded, nothing is live.
    expect(shape(messages, false)).toEqual([
      'user',
      'fold',
      'assistant',
      'user',
      'fold',
      'assistant',
      'user',
      'fold',
      'assistant',
    ]);
  });

  test('an empty transcript produces nothing', () => {
    expect(buildRows([], false)).toEqual([]);
  });

  test('a bare question with no work yet produces no marker and no fold', () => {
    expect(shape([user('go')], true)).toEqual(['user']);
  });
});

/** What tool rows key their expansion off. Getting this wrong expands a whole transcript at once. */
describe('buildRows — the live flag', () => {
  const live = (messages: ChatMessage[], isGenerating = false, historicalCount = messages.length) =>
    buildRows(messages, isGenerating, historicalCount)
      .filter((r) => r.kind === 'message')
      .map((r) => (r.kind === 'message' ? r.live : null));

  test('a turn from this sitting is live, generating or finished', () => {
    // Finishing is not what ends a turn's expansion any more — being reloaded is.
    expect(live([user('go'), tool('a'), say('answer')], true, 0)).toEqual([true, true, true]);
    expect(live([user('go'), tool('a'), say('answer')], false, 0)).toEqual([true, true, true]);
  });

  test('a folded turn is not live — only your question and my answer survive as rows', () => {
    expect(live([user('first'), tool('a'), say('answer'), user('second')])).toEqual([false, false, false]);
  });

  test('unfolded and live are now one claim', () => {
    // They came apart only because the old rule force-unfolded a window it could find no turn boundary in,
    // and opening every tool call in a lazily-loaded chunk would have been wrong. That window folds now,
    // so there is no case left where a row is on screen unfolded but not current.
    expect(live([say('note'), tool('a'), say('answer')])).toEqual([false]);
    expect(live([say('note'), tool('a'), say('answer')], false, 0)).toEqual([true, true, true]);
  });
});

describe('foldLabel', () => {
  test('reads as a sentence and singularises', () => {
    expect(foldLabel({ tools: 1, notes: 1, tasks: 0, failed: 0 })).toBe('1 tool call · 1 message');
    expect(foldLabel({ tools: 5, notes: 3, tasks: 0, failed: 0 })).toBe('5 tool calls · 3 messages');
  });

  test('failures are named on the summary, not left inside', () => {
    expect(foldLabel({ tools: 5, notes: 0, tasks: 0, failed: 2 })).toBe('5 tool calls · 2 failed');
  });

  test('background tasks are counted apart from tool calls', () => {
    expect(foldLabel({ tools: 0, notes: 0, tasks: 1, failed: 0 })).toBe('1 background task');
  });
});
