"""Tests for AI routing functionality."""

from __future__ import annotations

from pathlib import Path  # noqa: TC003
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from mindroom.agents import describe_agent
from mindroom.bot import AgentBot
from mindroom.config.agent import AgentConfig
from mindroom.config.main import Config
from mindroom.config.models import ModelConfig, RouterConfig
from mindroom.matrix.identity import MatrixID
from mindroom.matrix.users import AgentMatrixUser
from mindroom.routing import _AgentSuggestion, suggest_agent_for_message
from mindroom.thread_utils import (
    _has_any_agent_mentions_in_thread,
    check_agent_mentioned,
    extract_agent_name,
    has_multiple_non_agent_users_in_thread,
)
from tests.conftest import TEST_ACCESS_TOKEN, TEST_PASSWORD


class TestAIRouting:
    """Tests for AI routing in multi-agent threads."""

    @pytest.mark.asyncio
    async def test_suggest_agent_for_message_basic(self) -> None:
        """Test basic agent suggestion functionality."""
        # Create config with the agents we're testing
        config = Config(
            agents={
                "calculator": AgentConfig(display_name="Calculator", rooms=[]),
                "general": AgentConfig(display_name="General", rooms=[]),
            },
            router=RouterConfig(model="default"),
        )

        with patch("mindroom.routing.get_model_instance"):
            # Mock the Agent and response
            mock_agent = AsyncMock()
            mock_response = MagicMock()
            mock_response.content = _AgentSuggestion(
                agent_name="calculator",
                reasoning="User is asking about math calculation",
            )
            mock_agent.arun.return_value = mock_response

            with patch("mindroom.routing.Agent", return_value=mock_agent):
                # Create MatrixID objects for agents
                agents = [
                    MatrixID(username="mindroom_calculator", domain="localhost"),
                    MatrixID(username="mindroom_general", domain="localhost"),
                ]
                result = await suggest_agent_for_message("What is 2 + 2?", agents, config)

                assert result == "calculator"
                assert "calculator" in mock_agent.arun.call_args[0][0]
                assert "general" in mock_agent.arun.call_args[0][0]

    @pytest.mark.asyncio
    async def test_suggest_agent_with_thread_context(self) -> None:
        """Test agent suggestion with thread history."""
        # Create config with the agents we're testing
        config = Config(
            agents={
                "calculator": AgentConfig(display_name="Calculator", rooms=[]),
                "finance": AgentConfig(display_name="Finance", rooms=[]),
                "general": AgentConfig(display_name="General", rooms=[]),
            },
            router=RouterConfig(model="default"),
        )
        thread_context = [
            {"sender": "@user:localhost", "body": "I need help with my taxes"},
            {"sender": "@mindroom_finance:localhost", "body": "I can help with that"},
        ]

        with patch("mindroom.routing.get_model_instance"):
            mock_agent = AsyncMock()
            mock_response = MagicMock()
            mock_response.content = _AgentSuggestion(agent_name="finance", reasoning="Continuing financial discussion")
            mock_agent.arun.return_value = mock_response

            with patch("mindroom.routing.Agent", return_value=mock_agent):
                # Create MatrixID objects for agents
                agents = [
                    MatrixID(username="mindroom_calculator", domain="localhost"),
                    MatrixID(username="mindroom_finance", domain="localhost"),
                    MatrixID(username="mindroom_general", domain="localhost"),
                ]
                result = await suggest_agent_for_message(
                    "How do I calculate deductions?",
                    agents,
                    config,
                    thread_context,
                )

                assert result == "finance"
                # Check that context was included in prompt
                prompt = mock_agent.arun.call_args[0][0]
                assert "taxes" in prompt
                assert "Previous messages:" in prompt

    @pytest.mark.asyncio
    async def test_suggest_agent_unavailable_returns_none(self) -> None:
        """Test that suggesting unavailable agent returns None."""
        # Create config with the agents we're testing
        config = Config(
            agents={
                "calculator": AgentConfig(display_name="Calculator", rooms=[]),
                "general": AgentConfig(display_name="General", rooms=[]),
            },
            router=RouterConfig(model="default"),
        )

        with patch("mindroom.routing.get_model_instance"):
            mock_agent = AsyncMock()
            mock_response = MagicMock()
            # AI suggests an agent not in available list
            mock_response.content = _AgentSuggestion(
                agent_name="code",  # Not available
                reasoning="User asking about programming",
            )
            mock_agent.arun.return_value = mock_response

            with patch("mindroom.routing.Agent", return_value=mock_agent):
                # Create MatrixID objects for agents
                agents = [
                    MatrixID(username="mindroom_calculator", domain="localhost"),
                    MatrixID(username="mindroom_general", domain="localhost"),
                ]  # code not available
                result = await suggest_agent_for_message(
                    "How do I write a Python function?",
                    agents,
                    config,
                )
                # Should return None when agent is not available
                assert result is None

    @pytest.mark.asyncio
    async def test_suggest_agent_error_handling(self) -> None:
        """Test error handling in agent suggestion."""
        # Create config with the agents we're testing
        config = Config(
            agents={
                "general": AgentConfig(display_name="General", rooms=[]),
            },
            router=RouterConfig(model="default"),
        )

        with patch("mindroom.routing.get_model_instance") as mock_model:
            mock_model.side_effect = ValueError("Model error")

            agents = [MatrixID(username="mindroom_general", domain="localhost")]
            result = await suggest_agent_for_message("Test message", agents, config)

            assert result is None

    @pytest.mark.asyncio
    async def test_only_router_agent_routes(self, tmp_path: Path) -> None:
        """Test that only the router agent handles routing."""
        # Create general agent (not router)
        agent = AgentMatrixUser(
            agent_name="general",
            user_id="@mindroom_general:localhost",
            display_name="GeneralAgent",
            password=TEST_PASSWORD,
            access_token=TEST_ACCESS_TOKEN,
        )

        config = Config(router=RouterConfig(model="default"))

        bot = AgentBot(agent, tmp_path, config=config)

        mock_room = MagicMock()
        mock_room.users = MagicMock()
        mock_room.users.keys.return_value = [
            "@mindroom_calculator:localhost",
            "@mindroom_general:localhost",
            "@user:localhost",
        ]

        mock_event = MagicMock()
        mock_event.body = "Test message"

        with patch("mindroom.bot.suggest_agent_for_message") as mock_suggest:
            # Should raise AssertionError since general is not the router agent
            with pytest.raises(AssertionError):
                await bot._handle_ai_routing(mock_room, mock_event, [])

            # Should not call routing since it failed the assertion
            mock_suggest.assert_not_called()


