"""Tests for the transcribe agent."""

from __future__ import annotations

import asyncio
import json
import logging
from contextlib import nullcontext
from datetime import UTC, datetime, timedelta
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from agent_cli import config
from agent_cli.agents import transcribe
from tests.mocks.wyoming import MockASRClient


@pytest.mark.asyncio
@patch("agent_cli.services.asr.open_audio_stream")
@patch("agent_cli.services.asr.setup_input_stream")
@patch("agent_cli.agents.transcribe.process_and_update_clipboard", new_callable=AsyncMock)
@patch("agent_cli.services.asr.wyoming_client_context")
@patch("pyperclip.copy")
@patch("pyperclip.paste")
@patch("agent_cli.agents.transcribe.signal_handling_context")
async def test_transcribe_main_llm_enabled(
    mock_signal_handling_context: MagicMock,
    mock_pyperclip_paste: MagicMock,
    mock_pyperclip_copy: MagicMock,
    mock_wyoming_client_context: MagicMock,
    mock_process_and_update_clipboard: AsyncMock,
    mock_setup_input_stream: MagicMock,
    mock_open_audio_stream: MagicMock,
    caplog: pytest.LogCaptureFixture,
) -> None:
    """Test the main function of the transcribe agent with LLM enabled."""
    # Mock audio stream
    mock_stream = MagicMock()
    mock_stream.read.return_value = (MagicMock(tobytes=lambda: b"\0" * 1024), False)
    mock_open_audio_stream.return_value.__enter__.return_value = mock_stream
    assert mock_setup_input_stream  # Used to satisfy linter

    # Mock the Wyoming client
    mock_asr_client = MockASRClient("hello world")
    mock_wyoming_client_context.return_value.__aenter__.return_value = mock_asr_client

    # Setup stop event
    stop_event = asyncio.Event()
    mock_signal_handling_context.return_value.__enter__.return_value = stop_event
    asyncio.get_event_loop().call_later(0.1, stop_event.set)

    mock_pyperclip_paste.return_value = ""

    # The function we are testing
    with caplog.at_level(logging.INFO):
        provider_cfg = config.ProviderSelection(
            asr_provider="wyoming",
            llm_provider="ollama",
            tts_provider="wyoming",
        )
        general_cfg = config.General(
            log_level="INFO",
            log_file=None,
            quiet=True,
            list_devices=False,
            clipboard=True,
        )
        audio_in_cfg = config.AudioInput()
        wyoming_asr_cfg = config.WyomingASR(asr_wyoming_ip="localhost", asr_wyoming_port=12345)
        openai_asr_cfg = config.OpenAIASR(asr_openai_model="whisper-1")
        gemini_asr_cfg = config.GeminiASR(
            asr_gemini_model="gemini-2.0-flash",
            gemini_api_key="test-key",
        )
        ollama_cfg = config.Ollama(llm_ollama_model="test", llm_ollama_host="localhost")
        openai_llm_cfg = config.OpenAILLM(llm_openai_model="gpt-4", openai_base_url=None)
        gemini_llm_cfg = config.GeminiLLM(
            llm_gemini_model="gemini-1.5-flash",
            gemini_api_key="test-key",
        )

        await transcribe._async_main(
            extra_instructions=None,
            provider_cfg=provider_cfg,
            general_cfg=general_cfg,
            audio_in_cfg=audio_in_cfg,
            wyoming_asr_cfg=wyoming_asr_cfg,
            openai_asr_cfg=openai_asr_cfg,
            gemini_asr_cfg=gemini_asr_cfg,
            ollama_cfg=ollama_cfg,
            openai_llm_cfg=openai_llm_cfg,
            gemini_llm_cfg=gemini_llm_cfg,
            llm_enabled=True,
            transcription_log=None,
            save_recording=False,  # Disable for testing
        )

    # Assertions
    mock_process_and_update_clipboard.assert_called_once()
    mock_pyperclip_copy.assert_called_once_with("hello world")
    mock_pyperclip_paste.assert_called_once()
    assert "Copied raw transcript to clipboard before LLM processing." in caplog.text


