"""Tests for Direct Message (DM) functionality."""

from __future__ import annotations

from typing import TYPE_CHECKING
from unittest.mock import AsyncMock, MagicMock, patch

import nio
import pytest

from mindroom.bot import AgentBot
from mindroom.config.agent import AgentConfig
from mindroom.config.main import Config
from mindroom.matrix.cache.thread_history_result import thread_history_result
from mindroom.matrix.client_room_admin import RoomJoinOutcome
from mindroom.matrix.event_info import EventInfo
from mindroom.matrix.identity import MatrixID
from mindroom.matrix.users import AgentMatrixUser
from mindroom.orchestrator import _MultiAgentOrchestrator
from tests.conftest import (
    TEST_PASSWORD,
    agent_response_should_respond,
    bind_runtime_paths,
    drain_coalescing,
    install_generate_response_mock,
    install_runtime_cache_support,
    orchestrator_runtime_paths,
    runtime_paths_for,
    test_runtime_paths,
)
from tests.identity_helpers import entity_ids, persist_entity_accounts

if TYPE_CHECKING:
    from pathlib import Path


def _bind_runtime_paths(config: Config, path: Path) -> Config:
    bound_config = bind_runtime_paths(config, test_runtime_paths(path))
    persist_entity_accounts(bound_config, runtime_paths_for(bound_config))
    return bound_config


def _config(path: Path, **kwargs: object) -> Config:
    return _bind_runtime_paths(Config(**kwargs), path)


class TestDMResponseLogic:
    """Test agent response logic in DM rooms."""

    def test_should_respond_in_dm_mode_no_mention(self, tmp_path: Path) -> None:
        """Test that agents respond in DM mode without mentions."""
        config = _bind_runtime_paths(
            Config(
                agents={"test_agent": AgentConfig(display_name="Test Agent", role="Test")},
            ),
            tmp_path,
        )

        # Mock room with single agent - use the correct domain from config
        room = MagicMock()
        room.room_id = "!dm:localhost"
        # Use the actual MatrixID from config to ensure domain matches
        agent_matrix_id = entity_ids(config, runtime_paths_for(config))["test_agent"].full_id
        room.users = {agent_matrix_id: None}

        # In DM mode, agent should respond when no one else has
        should_respond = agent_response_should_respond(
            agent_name="test_agent",
            am_i_mentioned=False,  # Not mentioned
            is_thread=False,
            room=room,
            thread_history=[],  # No previous messages
            config=config,
            runtime_paths=runtime_paths_for(config),
            mentioned_agents=None,  # No agents mentioned
            sender_id="@user:localhost",
        )

        assert should_respond is True

    def test_should_respond_in_dm_mode_when_mentioned(self, tmp_path: Path) -> None:
        """Test that agents respond in DM mode when mentioned."""
        config = _bind_runtime_paths(
            Config(
                agents={"test_agent": AgentConfig(display_name="Test Agent", role="Test")},
            ),
            tmp_path,
        )

        # Mock room with single agent - use the correct domain from config
        room = MagicMock()
        room.room_id = "!dm:localhost"
        # Use the actual MatrixID from config to ensure domain matches
        agent_matrix_id = entity_ids(config, runtime_paths_for(config))["test_agent"].full_id
        room.users = {agent_matrix_id: None}

        # When mentioned, always respond
        should_respond = agent_response_should_respond(
            agent_name="test_agent",
            am_i_mentioned=True,  # Mentioned
            is_thread=False,
            room=room,
            thread_history=[],
            config=config,
            runtime_paths=runtime_paths_for(config),
            sender_id="@user:localhost",
        )

        assert should_respond is True

    def test_should_not_respond_in_dm_mode_when_other_mentioned(self, tmp_path: Path) -> None:
        """Test that agents don't respond when other agents are mentioned."""
        config = _bind_runtime_paths(
            Config(
                agents={
                    "test_agent": AgentConfig(display_name="Test Agent", role="Test"),
                    "other_agent": AgentConfig(display_name="Other Agent", role="Other"),
                },
            ),
            tmp_path,
        )

        # Mock room with multiple agents - use the correct domains from config
        room = MagicMock()
        room.room_id = "!dm:localhost"
        test_agent_id = entity_ids(config, runtime_paths_for(config))["test_agent"].full_id
        other_agent_id = entity_ids(config, runtime_paths_for(config))["other_agent"].full_id
        room.users = {test_agent_id: None, other_agent_id: None}

        # Another agent is mentioned, not this one
        should_respond = agent_response_should_respond(
            agent_name="test_agent",
            am_i_mentioned=False,
            is_thread=False,
            room=room,
            thread_history=[],
            config=config,
            runtime_paths=runtime_paths_for(config),
            mentioned_agents=[
                entity_ids(config, runtime_paths_for(config))["other_agent"],
            ],  # Other agent mentioned with correct domain
            sender_id="@user:localhost",
        )

        assert should_respond is False

    def test_multi_agent_dm_does_not_respond_individually(self, tmp_path: Path) -> None:
        """Test that multiple agents in DM room without mentions don't respond individually."""
        config = _bind_runtime_paths(
            Config(
                agents={
                    "test_agent": AgentConfig(display_name="Test Agent", role="Test"),
                    "other_agent": AgentConfig(display_name="Other Agent", role="Other"),
                },
            ),
            tmp_path,
        )

        # Mock room with multiple agents - use the correct domains from config
        room = MagicMock()
        room.room_id = "!dm:localhost"
        test_agent_id = entity_ids(config, runtime_paths_for(config))["test_agent"].full_id
        other_agent_id = entity_ids(config, runtime_paths_for(config))["other_agent"].full_id
        room.users = {test_agent_id: None, other_agent_id: None}

        # No mentions - agents should not respond individually (team formation happens at a higher level)
        should_respond_test = agent_response_should_respond(
            agent_name="test_agent",
            am_i_mentioned=False,
            is_thread=False,
            room=room,
            thread_history=[],
            config=config,
            runtime_paths=runtime_paths_for(config),
            mentioned_agents=None,  # No agents mentioned
            sender_id="@user:localhost",
        )

        should_respond_other = agent_response_should_respond(
            agent_name="other_agent",
            am_i_mentioned=False,
            is_thread=False,
            room=room,
            thread_history=[],
            config=config,
            runtime_paths=runtime_paths_for(config),
            mentioned_agents=None,  # No agents mentioned
            sender_id="@user:localhost",
        )

        # Agents should not respond individually - team formation is handled at bot level
        assert should_respond_test is False
        assert should_respond_other is False


