"""Tests for Matrix operations API endpoints."""

from pathlib import Path
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
import yaml
from fastapi.testclient import TestClient

from mindroom import constants
from mindroom.api import config_lifecycle, main
from mindroom.config.agent import AgentConfig, TeamConfig
from mindroom.config.main import Config


def _add_test_team_to_runtime_config() -> None:
    """Add a configured team to the API runtime config for one test."""
    api_state = config_lifecycle.require_api_state(main.app)
    with api_state.config_lock:
        context = api_state.snapshot
        if context.runtime_config is None:
            msg = "runtime config should be loaded"
            raise AssertionError(msg)
        context.config_data["teams"] = {
            "test_team": {
                "display_name": "Test Team",
                "role": "A test team",
                "agents": ["test_agent"],
                "rooms": ["team_room"],
                "mode": "coordinate",
            },
        }
        context.runtime_config.teams["test_team"] = TeamConfig(
            display_name="Test Team",
            role="A test team",
            agents=["test_agent"],
            rooms=["team_room"],
            mode="coordinate",
        )


@pytest.fixture
def mock_matrix_client() -> AsyncMock:
    """Create a mock Matrix client."""
    client = AsyncMock()
    client.close = AsyncMock()
    return client


@pytest.fixture
def mock_agent_user() -> MagicMock:
    """Create a mock agent user."""
    user = MagicMock()
    user.agent_name = "test_agent"
    user.user_id = "@mindroom_test_agent:localhost"
    user.display_name = "Test Agent"
    user.password = "test_password"  # noqa: S105
    user.access_token = "test_token"  # noqa: S105
    return user