@pytest.mark.asyncio
@patch("agent_cli.services.asr.open_audio_stream")
@patch("agent_cli.services.asr.setup_input_stream")
@patch("agent_cli.services.asr.wyoming_client_context")
@patch("pyperclip.copy")
@patch("agent_cli.agents.transcribe.signal_handling_context")
async def test_transcribe_main(
    mock_signal_handling_context: MagicMock,
    mock_pyperclip_copy: MagicMock,
    mock_wyoming_client_context: MagicMock,
    mock_setup_input_stream: MagicMock,
    mock_open_audio_stream: MagicMock,
    caplog: pytest.LogCaptureFixture,
) -> None:
    """Test the main function of the transcribe agent."""
    # Mock audio stream
    mock_stream = MagicMock()
    mock_stream.read.return_value = (MagicMock(tobytes=lambda: b"\0" * 1024), False)
    mock_open_audio_stream.return_value.__enter__.return_value = mock_stream
    assert mock_setup_input_stream  # Used to satisfy linter

    # Mock the Wyoming client
    mock_asr_client = MockASRClient("hello world")
    mock_wyoming_client_context.return_value.__aenter__.return_value = mock_asr_client

    # Setup stop event
    stop_event = asyncio.Event()
    mock_signal_handling_context.return_value.__enter__.return_value = stop_event
    asyncio.get_event_loop().call_later(0.1, stop_event.set)

    # The function we are testing
    with caplog.at_level(logging.INFO):
        provider_cfg = config.ProviderSelection(
            asr_provider="wyoming",
            llm_provider="ollama",
            tts_provider="wyoming",
        )
        general_cfg = config.General(
            log_level="INFO",
            log_file=None,
            quiet=True,
            list_devices=False,
            clipboard=True,
        )
        audio_in_cfg = config.AudioInput()
        wyoming_asr_cfg = config.WyomingASR(asr_wyoming_ip="localhost", asr_wyoming_port=12345)
        openai_asr_cfg = config.OpenAIASR(asr_openai_model="whisper-1")
        gemini_asr_cfg = config.GeminiASR(
            asr_gemini_model="gemini-2.0-flash",
            gemini_api_key="test-key",
        )
        ollama_cfg = config.Ollama(llm_ollama_model="", llm_ollama_host="")
        openai_llm_cfg = config.OpenAILLM(llm_openai_model="", openai_base_url=None)
        gemini_llm_cfg = config.GeminiLLM(
            llm_gemini_model="gemini-1.5-flash",
            gemini_api_key="test-key",
        )

        await transcribe._async_main(
            extra_instructions=None,
            provider_cfg=provider_cfg,
            general_cfg=general_cfg,
            audio_in_cfg=audio_in_cfg,
            wyoming_asr_cfg=wyoming_asr_cfg,
            openai_asr_cfg=openai_asr_cfg,
            gemini_asr_cfg=gemini_asr_cfg,
            ollama_cfg=ollama_cfg,
            openai_llm_cfg=openai_llm_cfg,
            gemini_llm_cfg=gemini_llm_cfg,
            llm_enabled=False,
            transcription_log=None,
            save_recording=False,  # Disable for testing
        )

    # Assertions
    assert "Copied transcript to clipboard." in caplog.text
    mock_pyperclip_copy.assert_called_once_with("hello world")
    mock_wyoming_client_context.assert_called_once()