class TestThreadUtils:
    """Test thread utility functions."""

    def setup_method(self) -> None:
        """Set up test config."""
        self.config = Config(
            agents={
                "calculator": AgentConfig(display_name="Calculator", rooms=["#test:example.org"]),
                "general": AgentConfig(display_name="General", rooms=["#test:example.org"]),
            },
            teams={},
            room_models={},
            models={"default": ModelConfig(provider="ollama", id="test-model")},
            mindroom_user={"username": "mindroom_user", "display_name": "MindRoomUser"},
        )

    def test_has_any_agent_mentions_in_thread_with_mentions(self) -> None:
        """Test detecting agent mentions in thread."""
        thread_history = [
            {
                "sender": "@user:example.org",
                "body": "Hello",
                "content": {},
            },
            {
                "sender": "@user:example.org",
                "body": "@calculator help me",
                "content": {"m.mentions": {"user_ids": ["@mindroom_calculator:localhost"]}},
            },
        ]

        assert _has_any_agent_mentions_in_thread(thread_history, self.config) is True

    def test_has_any_agent_mentions_in_thread_no_mentions(self) -> None:
        """Test thread with no agent mentions."""
        thread_history = [
            {
                "sender": "@user:example.org",
                "body": "Hello",
                "content": {},
            },
            {
                "sender": "@mindroom_calculator:localhost",
                "body": "Hi there!",
                "content": {},
            },
        ]

        assert _has_any_agent_mentions_in_thread(thread_history, self.config) is False

    def test_has_any_agent_mentions_in_thread_user_mentions(self) -> None:
        """Test thread with only user mentions (not agents)."""
        thread_history = [
            {
                "sender": "@user:example.org",
                "body": "@friend check this out",
                "content": {"m.mentions": {"user_ids": ["@friend:example.org"]}},
            },
        ]

        assert _has_any_agent_mentions_in_thread(thread_history, self.config) is False

    def test_extract_agent_name_rejects_unconfigured(self) -> None:
        """Test that unconfigured agents are not recognized."""
        # This should return None because "fake_agent" is not in config.yaml
        assert extract_agent_name("@mindroom_fake_agent:localhost", self.config) is None

        # But real agents should work
        assert extract_agent_name("@mindroom_calculator:localhost", self.config) == "calculator"

        # Regular users should still be rejected
        assert extract_agent_name(self.config.get_mindroom_user_id(), self.config) is None
        assert extract_agent_name("@regular_user:localhost", self.config) is None

    def test_has_multiple_non_agent_users_in_thread_true(self) -> None:
        """Detect more than one non-agent user posting in a thread."""
        history = [
            {"sender": "@alice:localhost", "body": "hello"},
            {"sender": "@bob:localhost", "body": "hi"},
        ]
        assert has_multiple_non_agent_users_in_thread(history, self.config) is True

    def test_has_multiple_non_agent_users_in_thread_false(self) -> None:
        """Do not trigger when only one non-agent user has posted."""
        history = [
            {"sender": "@alice:localhost", "body": "hello"},
            {"sender": "@mindroom_calculator:localhost", "body": "result"},
        ]
        assert has_multiple_non_agent_users_in_thread(history, self.config) is False

    def test_has_multiple_non_agent_users_in_thread_ignores_agents(self) -> None:
        """Agent senders should not count toward the non-agent tally."""
        history = [
            {"sender": "@alice:localhost", "body": "help"},
            {"sender": "@mindroom_calculator:localhost", "body": "sure"},
            {"sender": "@mindroom_general:localhost", "body": "me too"},
        ]
        assert has_multiple_non_agent_users_in_thread(history, self.config) is False

    def test_check_agent_mentioned_detects_non_agent_mentions(self) -> None:
        """Tagging a non-agent user sets has_non_agent_mentions."""
        event_source = {
            "content": {
                "m.mentions": {"user_ids": ["@bob:localhost"]},
            },
        }
        agent_id = MatrixID.from_username("mindroom_calculator", "localhost")
        _, am_i_mentioned, has_non_agent = check_agent_mentioned(event_source, agent_id, self.config)
        assert am_i_mentioned is False
        assert has_non_agent is True

    def test_check_agent_mentioned_no_non_agent_mentions(self) -> None:
        """Tagging only agents does not set has_non_agent_mentions."""
        event_source = {
            "content": {
                "m.mentions": {"user_ids": ["@mindroom_calculator:localhost"]},
            },
        }
        agent_id = MatrixID.from_username("mindroom_calculator", "localhost")
        _, am_i_mentioned, has_non_agent = check_agent_mentioned(event_source, agent_id, self.config)
        assert am_i_mentioned is True
        assert has_non_agent is False

    def test_bot_accounts_excluded_from_multi_human_detection(self) -> None:
        """Bot accounts listed in config should not count as non-agent users."""
        config = Config(
            agents={
                "calculator": AgentConfig(display_name="Calculator", rooms=["#test:example.org"]),
                "general": AgentConfig(display_name="General", rooms=["#test:example.org"]),
            },
            models={"default": ModelConfig(provider="ollama", id="test-model")},
            bot_accounts=["@telegram:localhost"],
        )
        history = [
            {"sender": "@alice:localhost", "body": "hello"},
            {"sender": "@telegram:localhost", "body": "relayed message"},
        ]
        assert has_multiple_non_agent_users_in_thread(history, config) is False

    def test_bot_accounts_not_excluded_when_unlisted(self) -> None:
        """Without bot_accounts config, bridge bots count as non-agent users."""
        history = [
            {"sender": "@alice:localhost", "body": "hello"},
            {"sender": "@telegram:localhost", "body": "relayed message"},
        ]
        assert has_multiple_non_agent_users_in_thread(history, self.config) is True

    def test_check_agent_mentioned_bot_account_not_flagged_as_non_agent(self) -> None:
        """Mentioning a bot_account should not set has_non_agent_mentions."""
        config = Config(
            agents={
                "calculator": AgentConfig(display_name="Calculator", rooms=["#test:example.org"]),
            },
            models={"default": ModelConfig(provider="ollama", id="test-model")},
            bot_accounts=["@telegram:localhost"],
        )
        event_source = {
            "content": {
                "m.mentions": {"user_ids": ["@telegram:localhost"]},
            },
        }
        agent_id = MatrixID.from_username("mindroom_calculator", "localhost")
        _, _, has_non_agent = check_agent_mentioned(event_source, agent_id, config)
        assert has_non_agent is False

    def test_check_agent_mentioned_mixed_bot_and_human_mentions(self) -> None:
        """Mentioning both a bot_account and a human should still flag non-agent."""
        config = Config(
            agents={
                "calculator": AgentConfig(display_name="Calculator", rooms=["#test:example.org"]),
            },
            models={"default": ModelConfig(provider="ollama", id="test-model")},
            bot_accounts=["@telegram:localhost"],
        )
        event_source = {
            "content": {
                "m.mentions": {"user_ids": ["@telegram:localhost", "@bob:localhost"]},
            },
        }
        agent_id = MatrixID.from_username("mindroom_calculator", "localhost")
        _, _, has_non_agent = check_agent_mentioned(event_source, agent_id, config)
        assert has_non_agent is True


