"""Test that agents respond correctly to voice transcriptions in threads."""

from __future__ import annotations

import tempfile
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch

import nio
import pytest
from agno.media import Audio

from mindroom.bot import ROUTER_AGENT_NAME, AgentBot
from mindroom.config.main import Config
from mindroom.constants import (
    ATTACHMENT_IDS_KEY,
    ORIGINAL_SENDER_KEY,
    VOICE_PREFIX,
    VOICE_RAW_AUDIO_FALLBACK_KEY,
)
from mindroom.matrix.identity import MatrixID
from mindroom.matrix.users import AgentMatrixUser
from mindroom.teams import TeamFormationDecision, TeamMode
from tests.conftest import TEST_ACCESS_TOKEN, TEST_PASSWORD


def _extract_agent_side_effect(user_id: str, config: Config) -> str | None:  # noqa: ARG001
    if user_id == f"@mindroom_{ROUTER_AGENT_NAME}:localhost":
        return ROUTER_AGENT_NAME
    if user_id == "@mindroom_home:localhost":
        return "home"
    return None


@pytest.fixture
def mock_home_bot() -> AgentBot:
    """Create a mock home assistant bot for testing."""
    agent_user = AgentMatrixUser(
        agent_name="home",
        user_id="@mindroom_home:localhost",
        display_name="HomeAssistant",
        password=TEST_PASSWORD,
        access_token=TEST_ACCESS_TOKEN,
    )
    config = Config.from_yaml()
    with tempfile.TemporaryDirectory() as tmpdir:
        bot = AgentBot(agent_user=agent_user, storage_path=Path(tmpdir), config=config, rooms=["!test:server"])
    bot.client = AsyncMock()
    bot.logger = MagicMock()
    bot._generate_response = AsyncMock()
    bot.response_tracker = MagicMock()
    bot.response_tracker.has_responded.return_value = False
    return bot


@pytest.mark.asyncio
async def test_agent_responds_to_voice_transcription_in_thread(mock_home_bot: AgentBot) -> None:
    """Test that agents respond to router's voice transcriptions in threads.

    Scenario:
    1. User starts a thread asking HomeAssistant something
    2. HomeAssistant responds in the thread
    3. User sends a voice message with "turn on the lights"
    4. Router transcribes it as "🎤 turn on the lights"
    5. HomeAssistant should respond (as the only agent in thread)
    """
    bot = mock_home_bot
    room = MagicMock(spec=nio.MatrixRoom)
    room.room_id = "!test:server"
    room.canonical_alias = None

    # Create a voice transcription message from the router
    voice_transcription_event = MagicMock(spec=nio.RoomMessageText)
    voice_transcription_event.event_id = "$transcription123"
    voice_transcription_event.sender = f"@mindroom_{ROUTER_AGENT_NAME}:localhost"  # Router's user ID
    voice_transcription_event.body = f"{VOICE_PREFIX}turn on the guest room lights"
    voice_transcription_event.source = {
        "content": {
            "body": f"{VOICE_PREFIX}turn on the guest room lights",
            ORIGINAL_SENDER_KEY: "@user:example.com",
            "m.relates_to": {
                "rel_type": "m.thread",
                "event_id": "$thread_root",
            },
        },
    }

    # Mock thread history showing HomeAssistant has participated
    thread_history = [
        {
            "event_id": "$thread_root",
            "sender": "@user:example.com",
            "content": {"body": "@home what lights are on?"},
        },
        {
            "event_id": "$home_response",
            "sender": "@mindroom_home:localhost",  # HomeAssistant's response
            "content": {"body": "The living room and kitchen lights are currently on."},
        },
    ]

    # Mock context extraction
    with (
        patch("mindroom.bot.fetch_thread_history", return_value=thread_history),
        patch("mindroom.bot.extract_agent_name") as mock_extract_agent,
        patch("mindroom.bot.get_agents_in_thread", return_value=[MatrixID.parse("@mindroom_home:localhost")]),
        patch("mindroom.bot.should_agent_respond", return_value=True),  # HomeAssistant should respond
    ):
        mock_extract_agent.side_effect = _extract_agent_side_effect

        # Process the voice transcription
        await bot._on_message(room, voice_transcription_event)

        # Verify that HomeAssistant generates a response
        bot._generate_response.assert_called_once()
        call_kwargs = bot._generate_response.call_args[1]

        # Should be responding to the voice transcription
        assert call_kwargs["prompt"] == f"{VOICE_PREFIX}turn on the guest room lights"
        assert call_kwargs["reply_to_event_id"] == "$transcription123"
        assert call_kwargs["thread_id"] == "$thread_root"