@pytest.mark.asyncio
@patch("agent_cli.agents.transcribe.signal_handling_context")
@patch("agent_cli.agents.transcribe.maybe_live")
@patch("agent_cli.agents.transcribe.asr.create_transcriber")
async def test_live_preview_console_disables_rich_live_status(
    mock_create_transcriber: MagicMock,
    mock_maybe_live: MagicMock,
    mock_signal_handling_context: MagicMock,
) -> None:
    """Console live preview should not be hidden by the Rich live status."""
    mock_maybe_live.return_value = nullcontext(None)
    mock_create_transcriber.return_value = AsyncMock(return_value="hello world")
    mock_signal_handling_context.return_value.__enter__.return_value = asyncio.Event()

    await transcribe._async_main(
        extra_instructions=None,
        provider_cfg=config.ProviderSelection(
            asr_provider="wyoming",
            llm_provider="ollama",
            tts_provider="wyoming",
        ),
        general_cfg=config.General(
            log_level="INFO",
            log_file=None,
            quiet=False,
            list_devices=False,
            clipboard=False,
        ),
        audio_in_cfg=config.AudioInput(),
        wyoming_asr_cfg=config.WyomingASR(asr_wyoming_ip="localhost", asr_wyoming_port=12345),
        openai_asr_cfg=config.OpenAIASR(asr_openai_model="whisper-1"),
        gemini_asr_cfg=config.GeminiASR(
            asr_gemini_model="gemini-2.0-flash",
            gemini_api_key="test-key",
        ),
        ollama_cfg=config.Ollama(llm_ollama_model="", llm_ollama_host=""),
        openai_llm_cfg=config.OpenAILLM(llm_openai_model="", openai_base_url=None),
        gemini_llm_cfg=config.GeminiLLM(
            llm_gemini_model="gemini-1.5-flash",
            gemini_api_key="test-key",
        ),
        llm_enabled=False,
        transcription_log=None,
        emit_output=False,
        live_preview_console=True,
    )

    mock_maybe_live.assert_called_once_with(False)


@pytest.mark.asyncio
@patch("agent_cli.agents.transcribe.signal_handling_context")
@patch("agent_cli.agents.transcribe.maybe_live")
@patch("agent_cli.agents.transcribe.asr.create_transcriber")
async def test_live_preview_console_keeps_rich_live_status_for_non_wyoming_provider(
    mock_create_transcriber: MagicMock,
    mock_maybe_live: MagicMock,
    mock_signal_handling_context: MagicMock,
) -> None:
    """Console preview should only hide Rich status when preview can run."""
    mock_maybe_live.return_value = nullcontext(None)
    mock_create_transcriber.return_value = AsyncMock(return_value="hello world")
    mock_signal_handling_context.return_value.__enter__.return_value = asyncio.Event()

    await transcribe._async_main(
        extra_instructions=None,
        provider_cfg=config.ProviderSelection(
            asr_provider="openai",
            llm_provider="ollama",
            tts_provider="wyoming",
        ),
        general_cfg=config.General(
            log_level="INFO",
            log_file=None,
            quiet=False,
            list_devices=False,
            clipboard=False,
        ),
        audio_in_cfg=config.AudioInput(),
        wyoming_asr_cfg=config.WyomingASR(asr_wyoming_ip="localhost", asr_wyoming_port=12345),
        openai_asr_cfg=config.OpenAIASR(asr_openai_model="whisper-1"),
        gemini_asr_cfg=config.GeminiASR(
            asr_gemini_model="gemini-2.0-flash",
            gemini_api_key="test-key",
        ),
        ollama_cfg=config.Ollama(llm_ollama_model="", llm_ollama_host=""),
        openai_llm_cfg=config.OpenAILLM(llm_openai_model="", openai_base_url=None),
        gemini_llm_cfg=config.GeminiLLM(
            llm_gemini_model="gemini-1.5-flash",
            gemini_api_key="test-key",
        ),
        llm_enabled=False,
        transcription_log=None,
        emit_output=False,
        live_preview_console=True,
    )

    mock_maybe_live.assert_called_once_with(True)
    mock_create_transcriber.return_value.assert_awaited_once()
    assert mock_create_transcriber.return_value.await_args.kwargs["live_preview_config"] is None


