import { mkdtempSync, rmSync, writeFileSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { ToolContext } from './core.js';
import { executeTool, TOOL_DEFS } from './speech.js';

let ws: string;

beforeEach(() => {
  ws = mkdtempSync(join(tmpdir(), 'speech-test-'));
  writeFileSync(join(ws, 'meeting.mp3'), Buffer.from('fake-mp3-bytes'));
});

afterEach(() => {
  rmSync(ws, { recursive: true, force: true });
  vi.unstubAllGlobals();
});

const ctx = (toolsConfig: Record<string, unknown> | undefined = { speechServerUrl: 'http://stt.local/v1/' }): ToolContext =>
  ({ workspacePath: ws, toolsConfig }) as unknown as ToolContext;

function jsonResponse(body: unknown, status = 200): Response {
  return new Response(JSON.stringify(body), {
    status,
    headers: { 'content-type': 'application/json' },
  });
}

describe('TranscribeAudio', () => {
  it('returns null for unknown tool names', async () => {
    expect(await executeTool('NotSpeech', {}, ctx())).toBeNull();
  });

  it('registers TranscribeAudio with file_path required', () => {
    expect(TOOL_DEFS['TranscribeAudio']?.function.parameters?.['required']).toEqual(['file_path']);
  });

  it('requires file_path', async () => {
    const res = await executeTool('TranscribeAudio', {}, ctx());
    expect(res?.isError).toBe(true);
    expect(res?.output).toContain('file_path は必須');
  });

  it('errors when speech_server_url is not configured', async () => {
    const res = await executeTool('TranscribeAudio', { file_path: 'meeting.mp3' }, ctx({}));
    expect(res?.isError).toBe(true);
    expect(res?.output).toContain('speech_server_url');
  });

  it('rejects unsupported extensions', async () => {
    writeFileSync(join(ws, 'video.mp4'), 'x');
    const res = await executeTool('TranscribeAudio', { file_path: 'video.mp4' }, ctx());
    expect(res?.isError).toBe(true);
    expect(res?.output).toContain('対応フォーマット');
  });

  it('errors for a missing file', async () => {
    const res = await executeTool('TranscribeAudio', { file_path: 'ghost.wav' }, ctx());
    expect(res?.isError).toBe(true);
    expect(res?.output).toContain('ファイルが見つかりません');
  });

  it('blocks paths escaping the workspace', async () => {
    await expect(
      executeTool('TranscribeAudio', { file_path: '../outside.mp3' }, ctx()),
    ).rejects.toThrow();
  });

  it('posts the file and formats diarized segments grouped by speaker', async () => {
    const fetchMock = vi.fn().mockImplementation(async () =>
      jsonResponse({
        segments: [
          { speaker: 'A', text: 'こんにちは。' },
          { speaker: 'A', text: '今日の議題です。' },
          { speaker: 'B', text: 'はい。' },
        ],
      }),
    );
    vi.stubGlobal('fetch', fetchMock);

    const res = await executeTool('TranscribeAudio', { file_path: 'meeting.mp3', prompt: '議題' }, ctx());

    expect(res?.isError).toBe(false);
    expect(res?.output).toContain('## 文字起こし結果: meeting.mp3');
    expect(res?.output).toContain('[A] こんにちは。今日の議題です。');
    expect(res?.output).toContain('[B] はい。');

    // URL is normalized (no double slash) and diarization header is sent.
    expect(String(fetchMock.mock.calls[0]?.[0])).toBe('http://stt.local/v1/audio/transcriptions');
    const init = fetchMock.mock.calls[0]?.[1] as RequestInit;
    expect((init.headers as Record<string, string>)['X-Diarize']).toBe('true');
    const form = init.body as FormData;
    expect(form.get('language')).toBe('ja');
    expect(form.get('prompt')).toBe('議題');
    expect(form.get('response_format')).toBe('verbose_json');
  });

  it('joins segments without speaker labels when diarize=false', async () => {
    const fetchMock = vi.fn().mockImplementation(async () =>
      jsonResponse({ segments: [{ text: 'ひとつ。' }, { text: 'ふたつ。' }] }),
    );
    vi.stubGlobal('fetch', fetchMock);

    const res = await executeTool(
      'TranscribeAudio',
      { file_path: 'meeting.mp3', diarize: false },
      ctx(),
    );
    expect(res?.isError).toBe(false);
    expect(res?.output).toContain('ひとつ。ふたつ。');
    expect(res?.output).not.toContain('[Unknown]');
    const init = fetchMock.mock.calls[0]?.[1] as RequestInit;
    expect((init.headers as Record<string, string>)['X-Diarize']).toBeUndefined();
  });

  it('falls back to plain text when no segments are returned', async () => {
    vi.stubGlobal('fetch', vi.fn().mockImplementation(async () => jsonResponse({ text: '全文テキスト' })));
    const res = await executeTool('TranscribeAudio', { file_path: 'meeting.mp3' }, ctx());
    expect(res?.isError).toBe(false);
    expect(res?.output).toContain('全文テキスト');
  });

  it('uses the configured speech language as default', async () => {
    const fetchMock = vi.fn().mockImplementation(async () => jsonResponse({ text: 'hi' }));
    vi.stubGlobal('fetch', fetchMock);
    await executeTool(
      'TranscribeAudio',
      { file_path: 'meeting.mp3' },
      ctx({ speechServerUrl: 'http://stt.local', speechLanguage: 'en' }),
    );
    const form = (fetchMock.mock.calls[0]?.[1] as RequestInit).body as FormData;
    expect(form.get('language')).toBe('en');
  });

  it('reports server errors with status code', async () => {
    vi.stubGlobal('fetch', vi.fn().mockImplementation(async () => new Response('boom', { status: 500 })));
    const res = await executeTool('TranscribeAudio', { file_path: 'meeting.mp3' }, ctx());
    expect(res?.isError).toBe(true);
    expect(res?.output).toContain('音声認識サーバーエラー (500)');
  });

  it('reports an empty transcription as an error', async () => {
    vi.stubGlobal('fetch', vi.fn().mockImplementation(async () => jsonResponse({})));
    const res = await executeTool('TranscribeAudio', { file_path: 'meeting.mp3' }, ctx());
    expect(res?.isError).toBe(true);
    expect(res?.output).toContain('文字起こし結果が空です');
  });

  it('reports connection failures with the server URL', async () => {
    vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('ECONNREFUSED')));
    const res = await executeTool('TranscribeAudio', { file_path: 'meeting.mp3' }, ctx());
    expect(res?.isError).toBe(true);
    expect(res?.output).toContain('音声認識サーバーに接続できません');
    expect(res?.output).toContain('ECONNREFUSED');
  });
});
