"""Tests for team-based agent collaboration."""

from __future__ import annotations

import tempfile
from pathlib import Path
from typing import TYPE_CHECKING, Any, cast
from unittest.mock import AsyncMock, patch

import nio
import pytest

from mindroom.bot import AgentBot
from mindroom.config.agent import AgentConfig, AgentPrivateConfig, TeamConfig
from mindroom.config.main import Config
from mindroom.config.models import ModelConfig, RouterConfig
from mindroom.matrix.users import AgentMatrixUser
from mindroom.teams import TeamIntent, TeamMemberStatus, TeamOutcome
from mindroom.thread_utils import get_agents_in_thread
from tests.conftest import (
    TEST_PASSWORD,
    bind_runtime_paths,
    make_visible_message,
    runtime_paths_for,
    test_runtime_paths,
)
from tests.identity_helpers import actual_entity_usernames, entity_ids, entity_name_for_id, persist_entity_accounts

if TYPE_CHECKING:
    from collections.abc import AsyncGenerator


def _runtime_bound_config(config: Config, runtime_root: Path | None = None) -> Config:
    """Return a runtime-bound config for team tests."""
    runtime_paths = test_runtime_paths(runtime_root or Path(tempfile.mkdtemp()))
    bound_config = bind_runtime_paths(config, runtime_paths)
    persist_entity_accounts(
        bound_config,
        runtime_paths,
        usernames=actual_entity_usernames(bound_config),
    )
    return bound_config


def _agent_names(ids: list[object], config: Config) -> list[str]:
    runtime_paths = runtime_paths_for(config)
    return [entity_name_for_id(mid, config, runtime_paths) for mid in ids]


def _matrix_room(room_id: str, user_ids: list[str]) -> nio.MatrixRoom:
    room = nio.MatrixRoom(room_id, "@test-user:localhost")
    room.members_synced = True
    for user_id in user_ids:
        room.add_member(user_id, None, None)
    return room


# Test fixtures for team agents
@pytest.fixture
def mock_research_agent() -> AgentMatrixUser:
    """Create a mock research agent."""
    return AgentMatrixUser(
        agent_name="research",
        user_id="@actual_research:localhost",
        display_name="ResearchAgent",
        password=TEST_PASSWORD,
    )


@pytest.fixture
def mock_analyst_agent() -> AgentMatrixUser:
    """Create a mock analyst agent."""
    return AgentMatrixUser(
        agent_name="analyst",
        user_id="@actual_analyst:localhost",
        display_name="AnalystAgent",
        password=TEST_PASSWORD,
    )


@pytest.fixture
def mock_code_agent() -> AgentMatrixUser:
    """Create a mock code agent."""
    return AgentMatrixUser(
        agent_name="code",
        user_id="@actual_code:localhost",
        display_name="CodeAgent",
        password=TEST_PASSWORD,
    )


@pytest.fixture
def mock_security_agent() -> AgentMatrixUser:
    """Create a mock security agent."""
    return AgentMatrixUser(
        agent_name="security",
        user_id="@actual_security:localhost",
        display_name="SecurityAgent",
        password=TEST_PASSWORD,
    )


@pytest.fixture
def team_room_id() -> str:
    """Room ID where team collaboration happens."""
    return "!team_room:localhost"


