import { describe, expect, it } from 'vitest';

import type { ToolTimelineEntry } from '../../store/chatRuntimeSlice';
import type { PersistedTranscriptItem } from '../../types/turnState';
import {
  buildProcessingBlocks,
  categorizeTool,
  extractAgentSources,
  formatTimelineEntry,
  formatToolName,
  isKnownClientTool,
  stripToolCallEnvelopes,
  summarizeToolGroup,
} from '../toolTimelineFormatting';

function entry(overrides: Partial<ToolTimelineEntry>): ToolTimelineEntry {
  return { id: 'x', name: 'delegate_notion', round: 1, status: 'running', ...overrides };
}

describe('formatTimelineEntry', () => {
  it('formats integration delegation tools with a user-facing provider label', () => {
    expect(
      formatTimelineEntry(
        entry({
          name: 'delegate_notion',
          argsBuffer: JSON.stringify({ prompt: 'Find the project brief in Notion.' }),
        })
      )
    ).toEqual({
      title: 'Working in your Notion workspace',
      detail: 'Find the project brief in Notion.',
    });
  });

  it('formats spawn_subagent for integrations_agent from toolkit args', () => {
    expect(
      formatTimelineEntry(
        entry({
          name: 'spawn_subagent',
          argsBuffer: JSON.stringify({
            agent_id: 'integrations_agent',
            prompt:
              'Get my 5 most recent emails. Show subject, sender, date, and a short preview for each.',
            toolkit: 'gmail',
          }),
        })
      )
    ).toEqual({
      title: 'Making requests to your Gmail account',
      detail:
        'Get my 5 most recent emails. Show subject, sender, date, and a short preview for each.',
    });
  });

  it('formats spawned integration agents with the inherited prompt', () => {
    expect(
      formatTimelineEntry(
        entry({
          name: 'subagent:integrations_agent',
          sourceToolName: 'delegate_notion',
          detail: 'Search Notion for the latest roadmap.',
        })
      )
    ).toEqual({
      title: 'Working in your Notion workspace',
      detail: 'Search Notion for the latest roadmap.',
    });
  });

  it('formats delegate_to_integrations_agent with a known toolkit arg', () => {
    expect(
      formatTimelineEntry(
        entry({
          name: 'delegate_to_integrations_agent',
          argsBuffer: JSON.stringify({
            toolkit: 'gmail',
            prompt: 'Find the latest invoice from Stripe.',
          }),
        })
      )
    ).toEqual({
      title: 'Making requests to your Gmail account',
      detail: 'Find the latest invoice from Stripe.',
    });
  });

  it('formats delegate_to_integrations_agent with an unknown toolkit arg', () => {
    expect(
      formatTimelineEntry(
        entry({
          name: 'delegate_to_integrations_agent',
          argsBuffer: JSON.stringify({ toolkit: 'slack_bot', prompt: 'post update' }),
        })
      )
    ).toEqual({ title: 'Checking your Slack Bot', detail: 'post update' });
  });

  it('formats delegate_to_integrations_agent without a toolkit arg as a generic connected-app label', () => {
    expect(
      formatTimelineEntry(
        entry({
          name: 'delegate_to_integrations_agent',
          argsBuffer: JSON.stringify({ prompt: 'do something useful' }),
        })
      )
    ).toEqual({ title: 'Checking your connected app', detail: 'do something useful' });
  });

  it('formats delegate_tools_agent with toolkit context from args', () => {
    expect(
      formatTimelineEntry(
        entry({
          name: 'delegate_tools_agent',
          argsBuffer: JSON.stringify({
            toolkit: 'github',
            prompt: 'List my open pull requests in GitHub.',
          }),
        })
      )
    ).toEqual({
      title: 'Making requests to your GitHub account',
      detail: 'List my open pull requests in GitHub.',
    });
  });

  it('falls back to humanized generic labels for non-integration subagents', () => {
    expect(formatTimelineEntry(entry({ name: 'subagent:researcher' }))).toEqual({
      title: 'Researching',
      detail: undefined,
    });
  });

  it('formats composio_list_connections with user-facing copy', () => {
    expect(formatTimelineEntry(entry({ name: 'composio_list_connections' }))).toEqual({
      title: 'Viewing your Connections',
      detail: undefined,
    });
  });

  it('formats shell tool with truncated command detail', () => {
    expect(
      formatTimelineEntry(
        entry({ name: 'shell', argsBuffer: JSON.stringify({ command: 'cargo test --lib' }) })
      )
    ).toEqual({ title: 'Running command', detail: 'cargo test --lib' });
  });

  it('formats web_fetch with hostname in title', () => {
    expect(
      formatTimelineEntry(
        entry({
          name: 'web_fetch',
          argsBuffer: JSON.stringify({ url: 'https://docs.example.com/api/v2/users' }),
        })
      )
    ).toEqual({
      title: 'Fetching docs.example.com',
      detail: 'https://docs.example.com/api/v2/users',
    });
  });

  it('formats web_search with query in title', () => {
    expect(
      formatTimelineEntry(
        entry({ name: 'web_search', argsBuffer: JSON.stringify({ query: 'rust async trait' }) })
      )
    ).toEqual({ title: 'Searching: rust async trait' });
  });

  it('formats file_read with shortened path', () => {
    expect(
      formatTimelineEntry(
        entry({
          name: 'file_read',
          argsBuffer: JSON.stringify({ path: 'src/openhuman/agent/progress.rs' }),
        })
      )
    ).toEqual({ title: 'Reading file', detail: '…/agent/progress.rs' });
  });

  it('formats edit tool with file path', () => {
    expect(
      formatTimelineEntry(
        entry({
          name: 'edit',
          argsBuffer: JSON.stringify({ file_path: 'app/src/components/App.tsx' }),
        })
      )
    ).toEqual({ title: 'Editing file', detail: '…/components/App.tsx' });
  });

  it('formats grep with pattern in title', () => {
    expect(
      formatTimelineEntry(
        entry({ name: 'grep', argsBuffer: JSON.stringify({ pattern: 'SubagentSpawned' }) })
      )
    ).toEqual({ title: 'Searching: SubagentSpawned' });
  });

  it('formats git_operations with subcommand', () => {
    expect(
      formatTimelineEntry(
        entry({ name: 'git_operations', argsBuffer: JSON.stringify({ command: 'diff --stat' }) })
      )
    ).toEqual({ title: 'Git diff', detail: 'diff --stat' });
  });

  it('formats screenshot as a simple label', () => {
    expect(formatTimelineEntry(entry({ name: 'screenshot' }))).toEqual({
      title: 'Taking screenshot',
    });
  });

  it('formats glob with pattern detail', () => {
    expect(
      formatTimelineEntry(
        entry({ name: 'glob', argsBuffer: JSON.stringify({ pattern: '**/*.test.ts' }) })
      )
    ).toEqual({ title: 'Finding: **/*.test.ts' });
  });

  it('formats list with directory path', () => {
    expect(
      formatTimelineEntry(
        entry({ name: 'list', argsBuffer: JSON.stringify({ path: 'src/openhuman/tools' }) })
      )
    ).toEqual({ title: 'Listing directory', detail: 'src/openhuman/tools' });
  });

  it('formats browser_open with hostname', () => {
    expect(
      formatTimelineEntry(
        entry({
          name: 'browser_open',
          argsBuffer: JSON.stringify({ url: 'https://github.com/tinyhumansai/openhuman' }),
        })
      )
    ).toEqual({ title: 'Browsing github.com' });
  });
});