@pytest.mark.asyncio
class TestDMMessageContext:
    """Test message context extraction for DMs."""

    async def test_extract_dm_context(self, tmp_path: Path) -> None:
        """Test extracting message context in DM mode."""
        config = _bind_runtime_paths(Config(), tmp_path)

        # Create a bot with mocked components
        # Use the correct MatrixID from config
        test_agent_matrix_id = (
            entity_ids(config, runtime_paths_for(config))["test_agent"]
            if "test_agent" in entity_ids(config, runtime_paths_for(config))
            else MatrixID.parse("@mindroom_test_agent:localhost")
        )
        agent_user = AgentMatrixUser(
            agent_name="test_agent",
            user_id=test_agent_matrix_id.full_id,
            display_name="Test Agent",
            password=TEST_PASSWORD,
        )
        bot = AgentBot(
            agent_user=agent_user,
            storage_path=tmp_path,
            config=config,
            runtime_paths=runtime_paths_for(config),
            rooms=[],  # Not configured for any rooms
        )

        # Mock the client
        bot.client = AsyncMock()
        bot.client.user_id = bot.agent_user.user_id

        # Mock fetch_thread_history - DMs now use threads like regular rooms
        with patch.object(
            bot._conversation_resolver,
            "fetch_thread_history",
            return_value=thread_history_result([], is_full_history=True),
        ) as mock_fetch:
            # Create a test event
            room = MagicMock()
            room.room_id = "!dm:localhost"
            room.name = "DM Room"

            event = MagicMock()
            event.source = {
                "content": {
                    "body": "Hello agent",
                    "m.mentions": {},
                },
            }
            event.event_id = "test_event"

            # Extract context
            context = await bot._conversation_resolver.extract_message_context(room, event)

            # DMs now use threads like regular rooms - no special fetch
            # fetch_thread_history should NOT be called for non-thread messages
            mock_fetch.assert_not_called()
            assert not context.is_thread
            assert context.thread_history == []


