import { describe, it, expect } from 'vitest';
import {
  splitIntoTurns,
  buildSummaryPrompt,
  summarizeHistory,
  summarizeForceTransition,
  SUMMARY_MARKER_PREFIX,
} from './history-compactor.js';
import type { Message } from '../../llm/openai-compat.js';

function turn(callId: string, toolName: string, args: Record<string, unknown>, toolResult: string): Message[] {
  return [
    {
      role: 'assistant',
      tool_calls: [{
        id: callId,
        type: 'function',
        function: { name: toolName, arguments: JSON.stringify(args) },
      }],
    },
    { role: 'tool', tool_call_id: callId, content: toolResult },
  ];
}

describe('splitIntoTurns', () => {
  it('separates preamble (system + user) from assistant turns', () => {
    const messages: Message[] = [
      { role: 'system', content: 'sys' },
      { role: 'user', content: 'task' },
      ...turn('c1', 'Read', { file_path: 'a.ts' }, 'a content'),
      ...turn('c2', 'Read', { file_path: 'b.ts' }, 'b content'),
    ];
    const { preambleEnd, turns } = splitIntoTurns(messages);
    expect(preambleEnd).toBe(2);
    expect(turns).toHaveLength(2);
    expect(turns[0]).toEqual({ assistantIndex: 2, toolEnd: 4 });
    expect(turns[1]).toEqual({ assistantIndex: 4, toolEnd: 6 });
  });

  it('groups consecutive tool messages with the preceding assistant message', () => {
    const messages: Message[] = [
      { role: 'system', content: 'sys' },
      { role: 'user', content: 'task' },
      {
        role: 'assistant',
        tool_calls: [
          { id: 'p1', type: 'function', function: { name: 'Read', arguments: '{}' } },
          { id: 'p2', type: 'function', function: { name: 'Read', arguments: '{}' } },
        ],
      },
      { role: 'tool', tool_call_id: 'p1', content: 'a' },
      { role: 'tool', tool_call_id: 'p2', content: 'b' },
    ];
    const { turns } = splitIntoTurns(messages);
    expect(turns).toHaveLength(1);
    expect(turns[0]).toEqual({ assistantIndex: 2, toolEnd: 5 });
  });

  it('handles a conversation with no assistant messages yet', () => {
    const messages: Message[] = [
      { role: 'system', content: 'sys' },
      { role: 'user', content: 'task' },
    ];
    const { preambleEnd, turns } = splitIntoTurns(messages);
    expect(preambleEnd).toBe(2);
    expect(turns).toEqual([]);
  });
});

describe('buildSummaryPrompt', () => {
  it('produces a system + user message pair', () => {
    const middle: Message[] = [
      ...turn('c1', 'Read', { file_path: 'a.ts' }, 'lots of content'),
    ];
    const prompt = buildSummaryPrompt(middle, null, 100);
    expect(prompt).toHaveLength(2);
    expect(prompt[0]!.role).toBe('system');
    expect(prompt[1]!.role).toBe('user');
    expect(prompt[1]!.content).toContain('## テンプレート');
    expect(prompt[1]!.content).toContain('Read');
  });

  it('switches to update-mode directive when previous summary is provided', () => {
    const previous = '# 会話履歴の要約（システム生成）\n\n## ゴール\nold goal';
    const prompt = buildSummaryPrompt([], previous, 100);
    const userMessage = prompt[1]!.content as string;
    expect(userMessage).toContain('要約を更新してください');
    expect(userMessage).toContain('## 前回の要約');
    expect(userMessage).toContain('old goal');
  });

  it('truncates oversized tool output in the transcript', () => {
    const huge = 'X'.repeat(50_000);
    const middle: Message[] = [...turn('c1', 'Read', {}, huge)];
    const prompt = buildSummaryPrompt(middle, null, 500);
    const userMessage = prompt[1]!.content as string;
    expect(userMessage).toContain('truncated');
    expect(userMessage.length).toBeLessThan(huge.length / 4);
  });
});