describe('formatToolName', () => {
  it('returns human-readable names for known tools', () => {
    expect(formatToolName('shell')).toBe('Running command');
    expect(formatToolName('web_fetch')).toBe('Fetching');
    expect(formatToolName('file_read')).toBe('Reading file');
    expect(formatToolName('edit')).toBe('Editing file');
    expect(formatToolName('grep')).toBe('Searching code');
    expect(formatToolName('git_operations')).toBe('Git operation');
    expect(formatToolName('screenshot')).toBe('Taking screenshot');
    expect(formatToolName('lsp')).toBe('Code intelligence');
  });

  it('falls back to humanized identifier for unknown tools', () => {
    expect(formatToolName('custom_fancy_tool')).toBe('Custom Fancy Tool');
  });
});

describe('stripToolCallEnvelopes', () => {
  it('removes a complete <tool_call>…</tool_call> envelope, keeping the prose', () => {
    const out = stripToolCallEnvelopes(
      'Searching now. <tool_call> {"name": "NOTION_SEARCH", "arguments": {"q": "x"}} </tool_call> done.'
    );
    expect(out).toContain('Searching now.');
    expect(out).toContain('done.');
    expect(out).not.toContain('tool_call');
    expect(out).not.toContain('NOTION_SEARCH');
  });

  it('removes a trailing, still-streaming unclosed <tool_call>…', () => {
    const out = stripToolCallEnvelopes('Let me check. <tool_call> {"name": "X", "argum');
    expect(out.trim()).toBe('Let me check.');
    expect(out).not.toContain('tool_call');
  });

  it('leaves text without an envelope untouched', () => {
    expect(stripToolCallEnvelopes('just a normal sentence')).toBe('just a normal sentence');
  });
});

describe('isKnownClientTool', () => {
  it('recognizes built-ins and special agent rows', () => {
    expect(isKnownClientTool('file_read')).toBe(true);
    expect(isKnownClientTool('shell')).toBe(true);
    expect(isKnownClientTool('subagent:researcher')).toBe(true);
    expect(isKnownClientTool('delegate_to_integrations_agent')).toBe(true);
  });

  it('does not recognize dynamic Composio/MCP actions (server labels them)', () => {
    expect(isKnownClientTool('GMAIL_SEND_EMAIL')).toBe(false);
    expect(isKnownClientTool('composio_notion_create_page')).toBe(false);
    expect(isKnownClientTool('some_random_mcp_tool')).toBe(false);
  });
});

