"""Tests for mcp_bridge module.

Tests the MCP bridge functions for calling agent tools and reading state files.
"""

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

import pytest
from returns.result import Failure, Success

from anima.core.schemas import (
    AgentNotFoundError,
    AgentState,
    AgentStateData,
    JobSpec,
    LifecycleType,
    MCPSocketNotFoundError,
    MCPConnectionError,
    TokenUsage,
)
from anima.shell import mcp_bridge


@pytest.fixture
def tmp_state_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
    """Create a temporary state directory and set ANIMA_STATE."""
    state_dir = tmp_path / "anima_state"
    state_dir.mkdir()
    monkeypatch.setenv("ANIMA_STATE", str(state_dir))
    return state_dir


@pytest.fixture
def sample_agent_state() -> AgentStateData:
    """Create sample agent state data."""
    return AgentStateData(
        instance_id="inst_test123",
        trace_id="trace_abc",
        persona_name="test_persona",
        spec_id="spec_001",
        spec_digest="abc123",
        model="test-model",
        state=AgentState.running,
        lifecycle=LifecycleType.ephemeral,
        parent_id=None,
        children=[],
        depth=0,
        max_depth=3,
        pid=12345,
        mcp_socket="/tmp/anima/agents/inst_test123/mcp.sock",
        created_at="2024-01-01T00:00:00Z",
        updated_at="2024-01-01T00:00:00Z",
        token_usage=TokenUsage(),
        tools_called=[],
        current_turn=0,
        max_turns=100,
        budget_total=100000,
        grace_budget=1000,
        error=None,
        result_ref=None,
    )


class TestGetMcpSocketPath:
    """Tests for get_mcp_socket_path function."""

    def test_socket_path_format(self, tmp_state_dir: Path) -> None:
        """Test that socket path is correctly constructed."""
        result = mcp_bridge.get_mcp_socket_path("inst_abc123")
        expected = tmp_state_dir / "agents" / "inst_abc123" / "mcp.sock"
        assert result == expected