@pytest.mark.asyncio
@patch("agent_cli.agents.transcribe.create_recorded_audio_transcriber")
@patch("agent_cli.agents.transcribe.load_audio_from_file")
@patch("agent_cli.agents.transcribe.maybe_live")
async def test_live_preview_console_keeps_rich_live_status_for_file_transcription(
    mock_maybe_live: MagicMock,
    mock_load_audio_from_file: MagicMock,
    mock_create_recorded_audio_transcriber: MagicMock,
    tmp_path: Path,
) -> None:
    """File transcription cannot emit console preview, so Rich status stays visible."""
    audio_file = tmp_path / "sample.wav"
    audio_file.write_bytes(b"audio")
    mock_maybe_live.return_value = nullcontext(None)
    mock_load_audio_from_file.return_value = b"audio"
    mock_create_recorded_audio_transcriber.return_value = AsyncMock(return_value="hello world")

    await transcribe._async_main(
        audio_file_path=audio_file,
        extra_instructions=None,
        provider_cfg=config.ProviderSelection(
            asr_provider="wyoming",
            llm_provider="ollama",
            tts_provider="wyoming",
        ),
        general_cfg=config.General(
            log_level="INFO",
            log_file=None,
            quiet=False,
            list_devices=False,
            clipboard=False,
        ),
        wyoming_asr_cfg=config.WyomingASR(asr_wyoming_ip="localhost", asr_wyoming_port=12345),
        openai_asr_cfg=config.OpenAIASR(asr_openai_model="whisper-1"),
        gemini_asr_cfg=config.GeminiASR(
            asr_gemini_model="gemini-2.0-flash",
            gemini_api_key="test-key",
        ),
        ollama_cfg=config.Ollama(llm_ollama_model="", llm_ollama_host=""),
        openai_llm_cfg=config.OpenAILLM(llm_openai_model="", openai_base_url=None),
        gemini_llm_cfg=config.GeminiLLM(
            llm_gemini_model="gemini-1.5-flash",
            gemini_api_key="test-key",
        ),
        llm_enabled=False,
        transcription_log=None,
        emit_output=False,
        live_preview_console=True,
    )

    mock_maybe_live.assert_called_once_with(True)


def test_log_transcription(tmp_path: Path) -> None:
    """Test the log_transcription function."""
    log_file = tmp_path / "test_log.jsonl"

    # Test logging without processed transcript
    transcribe.log_transcription(
        log_file=log_file,
        role="user",
        raw_transcript="hello world",
        processed_transcript=None,
        model_info="local:whisper",
    )

    # Test logging with processed transcript
    transcribe.log_transcription(
        log_file=log_file,
        role="assistant",
        raw_transcript="hello world",
        processed_transcript="Hello, world!",
        model_info="local:gemma3:4b",
    )

    # Read and verify log entries
    with log_file.open("r") as f:
        log_entries = [json.loads(line.strip()) for line in f]

    assert len(log_entries) == 2

    # Check first entry (user/raw)
    first_entry = log_entries[0]
    assert first_entry["role"] == "user"
    assert first_entry["model"] == "local:whisper"
    assert first_entry["raw_output"] == "hello world"
    assert first_entry["processed_output"] is None
    assert "timestamp" in first_entry
    assert "hostname" in first_entry

    # Check second entry (assistant/processed)
    second_entry = log_entries[1]
    assert second_entry["role"] == "assistant"
    assert second_entry["model"] == "local:gemma3:4b"
    assert second_entry["raw_output"] == "hello world"
    assert second_entry["processed_output"] == "Hello, world!"
    assert "timestamp" in second_entry
    assert "hostname" in second_entry


def test_log_transcription_creates_parent_dirs(tmp_path: Path) -> None:
    """Transcription logging should create missing parent directories."""
    log_file = tmp_path / "missing" / "parents" / "transcriptions.jsonl"

    transcribe.log_transcription(
        log_file=log_file,
        role="user",
        raw_transcript="hello world",
    )

    assert log_file.exists()