@pytest.mark.asyncio
async def test_voice_transcription_permissions_use_original_sender(mock_home_bot: AgentBot) -> None:
    """Per-user reply permissions should apply to the original voice sender, not router."""
    bot = mock_home_bot
    room = MagicMock(spec=nio.MatrixRoom)
    room.room_id = "!test:server"
    room.canonical_alias = None
    room.users = {
        "@mindroom_home:localhost": MagicMock(),
        f"@mindroom_{ROUTER_AGENT_NAME}:localhost": MagicMock(),
    }

    # Allow only Alice for this agent.
    bot.config.authorization.agent_reply_permissions = {"home": ["@alice:localhost"]}

    # Router-posted transcription carrying original sender metadata (Bob).
    voice_transcription_event = MagicMock(spec=nio.RoomMessageText)
    voice_transcription_event.event_id = "$transcription_permissions"
    voice_transcription_event.sender = f"@mindroom_{ROUTER_AGENT_NAME}:localhost"
    voice_transcription_event.body = f"{VOICE_PREFIX}turn on the guest room lights"
    voice_transcription_event.source = {
        "content": {
            "body": f"{VOICE_PREFIX}turn on the guest room lights",
            ORIGINAL_SENDER_KEY: "@bob:localhost",
            "m.relates_to": {
                "rel_type": "m.thread",
                "event_id": "$thread_root",
            },
        },
    }

    thread_history = [
        {
            "event_id": "$thread_root",
            "sender": "@user:localhost",
            "content": {"body": "@home status?"},
        },
        {
            "event_id": "$home_response",
            "sender": "@mindroom_home:localhost",
            "content": {"body": "All good"},
        },
    ]

    with (
        patch("mindroom.bot.fetch_thread_history", return_value=thread_history),
        patch("mindroom.bot.decide_team_formation", new_callable=AsyncMock) as mock_decide_team,
        patch("mindroom.bot.extract_agent_name") as mock_extract_agent,
    ):
        mock_decide_team.return_value = TeamFormationDecision(
            should_form_team=False,
            agents=[],
            mode=TeamMode.COLLABORATE,
        )

        mock_extract_agent.side_effect = _extract_agent_side_effect

        await bot._on_message(room, voice_transcription_event)

    # Bob is disallowed, so no reply should be generated.
    bot._generate_response.assert_not_called()


@pytest.mark.asyncio
async def test_agent_ignores_non_voice_router_messages(mock_home_bot: AgentBot) -> None:
    """Test that agents still ignore regular router messages (not voice)."""
    bot = mock_home_bot
    room = MagicMock(spec=nio.MatrixRoom)
    room.room_id = "!test:server"
    room.canonical_alias = None

    # Create a regular message from the router (not voice)
    router_message = MagicMock(spec=nio.RoomMessageText)
    router_message.event_id = "$router_msg"
    router_message.sender = f"@mindroom_{ROUTER_AGENT_NAME}:localhost"
    router_message.body = "I'll help you with that"  # No voice prefix
    router_message.source = {
        "content": {
            "body": "I'll help you with that",
            "m.relates_to": {
                "rel_type": "m.thread",
                "event_id": "$thread_root",
            },
        },
    }

    # Mock context extraction
    with (
        patch("mindroom.bot.fetch_thread_history", return_value=[]),
        patch("mindroom.bot.extract_agent_name") as mock_extract_agent,
    ):
        mock_extract_agent.side_effect = _extract_agent_side_effect

        # Process the regular router message
        await bot._on_message(room, router_message)

        # Verify that HomeAssistant does NOT generate a response
        bot._generate_response.assert_not_called()