describe('summarizeHistory', () => {
  it('skips when there are not enough turns to summarize', async () => {
    const messages: Message[] = [
      { role: 'system', content: 'sys' },
      { role: 'user', content: 'task' },
      ...turn('c1', 'Read', { file_path: 'a.ts' }, 'a'),
    ];
    const result = await summarizeHistory(messages, {
      tailTurns: 2,
      runIsolatedLlm: async () => 'should not be called',
    });
    expect(result.summarized).toBe(false);
    expect(result.reason).toBe('not enough turns to summarize');
  });

  it('replaces middle turns with a single summary user message', async () => {
    const messages: Message[] = [
      { role: 'system', content: 'sys' },
      { role: 'user', content: 'task' },
      ...turn('c1', 'Read', { file_path: 'a.ts' }, 'a content'),
      ...turn('c2', 'Read', { file_path: 'b.ts' }, 'b content'),
      ...turn('c3', 'Read', { file_path: 'c.ts' }, 'c content'),
      ...turn('c4', 'Read', { file_path: 'd.ts' }, 'd content'),
    ];
    const before = messages.length;
    const result = await summarizeHistory(messages, {
      tailTurns: 2,
      runIsolatedLlm: async () => 'fake summary body',
    });
    expect(result.summarized).toBe(true);
    expect(messages.length).toBeLessThan(before);
    // preamble retained
    expect(messages[0]!.role).toBe('system');
    expect(messages[1]!.role).toBe('user');
    expect(messages[1]!.content).toBe('task');
    // summary inserted at position 2
    expect(messages[2]!.role).toBe('user');
    expect(messages[2]!.content).toContain(SUMMARY_MARKER_PREFIX);
    expect(messages[2]!.content).toContain('fake summary body');
    // tail (last 2 turns) retained
    const lastTwoAssistants = messages.filter((m) => m.role === 'assistant');
    expect(lastTwoAssistants.length).toBe(2);
  });

  it('preserves the tail turn content verbatim', async () => {
    const messages: Message[] = [
      { role: 'system', content: 'sys' },
      { role: 'user', content: 'task' },
      ...turn('c1', 'Read', { file_path: 'a.ts' }, 'old read'),
      ...turn('c2', 'Read', { file_path: 'b.ts' }, 'middle read'),
      ...turn('c3', 'Read', { file_path: 'c.ts' }, 'tail read 1'),
      ...turn('c4', 'Read', { file_path: 'd.ts' }, 'tail read 2'),
    ];
    await summarizeHistory(messages, {
      tailTurns: 2,
      runIsolatedLlm: async () => 'summary',
    });
    const toolMessages = messages.filter((m) => m.role === 'tool');
    expect(toolMessages.map((m) => m.content)).toEqual(['tail read 1', 'tail read 2']);
  });

  it('returns summarized=false when LLM throws', async () => {
    const messages: Message[] = [
      { role: 'system', content: 'sys' },
      { role: 'user', content: 'task' },
      ...turn('c1', 'Read', {}, 'a'),
      ...turn('c2', 'Read', {}, 'b'),
      ...turn('c3', 'Read', {}, 'c'),
    ];
    const before = JSON.stringify(messages);
    const result = await summarizeHistory(messages, {
      tailTurns: 1,
      runIsolatedLlm: async () => { throw new Error('boom'); },
    });
    expect(result.summarized).toBe(false);
    expect(result.reason).toContain('boom');
    expect(JSON.stringify(messages)).toBe(before); // unchanged on failure
  });

  it('returns summarized=false when LLM returns empty string', async () => {
    const messages: Message[] = [
      { role: 'system', content: 'sys' },
      { role: 'user', content: 'task' },
      ...turn('c1', 'Read', {}, 'a'),
      ...turn('c2', 'Read', {}, 'b'),
      ...turn('c3', 'Read', {}, 'c'),
    ];
    const result = await summarizeHistory(messages, {
      tailTurns: 1,
      runIsolatedLlm: async () => '   ',
    });
    expect(result.summarized).toBe(false);
    expect(result.reason).toContain('empty');
  });

  it('passes existing summary into prompt for update', async () => {
    const messages: Message[] = [
      { role: 'system', content: 'sys' },
      { role: 'user', content: 'task' },
      { role: 'user', content: '# 会話履歴の要約（システム生成）\n\n## ゴール\nprevious' },
      ...turn('c1', 'Read', {}, 'middle1'),
      ...turn('c2', 'Read', {}, 'middle2'),
      ...turn('c3', 'Read', {}, 'tail1'),
      ...turn('c4', 'Read', {}, 'tail2'),
    ];
    let capturedPrompt: Message[] | null = null;
    await summarizeHistory(messages, {
      tailTurns: 2,
      runIsolatedLlm: async (prompt) => {
        capturedPrompt = prompt;
        return 'updated summary';
      },
    });
    expect(capturedPrompt).not.toBeNull();
    const userText = (capturedPrompt![1]!.content as string);
    expect(userText).toContain('## 前回の要約');
    expect(userText).toContain('previous');
  });
});

describe('summarizeForceTransition', () => {
  it('returns the LLM summary text on success', async () => {
    const messages: Message[] = [
      { role: 'system', content: 'sys' },
      { role: 'user', content: 'task' },
      ...turn('c1', 'Read', {}, 'a'),
    ];
    const result = await summarizeForceTransition(messages, async () => '### Status\nhalf done');
    expect(result).toContain('half done');
  });

  it('returns null when LLM throws', async () => {
    const messages: Message[] = [
      { role: 'system', content: 'sys' },
      { role: 'user', content: 'task' },
    ];
    const result = await summarizeForceTransition(messages, async () => { throw new Error('boom'); });
    expect(result).toBeNull();
  });

  it('returns null when LLM returns empty', async () => {
    const messages: Message[] = [
      { role: 'system', content: 'sys' },
      { role: 'user', content: 'task' },
    ];
    const result = await summarizeForceTransition(messages, async () => '   ');
    expect(result).toBeNull();
  });
});
