"""Tests for lifecycle_cmds MCP-first behavior.

Tests the MCP-first pattern in approve_impl, reject_impl, and resume_impl:
- MCP is tried first for alive agents
- Falls back to file/state operations if MCP not available
"""

import sys
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest
from returns.result import Failure, Success
from rich.console import Console

from anima.core.schemas import (
    AgentState,
    AgentStateData,
    LifecycleType,
    TokenUsage,
)
from anima.shell.cli_commands import lifecycle_cmds


@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_awaiting_approval_state() -> AgentStateData:
    """Create sample agent state in awaiting_approval state."""
    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.awaiting_approval,
        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,
    )


@pytest.fixture
def sample_suspended_state() -> AgentStateData:
    """Create sample agent state in suspended state."""
    return AgentStateData(
        instance_id="inst_suspended",
        trace_id="trace_def",
        persona_name="test_persona",
        spec_id="spec_001",
        spec_digest="abc123",
        model="test-model",
        state=AgentState.suspended,
        lifecycle=LifecycleType.ephemeral,
        parent_id=None,
        children=[],
        depth=0,
        max_depth=3,
        pid=12345,
        mcp_socket="/tmp/anima/agents/inst_suspended/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,
    )


@pytest.fixture
def console() -> Console:
    """Create a Console instance for testing."""
    return Console(force_terminal=True, width=80)