class TestBridgeMentionFallback:
    """Tests for detecting mentions from bridged messages (HTML pills fallback)."""

    @pytest.fixture(autouse=True)
    def _setup(self) -> None:
        self.config = Config(
            agents={
                "calculator": AgentConfig(display_name="Calculator", rooms=["#test:example.org"]),
                "general": AgentConfig(display_name="General", rooms=["#test:example.org"]),
            },
            models={"default": ModelConfig(provider="ollama", id="test-model")},
        )

    def test_html_pill_agent_mention(self) -> None:
        """Bridge HTML pill mentioning an agent is detected."""
        event_source = {
            "content": {
                "body": "@mindroom_calculator do the math",
                "formatted_body": '<a href="https://matrix.to/#/@mindroom_calculator:localhost">@mindroom_calculator</a> do the math',
            },
        }
        agent_id = MatrixID.from_username("mindroom_calculator", "localhost")
        mentioned_agents, am_i_mentioned, _ = check_agent_mentioned(event_source, agent_id, self.config)
        assert am_i_mentioned is True
        assert len(mentioned_agents) == 1
        assert mentioned_agents[0].full_id == "@mindroom_calculator:localhost"

    def test_html_pill_non_agent_mention(self) -> None:
        """Bridge HTML pill mentioning a non-agent sets has_non_agent_mentions."""
        event_source = {
            "content": {
                "body": "@alice hey",
                "formatted_body": '<a href="https://matrix.to/#/@alice:localhost">@alice</a> hey',
            },
        }
        agent_id = MatrixID.from_username("mindroom_calculator", "localhost")
        _, am_i_mentioned, has_non_agent = check_agent_mentioned(event_source, agent_id, self.config)
        assert am_i_mentioned is False
        assert has_non_agent is True

    def test_m_mentions_takes_precedence_over_pills(self) -> None:
        """When m.mentions is present, formatted_body pills are not parsed."""
        event_source = {
            "content": {
                "m.mentions": {"user_ids": ["@mindroom_general:localhost"]},
                "formatted_body": '<a href="https://matrix.to/#/@mindroom_calculator:localhost">@mindroom_calculator</a>',
            },
        }
        agent_id = MatrixID.from_username("mindroom_calculator", "localhost")
        mentioned_agents, am_i_mentioned, _ = check_agent_mentioned(event_source, agent_id, self.config)
        # m.mentions wins — only general is mentioned, not calculator
        assert am_i_mentioned is False
        assert len(mentioned_agents) == 1
        assert mentioned_agents[0].full_id == "@mindroom_general:localhost"

    def test_html_pill_in_thread_history(self) -> None:
        """Bridge pills in thread history are detected by has_any_agent_mentions_in_thread."""
        thread_history = [
            {
                "sender": "@alice:localhost",
                "content": {
                    "body": "@mindroom_calculator compute",
                    "formatted_body": '<a href="https://matrix.to/#/@mindroom_calculator:localhost">@mindroom_calculator</a> compute',
                },
            },
        ]
        assert _has_any_agent_mentions_in_thread(thread_history, self.config) is True

    def test_no_pills_no_mentions(self) -> None:
        """No m.mentions and no pills means no mentions detected."""
        event_source = {
            "content": {
                "body": "just a normal message",
            },
        }
        agent_id = MatrixID.from_username("mindroom_calculator", "localhost")
        mentioned_agents, am_i_mentioned, has_non_agent = check_agent_mentioned(event_source, agent_id, self.config)
        assert mentioned_agents == []
        assert am_i_mentioned is False
        assert has_non_agent is False

    def test_multiple_pills(self) -> None:
        """Multiple HTML pills in one message are all detected."""
        event_source = {
            "content": {
                "formatted_body": (
                    '<a href="https://matrix.to/#/@mindroom_calculator:localhost">calc</a> and '
                    '<a href="https://matrix.to/#/@mindroom_general:localhost">general</a>'
                ),
            },
        }
        agent_id = MatrixID.from_username("mindroom_calculator", "localhost")
        mentioned_agents, am_i_mentioned, _ = check_agent_mentioned(event_source, agent_id, self.config)
        assert am_i_mentioned is True
        assert len(mentioned_agents) == 2

    def test_single_quote_pills(self) -> None:
        """Mautrix bridges use single-quoted href attributes."""
        event_source = {
            "content": {
                "formatted_body": "<a href='https://matrix.to/#/@mindroom_calculator:localhost'>@mindroom_calculator</a> compute",
            },
        }
        agent_id = MatrixID.from_username("mindroom_calculator", "localhost")
        mentioned_agents, am_i_mentioned, _ = check_agent_mentioned(event_source, agent_id, self.config)
        assert am_i_mentioned is True
        assert len(mentioned_agents) == 1