@pytest.mark.asyncio
class TestDMIntegration:
    """Integration tests for DM functionality."""

    async def test_agent_accepts_dm_invites(self, tmp_path: Path) -> None:
        """Test that agents accept DM invitations when configured."""
        config = _config(
            tmp_path,
            agents={"test_agent": AgentConfig(display_name="Test Agent", role="Test")},
        )

        # Use the correct MatrixID from config
        test_agent_matrix_id = entity_ids(config, runtime_paths_for(config))["test_agent"]
        agent_user = AgentMatrixUser(
            agent_name="test_agent",
            user_id=test_agent_matrix_id.full_id,
            display_name="Test Agent",
            password=TEST_PASSWORD,
        )

        bot = AgentBot(
            agent_user=agent_user,
            storage_path=tmp_path,
            config=config,
            runtime_paths=runtime_paths_for(config),
            rooms=[],
        )
        install_runtime_cache_support(bot)

        bot.client = AsyncMock()
        bot.client.user_id = bot.agent_user.user_id
        bot.logger = MagicMock()

        # Mock join_room to return success
        with (
            patch("mindroom.bot_room_lifecycle.is_authorized_sender", return_value=True),
            patch(
                "mindroom.bot_room_lifecycle.join_room",
                return_value=RoomJoinOutcome.JOINED,
            ) as mock_join,
        ):
            room = MagicMock()
            room.room_id = "!dm:localhost"
            room.canonical_alias = None
            event = MagicMock()
            event.sender = "@user:localhost"

            await bot._on_invite(room, event)

            mock_join.assert_called_once()
            bot.logger.info.assert_any_call("Joined room", room_id="!dm:localhost")

    async def test_dm_response_flow(self, tmp_path: Path) -> None:
        """Test the complete flow of responding in a DM."""
        # This is a more complex integration test
        orchestrator = _MultiAgentOrchestrator(runtime_paths=orchestrator_runtime_paths(tmp_path))

        config = _config(tmp_path)
        config.agents = {"researcher": MagicMock()}

        # Create and configure a bot
        # Use the correct MatrixID from config
        researcher_matrix_id = (
            entity_ids(config, runtime_paths_for(config))["researcher"]
            if "researcher" in entity_ids(config, runtime_paths_for(config))
            else MatrixID.parse("@mindroom_researcher:localhost")
        )
        agent_user = AgentMatrixUser(
            agent_name="researcher",
            user_id=researcher_matrix_id.full_id,
            display_name="Researcher",
            password=TEST_PASSWORD,
        )

        # Important: bot is NOT configured for the DM room
        bot = AgentBot(
            agent_user=agent_user,
            storage_path=tmp_path,
            config=config,
            runtime_paths=runtime_paths_for(config),
            rooms=[],  # Empty rooms list - not configured for any room
        )

        bot.client = AsyncMock()
        bot.client.user_id = (
            entity_ids(config, runtime_paths_for(config))["researcher"].full_id
            if "researcher" in entity_ids(config, runtime_paths_for(config))
            else "@mindroom_researcher:localhost"
        )
        bot.orchestrator = orchestrator
        generate_response = AsyncMock()
        install_generate_response_mock(bot, generate_response)

        # Mock helper functions
        async def mock_handle(*args: object, **kwargs: object) -> None:
            pass

        with (
            patch.object(
                bot._conversation_resolver,
                "fetch_thread_history",
                return_value=thread_history_result([], is_full_history=True),
            ),
            patch("mindroom.conversation_resolver.check_agent_mentioned", return_value=([], False, False)),
            patch("mindroom.matrix.event_info.EventInfo.from_event") as mock_thread_info,
            patch("mindroom.conversation_resolver._should_skip_mentions", return_value=False),
            patch("mindroom.text_ingress_dispatch.is_dm_room", return_value=True),  # This is a DM room
            patch("mindroom.turn_controller.interactive.handle_text_response", new=mock_handle),
        ):
            # Mock thread info to return no thread
            mock_thread_info.return_value = EventInfo(
                is_thread=False,
                thread_id=None,
                can_be_thread_root=True,
                has_relations=False,
                relation_type=None,
                is_edit=False,
                original_event_id=None,
                is_reply=False,
                reply_to_event_id=None,
                is_reaction=False,
                reaction_key=None,
                reaction_target_event_id=None,
                relates_to_event_id=None,
            )

            # Create a test message event
            room = MagicMock()
            room.room_id = "!dm:localhost"
            room.name = "DM with researcher"
            # Use the correct MatrixID from config
            researcher_id = (
                entity_ids(config, runtime_paths_for(config))["researcher"].full_id
                if "researcher" in entity_ids(config, runtime_paths_for(config))
                else "@mindroom_researcher:localhost"
            )
            room.users = {researcher_id: None}  # Single agent in room

            event = MagicMock(spec=nio.RoomMessageText)
            event.body = "Hello researcher, can you help?"
            event.sender = "@user:localhost"
            event.event_id = "test_event"
            event.server_timestamp = 1234567890
            event.source = {
                "content": {
                    "body": "Hello researcher, can you help?",
                    "m.mentions": {},
                },
            }

            # Process the message
            await bot._on_message(room, event)
            await drain_coalescing(bot)

            # Verify the bot decided to respond even though not configured for the room
            generate_response.assert_called_once()
            call_args = generate_response.call_args
            assert call_args.kwargs["response_envelope"].target.room_id == "!dm:localhost"
            assert call_args.kwargs["prompt"] == "Hello researcher, can you help?"

    async def test_agent_processes_dm_messages_when_not_configured_for_room(self, tmp_path: Path) -> None:
        """Test that agents process messages in DM rooms even when not configured for them."""
        config = _config(
            tmp_path,
            agents={"test_agent": AgentConfig(display_name="Test Agent", role="Test")},
        )

        # Use the correct MatrixID from config
        test_agent_matrix_id = entity_ids(config, runtime_paths_for(config))["test_agent"]
        agent_user = AgentMatrixUser(
            agent_name="test_agent",
            user_id=test_agent_matrix_id.full_id,
            display_name="Test Agent",
            password=TEST_PASSWORD,
        )

        # Agent is NOT configured for any rooms
        bot = AgentBot(
            agent_user=agent_user,
            storage_path=tmp_path,
            config=config,
            runtime_paths=runtime_paths_for(config),
            rooms=[],  # Empty - not configured for any room
        )

        bot.client = AsyncMock()
        bot.client.user_id = entity_ids(config, runtime_paths_for(config))["test_agent"].full_id
        bot.logger = MagicMock()
        generate_response = AsyncMock()
        install_generate_response_mock(bot, generate_response)

        async def mock_handle(*args: object, **kwargs: object) -> None:
            pass

        with (
            patch.object(
                bot._conversation_resolver,
                "fetch_thread_history",
                return_value=thread_history_result([], is_full_history=True),
            ),
            patch("mindroom.conversation_resolver.check_agent_mentioned", return_value=([], False, False)),
            patch("mindroom.matrix.event_info.EventInfo.from_event") as mock_thread_info,
            patch("mindroom.conversation_resolver._should_skip_mentions", return_value=False),
            patch("mindroom.text_ingress_dispatch.is_dm_room", return_value=True),  # This is a DM room
            patch("mindroom.turn_controller.interactive.handle_text_response", new=mock_handle),
        ):
            # Mock thread info to return no thread
            mock_thread_info.return_value = EventInfo(
                is_thread=False,
                thread_id=None,
                can_be_thread_root=True,
                has_relations=False,
                relation_type=None,
                is_edit=False,
                original_event_id=None,
                is_reply=False,
                reply_to_event_id=None,
                is_reaction=False,
                reaction_key=None,
                reaction_target_event_id=None,
                relates_to_event_id=None,
            )

            # Create a test message event in a DM room
            room = MagicMock()
            room.room_id = "!dm:localhost"  # This room is NOT in bot.rooms
            room.name = "DM Room"
            # Use the correct MatrixID from config
            test_agent_id = entity_ids(config, runtime_paths_for(config))["test_agent"].full_id
            room.users = {test_agent_id: None}  # Single agent in room

            event = MagicMock(spec=nio.RoomMessageText)
            event.body = "Hello agent!"
            event.sender = "@user:localhost"
            event.event_id = "test_event"
            event.server_timestamp = 1234567890
            event.source = {
                "content": {
                    "body": "Hello agent!",
                    "m.mentions": {},
                },
            }

            # Process the message
            await bot._on_message(room, event)
            await drain_coalescing(bot)

            # Verify the bot decided to respond in the DM room
            generate_response.assert_called_once()
            call_args = generate_response.call_args
            assert call_args[1]["response_envelope"].target.room_id == "!dm:localhost"
            assert call_args[1]["prompt"] == "Hello agent!"