@pytest.mark.asyncio
async def test_agent_ignores_visible_router_voice_echo(mock_home_bot: AgentBot) -> None:
    """Display-only router voice echoes should not trigger a second agent reply."""
    bot = mock_home_bot
    bot._derive_conversation_context = AsyncMock(return_value=(False, None, []))
    room = MagicMock(spec=nio.MatrixRoom)
    room.room_id = "!test:server"
    room.canonical_alias = None

    router_voice_echo = MagicMock(spec=nio.RoomMessageText)
    router_voice_echo.event_id = "$router_voice_echo"
    router_voice_echo.sender = f"@mindroom_{ROUTER_AGENT_NAME}:localhost"
    router_voice_echo.body = f"{VOICE_PREFIX}@home turn on the guest room lights"
    router_voice_echo.source = {
        "content": {
            "body": f"{VOICE_PREFIX}@home turn on the guest room lights",
            "com.mindroom.skip_mentions": True,
        },
    }

    with (
        patch("mindroom.bot.extract_agent_name") as mock_extract_agent,
        patch("mindroom.bot.interactive.handle_text_response", new_callable=AsyncMock),
    ):
        mock_extract_agent.side_effect = _extract_agent_side_effect
        await bot._on_message(room, router_voice_echo)

    bot._generate_response.assert_not_called()


@pytest.mark.asyncio
async def test_agent_receives_thread_audio_on_voice_raw_fallback(mock_home_bot: AgentBot) -> None:
    """Voice fallback relays should pass resolved attachment audio into generation."""
    bot = mock_home_bot
    room = MagicMock(spec=nio.MatrixRoom)
    room.room_id = "!test:server"
    room.canonical_alias = None

    fallback_event = MagicMock(spec=nio.RoomMessageText)
    fallback_event.event_id = "$voice_fallback_event"
    fallback_event.sender = f"@mindroom_{ROUTER_AGENT_NAME}:localhost"
    fallback_event.body = f"{VOICE_PREFIX}[Attached voice message]"
    fallback_event.source = {
        "content": {
            "body": f"{VOICE_PREFIX}[Attached voice message]",
            ORIGINAL_SENDER_KEY: "@user:example.com",
            VOICE_RAW_AUDIO_FALLBACK_KEY: True,
            "m.relates_to": {
                "rel_type": "m.thread",
                "event_id": "$thread_root",
            },
        },
    }

    thread_history = [
        {
            "event_id": "$thread_root",
            "sender": "@user:example.com",
            "content": {"body": "Voice root"},
        },
        {
            "event_id": "$home_response",
            "sender": "@mindroom_home:localhost",
            "content": {"body": "Ready"},
        },
    ]

    with (
        patch("mindroom.bot.fetch_thread_history", return_value=thread_history),
        patch("mindroom.bot.extract_agent_name") as mock_extract_agent,
        patch("mindroom.bot.resolve_thread_attachment_ids", new_callable=AsyncMock, return_value=["att_voice_root"]),
        patch(
            "mindroom.bot.resolve_attachment_media",
            return_value=(
                ["att_voice_root"],
                [Audio(content=b"voice-bytes", mime_type="audio/ogg")],
                [],
                [],
                [],
            ),
        ),
        patch("mindroom.bot.should_agent_respond", return_value=True),
    ):
        mock_extract_agent.side_effect = _extract_agent_side_effect

        await bot._on_message(room, fallback_event)

    bot._generate_response.assert_called_once()
    call_kwargs = bot._generate_response.call_args.kwargs
    assert call_kwargs["media"].audio
    assert call_kwargs["prompt"].startswith(f"{VOICE_PREFIX}[Attached voice message]")
    assert "att_voice_root" in call_kwargs["prompt"]