describe('summarizeToolGroup', () => {
  it('uses the specific label for a single row', () => {
    expect(summarizeToolGroup([entry({ name: 'file_read', argsBuffer: '{"path":"a.ts"}' })])).toBe(
      'Reading file'
    );
  });

  it('counts a homogeneous group', () => {
    expect(
      summarizeToolGroup([
        entry({ id: 'a', name: 'file_read' }),
        entry({ id: 'b', name: 'file_read' }),
      ])
    ).toBe('Read 2 files');
  });

  it('joins distinct category phrases for a mixed group', () => {
    expect(
      summarizeToolGroup([
        entry({ id: 'a', name: 'file_write' }),
        entry({ id: 'b', name: 'shell' }),
        entry({ id: 'c', name: 'shell' }),
      ])
    ).toBe('Edited 1 file, ran 2 commands');
  });
});

describe('categorizeTool', () => {
  it('maps tools (incl. subagent-prefixed) to a category', () => {
    expect(categorizeTool('grep')).toBe('search');
    expect(categorizeTool('subagent:web_fetch')).toBe('fetch');
    expect(categorizeTool('GMAIL_SEND_EMAIL')).toBe('other');
  });
});

describe('buildProcessingBlocks', () => {
  const tx = (over: Partial<PersistedTranscriptItem> & { kind: PersistedTranscriptItem['kind'] }) =>
    ({ round: 1, seq: 0, ...over }) as PersistedTranscriptItem;

  it('interleaves prose with grouped consecutive tool rows in seq order', () => {
    const entries = [
      entry({ id: 'c1', name: 'file_read' }),
      entry({ id: 'c2', name: 'file_read' }),
    ];
    const transcript: PersistedTranscriptItem[] = [
      tx({ kind: 'thinking', seq: 0, text: 'Let me look' }),
      tx({ kind: 'narration', seq: 1, text: 'Reading the files' }),
      tx({ kind: 'toolCall', seq: 2, callId: 'c1' }),
      tx({ kind: 'toolCall', seq: 3, callId: 'c2' }),
      tx({ kind: 'narration', seq: 4, text: 'Done' }),
    ];
    const blocks = buildProcessingBlocks(transcript, entries);
    expect(blocks.map(b => b.kind)).toEqual(['thinking', 'narration', 'toolGroup', 'narration']);
    const group = blocks[2];
    if (group.kind !== 'toolGroup') throw new Error('expected toolGroup');
    expect(group.summary).toBe('Read 2 files');
    expect(group.entries).toHaveLength(2);
  });

  it('skips tool pointers with no matching entry', () => {
    const blocks = buildProcessingBlocks([tx({ kind: 'toolCall', seq: 0, callId: 'missing' })], []);
    expect(blocks).toHaveLength(0);
  });

  it('falls back to a single group when there is no transcript', () => {
    const entries = [entry({ id: 'c1', name: 'shell' })];
    const blocks = buildProcessingBlocks([], entries);
    expect(blocks).toHaveLength(1);
    expect(blocks[0].kind).toBe('toolGroup');
  });
});

describe('formatter null-safety (malformed / legacy snapshot guard)', () => {
  it('does not throw on undefined input', () => {
    // A snake_case (legacy) persisted transcript item yields undefined
    // camelCase fields; the formatter must degrade, not crash the app.
    expect(formatToolName(undefined)).toBe('');
    expect(stripToolCallEnvelopes(undefined)).toBe('');
    expect(stripToolCallEnvelopes(null)).toBe('');
  });
});

describe('extractAgentSources', () => {
  const source = (id: string, url: string): ToolTimelineEntry =>
    entry({ id, name: 'web_fetch', argsBuffer: JSON.stringify({ url }) });

  it('surfaces http(s) sources with hostname titles, deduped in first-seen order', () => {
    const out = extractAgentSources([
      source('a', 'https://example.com/docs'),
      source('b', 'http://blog.example.org/post'),
      source('c', 'https://example.com/docs'), // dup URL — dropped
    ]);
    expect(out).toEqual([
      { id: 'a', title: 'example.com', url: 'https://example.com/docs' },
      { id: 'b', title: 'blog.example.org', url: 'http://blog.example.org/post' },
    ]);
  });

  it('drops non-http(s) URLs so a hostile scheme never reaches an anchor href', () => {
    const out = extractAgentSources([
      source('js', 'javascript:alert(1)'),
      source('data', 'data:text/html,<script>alert(1)</script>'),
      source('file', 'file:///etc/passwd'),
      source('ok', 'https://safe.example.com'),
    ]);
    expect(out).toEqual([{ id: 'ok', title: 'safe.example.com', url: 'https://safe.example.com' }]);
  });

  it('ignores entries from non-URL tools', () => {
    expect(
      extractAgentSources([
        entry({ id: 'g', name: 'grep', argsBuffer: JSON.stringify({ url: 'https://x.com' }) }),
      ])
    ).toEqual([]);
  });
});