def test_gather_recent_transcription_context(tmp_path: Path) -> None:
    """Ensure only recent log entries are used for context."""
    log_file = tmp_path / "transcriptions.jsonl"
    now = datetime(2024, 1, 1, 12, 0, tzinfo=UTC)

    entries = [
        {
            "timestamp": (now - timedelta(minutes=70)).isoformat(),
            "role": "assistant",
            "processed_output": "Too old",
        },
        {
            "timestamp": (now - timedelta(minutes=30)).isoformat(),
            "role": "user",
            "raw_output": "Keep this one",
        },
        {
            "timestamp": (now - timedelta(minutes=5)).isoformat(),
            "role": "assistant",
            "processed_output": "Also keep this",
        },
    ]

    with log_file.open("w", encoding="utf-8") as f:
        for entry in entries:
            f.write(json.dumps(entry) + "\n")

    context = transcribe._gather_recent_transcription_context(
        log_file,
        now=now,
        max_age_seconds=3600,
        max_entries=2,
    )

    assert context is not None
    lines = context.splitlines()
    assert lines[0].startswith("Recent transcript history")
    assert len(lines) == 2
    assert "Keep this one" in lines[1]


def test_gather_recent_transcription_context_chunked_read(tmp_path: Path) -> None:
    """Ensure we can read recent entries without loading entire file."""
    log_file = tmp_path / "chunked.jsonl"
    now = datetime(2024, 1, 1, 12, 0, tzinfo=UTC)

    with log_file.open("w", encoding="utf-8") as f:
        for i in reversed(range(10)):
            entry = {
                "timestamp": (now - timedelta(minutes=i * 5)).isoformat(),
                "role": "user",
                "raw_output": f"Entry {i}",
                "processed_output": None,
            }
            f.write(json.dumps(entry) + "\n")

    context = transcribe._gather_recent_transcription_context(
        log_file,
        now=now,
        max_age_seconds=3600,
        max_entries=3,
        chunk_size=32,
    )

    assert context is not None
    lines = context.splitlines()
    assert lines[0].startswith("Recent transcript history")
    assert len(lines) == 4
    assert lines[1].endswith("Entry 2")
    assert lines[-1].endswith("Entry 0")


def test_gather_recent_context_prefers_raw(tmp_path: Path) -> None:
    """Ensure raw transcripts are preferred when both raw and cleaned exist."""
    log_file = tmp_path / "prefers_raw.jsonl"
    now = datetime(2024, 1, 1, 12, 0, tzinfo=UTC)

    entry = {
        "timestamp": now.isoformat(),
        "role": "assistant",
        "raw_output": "Raw version",
        "processed_output": "Clean version",
    }
    with log_file.open("w", encoding="utf-8") as f:
        f.write(json.dumps(entry) + "\n")

    context = transcribe._gather_recent_transcription_context(
        log_file,
        now=now,
        max_age_seconds=3600,
        max_entries=1,
    )

    assert context is not None
    assert "Raw version" in context
    assert "Clean version" not in context