class TestTeamFormation:
    """Test team formation logic."""

    def setup_method(self) -> None:
        """Set up test config."""
        self.config = _runtime_bound_config(
            Config(
                agents={
                    "code": AgentConfig(display_name="Code", rooms=["#test:example.org"]),
                    "security": AgentConfig(display_name="Security", rooms=["#test:example.org"]),
                    "research": AgentConfig(display_name="Research", rooms=["#test:example.org"]),
                    "analyst": AgentConfig(display_name="Analyst", rooms=["#test:example.org"]),
                },
                teams={},
                room_models={},
                models={"default": ModelConfig(provider="ollama", id="test-model")},
            ),
        )

    @pytest.mark.asyncio
    async def test_multiple_agents_tagged_form_team(
        self,
        mock_research_agent: AgentMatrixUser,
        mock_analyst_agent: AgentMatrixUser,
        team_room_id: str,
        tmp_path: Path,
    ) -> None:
        """Test that multiple agents tagged in a message form a team."""
        # Create bots
        config = _runtime_bound_config(Config(router=RouterConfig(model="default")), tmp_path)

        research_bot = AgentBot(mock_research_agent, tmp_path, config, runtime_paths_for(config), rooms=[team_room_id])
        config = _runtime_bound_config(Config(router=RouterConfig(model="default")), tmp_path)

        analyst_bot = AgentBot(mock_analyst_agent, tmp_path, config, runtime_paths_for(config), rooms=[team_room_id])

        # Setup bots
        research_bot.client = AsyncMock()
        analyst_bot.client = AsyncMock()

        # Create message mentioning both agents
        message_event: dict[str, Any] = {
            "type": "m.room.message",
            "content": {
                "msgtype": "m.text",
                "body": f"@{mock_research_agent.display_name} @{mock_analyst_agent.display_name} analyze the market trends",
                "m.mentions": {
                    "user_ids": [
                        mock_research_agent.user_id,
                        mock_analyst_agent.user_id,
                    ],
                },
            },
            "sender": "@user:localhost",
            "room_id": team_room_id,
            "event_id": "$test_event",
            "origin_server_ts": 1234567890,
        }

        # Add thread relation
        message_event["content"]["m.relates_to"] = {
            "rel_type": "m.thread",
            "event_id": "$thread_root",
        }

        # Both agents should recognize they're part of a team request
        # This test verifies the setup - actual team behavior will be tested
        # once implementation is done
        assert mock_research_agent.user_id in message_event["content"]["m.mentions"]["user_ids"]
        assert mock_analyst_agent.user_id in message_event["content"]["m.mentions"]["user_ids"]

    @pytest.mark.asyncio
    async def test_multiple_agents_in_thread_form_team(
        self,
        mock_code_agent: AgentMatrixUser,
        mock_security_agent: AgentMatrixUser,
        team_room_id: str,  # noqa: ARG002
        tmp_path: Path,  # noqa: ARG002
    ) -> None:
        """Test that multiple agents already in a thread form a team when no one is mentioned."""
        # Mock thread history showing both agents have participated
        thread_history = [
            make_visible_message(sender="@user:localhost", body="How should we implement authentication?"),
            make_visible_message(sender=mock_code_agent.user_id, body="I suggest using JWT tokens..."),
            make_visible_message(sender=mock_security_agent.user_id, body="We should also add rate limiting..."),
        ]

        # Message with no mentions would trigger team formation
        # (message_event setup omitted as it's tested via thread_history)

        # Verify both agents are in thread
        agents_in_thread = get_agents_in_thread(thread_history, self.config, runtime_paths_for(self.config))
        agent_names = _agent_names(agents_in_thread, self.config)
        assert "code" in agent_names
        assert "security" in agent_names
        assert len(agent_names) == 2


class TestTeamCollaboration:
    """Test team collaboration behaviors."""

    @pytest.mark.asyncio
    @patch("mindroom.response_runner.stream_agent_response")
    async def test_team_coordinate_mode(
        self,
        mock_stream_agent_response: AsyncMock,  # noqa: ARG002
        mock_research_agent: AgentMatrixUser,  # noqa: ARG002
        mock_analyst_agent: AgentMatrixUser,  # noqa: ARG002
        team_room_id: str,  # noqa: ARG002
        tmp_path: Path,  # noqa: ARG002
    ) -> None:
        """Test team coordination mode where agents build on each other's work."""

        # Setup responses for coordinate mode
        async def research_response() -> AsyncGenerator[str, None]:
            yield "I've gathered the following data on renewable energy:\n"
            yield "- Solar capacity increased 23% YoY\n"
            yield "- Wind energy adoption up 18%"

        async def analyst_response() -> AsyncGenerator[str, None]:
            yield "Based on the research data:\n"
            yield "- The 23% solar growth indicates strong market momentum\n"
            yield "- Combined renewable growth of 20.5% exceeds projections"

        # This test sets up the expected behavior for coordinate mode
        # Implementation will ensure agents respond sequentially

        # Expected: Research agent provides data, then analyst builds on it
        research_chunks = [chunk async for chunk in research_response()]

        analyst_chunks = [chunk async for chunk in analyst_response()]

        # Verify responses can be combined coherently
        combined = "".join(research_chunks) + "\n\n" + "".join(analyst_chunks)
        assert "gathered the following data" in combined
        assert "Based on the research data" in combined

    @pytest.mark.asyncio
    async def test_team_collaborate_mode(
        self,
        mock_code_agent: AgentMatrixUser,  # noqa: ARG002
        mock_security_agent: AgentMatrixUser,  # noqa: ARG002
        team_room_id: str,  # noqa: ARG002
        tmp_path: Path,  # noqa: ARG002
    ) -> None:
        """Test team collaboration mode where agents work in parallel."""
        # In collaborate mode, multiple agents analyze the same problem
        # and provide different perspectives simultaneously

        # In collaborate mode, multiple agents analyze the same problem

        # Team synthesis would combine these perspectives
        expected_synthesis = (
            "Team Response:\n"
            "Implementation approach: JWT tokens with refresh tokens\n"
            "Security requirements: Multi-factor authentication and rate limiting"
        )

        # Verify the perspectives can be synthesized
        assert "JWT tokens" in expected_synthesis
        assert "Multi-factor authentication" in expected_synthesis

    @pytest.mark.asyncio
    async def test_team_route_mode(
        self,
        mock_research_agent: AgentMatrixUser,
        mock_code_agent: AgentMatrixUser,
        mock_analyst_agent: AgentMatrixUser,
        team_room_id: str,  # noqa: ARG002
        tmp_path: Path,  # noqa: ARG002
    ) -> None:
        """Test team route mode where lead agent delegates to specialists."""
        # In route mode, a lead agent determines who should handle what

        # In route mode, a lead agent determines who should handle what

        expected_delegations = {
            "research_task": mock_research_agent.agent_name,
            "analysis_task": mock_analyst_agent.agent_name,
            "visualization_task": mock_code_agent.agent_name,
        }

        # Verify routing logic (to be implemented)
        for agent in expected_delegations.values():
            assert agent in ["research", "code", "analyst"]


