import { describe, expect, it } from 'vitest';
import {
  extractToolCallBlocks,
  stripToolMarkup,
} from '../inference/tool-call-parser.js';

describe('extractToolCallBlocks (v1.13.16 — unified extraction)', () => {
  it('extracts a single <invoke> block (spec case 1)', () => {
    const input = '<invoke name="view_file"><parameter name="path">/tmp/foo</parameter></invoke>';
    const result = extractToolCallBlocks(input);
    expect(result.calls).toEqual([{ name: 'view_file', args: { path: '/tmp/foo' } }]);
    expect(result.flushed).toBe('');
    expect(result.remaining).toBe('');
  });

  it('holds the partial <invoke> chunk when the closer has not arrived (spec case 5, first chunk)', () => {
    const firstChunk = '<invoke name="view_file"><parameter name="path">/tmp/foo</parameter>';
    const result = extractToolCallBlocks(firstChunk);
    expect(result.calls).toEqual([]);
    expect(result.flushed).toBe('');
    expect(result.remaining).toBe(firstChunk);
  });

  it('extracts the block once the closer arrives in a later chunk (spec case 5, completion)', () => {
    const firstChunk = '<invoke name="view_file"><parameter name="path">/tmp/foo</parameter>';
    const r1 = extractToolCallBlocks(firstChunk);
    const combined = r1.remaining + '</invoke>';
    const r2 = extractToolCallBlocks(combined);
    expect(r2.calls).toEqual([{ name: 'view_file', args: { path: '/tmp/foo' } }]);
    expect(r2.flushed).toBe('');
    expect(r2.remaining).toBe('');
  });

  it('flushes prose around a recognized block but not the markup itself (spec case 6)', () => {
    const input = 'I will read the file.\n<invoke name="view_file"><parameter name="path">/tmp/foo</parameter></invoke>\nThanks.';
    const result = extractToolCallBlocks(input);
    expect(result.calls).toEqual([{ name: 'view_file', args: { path: '/tmp/foo' } }]);
    expect(result.flushed).toBe('I will read the file.\n\nThanks.');
    expect(result.remaining).toBe('');
  });

  it('extracts a <tool_call> Qwen block alongside the new code path (spec case 7 regression)', () => {
    const input = '<tool_call><function=view_file><parameter=path>/tmp/foo</parameter></function></tool_call>';
    const result = extractToolCallBlocks(input);
    expect(result.calls).toEqual([{ name: 'view_file', args: { path: '/tmp/foo' } }]);
    expect(result.flushed).toBe('');
    expect(result.remaining).toBe('');
  });

  it('extracts mixed-format blocks in source order (hand-back: shared counter)', () => {
    const input =
      '<invoke name="view_file"><parameter name="path">/a</parameter></invoke>' +
      ' middle ' +
      '<tool_call><function=grep><parameter=pattern>foo</parameter></function></tool_call>';
    const result = extractToolCallBlocks(input);
    expect(result.calls).toEqual([
      { name: 'view_file', args: { path: '/a' } },
      { name: 'grep', args: { pattern: 'foo' } },
    ]);
    expect(result.flushed).toBe(' middle ');
    expect(result.remaining).toBe('');
  });

  it('drops a malformed <invoke> block silently (matches existing <tool_call> behavior)', () => {
    const input = 'prose <invoke><parameter name="path">/a</parameter></invoke> trailing';
    const result = extractToolCallBlocks(input);
    expect(result.calls).toEqual([]);
    expect(result.flushed).toBe('prose  trailing');
    expect(result.remaining).toBe('');
  });

  it('holds a tail with a fresh partial opener after extracting earlier complete blocks', () => {
    const input = '<invoke name="view_file"><parameter name="path">/a</parameter></invoke> next: <tool_';
    const result = extractToolCallBlocks(input);
    expect(result.calls).toEqual([{ name: 'view_file', args: { path: '/a' } }]);
    expect(result.flushed).toBe(' next: ');
    expect(result.remaining).toBe('<tool_');
  });

  it('passes plain prose straight through when no markup is present', () => {
    const input = 'just some text with a < character but no opener';
    const result = extractToolCallBlocks(input);
    expect(result.calls).toEqual([]);
    expect(result.flushed).toBe(input);
    expect(result.remaining).toBe('');
  });

  describe('placeholder arg rejection (qwen3.6 answer-then-spurious-tools)', () => {
    it('rejects <invoke> with path "..." — 0 calls, block in flushed', () => {
      const block = '<invoke name="view_file"><parameter name="path">...</parameter></invoke>';
      const result = extractToolCallBlocks(`Answer text.\n${block}`);
      expect(result.calls).toEqual([]);
      expect(result.flushed).toContain('Answer text.');
      expect(result.flushed).toContain(block);
      expect(result.remaining).toBe('');
    });

    it('rejects <invoke> with empty path — 0 calls, block in flushed', () => {
      const block = '<invoke name="view_file"><parameter name="path"></parameter></invoke>';
      const result = extractToolCallBlocks(block);
      expect(result.calls).toEqual([]);
      expect(result.flushed).toBe(block);
      expect(result.remaining).toBe('');
    });

    it('rejects <invoke> with path "<path>" — 0 calls', () => {
      const block = '<invoke name="view_file"><parameter name="path"><path></parameter></invoke>';
      const result = extractToolCallBlocks(block);
      expect(result.calls).toEqual([]);
      expect(result.flushed).toBe(block);
    });

    it('returns 1 valid call and flushes placeholder block when mixed in same buffer', () => {
      const valid =
        '<invoke name="view_file"><parameter name="path">/opt/boocode/README.md</parameter></invoke>';
      const placeholder =
        '<invoke name="view_file"><parameter name="path">...</parameter></invoke>';
      const result = extractToolCallBlocks(`${valid} tail ${placeholder}`);
      expect(result.calls).toEqual([{ name: 'view_file', args: { path: '/opt/boocode/README.md' } }]);
      expect(result.flushed).toContain('tail');
      expect(result.flushed).toContain(placeholder);
      expect(result.remaining).toBe('');
    });
  });
});