@pytest.mark.asyncio
@patch("agent_cli.agents.transcribe.signal_handling_context")
@patch("pyperclip.copy")
@patch("pyperclip.paste")
@patch("agent_cli.services.asr.wyoming_client_context")
@patch("agent_cli.agents.transcribe.process_and_update_clipboard", new_callable=AsyncMock)
@patch("agent_cli.services.asr.open_audio_stream")
@patch("agent_cli.services.asr.setup_input_stream")
async def test_transcribe_includes_clipboard_context(
    mock_setup_input_stream: MagicMock,
    mock_open_audio_stream: MagicMock,
    mock_process_and_update_clipboard: AsyncMock,
    mock_wyoming_client_context: MagicMock,
    mock_pyperclip_paste: MagicMock,
    mock_pyperclip_copy: MagicMock,
    mock_signal_handling_context: MagicMock,
) -> None:
    """Ensure clipboard content is forwarded to the LLM context."""
    # Mock audio stream
    mock_stream = MagicMock()
    mock_stream.read.return_value = (MagicMock(tobytes=lambda: b"\0" * 1024), False)
    mock_open_audio_stream.return_value.__enter__.return_value = mock_stream
    assert mock_setup_input_stream  # Used to satisfy linter
    assert mock_pyperclip_copy  # Used to satisfy linter

    mock_pyperclip_paste.return_value = "Clipboard reference text"

    mock_asr_client = MockASRClient("hello world")
    mock_wyoming_client_context.return_value.__aenter__.return_value = mock_asr_client

    stop_event = asyncio.Event()
    mock_signal_handling_context.return_value.__enter__.return_value = stop_event
    asyncio.get_event_loop().call_later(0.1, stop_event.set)

    provider_cfg = config.ProviderSelection(
        asr_provider="wyoming",
        llm_provider="ollama",
        tts_provider="wyoming",
    )
    general_cfg = config.General(
        log_level="INFO",
        log_file=None,
        quiet=True,
        list_devices=False,
        clipboard=True,
    )
    audio_in_cfg = config.AudioInput()
    wyoming_asr_cfg = config.WyomingASR(asr_wyoming_ip="localhost", asr_wyoming_port=12345)
    openai_asr_cfg = config.OpenAIASR(asr_openai_model="whisper-1")
    gemini_asr_cfg = config.GeminiASR(
        asr_gemini_model="gemini-2.0-flash",
        gemini_api_key="test-key",
    )
    ollama_cfg = config.Ollama(llm_ollama_model="test", llm_ollama_host="localhost")
    openai_llm_cfg = config.OpenAILLM(llm_openai_model="gpt-4", openai_base_url=None)
    gemini_llm_cfg = config.GeminiLLM(
        llm_gemini_model="gemini-1.5-flash",
        gemini_api_key="test-key",
    )

    await transcribe._async_main(
        extra_instructions=None,
        provider_cfg=provider_cfg,
        general_cfg=general_cfg,
        audio_in_cfg=audio_in_cfg,
        wyoming_asr_cfg=wyoming_asr_cfg,
        openai_asr_cfg=openai_asr_cfg,
        gemini_asr_cfg=gemini_asr_cfg,
        ollama_cfg=ollama_cfg,
        openai_llm_cfg=openai_llm_cfg,
        gemini_llm_cfg=gemini_llm_cfg,
        llm_enabled=True,
        transcription_log=None,
        save_recording=False,
    )

    mock_process_and_update_clipboard.assert_called_once()
    context = mock_process_and_update_clipboard.call_args.kwargs["context"]
    assert context is not None
    assert "Clipboard content captured before this recording" in context
    assert "Clipboard reference text" in context