@pytest.mark.asyncio
async def test_agent_voice_fallback_uses_attachment_audio_without_refetch(mock_home_bot: AgentBot) -> None:
    """Fallback relays with attachment audio should skip thread-audio redownload."""
    bot = mock_home_bot
    room = MagicMock(spec=nio.MatrixRoom)
    room.room_id = "!test:server"
    room.canonical_alias = None

    fallback_event = MagicMock(spec=nio.RoomMessageText)
    fallback_event.event_id = "$voice_fallback_attached"
    fallback_event.sender = f"@mindroom_{ROUTER_AGENT_NAME}:localhost"
    fallback_event.body = f"{VOICE_PREFIX}[Attached voice message]"
    fallback_event.source = {
        "content": {
            "body": f"{VOICE_PREFIX}[Attached voice message]",
            ORIGINAL_SENDER_KEY: "@user:example.com",
            VOICE_RAW_AUDIO_FALLBACK_KEY: True,
            ATTACHMENT_IDS_KEY: ["att_voice"],
            "m.relates_to": {
                "rel_type": "m.thread",
                "event_id": "$thread_root",
            },
        },
    }

    thread_history = [
        {
            "event_id": "$thread_root",
            "sender": "@user:example.com",
            "content": {"body": "Voice root"},
        },
        {
            "event_id": "$home_response",
            "sender": "@mindroom_home:localhost",
            "content": {"body": "Ready"},
        },
    ]
    attachment_audio = [Audio(content=b"voice-bytes", mime_type="audio/ogg")]

    with (
        patch("mindroom.bot.fetch_thread_history", return_value=thread_history),
        patch("mindroom.bot.extract_agent_name") as mock_extract_agent,
        patch("mindroom.bot.resolve_attachment_media", return_value=(["att_voice"], attachment_audio, [], [], [])),
        patch("mindroom.bot.resolve_thread_attachment_ids", new_callable=AsyncMock, return_value=[]),
        patch("mindroom.bot.should_agent_respond", return_value=True),
    ):
        mock_extract_agent.side_effect = _extract_agent_side_effect

        await bot._on_message(room, fallback_event)

    bot._generate_response.assert_called_once()
    call_kwargs = bot._generate_response.call_args.kwargs
    assert list(call_kwargs["media"].audio) == attachment_audio
    assert "att_voice" in call_kwargs["prompt"]


@pytest.mark.asyncio
async def test_followup_text_in_voice_thread_recovers_audio(mock_home_bot: AgentBot) -> None:
    """A follow-up text message in a voice-fallback thread should recover the audio attachment."""
    bot = mock_home_bot
    room = MagicMock(spec=nio.MatrixRoom)
    room.room_id = "!test:server"
    room.canonical_alias = None

    # Simulate a plain follow-up text in a thread rooted at a voice event.
    followup_event = MagicMock(spec=nio.RoomMessageText)
    followup_event.event_id = "$followup_text"
    followup_event.sender = "@user:example.com"
    followup_event.body = "Can you repeat that?"
    followup_event.source = {
        "content": {
            "body": "Can you repeat that?",
            "m.relates_to": {
                "rel_type": "m.thread",
                "event_id": "$voice_root",
            },
        },
    }

    thread_history = [
        {
            "event_id": "$voice_root",
            "sender": "@user:example.com",
            "content": {"body": "audio.ogg"},
        },
        {
            "event_id": "$fallback_relay",
            "sender": f"@mindroom_{ROUTER_AGENT_NAME}:localhost",
            "content": {"body": f"{VOICE_PREFIX}[Attached voice message]"},
        },
        {
            "event_id": "$home_response",
            "sender": "@mindroom_home:localhost",
            "content": {"body": "I heard you"},
        },
    ]

    # The voice root is an audio event; resolve_thread_attachment_ids should
    # discover the existing audio attachment record registered by the voice
    # fallback handler.
    attachment_audio = [Audio(content=b"voice-bytes", mime_type="audio/ogg")]

    with (
        patch("mindroom.bot.fetch_thread_history", return_value=thread_history),
        patch("mindroom.bot.extract_agent_name") as mock_extract_agent,
        patch(
            "mindroom.bot.resolve_thread_attachment_ids",
            new_callable=AsyncMock,
            return_value=["att_voiceroot"],
        ),
        patch(
            "mindroom.bot.resolve_attachment_media",
            return_value=(["att_voiceroot"], attachment_audio, [], [], []),
        ),
        patch("mindroom.bot.should_agent_respond", return_value=True),
    ):
        mock_extract_agent.side_effect = _extract_agent_side_effect

        await bot._on_message(room, followup_event)

    bot._generate_response.assert_called_once()
    call_kwargs = bot._generate_response.call_args.kwargs
    # Audio from the voice-root attachment should be present despite no
    # fallback flag on the current event.
    assert list(call_kwargs["media"].audio) == attachment_audio
    assert "att_voiceroot" in call_kwargs["prompt"]