describe('stripToolMarkup', () => {
  it('strips closed <tool_call> blocks', () => {
    const input = 'before <tool_call>{"name":"x"}</tool_call> after';
    expect(stripToolMarkup(input)).toBe('before  after');
  });

  it('strips closed <function=...> blocks', () => {
    const input = 'before <function=x><parameter=y>z</parameter></function> after';
    expect(stripToolMarkup(input)).toBe('before  after');
  });

  it('strips closed <invoke> blocks', () => {
    const input = 'before <invoke name="x"><parameter name="y">z</parameter></invoke> after';
    expect(stripToolMarkup(input)).toBe('before  after');
  });

  it('leaves trailing unclosed block when final=false', () => {
    const input = 'text <tool_call>{"name":"x"';
    expect(stripToolMarkup(input)).toBe('text <tool_call>{"name":"x"');
  });

  it('strips trailing unclosed <tool_call> when final=true', () => {
    const input = 'text <tool_call>{"name":"x"';
    expect(stripToolMarkup(input, { final: true })).toBe('text');
  });

  it('strips trailing unclosed <function= when final=true', () => {
    const input = 'text <function=run_bash><parameter=command>ls';
    expect(stripToolMarkup(input, { final: true })).toBe('text');
  });

  it('strips trailing unclosed <invoke when final=true', () => {
    const input = 'text <invoke name="x"><parameter name="y">val';
    expect(stripToolMarkup(input, { final: true })).toBe('text');
  });

  it('trims whitespace when final=true', () => {
    const input = '  text  <tool_call>partial';
    expect(stripToolMarkup(input, { final: true })).toBe('text');
  });

  it('strips multiple closed blocks', () => {
    const input = '<tool_call>a</tool_call> mid <tool_call>b</tool_call>';
    expect(stripToolMarkup(input)).toBe(' mid ');
  });
});