class TestApproveImplMcpFirst:
    """Tests for approve_impl MCP-first behavior."""

    @patch("anima.shell.cli_commands.lifecycle_cmds.try_mcp_with_fallback")
    @patch("anima.shell.cli_commands.lifecycle_cmds.resolve_agent")
    def test_approve_uses_mcp_first_when_alive(
        self,
        mock_resolve: MagicMock,
        mock_mcp: MagicMock,
        tmp_state_dir: Path,
        sample_awaiting_approval_state: AgentStateData,
        console: Console,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Test that approve_impl tries MCP first for alive agents."""
        # Setup: create socket (alive agent)
        socket_path = (
            tmp_state_dir
            / "agents"
            / sample_awaiting_approval_state.instance_id
            / "mcp.sock"
        )
        socket_path.parent.mkdir(parents=True)
        socket_path.touch()

        # Setup mock state
        mock_resolve.return_value = sample_awaiting_approval_state

        # Setup MCP to succeed
        mock_mcp.return_value = Success({"acknowledged": True})

        # Run approve
        monkeypatch.setattr(sys, "exit", lambda code: None)
        lifecycle_cmds.approve_impl(
            sample_awaiting_approval_state.instance_id,
            note="test note",
            json_output=False,
            console=console,
        )

        # Verify MCP was called
        mock_mcp.assert_called_once()
        call_args = mock_mcp.call_args
        assert call_args[0][0] == sample_awaiting_approval_state.instance_id
        assert call_args[0][1] == "anima.approve"

    @patch("anima.shell.cli_commands.lifecycle_cmds.try_mcp_with_fallback")
    @patch("anima.shell.cli_commands.lifecycle_cmds.resolve_agent")
    def test_approve_fallback_when_agent_not_alive(
        self,
        mock_resolve: MagicMock,
        mock_mcp: MagicMock,
        tmp_state_dir: Path,
        sample_awaiting_approval_state: AgentStateData,
        console: Console,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Test that approve_impl falls back to file operations when MCP not available."""
        # Setup: NO socket (dead agent)
        # Agent dir does not exist

        # Setup mock state
        mock_resolve.return_value = sample_awaiting_approval_state

        # Setup MCP to fail (agent not alive)
        mock_mcp.return_value = Failure(Exception("Agent not alive"))

        # Run approve
        monkeypatch.setattr(sys, "exit", lambda code: None)
        lifecycle_cmds.approve_impl(
            sample_awaiting_approval_state.instance_id,
            note="test note",
            json_output=False,
            console=console,
        )

        # Verify MCP was tried and failed
        mock_mcp.assert_called_once()


class TestRejectImplMcpFirst:
    """Tests for reject_impl MCP-first behavior."""

    @patch("anima.shell.cli_commands.lifecycle_cmds.try_mcp_with_fallback")
    @patch("anima.shell.cli_commands.lifecycle_cmds.resolve_agent")
    def test_reject_uses_mcp_first_when_alive(
        self,
        mock_resolve: MagicMock,
        mock_mcp: MagicMock,
        tmp_state_dir: Path,
        sample_awaiting_approval_state: AgentStateData,
        console: Console,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Test that reject_impl tries MCP first for alive agents."""
        # Setup: create socket (alive agent)
        socket_path = (
            tmp_state_dir
            / "agents"
            / sample_awaiting_approval_state.instance_id
            / "mcp.sock"
        )
        socket_path.parent.mkdir(parents=True)
        socket_path.touch()

        # Setup mock state
        mock_resolve.return_value = sample_awaiting_approval_state

        # Setup MCP to succeed
        mock_mcp.return_value = Success({"acknowledged": True})

        # Run reject
        monkeypatch.setattr(sys, "exit", lambda code: None)
        lifecycle_cmds.reject_impl(
            sample_awaiting_approval_state.instance_id,
            reason="test reason",
            json_output=False,
            console=console,
        )

        # Verify MCP was called
        mock_mcp.assert_called_once()
        call_args = mock_mcp.call_args
        assert call_args[0][0] == sample_awaiting_approval_state.instance_id
        assert call_args[0][1] == "anima.reject"


class TestResumeImplMcpFirst:
    """Tests for resume_impl MCP-first behavior."""

    @patch("anima.shell.cli_commands.lifecycle_cmds.try_mcp_with_fallback")
    @patch("anima.shell.cli_commands.lifecycle_cmds.resolve_agent")
    def test_resume_uses_mcp_first_when_alive(
        self,
        mock_resolve: MagicMock,
        mock_mcp: MagicMock,
        tmp_state_dir: Path,
        sample_suspended_state: AgentStateData,
        console: Console,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Test that resume_impl tries MCP first for alive agents."""
        # Setup: create socket (alive agent)
        socket_path = (
            tmp_state_dir / "agents" / sample_suspended_state.instance_id / "mcp.sock"
        )
        socket_path.parent.mkdir(parents=True)
        socket_path.touch()

        # Setup mock state
        mock_resolve.return_value = sample_suspended_state

        # Setup MCP to succeed
        mock_mcp.return_value = Success({"acknowledged": True})

        # Run resume
        monkeypatch.setattr(sys, "exit", lambda code: None)
        lifecycle_cmds.resume_impl(
            sample_suspended_state.instance_id,
            json_output=False,
            console=console,
        )

        # Verify MCP was called
        mock_mcp.assert_called_once()
        call_args = mock_mcp.call_args
        assert call_args[0][0] == sample_suspended_state.instance_id
        assert call_args[0][1] == "anima.resume"

    @patch("anima.shell.cli_commands.lifecycle_cmds.try_mcp_with_fallback")
    @patch("anima.shell.cli_commands.lifecycle_cmds.resolve_agent")
    def test_resume_fallback_when_agent_not_alive(
        self,
        mock_resolve: MagicMock,
        mock_mcp: MagicMock,
        tmp_state_dir: Path,
        sample_suspended_state: AgentStateData,
        console: Console,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Test that resume_impl falls back to file operations when MCP not available."""
        # Setup: NO socket (dead agent)

        # Setup mock state
        mock_resolve.return_value = sample_suspended_state

        # Setup MCP to fail (agent not alive)
        mock_mcp.return_value = Failure(Exception("Agent not alive"))

        # Run resume
        monkeypatch.setattr(sys, "exit", lambda code: None)
        lifecycle_cmds.resume_impl(
            sample_suspended_state.instance_id,
            json_output=False,
            console=console,
        )

        # Verify MCP was tried and failed - fallback should have been attempted
        mock_mcp.assert_called_once()