@pytest.mark.asyncio
@patch("agent_cli.agents.transcribe.signal_handling_context")
@patch("pyperclip.copy")
@patch("pyperclip.paste")
@patch("agent_cli.services.asr.wyoming_client_context")
@patch("agent_cli.agents.transcribe.process_and_update_clipboard", new_callable=AsyncMock)
@patch("agent_cli.services.asr.open_audio_stream")
@patch("agent_cli.services.asr.setup_input_stream")
async def test_transcribe_with_logging(
    mock_setup_input_stream: MagicMock,
    mock_open_audio_stream: MagicMock,
    mock_process_and_update_clipboard: AsyncMock,
    mock_wyoming_client_context: MagicMock,
    mock_pyperclip_paste: MagicMock,
    mock_pyperclip_copy: MagicMock,
    mock_signal_handling_context: MagicMock,
    tmp_path: Path,
) -> None:
    """Test transcription with logging enabled."""
    log_file = tmp_path / "transcription.jsonl"

    # Mock audio stream
    mock_stream = MagicMock()
    mock_stream.read.return_value = (MagicMock(tobytes=lambda: b"\0" * 1024), False)
    mock_open_audio_stream.return_value.__enter__.return_value = mock_stream
    assert mock_setup_input_stream  # Used to satisfy linter
    assert mock_pyperclip_copy  # Used to satisfy linter

    # Mock the Wyoming client
    mock_asr_client = MockASRClient("hello world")
    mock_wyoming_client_context.return_value.__aenter__.return_value = mock_asr_client

    # Setup stop event
    stop_event = asyncio.Event()
    mock_signal_handling_context.return_value.__enter__.return_value = stop_event
    asyncio.get_event_loop().call_later(0.1, stop_event.set)

    # Mock clipboard and LLM response
    mock_pyperclip_paste.return_value = ""
    mock_process_and_update_clipboard.return_value = "Hello, world!"

    provider_cfg = config.ProviderSelection(
        asr_provider="wyoming",
        llm_provider="ollama",
        tts_provider="wyoming",
    )
    general_cfg = config.General(
        log_level="INFO",
        log_file=None,
        quiet=True,
        list_devices=False,
        clipboard=True,
    )
    audio_in_cfg = config.AudioInput()
    wyoming_asr_cfg = config.WyomingASR(asr_wyoming_ip="localhost", asr_wyoming_port=12345)
    openai_asr_cfg = config.OpenAIASR(asr_openai_model="whisper-1")
    gemini_asr_cfg = config.GeminiASR(
        asr_gemini_model="gemini-2.0-flash",
        gemini_api_key="test-key",
    )
    ollama_cfg = config.Ollama(llm_ollama_model="gemma3:4b", llm_ollama_host="localhost")
    openai_llm_cfg = config.OpenAILLM(llm_openai_model="gpt-4", openai_base_url=None)
    gemini_llm_cfg = config.GeminiLLM(
        llm_gemini_model="gemini-1.5-flash",
        gemini_api_key="test-key",
    )

    await transcribe._async_main(
        extra_instructions=None,
        provider_cfg=provider_cfg,
        general_cfg=general_cfg,
        audio_in_cfg=audio_in_cfg,
        wyoming_asr_cfg=wyoming_asr_cfg,
        openai_asr_cfg=openai_asr_cfg,
        gemini_asr_cfg=gemini_asr_cfg,
        ollama_cfg=ollama_cfg,
        openai_llm_cfg=openai_llm_cfg,
        gemini_llm_cfg=gemini_llm_cfg,
        llm_enabled=True,
        transcription_log=log_file,
        save_recording=False,  # Disable for testing
    )

    mock_pyperclip_paste.assert_called_once()

    # Verify log file was created and contains expected entry
    assert log_file.exists()

    with log_file.open("r") as f:
        log_entries = [json.loads(line.strip()) for line in f]

    assert len(log_entries) == 1
    entry = log_entries[0]
    assert entry["role"] == "assistant"
    assert entry["model"] == "ollama:gemma3:4b"
    assert entry["raw_output"] == "hello world"
    assert entry["processed_output"] == "Hello, world!"
    assert "timestamp" in entry
    assert "hostname" in entry


def test_transcription_log_path_expansion() -> None:
    """Test that transcription log paths with ~ are expanded."""
    # Create a test case that would use ~ expansion
    home_relative_path = Path("~/test_transcription.log")
    expanded_path = home_relative_path.expanduser()

    # Verify expansion works as expected
    assert home_relative_path.parts[0] == "~"
    assert home_relative_path.parts[-1] == "test_transcription.log"
    assert expanded_path == Path.home() / "test_transcription.log"
    assert expanded_path.is_absolute()

    # Test the actual expansion logic from transcribe function
    test_path = Path("~/test.log")
    expanded = test_path.expanduser()
    assert expanded.is_absolute()
    assert "~" not in str(expanded)
