import { describe, it, expect } from 'vitest';
import { executeTool } from './workspace-task-search.js';
import type { ToolContext } from './core.js';

function ctxWith(io: any): ToolContext {
  return { workspacePath: '/tmp', editAllowed: false, workspaceTaskSearch: io } as any;
}

describe('SearchWorkspaceTasks tool', () => {
  it('returns a friendly no-context message when IO is unset', async () => {
    const res = await executeTool('SearchWorkspaceTasks', { query: 'x' }, ctxWith(undefined));
    expect(res!.isError).toBe(false);
    expect(res!.output).toMatch(/利用できません|利用不可/);
  });

  it('formats hits with taskId, title and excerpt', async () => {
    const io = {
      fts5Available: true,
      search: async () => [{ taskId: 42, taskTitle: '請求書OCR', commentId: 7, author: 'agent', kind: 'result', createdAt: '2026-06-28', text: 'Gitea 認証エラーは再ログインで解消' }],
      around: async () => null,
    };
    const res = await executeTool('SearchWorkspaceTasks', { query: '認証エラー' }, ctxWith(io));
    expect(res!.isError).toBe(false);
    expect(res!.output).toContain('task:42');
    expect(res!.output).toContain('請求書OCR');
    expect(res!.output).toContain('comment:7');
  });

  it('degrades when FTS5 unavailable', async () => {
    const io = { fts5Available: false, search: async () => [], around: async () => null };
    const res = await executeTool('SearchWorkspaceTasks', { query: 'x' }, ctxWith(io));
    expect(res!.output).toMatch(/利用不可|この環境/);
  });

  it('returns null for an unowned tool name', async () => {
    expect(await executeTool('SomethingElse', {}, ctxWith(undefined))).toBeNull();
  });

  it('errors when both query and around_ref are given', async () => {
    const io = { fts5Available: true, search: async () => [], around: async () => null };
    const res = await executeTool('SearchWorkspaceTasks', { query: 'x', around_ref: 'comment:1' }, ctxWith(io));
    expect(res!.isError).toBe(true);
    expect(res!.output).toMatch(/どちらか一方/);
  });

  it('errors when neither query nor around_ref is given', async () => {
    const io = { fts5Available: true, search: async () => [], around: async () => null };
    const res = await executeTool('SearchWorkspaceTasks', {}, ctxWith(io));
    expect(res!.isError).toBe(true);
    expect(res!.output).toMatch(/どちらか一方/);
  });

  it('around_ref renders the comment window with the center marked', async () => {
    const io = {
      fts5Available: true,
      search: async () => [],
      around: async (id: number, before: number, after: number) => ({
        taskId: 9,
        taskTitle: '設計タスク',
        comments: [
          { commentId: 4, author: 'user', kind: 'comment', createdAt: 'd1', body: '前のコメント', isCenter: false },
          { commentId: 5, author: 'user', kind: 'request', createdAt: 'd2', body: '中心の依頼', isCenter: true },
          { commentId: 6, author: 'agent', kind: 'progress', createdAt: 'd3', body: '後のコメント', isCenter: false },
        ],
      }),
    };
    const res = await executeTool('SearchWorkspaceTasks', { around_ref: 'comment:5' }, ctxWith(io));
    expect(res!.isError).toBe(false);
    expect(res!.output).toContain('task:9');
    expect(res!.output).toContain('設計タスク');
    expect(res!.output).toContain('comment:5');
    expect(res!.output).toContain('←'); // center marker
    expect(res!.output).toContain('中心の依頼'); // raw body returned
  });

  it('around_ref with an invalid ref format is an error', async () => {
    const io = { fts5Available: true, search: async () => [], around: async () => null };
    const res = await executeTool('SearchWorkspaceTasks', { around_ref: 'not-a-ref' }, ctxWith(io));
    expect(res!.isError).toBe(true);
    expect(res!.output).toMatch(/不正|comment:/);
  });

  it('around_ref that resolves to null (out of scope) is a friendly non-error', async () => {
    const io = { fts5Available: true, search: async () => [], around: async () => null };
    const res = await executeTool('SearchWorkspaceTasks', { around_ref: 'comment:123' }, ctxWith(io));
    expect(res!.isError).toBe(false);
    expect(res!.output).toMatch(/見つからない|スコープ外/);
  });

  it('centers the excerpt on the first token hit for multi-word (AND) queries', async () => {
    // FTS5 matches tokens non-contiguously, so the excerpt must center on the
    // first token occurrence — not require the whole query as one substring.
    const text = 'x'.repeat(300) + ' 認証エラー が発生、その後 再ログイン で解消 ' + 'y'.repeat(300);
    const io = {
      fts5Available: true,
      search: async () => [{ taskId: 1, taskTitle: 'T', commentId: 2, author: 'agent', kind: 'result', createdAt: 'd', text }],
      around: async () => null,
    };
    const res = await executeTool('SearchWorkspaceTasks', { query: '認証エラー 再ログイン' }, ctxWith(io));
    expect(res!.isError).toBe(false);
    // The excerpt line (2-space indent) must show the hit, not the head of the body.
    const excerptLine = res!.output.split('\n').find((l) => l.startsWith('  ') && !l.startsWith('    '));
    expect(excerptLine).toBeDefined();
    expect(excerptLine!).toContain('認証エラー');
  });

  it('context>0 attaches neighbor comments under each hit', async () => {
    let aroundCalledWith: { id: number; before: number; after: number } | null = null;
    const io = {
      fts5Available: true,
      search: async () => [{ taskId: 42, taskTitle: '請求書OCR', commentId: 7, author: 'agent', kind: 'result', createdAt: '2026-06-28', text: '認証エラーは再ログインで解消' }],
      around: async (id: number, before: number, after: number) => {
        aroundCalledWith = { id, before, after };
        return {
          taskId: 42,
          taskTitle: '請求書OCR',
          comments: [
            { commentId: 6, author: 'user', kind: 'request', createdAt: 'd0', body: '直前のコメント本文', isCenter: false },
            { commentId: 7, author: 'agent', kind: 'result', createdAt: '2026-06-28', body: '認証エラーは再ログインで解消', isCenter: true },
            { commentId: 8, author: 'agent', kind: 'progress', createdAt: 'd2', body: '直後のコメント本文', isCenter: false },
          ],
        };
      },
    };
    const res = await executeTool('SearchWorkspaceTasks', { query: '認証エラー', context: 2 }, ctxWith(io));
    expect(res!.isError).toBe(false);
    // around() was invoked with the hit's commentId and the clamped context window
    expect(aroundCalledWith).toEqual({ id: 7, before: 2, after: 2 });
    // neighbors rendered (indented comment lines for the surrounding comments)
    expect(res!.output).toContain('comment:6');
    expect(res!.output).toContain('comment:8');
    expect(res!.output).toContain('直前のコメント本文');
    expect(res!.output).toContain('直後のコメント本文');
  });
});