class TestMatrixOperations:
    """Test Matrix operations API endpoints."""

    @pytest.mark.asyncio
    async def test_get_all_agents_rooms(
        self,
        test_client: TestClient,
        mock_agent_user: Any,  # noqa: ANN401
        mock_matrix_client: Any,  # noqa: ANN401
    ) -> None:
        """Test getting room information for configured agents and teams."""
        _add_test_team_to_runtime_config()

        with (
            patch("mindroom.api.matrix_operations.create_agent_user", return_value=mock_agent_user),
            patch("mindroom.api.matrix_operations.login_agent_user", return_value=mock_matrix_client),
            patch(
                "mindroom.api.matrix_operations.get_joined_rooms",
                return_value=["test_room", "team_room", "!extra_room:localhost", "!dm_room:localhost"],
            ),
            patch(
                "mindroom.matrix.rooms.is_dm_room",
                side_effect=lambda _client, room_id: room_id == "!dm_room:localhost",
            ),
        ):
            response = test_client.get("/api/matrix/agents/rooms")

            assert response.status_code == 200
            data = response.json()
            assert "agents" in data
            assert len(data["agents"]) == 2

            entities_by_id = {entity["agent_id"]: entity for entity in data["agents"]}

            assert set(entities_by_id) == {"test_agent", "test_team"}
            assert entities_by_id["test_agent"]["display_name"] == "Test Agent"
            assert "test_room" in entities_by_id["test_agent"]["configured_rooms"]
            assert "!extra_room:localhost" in entities_by_id["test_agent"]["unconfigured_rooms"]
            assert "!dm_room:localhost" not in entities_by_id["test_agent"]["unconfigured_rooms"]

            assert entities_by_id["test_team"]["display_name"] == "Test Team"
            assert "team_room" in entities_by_id["test_team"]["configured_rooms"]
            assert "!extra_room:localhost" in entities_by_id["test_team"]["unconfigured_rooms"]
            assert "!dm_room:localhost" not in entities_by_id["test_team"]["unconfigured_rooms"]

    @pytest.mark.asyncio
    async def test_get_specific_agent_rooms(
        self,
        test_client: TestClient,
        mock_agent_user: Any,  # noqa: ANN401
        mock_matrix_client: Any,  # noqa: ANN401
    ) -> None:
        """Test getting room information for a specific agent."""
        with (
            patch("mindroom.api.matrix_operations.create_agent_user", return_value=mock_agent_user),
            patch("mindroom.api.matrix_operations.login_agent_user", return_value=mock_matrix_client),
            patch(
                "mindroom.api.matrix_operations.get_joined_rooms",
                return_value=["test_room", "!extra_room:localhost"],
            ),
        ):
            response = test_client.get("/api/matrix/agents/test_agent/rooms")

            assert response.status_code == 200
            data = response.json()
            assert data["agent_id"] == "test_agent"
            assert data["display_name"] == "Test Agent"
            assert len(data["configured_rooms"]) == 1
            assert len(data["unconfigured_rooms"]) == 1
            assert "!extra_room:localhost" in data["unconfigured_rooms"]

    @pytest.mark.asyncio
    async def test_get_specific_team_rooms(
        self,
        test_client: TestClient,
        mock_agent_user: Any,  # noqa: ANN401
        mock_matrix_client: Any,  # noqa: ANN401
    ) -> None:
        """Test getting rooms for a specific configured team."""
        _add_test_team_to_runtime_config()

        with (
            patch("mindroom.api.matrix_operations.create_agent_user", return_value=mock_agent_user),
            patch("mindroom.api.matrix_operations.login_agent_user", return_value=mock_matrix_client),
            patch(
                "mindroom.api.matrix_operations.get_joined_rooms",
                return_value=["team_room", "!external_room:localhost"],
            ),
        ):
            response = test_client.get("/api/matrix/agents/test_team/rooms")

            assert response.status_code == 200
            data = response.json()
            assert data["agent_id"] == "test_team"
            assert data["display_name"] == "Test Team"
            assert data["configured_rooms"] == ["team_room"]
            assert data["unconfigured_rooms"] == ["!external_room:localhost"]

    @pytest.mark.asyncio
    async def test_get_agent_rooms_treats_trigger_only_room_as_unconfigured(
        self,
        tmp_path: Path,
        mock_agent_user: Any,  # noqa: ANN401
        mock_matrix_client: Any,  # noqa: ANN401
    ) -> None:
        """Tool-managed trigger rooms should not widen authored room membership."""
        config_path = tmp_path / "config.yaml"
        config_path.write_text(
            yaml.safe_dump(
                {
                    "models": {"default": {"provider": "ollama", "id": "test-model"}},
                    "agents": {
                        "test_agent": {
                            "display_name": "Test Agent",
                            "role": "A test agent",
                            "rooms": ["test_room"],
                        },
                    },
                },
            ),
            encoding="utf-8",
        )
        runtime_paths = constants.resolve_primary_runtime_paths(config_path=config_path, process_env={})
        main.initialize_api_app(main.app, runtime_paths)
        assert config_lifecycle.load_config_into_app(runtime_paths, main.app) is True

        with (
            patch("mindroom.api.matrix_operations.create_agent_user", return_value=mock_agent_user),
            patch("mindroom.api.matrix_operations.login_agent_user", return_value=mock_matrix_client),
            patch(
                "mindroom.api.matrix_operations.get_joined_rooms",
                return_value=["test_room", "!campground:localhost", "!extra_room:localhost"],
            ),
        ):
            response = TestClient(main.app).get("/api/matrix/agents/test_agent/rooms")

        assert response.status_code == 200
        data = response.json()
        assert data["configured_rooms"] == ["test_room"]
        assert data["unconfigured_rooms"] == ["!campground:localhost", "!extra_room:localhost"]

    @pytest.mark.asyncio
    async def test_get_agent_rooms_not_found(self, test_client: TestClient) -> None:
        """Test getting rooms for non-existent agent."""
        response = test_client.get("/api/matrix/agents/nonexistent/rooms")
        assert response.status_code == 404
        assert "not found" in response.json()["detail"].lower()

    @pytest.mark.asyncio
    async def test_leave_room(
        self,
        test_client: TestClient,
        mock_agent_user: Any,  # noqa: ANN401
        mock_matrix_client: Any,  # noqa: ANN401
    ) -> None:
        """Test leaving a room."""
        with (
            patch("mindroom.api.matrix_operations.create_agent_user", return_value=mock_agent_user),
            patch("mindroom.api.matrix_operations.login_agent_user", return_value=mock_matrix_client),
            patch("mindroom.api.matrix_operations.leave_room", return_value=True),
        ):
            response = test_client.post(
                "/api/matrix/rooms/leave",
                json={"agent_id": "test_agent", "room_id": "!room_to_leave:localhost"},
            )

            assert response.status_code == 200
            assert response.json()["success"] is True

    @pytest.mark.asyncio
    async def test_leave_room_failure(
        self,
        test_client: TestClient,
        mock_agent_user: Any,  # noqa: ANN401
        mock_matrix_client: Any,  # noqa: ANN401
    ) -> None:
        """Test failing to leave a room."""
        with (
            patch("mindroom.api.matrix_operations.create_agent_user", return_value=mock_agent_user),
            patch("mindroom.api.matrix_operations.login_agent_user", return_value=mock_matrix_client),
            patch("mindroom.api.matrix_operations.leave_room", return_value=False),
        ):
            response = test_client.post(
                "/api/matrix/rooms/leave",
                json={"agent_id": "test_agent", "room_id": "!room_to_leave:localhost"},
            )

            assert response.status_code == 500
            assert "Failed to leave room" in response.json()["detail"]

    @pytest.mark.asyncio
    async def test_leave_room_for_team(
        self,
        test_client: TestClient,
        mock_agent_user: Any,  # noqa: ANN401
        mock_matrix_client: Any,  # noqa: ANN401
    ) -> None:
        """Test leaving a room for a configured team."""
        _add_test_team_to_runtime_config()

        with (
            patch("mindroom.api.matrix_operations.create_agent_user", return_value=mock_agent_user),
            patch("mindroom.api.matrix_operations.login_agent_user", return_value=mock_matrix_client),
            patch("mindroom.api.matrix_operations.leave_room", return_value=True),
        ):
            response = test_client.post(
                "/api/matrix/rooms/leave",
                json={"agent_id": "test_team", "room_id": "!room_to_leave:localhost"},
            )

            assert response.status_code == 200
            assert response.json()["success"] is True

    @pytest.mark.asyncio
    async def test_leave_room_agent_not_found(self, test_client: TestClient) -> None:
        """Test leaving room with non-existent agent."""
        response = test_client.post(
            "/api/matrix/rooms/leave",
            json={"agent_id": "nonexistent", "room_id": "!room:localhost"},
        )

        assert response.status_code == 404
        assert "not found" in response.json()["detail"].lower()

    @pytest.mark.asyncio
    async def test_leave_rooms_bulk(
        self,
        test_client: TestClient,
        mock_agent_user: Any,  # noqa: ANN401
        mock_matrix_client: Any,  # noqa: ANN401
    ) -> None:
        """Test bulk leaving rooms."""
        with (
            patch("mindroom.api.matrix_operations.create_agent_user", return_value=mock_agent_user),
            patch("mindroom.api.matrix_operations.login_agent_user", return_value=mock_matrix_client),
            patch("mindroom.api.matrix_operations.leave_room", return_value=True),
        ):
            requests = [
                {"agent_id": "test_agent", "room_id": "!room1:localhost"},
                {"agent_id": "test_agent", "room_id": "!room2:localhost"},
            ]

            response = test_client.post("/api/matrix/rooms/leave-bulk", json=requests)

            assert response.status_code == 200
            data = response.json()
            assert data["success"] is True
            assert len(data["results"]) == 2
            assert all(r["success"] for r in data["results"])

    @pytest.mark.asyncio
    async def test_leave_rooms_bulk_partial_failure(
        self,
        test_client: TestClient,
        mock_agent_user: Any,  # noqa: ANN401
        mock_matrix_client: Any,  # noqa: ANN401
    ) -> None:
        """Test bulk leaving rooms with partial failure."""
        # Mock different behaviors for different calls
        leave_room_results = [True, False]
        leave_room_mock = AsyncMock(side_effect=leave_room_results)

        with (
            patch("mindroom.api.matrix_operations.create_agent_user", return_value=mock_agent_user),
            patch("mindroom.api.matrix_operations.login_agent_user", return_value=mock_matrix_client),
            patch("mindroom.api.matrix_operations.leave_room", new=leave_room_mock),
        ):
            requests = [
                {"agent_id": "test_agent", "room_id": "!room1:localhost"},
                {"agent_id": "test_agent", "room_id": "!room2:localhost"},
            ]

            response = test_client.post("/api/matrix/rooms/leave-bulk", json=requests)

            assert response.status_code == 200
            data = response.json()
            assert data["success"] is False  # Overall failure due to partial failure
            assert len(data["results"]) == 2
            assert data["results"][0]["success"] is True
            assert data["results"][1]["success"] is False

    @pytest.mark.asyncio
    async def test_get_agent_rooms_uses_one_runtime_snapshot(
        self,
        test_client: TestClient,
        tmp_path: Path,
        mock_agent_user: Any,  # noqa: ANN401
        mock_matrix_client: Any,  # noqa: ANN401
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Agent-room reads should use the same runtime snapshot as the committed config read."""
        first_runtime = constants.resolve_primary_runtime_paths(config_path=tmp_path / "first.yaml", process_env={})
        second_runtime = constants.resolve_primary_runtime_paths(config_path=tmp_path / "second.yaml", process_env={})

        def _fake_runtime_snapshot_read(_request: Any) -> tuple[Config, constants.RuntimePaths]:  # noqa: ANN401
            main.initialize_api_app(main.app, second_runtime)
            return Config(
                agents={
                    "old_agent": AgentConfig(
                        display_name="Old",
                        role="Test",
                        rooms=[],
                    ),
                },
            ), first_runtime

        monkeypatch.setattr(
            "mindroom.api.matrix_operations.read_committed_runtime_config",
            _fake_runtime_snapshot_read,
        )

        with (
            patch("mindroom.api.matrix_operations.create_agent_user", return_value=mock_agent_user) as create_user,
            patch("mindroom.api.matrix_operations.login_agent_user", return_value=mock_matrix_client),
            patch("mindroom.api.matrix_operations.get_joined_rooms", return_value=[]),
        ):
            response = test_client.get("/api/matrix/agents/old_agent/rooms")

        assert response.status_code == 200
        assert create_user.await_args.kwargs["runtime_paths"] == first_runtime

    @pytest.mark.asyncio
    async def test_leave_room_uses_one_runtime_snapshot(
        self,
        test_client: TestClient,
        tmp_path: Path,
        mock_agent_user: Any,  # noqa: ANN401
        mock_matrix_client: Any,  # noqa: ANN401
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Room-leave requests should not mix committed config with a newer runtime swap."""
        first_runtime = constants.resolve_primary_runtime_paths(config_path=tmp_path / "first.yaml", process_env={})
        second_runtime = constants.resolve_primary_runtime_paths(config_path=tmp_path / "second.yaml", process_env={})

        def _fake_snapshot_read(
            _request: Any,  # noqa: ANN401
            reader: Any,  # noqa: ANN401
        ) -> tuple[dict[str, Any], constants.RuntimePaths]:
            main.initialize_api_app(main.app, second_runtime)
            return (
                reader(
                    {
                        "agents": {
                            "old_agent": {
                                "display_name": "Old",
                                "role": "Test",
                                "rooms": [],
                            },
                        },
                    },
                ),
                first_runtime,
            )

        monkeypatch.setattr(
            "mindroom.api.matrix_operations.read_committed_config_and_runtime",
            _fake_snapshot_read,
        )

        with (
            patch("mindroom.api.matrix_operations.create_agent_user", return_value=mock_agent_user) as create_user,
            patch("mindroom.api.matrix_operations.login_agent_user", return_value=mock_matrix_client),
            patch("mindroom.api.matrix_operations.leave_room", return_value=True),
        ):
            response = test_client.post(
                "/api/matrix/rooms/leave",
                json={"agent_id": "old_agent", "room_id": "!room:localhost"},
            )

        assert response.status_code == 200
        assert create_user.await_args.kwargs["runtime_paths"] == first_runtime


@pytest.mark.parametrize(
    ("method", "path", "payload"),
    [
        ("get", "/api/matrix/agents/rooms", None),
        ("get", "/api/matrix/agents/test_agent/rooms", None),
        ("post", "/api/matrix/rooms/leave", {"agent_id": "test_agent", "room_id": "!room:localhost"}),
        (
            "post",
            "/api/matrix/rooms/leave-bulk",
            [{"agent_id": "test_agent", "room_id": "!room:localhost"}],
        ),
    ],
)
def test_matrix_operations_refuse_stale_config_after_invalid_reload(
    test_client: TestClient,
    temp_config_file: Path,
    mock_agent_user: Any,  # noqa: ANN401
    mock_matrix_client: Any,  # noqa: ANN401
    method: str,
    path: str,
    payload: dict[str, Any] | list[dict[str, Any]] | None,
) -> None:
    """Matrix operations should surface malformed current config instead of stale cached entities."""
    runtime_paths = constants.resolve_primary_runtime_paths(config_path=temp_config_file, process_env={})
    temp_config_file.write_text("agents:\n  broken: [\n", encoding="utf-8")
    assert config_lifecycle.load_config_into_app(runtime_paths, main.app) is False

    with (
        patch("mindroom.api.matrix_operations.create_agent_user", return_value=mock_agent_user),
        patch("mindroom.api.matrix_operations.login_agent_user", return_value=mock_matrix_client),
        patch("mindroom.api.matrix_operations.get_joined_rooms", return_value=["test_room"]),
        patch("mindroom.api.matrix_operations.leave_room", return_value=True),
    ):
        if payload is None:
            response = getattr(test_client, method)(path)
        else:
            response = getattr(test_client, method)(path, json=payload)

    assert response.status_code == 422
    assert "Could not parse configuration YAML" in response.json()["detail"][0]["msg"]