class TestTeamResponseBehavior:
    """Test specific team response behaviors."""

    def setup_method(self) -> None:
        """Set up test config."""
        self.config = _runtime_bound_config(
            Config(
                agents={
                    "code": AgentConfig(display_name="Code", rooms=["#test:example.org"]),
                    "security": AgentConfig(display_name="Security", rooms=["#test:example.org"]),
                    "research": AgentConfig(display_name="Research", rooms=["#test:example.org"]),
                    "analyst": AgentConfig(display_name="Analyst", rooms=["#test:example.org"]),
                },
                teams={},
                room_models={},
                models={"default": ModelConfig(provider="ollama", id="test-model")},
            ),
        )

    @pytest.mark.asyncio
    async def test_single_agent_still_continues_conversation(
        self,
        mock_code_agent: AgentMatrixUser,
        team_room_id: str,  # noqa: ARG002
        tmp_path: Path,  # noqa: ARG002
    ) -> None:
        """Test that single agent behavior remains unchanged."""
        # Thread with only one agent
        thread_history = [
            make_visible_message(sender="@user:localhost", body="Can you help with Python?"),
            make_visible_message(sender=mock_code_agent.user_id, body="Sure, I can help with Python!"),
        ]

        # No mentions in follow-up would cause single agent to continue

        agents_in_thread = get_agents_in_thread(thread_history, self.config, runtime_paths_for(self.config))
        agent_names = _agent_names(agents_in_thread, self.config)
        assert agent_names == ["code"]
        # Single agent should continue responding

    @pytest.mark.asyncio
    async def test_explicit_mention_overrides_team(
        self,
        mock_code_agent: AgentMatrixUser,
        mock_security_agent: AgentMatrixUser,  # noqa: ARG002
        team_room_id: str,  # noqa: ARG002
        tmp_path: Path,  # noqa: ARG002
    ) -> None:
        """Test that explicit mention of one agent prevents team formation."""
        # Thread with multiple agents (thread_history would show both agents)

        # Explicitly mention only one agent
        message_event = {
            "type": "m.room.message",
            "content": {
                "msgtype": "m.text",
                "body": f"@{mock_code_agent.display_name} can you add error handling?",
                "m.mentions": {"user_ids": [mock_code_agent.user_id]},
            },
        }

        # Only mentioned agent should respond, not the team
        content = cast("dict[str, Any]", message_event["content"])
        mentions = cast("dict[str, Any]", content["m.mentions"])
        user_ids = cast("list[str]", mentions["user_ids"])
        assert len(user_ids) == 1
        assert mock_code_agent.user_id in user_ids

    @pytest.mark.asyncio
    async def test_team_with_invited_agents(
        self,
        mock_research_agent: AgentMatrixUser,
        mock_analyst_agent: AgentMatrixUser,
        team_room_id: str,  # noqa: ARG002
        tmp_path: Path,  # noqa: ARG002
    ) -> None:
        """Test team formation with invited agents."""
        # One agent is native to room, another is invited
        native_agent = mock_research_agent
        invited_agent = mock_analyst_agent

        # Both should form team when working together
        thread_with_both = [
            make_visible_message(sender=native_agent.user_id, body="Research findings..."),
            make_visible_message(sender=invited_agent.user_id, body="Analysis of findings..."),
        ]

        agents = get_agents_in_thread(thread_with_both, self.config, runtime_paths_for(self.config))
        agent_names = _agent_names(agents, self.config)
        assert len(agents) == 2
        assert "research" in agent_names
        assert "analyst" in agent_names


class TestTeamEdgeCases:
    """Test edge cases and error scenarios."""

    @pytest.mark.asyncio
    async def test_team_member_unavailable(
        self,
        mock_code_agent: AgentMatrixUser,
        mock_security_agent: AgentMatrixUser,
        team_room_id: str,
        tmp_path: Path,
    ) -> None:
        """Test behavior when a team member is unavailable."""
        # Setup scenario where one agent is offline/unavailable
        # Team should adapt and continue with available members

    @pytest.mark.asyncio
    async def test_conflicting_team_responses(
        self,
        mock_analyst_agent: AgentMatrixUser,
        mock_research_agent: AgentMatrixUser,
        team_room_id: str,
        tmp_path: Path,
    ) -> None:
        """Test handling of conflicting information from team members."""
        # Agents might have different data or opinions
        # Team synthesis should handle gracefully

    @pytest.mark.asyncio
    async def test_team_context_overflow(
        self,
        mock_code_agent: AgentMatrixUser,
        mock_security_agent: AgentMatrixUser,
        mock_analyst_agent: AgentMatrixUser,
        team_room_id: str,
        tmp_path: Path,
    ) -> None:
        """Test team behavior when context window is nearly full."""
        # Large thread history approaching token limits
        # Team should coordinate to provide concise responses