class TestAgentDescription:
    """Test agent description functionality."""

    def test_describe_agent_with_tools(self) -> None:
        """Test describing an agent with tools."""
        config = Config.from_yaml()
        description = describe_agent("calculator", config)

        assert "calculator" in description
        assert "Solve mathematical problems" in description
        assert "Tools: calculator" in description
        assert "Use the calculator tools" in description

    def test_describe_agent_without_tools(self) -> None:
        """Test describing an agent without tools."""
        config = Config.from_yaml()
        config.defaults.tools = []
        description = describe_agent("general", config)

        assert "general" in description
        assert "general-purpose assistant" in description
        assert "Tools:" not in description  # No tools section
        assert "Always provide a clear" in description

    def test_describe_agent_includes_default_tools(self) -> None:
        """Agent descriptions include defaults.tools when agent has no local tools."""
        config = Config.from_yaml()
        config.defaults.tools = ["scheduler"]
        config.agents["general"].tools = []

        description = describe_agent("general", config)

        assert "Tools: scheduler" in description

    def test_describe_agent_can_opt_out_of_default_tools(self) -> None:
        """Agent descriptions omit defaults.tools when include_default_tools is false."""
        config = Config.from_yaml()
        config.defaults.tools = ["scheduler"]
        config.agents["general"].tools = []
        config.agents["general"].include_default_tools = False

        description = describe_agent("general", config)

        assert "Tools:" not in description

    def test_describe_unknown_agent(self) -> None:
        """Test describing an unknown agent."""
        config = Config.from_yaml()
        description = describe_agent("nonexistent", config)

        assert description == "nonexistent: Unknown agent or team"