class TestIsAgentAlive:
    """Tests for is_agent_alive function."""

    def test_returns_true_when_socket_exists(
        self, tmp_state_dir: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        """Test that agent is considered alive when socket exists."""
        socket_path = tmp_state_dir / "agents" / "inst_alive" / "mcp.sock"
        socket_path.parent.mkdir(parents=True)
        socket_path.touch()

        result = mcp_bridge.is_agent_alive("inst_alive")
        assert result is True

    def test_returns_false_when_socket_missing(
        self, tmp_state_dir: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        """Test that agent is considered dead when socket doesn't exist."""
        result = mcp_bridge.is_agent_alive("inst_nonexistent")
        assert result is False


class TestCallAgentMcpTool:
    """Tests for call_agent_mcp_tool function."""

    def test_returns_socket_not_found_when_socket_missing(
        self, tmp_state_dir: Path, sample_agent_state: AgentStateData
    ) -> None:
        """Test that MCPSocketNotFoundError is returned when socket doesn't exist."""
        # Create agent state directory but no socket
        agent_dir = tmp_state_dir / "agents" / sample_agent_state.instance_id
        agent_dir.mkdir(parents=True)
        state_file = agent_dir / "state.json"
        state_file.write_text(sample_agent_state.model_dump_json())

        result = mcp_bridge.call_agent_mcp_tool(
            sample_agent_state.instance_id, "anima.status"
        )

        assert isinstance(result, Failure)
        assert isinstance(result.failure(), MCPSocketNotFoundError)

    def test_returns_socket_not_found_when_state_missing(
        self, tmp_state_dir: Path
    ) -> None:
        """Test that MCPSocketNotFoundError is returned when agent doesn't exist.

        The MCP bridge checks socket first, so missing agents return socket not found
        rather than agent not found.
        """
        result = mcp_bridge.call_agent_mcp_tool("inst_nonexistent", "anima.status")

        assert isinstance(result, Failure)
        assert isinstance(result.failure(), MCPSocketNotFoundError)

    @patch("anima.shell.mcp_bridge._call_tool_sync")
    def test_returns_mcp_result_when_socket_exists(
        self,
        mock_call_tool: MagicMock,
        tmp_state_dir: Path,
        sample_agent_state: AgentStateData,
    ) -> None:
        """Test that MCP tool result is returned when socket exists."""
        # Create agent state and socket
        socket_path = (
            tmp_state_dir / "agents" / sample_agent_state.instance_id / "mcp.sock"
        )
        socket_path.parent.mkdir(parents=True)
        socket_path.touch()

        agent_dir = tmp_state_dir / "agents" / sample_agent_state.instance_id
        state_file = agent_dir / "state.json"
        state_file.write_text(sample_agent_state.model_dump_json())

        # Mock the sync tool call
        mock_call_tool.return_value = Success({"status": "running"})

        result = mcp_bridge.call_agent_mcp_tool(
            sample_agent_state.instance_id, "anima.status"
        )

        assert isinstance(result, Success)
        assert result.unwrap() == {"status": "running"}


class TestReadAgentStateFile:
    """Tests for read_agent_state_file function."""

    def test_returns_state_when_exists(
        self, tmp_state_dir: Path, sample_agent_state: AgentStateData
    ) -> None:
        """Test that state is read successfully when file exists."""
        # Create agent state directory and file
        agent_dir = tmp_state_dir / "agents" / sample_agent_state.instance_id
        agent_dir.mkdir(parents=True)
        state_file = agent_dir / "state.json"
        state_file.write_text(sample_agent_state.model_dump_json())

        result = mcp_bridge.read_agent_state_file(sample_agent_state.instance_id)

        assert isinstance(result, Success)
        data = result.unwrap()
        assert data["instance_id"] == sample_agent_state.instance_id
        assert data["state"] == "running"

    def test_returns_agent_not_found_when_missing(self, tmp_state_dir: Path) -> None:
        """Test that AgentNotFoundError is returned when state file doesn't exist."""
        result = mcp_bridge.read_agent_state_file("inst_nonexistent")

        assert isinstance(result, Failure)
        assert isinstance(result.failure(), AgentNotFoundError)

    def test_returns_internal_error_when_invalid_json(
        self, tmp_state_dir: Path
    ) -> None:
        """Test that InternalError is returned when state file has invalid JSON."""
        agent_dir = tmp_state_dir / "agents" / "inst_invalid"
        agent_dir.mkdir(parents=True)
        state_file = agent_dir / "state.json"
        state_file.write_text("invalid json content")

        result = mcp_bridge.read_agent_state_file("inst_invalid")

        assert isinstance(result, Failure)
        # Should be InternalError due to JSON decode failure
        error = result.failure()
        assert isinstance(error, (json.JSONDecodeError, Exception))


class TestReadAgentStateWithFallback:
    """Tests for read_agent_state_with_fallback function."""

    def test_uses_mcp_when_agent_alive(
        self, tmp_state_dir: Path, sample_agent_state: AgentStateData
    ) -> None:
        """Test that MCP is called when agent is alive."""
        # Create agent state and socket
        socket_path = (
            tmp_state_dir / "agents" / sample_agent_state.instance_id / "mcp.sock"
        )
        socket_path.parent.mkdir(parents=True)
        socket_path.touch()

        agent_dir = tmp_state_dir / "agents" / sample_agent_state.instance_id
        state_file = agent_dir / "state.json"
        state_file.write_text(sample_agent_state.model_dump_json())

        with patch("anima.shell.mcp_bridge.call_agent_mcp_tool") as mock_mcp:
            mock_mcp.return_value = Success({"status": "running"})

            result = mcp_bridge.read_agent_state_with_fallback(
                sample_agent_state.instance_id
            )

            mock_mcp.assert_called_once()
            assert isinstance(result, Success)

    def test_fallback_to_file_when_agent_dead(
        self, tmp_state_dir: Path, sample_agent_state: AgentStateData
    ) -> None:
        """Test that state file is read when agent is dead (no socket)."""
        # Create agent state directory but no socket
        agent_dir = tmp_state_dir / "agents" / sample_agent_state.instance_id
        agent_dir.mkdir(parents=True)
        state_file = agent_dir / "state.json"
        state_file.write_text(sample_agent_state.model_dump_json())

        with patch("anima.shell.mcp_bridge.call_agent_mcp_tool") as mock_mcp:
            mock_mcp.return_value = Failure(MCPSocketNotFoundError("Socket not found"))

            result = mcp_bridge.read_agent_state_with_fallback(
                sample_agent_state.instance_id
            )

            # Should fallback to file
            assert isinstance(result, Success)
            data = result.unwrap()
            assert data["instance_id"] == sample_agent_state.instance_id


class TestMcpBridgeModule:
    """Integration tests for mcp_bridge module exports."""

    def test_module_exports(self) -> None:
        """Test that module exports expected functions."""
        assert hasattr(mcp_bridge, "call_agent_mcp_tool")
        assert hasattr(mcp_bridge, "read_agent_state_file")
        assert hasattr(mcp_bridge, "read_agent_state_with_fallback")
        assert hasattr(mcp_bridge, "get_mcp_socket_path")
        assert hasattr(mcp_bridge, "is_agent_alive")
        assert hasattr(mcp_bridge, "try_mcp_with_fallback")
        assert hasattr(mcp_bridge, "try_mcp_with_fallback_async")


class TestTryMcpWithFallback:
    """Tests for try_mcp_with_fallback function (MCP-first pattern for lifecycle)."""

    def test_returns_failure_when_agent_not_alive(self, tmp_state_dir: Path) -> None:
        """Test that Failure is returned when agent has no socket (not alive)."""
        # No socket created - agent not alive
        result = mcp_bridge.try_mcp_with_fallback(
            "inst_nonexistent",
            "anima.approve",
        )

        assert isinstance(result, Failure)
        error = result.failure()
        assert isinstance(error, MCPSocketNotFoundError)

    def test_calls_mcp_tool_when_agent_alive(
        self, tmp_state_dir: Path, sample_agent_state: AgentStateData
    ) -> None:
        """Test that MCP tool is called when agent is alive (socket exists)."""
        # Create agent state with socket (alive agent)
        socket_path = (
            tmp_state_dir / "agents" / sample_agent_state.instance_id / "mcp.sock"
        )
        socket_path.parent.mkdir(parents=True)
        socket_path.touch()

        agent_dir = tmp_state_dir / "agents" / sample_agent_state.instance_id
        state_file = agent_dir / "state.json"
        state_file.write_text(sample_agent_state.model_dump_json())

        with patch("anima.shell.mcp_bridge.call_agent_mcp_tool") as mock_mcp:
            mock_mcp.return_value = Success({"acknowledged": True})

            result = mcp_bridge.try_mcp_with_fallback(
                sample_agent_state.instance_id,
                "anima.approve",
                arguments={"note": "test"},
            )

            # MCP should be called
            mock_mcp.assert_called_once_with(
                sample_agent_state.instance_id,
                "anima.approve",
                arguments={"note": "test"},
                timeout=30.0,
            )
            assert isinstance(result, Success)
            assert result.unwrap() == {"acknowledged": True}

    def test_returns_mcp_failure_when_call_fails(
        self, tmp_state_dir: Path, sample_agent_state: AgentStateData
    ) -> None:
        """Test that MCP failure is propagated when agent alive but call fails."""
        # Create agent state with socket (alive agent)
        socket_path = (
            tmp_state_dir / "agents" / sample_agent_state.instance_id / "mcp.sock"
        )
        socket_path.parent.mkdir(parents=True)
        socket_path.touch()

        agent_dir = tmp_state_dir / "agents" / sample_agent_state.instance_id
        state_file = agent_dir / "state.json"
        state_file.write_text(sample_agent_state.model_dump_json())

        with patch("anima.shell.mcp_bridge.call_agent_mcp_tool") as mock_mcp:
            mock_mcp.return_value = Failure(MCPConnectionError("Connection failed"))

            result = mcp_bridge.try_mcp_with_fallback(
                sample_agent_state.instance_id,
                "anima.approve",
            )

            # MCP failure should be propagated
            assert isinstance(result, Failure)