class TestRouterTeamFormation:
    """Test router-initiated team formation."""

    @pytest.mark.asyncio
    async def test_router_forms_team_for_complex_query(
        self,
        team_room_id: str,  # noqa: ARG002
        tmp_path: Path,  # noqa: ARG002
    ) -> None:
        """Test router creating a team for multi-domain queries."""
        # Complex query requiring multiple agents:
        # "I need to build a secure web API with authentication,
        # analyze performance requirements, and create documentation"

        # Router should identify need for: code, security, analyst agents
        expected_team_members = ["code", "security", "analyst"]

        # Verify router would select appropriate team
        # (Implementation will use AI to determine this)
        assert len(expected_team_members) > 1

    @pytest.mark.asyncio
    async def test_router_single_agent_for_simple_query(
        self,
        team_room_id: str,  # noqa: ARG002
        tmp_path: Path,  # noqa: ARG002
    ) -> None:
        """Test router selecting single agent for simple queries."""
        # Simple query = "What's 2 + 2?"
        # Router should select only calculator agent, not form a team
        expected_agent = "calculator"

        # Verify simple queries don't trigger team formation
        assert expected_agent == "calculator"

    @pytest.mark.asyncio
    async def test_dm_room_team_formation(self) -> None:
        """Test that multiple agents in a DM room form a team when no one is mentioned."""
        from mindroom.config.agent import AgentConfig  # noqa: PLC0415
        from mindroom.config.main import Config  # noqa: PLC0415
        from mindroom.config.models import ModelConfig  # noqa: PLC0415
        from mindroom.teams import decide_team_formation  # noqa: PLC0415

        config = _runtime_bound_config(
            Config(
                agents={
                    "agent1": AgentConfig(display_name="Agent 1", role="First agent"),
                    "agent2": AgentConfig(display_name="Agent 2", role="Second agent"),
                },
                models={"default": ModelConfig(provider="ollama", id="test-model")},
            ),
        )

        ids = entity_ids(config, runtime_paths_for(config))
        room = _matrix_room("!dm:localhost", [ids["agent1"].full_id, ids["agent2"].full_id])

        # Test DM room with multiple agents and no mentions
        result = decide_team_formation(
            tagged_agents=[],  # No agents mentioned
            agents_in_thread=[],  # No agents have spoken yet
            all_mentioned_in_thread=[],  # No mentions in thread
            runtime_paths=runtime_paths_for(config),
            config=config,
            is_dm_room=True,  # This is a DM room
            room=room,
        )

        # Should form a team with both agents
        assert result.outcome is TeamOutcome.TEAM
        assert result.intent is TeamIntent.DM_AUTO_TEAM
        agent_names = sorted(_agent_names(result.eligible_members, config))
        assert agent_names == ["agent1", "agent2"]

        # Test DM room with single agent (should not form team)
        room = _matrix_room("!dm:localhost", [ids["agent1"].full_id])
        result = decide_team_formation(
            tagged_agents=[],
            agents_in_thread=[],
            all_mentioned_in_thread=[],
            runtime_paths=runtime_paths_for(config),
            config=config,
            is_dm_room=True,
            room=room,
        )

        # Should not form a team with single agent
        assert result.outcome is TeamOutcome.NONE

    @pytest.mark.asyncio
    async def test_dm_room_thread_single_agent_no_team(self) -> None:
        """In a DM with multiple agents, a thread with a single agent should not form a team."""
        from mindroom.config.agent import AgentConfig  # noqa: PLC0415
        from mindroom.config.main import Config  # noqa: PLC0415
        from mindroom.config.models import ModelConfig  # noqa: PLC0415
        from mindroom.teams import decide_team_formation  # noqa: PLC0415

        config = _runtime_bound_config(
            Config(
                agents={
                    "calculator": AgentConfig(display_name="Calculator", role="Math"),
                    "general": AgentConfig(display_name="General", role="General"),
                },
                models={"default": ModelConfig(provider="ollama", id="test-model")},
            ),
        )

        # DM room with multiple agents
        room = _matrix_room(
            "!dm:localhost",
            [
                entity_ids(config, runtime_paths_for(config))["calculator"].full_id,
                entity_ids(config, runtime_paths_for(config))["general"].full_id,
            ],
        )

        # Thread has only calculator participating so far
        agents_in_thread = [entity_ids(config, runtime_paths_for(config))["calculator"]]

        # Should NOT form a team inside a thread with a single agent
        result = decide_team_formation(
            tagged_agents=[],
            agents_in_thread=agents_in_thread,
            all_mentioned_in_thread=[],
            runtime_paths=runtime_paths_for(config),
            config=config,
            is_dm_room=True,
            is_thread=True,
            room=room,
        )

        assert result.outcome is TeamOutcome.NONE

    @pytest.mark.asyncio
    async def test_dm_room_ignores_private_agents_for_team_formation(self) -> None:
        """DM fallback should degrade to the remaining supported single agent."""
        from mindroom.teams import decide_team_formation  # noqa: PLC0415

        config = _runtime_bound_config(
            Config(
                agents={
                    "calculator": AgentConfig(display_name="Calculator", role="Math"),
                    "private_worker": AgentConfig(
                        display_name="PrivateWorker",
                        role="Private assistant",
                        private=AgentPrivateConfig(per="user", root="private_worker_data"),
                    ),
                },
                models={"default": ModelConfig(provider="ollama", id="test-model")},
            ),
        )

        room = _matrix_room(
            "!dm:localhost",
            [
                entity_ids(config, runtime_paths_for(config))["calculator"].full_id,
                entity_ids(config, runtime_paths_for(config))["private_worker"].full_id,
            ],
        )

        result = decide_team_formation(
            tagged_agents=[],
            agents_in_thread=[],
            all_mentioned_in_thread=[],
            runtime_paths=runtime_paths_for(config),
            config=config,
            is_dm_room=True,
            room=room,
        )

        assert result.outcome is TeamOutcome.INDIVIDUAL
        assert result.intent is TeamIntent.DM_AUTO_TEAM
        assert _agent_names(result.eligible_members, config) == ["calculator"]

    @pytest.mark.asyncio
    async def test_thread_history_unavailable_agents_degrade_to_individual(self) -> None:
        """Implicit thread continuation should not reject when one historical agent is off-room."""
        from mindroom.teams import decide_team_formation  # noqa: PLC0415

        config = _runtime_bound_config(
            Config(
                agents={
                    "calculator": AgentConfig(display_name="Calculator", role="Math"),
                    "general": AgentConfig(display_name="General", role="General"),
                },
                models={"default": ModelConfig(provider="ollama", id="test-model")},
            ),
        )

        room = _matrix_room(
            "!thread:localhost",
            [entity_ids(config, runtime_paths_for(config))["calculator"].full_id],
        )

        result = decide_team_formation(
            tagged_agents=[],
            agents_in_thread=[
                entity_ids(config, runtime_paths_for(config))["calculator"],
                entity_ids(config, runtime_paths_for(config))["general"],
            ],
            all_mentioned_in_thread=[],
            runtime_paths=runtime_paths_for(config),
            config=config,
            room=room,
            is_thread=True,
        )

        assert result.outcome is TeamOutcome.INDIVIDUAL
        assert result.intent is TeamIntent.IMPLICIT_THREAD_TEAM
        assert _agent_names(result.eligible_members, config) == ["calculator"]

    @pytest.mark.asyncio
    async def test_thread_history_ignores_configured_team_participants(self) -> None:
        """Implicit thread teams must ignore configured team bots instead of treating them as leaf members."""
        from mindroom.teams import decide_team_formation  # noqa: PLC0415

        config = _runtime_bound_config(
            Config(
                agents={
                    "agent_alpha": AgentConfig(display_name="Agent Alpha", role="Alpha"),
                    "agent_beta": AgentConfig(display_name="Agent Beta", role="Beta"),
                    "agent_gamma": AgentConfig(display_name="Agent Gamma", role="Gamma"),
                },
                teams={
                    "meta_team": TeamConfig(
                        display_name="Meta Team",
                        role="Combined agent team",
                        agents=["agent_alpha", "agent_beta", "agent_gamma"],
                        mode="coordinate",
                    ),
                },
                models={"default": ModelConfig(provider="ollama", id="test-model")},
            ),
        )

        room = _matrix_room(
            "!thread:localhost",
            [
                entity_ids(config, runtime_paths_for(config))["meta_team"].full_id,
                entity_ids(config, runtime_paths_for(config))["agent_alpha"].full_id,
                entity_ids(config, runtime_paths_for(config))["agent_beta"].full_id,
                entity_ids(config, runtime_paths_for(config))["agent_gamma"].full_id,
            ],
        )

        result = decide_team_formation(
            tagged_agents=[],
            agents_in_thread=[
                entity_ids(config, runtime_paths_for(config))["meta_team"],
                entity_ids(config, runtime_paths_for(config))["agent_alpha"],
                entity_ids(config, runtime_paths_for(config))["agent_beta"],
                entity_ids(config, runtime_paths_for(config))["agent_gamma"],
            ],
            all_mentioned_in_thread=[],
            runtime_paths=runtime_paths_for(config),
            config=config,
            room=room,
            is_thread=True,
        )

        assert result.outcome is TeamOutcome.TEAM
        assert result.intent is TeamIntent.IMPLICIT_THREAD_TEAM
        assert sorted(_agent_names(result.requested_members, config)) == [
            "agent_alpha",
            "agent_beta",
            "agent_gamma",
        ]
        assert sorted(_agent_names(result.eligible_members, config)) == [
            "agent_alpha",
            "agent_beta",
            "agent_gamma",
        ]

    @pytest.mark.asyncio
    async def test_previously_mentioned_off_room_agents_degrade_to_individual(self) -> None:
        """Implicit thread mentions should degrade instead of surfacing explicit-request rejection."""
        from mindroom.teams import decide_team_formation  # noqa: PLC0415

        config = _runtime_bound_config(
            Config(
                agents={
                    "calculator": AgentConfig(display_name="Calculator", role="Math"),
                    "general": AgentConfig(display_name="General", role="General"),
                },
                models={"default": ModelConfig(provider="ollama", id="test-model")},
            ),
        )

        room = _matrix_room(
            "!thread:localhost",
            [entity_ids(config, runtime_paths_for(config))["calculator"].full_id],
        )

        result = decide_team_formation(
            tagged_agents=[],
            agents_in_thread=[],
            all_mentioned_in_thread=[
                entity_ids(config, runtime_paths_for(config))["calculator"],
                entity_ids(config, runtime_paths_for(config))["general"],
            ],
            runtime_paths=runtime_paths_for(config),
            config=config,
            room=room,
            is_thread=True,
        )

        assert result.outcome is TeamOutcome.INDIVIDUAL
        assert result.intent is TeamIntent.IMPLICIT_THREAD_TEAM
        assert _agent_names(result.eligible_members, config) == ["calculator"]

    @pytest.mark.asyncio
    async def test_tagged_off_room_agents_reject_the_entire_team_request(self) -> None:
        """Explicit team requests must reject members that are not available in the room."""
        from mindroom.teams import decide_team_formation  # noqa: PLC0415

        config = _runtime_bound_config(
            Config(
                agents={
                    "calculator": AgentConfig(display_name="Calculator", role="Math"),
                    "general": AgentConfig(display_name="General", role="General"),
                },
                models={"default": ModelConfig(provider="ollama", id="test-model")},
            ),
        )

        room = _matrix_room(
            "!room:localhost",
            [entity_ids(config, runtime_paths_for(config))["calculator"].full_id],
        )

        result = decide_team_formation(
            tagged_agents=[
                entity_ids(config, runtime_paths_for(config))["calculator"],
                entity_ids(config, runtime_paths_for(config))["general"],
            ],
            agents_in_thread=[],
            all_mentioned_in_thread=[],
            runtime_paths=runtime_paths_for(config),
            config=config,
            room=room,
        )

        assert result.outcome is TeamOutcome.REJECT
        assert result.intent is TeamIntent.EXPLICIT_MEMBERS
        assert _agent_names(result.eligible_members, config) == ["calculator"]
        assert result.reason == "Team request includes agent 'general' that is not available in this room."
        assert {member.name: member.status for member in result.member_statuses} == {
            "calculator": TeamMemberStatus.ELIGIBLE,
            "general": TeamMemberStatus.NOT_IN_ROOM,
        }

    @pytest.mark.asyncio
    async def test_tagged_agents_reject_when_sender_can_talk_to_zero_agents(self) -> None:
        """Explicit sender visibility of [] must not fall back to room-visible agents."""
        from mindroom.teams import decide_team_formation  # noqa: PLC0415

        config = _runtime_bound_config(
            Config(
                agents={
                    "calculator": AgentConfig(display_name="Calculator", role="Math"),
                    "general": AgentConfig(display_name="General", role="General"),
                },
                models={"default": ModelConfig(provider="ollama", id="test-model")},
            ),
        )

        room = _matrix_room(
            "!room:localhost",
            [
                entity_ids(config, runtime_paths_for(config))["calculator"].full_id,
                entity_ids(config, runtime_paths_for(config))["general"].full_id,
            ],
        )

        result = decide_team_formation(
            tagged_agents=[
                entity_ids(config, runtime_paths_for(config))["calculator"],
                entity_ids(config, runtime_paths_for(config))["general"],
            ],
            agents_in_thread=[],
            all_mentioned_in_thread=[],
            runtime_paths=runtime_paths_for(config),
            config=config,
            room=room,
            available_responders_in_room=[],
        )

        assert result.outcome is TeamOutcome.REJECT
        assert result.eligible_members == []
        assert result.reason == (
            "Team request includes agents 'calculator', 'general' that are not available to you in this room."
        )

    @pytest.mark.asyncio
    async def test_tagged_agents_use_supplied_responder_boundary_over_room_cache(self) -> None:
        """Supplied responder candidates are authoritative for explicit team requests."""
        from mindroom.teams import decide_team_formation  # noqa: PLC0415

        config = _runtime_bound_config(
            Config(
                agents={
                    "calculator": AgentConfig(display_name="Calculator", role="Math"),
                    "general": AgentConfig(display_name="General", role="General"),
                },
                models={"default": ModelConfig(provider="ollama", id="test-model")},
            ),
        )
        runtime_paths = runtime_paths_for(config)

        room = _matrix_room("!room:localhost", [entity_ids(config, runtime_paths)["calculator"].full_id])

        result = decide_team_formation(
            tagged_agents=[
                entity_ids(config, runtime_paths)["calculator"],
                entity_ids(config, runtime_paths)["general"],
            ],
            agents_in_thread=[],
            all_mentioned_in_thread=[],
            runtime_paths=runtime_paths,
            config=config,
            room=room,
            available_responders_in_room=[
                entity_ids(config, runtime_paths)["calculator"],
                entity_ids(config, runtime_paths)["general"],
            ],
            materializable_agent_names={"calculator", "general"},
        )

        assert result.outcome is TeamOutcome.TEAM
        assert result.intent is TeamIntent.EXPLICIT_MEMBERS
        assert _agent_names(result.eligible_members, config) == ["calculator", "general"]

    @pytest.mark.asyncio
    async def test_tagged_off_room_agents_reject_without_collapsing_requested_members(self) -> None:
        """Explicit rejects should preserve the full requested-member failure state."""
        from mindroom.teams import decide_team_formation  # noqa: PLC0415

        config = _runtime_bound_config(
            Config(
                agents={
                    "calculator": AgentConfig(display_name="Calculator", role="Math"),
                    "general": AgentConfig(display_name="General", role="General"),
                    "research": AgentConfig(display_name="Research", role="Research"),
                },
                models={"default": ModelConfig(provider="ollama", id="test-model")},
            ),
        )

        room = _matrix_room(
            "!room:localhost",
            [entity_ids(config, runtime_paths_for(config))["calculator"].full_id],
        )

        result = decide_team_formation(
            tagged_agents=[
                entity_ids(config, runtime_paths_for(config))["general"],
                entity_ids(config, runtime_paths_for(config))["research"],
            ],
            agents_in_thread=[],
            all_mentioned_in_thread=[],
            runtime_paths=runtime_paths_for(config),
            config=config,
            room=room,
            available_responders_in_room=[entity_ids(config, runtime_paths_for(config))["calculator"]],
            materializable_agent_names={"calculator"},
        )

        assert result.outcome is TeamOutcome.REJECT
        assert {member.name: member.status for member in result.member_statuses} == {
            "general": TeamMemberStatus.HIDDEN_FROM_SENDER,
            "research": TeamMemberStatus.HIDDEN_FROM_SENDER,
        }
        assert result.reason == (
            "Team request includes agents 'general', 'research' that are not available to you in this room."
        )

    @pytest.mark.asyncio
    async def test_tagged_private_agent_can_join_explicit_ad_hoc_team(self) -> None:
        """Explicit shared/private mentions should form one ad hoc team."""
        from mindroom.teams import decide_team_formation  # noqa: PLC0415

        config = _runtime_bound_config(
            Config(
                agents={
                    "calculator": AgentConfig(display_name="Calculator", role="Math"),
                    "general": AgentConfig(display_name="General", role="General"),
                    "private_worker": AgentConfig(
                        display_name="PrivateWorker",
                        role="Private assistant",
                        private=AgentPrivateConfig(per="user", root="private_worker_data"),
                    ),
                },
                models={"default": ModelConfig(provider="ollama", id="test-model")},
            ),
        )

        room = _matrix_room(
            "!room:localhost",
            [
                entity_ids(config, runtime_paths_for(config))["calculator"].full_id,
                entity_ids(config, runtime_paths_for(config))["general"].full_id,
                entity_ids(config, runtime_paths_for(config))["private_worker"].full_id,
            ],
        )
        result = decide_team_formation(
            tagged_agents=[
                entity_ids(config, runtime_paths_for(config))["calculator"],
                entity_ids(config, runtime_paths_for(config))["general"],
                entity_ids(config, runtime_paths_for(config))["private_worker"],
            ],
            agents_in_thread=[],
            all_mentioned_in_thread=[],
            runtime_paths=runtime_paths_for(config),
            config=config,
            room=room,
            allow_explicit_private_agents=True,
        )

        assert result.outcome is TeamOutcome.TEAM
        assert result.intent is TeamIntent.EXPLICIT_MEMBERS
        assert {member.name: member.status for member in result.member_statuses} == {
            "calculator": TeamMemberStatus.ELIGIBLE,
            "general": TeamMemberStatus.ELIGIBLE,
            "private_worker": TeamMemberStatus.ELIGIBLE,
        }
        assert _agent_names(result.eligible_members, config) == ["calculator", "general", "private_worker"]

    @pytest.mark.asyncio
    async def test_tagged_unsupported_non_materializable_member_keeps_requested_member_statuses(self) -> None:
        """Explicit rejects should keep requested-member eligibility separate from delivery ownership."""
        from mindroom.teams import decide_team_formation  # noqa: PLC0415

        config = _runtime_bound_config(
            Config(
                agents={
                    "alpha": AgentConfig(
                        display_name="Alpha",
                        role="Private assistant",
                        private=AgentPrivateConfig(per="user", root="alpha_data"),
                    ),
                    "calculator": AgentConfig(display_name="Calculator", role="Math"),
                },
                models={"default": ModelConfig(provider="ollama", id="test-model")},
            ),
        )

        room = _matrix_room(
            "!room:localhost",
            [
                entity_ids(config, runtime_paths_for(config))["alpha"].full_id,
                entity_ids(config, runtime_paths_for(config))["calculator"].full_id,
            ],
        )

        result = decide_team_formation(
            tagged_agents=[
                entity_ids(config, runtime_paths_for(config))["alpha"],
                entity_ids(config, runtime_paths_for(config))["calculator"],
            ],
            agents_in_thread=[],
            all_mentioned_in_thread=[],
            runtime_paths=runtime_paths_for(config),
            config=config,
            room=room,
            available_responders_in_room=[
                entity_ids(config, runtime_paths_for(config))["alpha"],
                entity_ids(config, runtime_paths_for(config))["calculator"],
            ],
            materializable_agent_names={"calculator"},
        )

        assert result.outcome is TeamOutcome.REJECT
        assert {member.name: member.status for member in result.member_statuses} == {
            "alpha": TeamMemberStatus.UNSUPPORTED_FOR_TEAM,
            "calculator": TeamMemberStatus.ELIGIBLE,
        }
        assert result.eligible_members == [entity_ids(config, runtime_paths_for(config))["calculator"]]

    @pytest.mark.asyncio
    async def test_tagged_mixed_reject_causes_report_member_specific_reasons(self) -> None:
        """Mixed reject causes should explain the actual member failures instead of flattening them."""
        from mindroom.teams import decide_team_formation  # noqa: PLC0415

        config = _runtime_bound_config(
            Config(
                agents={
                    "alpha": AgentConfig(
                        display_name="Alpha",
                        role="Private assistant",
                        private=AgentPrivateConfig(per="user", root="alpha_data"),
                    ),
                    "general": AgentConfig(display_name="General", role="General"),
                    "calculator": AgentConfig(display_name="Calculator", role="Math"),
                },
                models={"default": ModelConfig(provider="ollama", id="test-model")},
            ),
        )

        room = _matrix_room(
            "!room:localhost",
            [
                entity_ids(config, runtime_paths_for(config))["alpha"].full_id,
                entity_ids(config, runtime_paths_for(config))["general"].full_id,
                entity_ids(config, runtime_paths_for(config))["calculator"].full_id,
            ],
        )

        result = decide_team_formation(
            tagged_agents=[
                entity_ids(config, runtime_paths_for(config))["alpha"],
                entity_ids(config, runtime_paths_for(config))["general"],
            ],
            agents_in_thread=[],
            all_mentioned_in_thread=[],
            runtime_paths=runtime_paths_for(config),
            config=config,
            room=room,
            available_responders_in_room=[
                entity_ids(config, runtime_paths_for(config))["alpha"],
                entity_ids(config, runtime_paths_for(config))["general"],
                entity_ids(config, runtime_paths_for(config))["calculator"],
            ],
            materializable_agent_names={"alpha", "calculator"},
        )

        assert result.outcome is TeamOutcome.REJECT
        assert result.reason == (
            "Team request cannot be satisfied: "
            "agent 'alpha' is private and can only join explicit Matrix ad hoc teams with requester identity; "
            "agent 'general' could not be materialized for this request"
        )

    @pytest.mark.asyncio
    async def test_tagged_agents_that_delegate_to_private_reject_the_entire_team_request(
        self,
    ) -> None:
        """Ad hoc team formation must reject explicit member sets that reach private agents."""
        from mindroom.teams import decide_team_formation  # noqa: PLC0415

        config = _runtime_bound_config(
            Config(
                agents={
                    "general": AgentConfig(
                        display_name="General",
                        role="Coordinator",
                        delegate_to=["research"],
                    ),
                    "code": AgentConfig(display_name="Code", role="Coder"),
                    "analyst": AgentConfig(display_name="Analyst", role="Analyst"),
                    "research": AgentConfig(
                        display_name="Research",
                        role="Private researcher",
                        private=AgentPrivateConfig(per="user", root="research_data"),
                    ),
                },
                models={"default": ModelConfig(provider="ollama", id="test-model")},
            ),
        )

        room = _matrix_room(
            "!room:localhost",
            [
                entity_ids(config, runtime_paths_for(config))["general"].full_id,
                entity_ids(config, runtime_paths_for(config))["code"].full_id,
                entity_ids(config, runtime_paths_for(config))["analyst"].full_id,
                entity_ids(config, runtime_paths_for(config))["research"].full_id,
            ],
        )
        result = decide_team_formation(
            tagged_agents=[
                entity_ids(config, runtime_paths_for(config))["general"],
                entity_ids(config, runtime_paths_for(config))["code"],
                entity_ids(config, runtime_paths_for(config))["analyst"],
            ],
            agents_in_thread=[],
            all_mentioned_in_thread=[],
            runtime_paths=runtime_paths_for(config),
            config=config,
            room=room,
        )

        assert result.outcome is TeamOutcome.REJECT
