"""Tests for ctl_child.py: ctl_poll, ctl_cancel, ctl_child_logs.

Phase 2 subagent coordination tools. All tests are xfail(strict=True)
because the implementations are stubs raising NotImplementedError.

Uses real tmp filesystem — no mocking state layer.
"""

import json
from pathlib import Path

import pytest
from returns.result import Failure, Success

from anima.core.schemas import (
    AgentState,
    AgentStateData,
    InvalidInputError,
    LifecycleType,
)
from anima.shell.ctl_child import ctl_cancel, ctl_child_logs, ctl_poll
from anima.shell.state import append_log, write_state


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@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 parent_with_child(tmp_state_dir: Path) -> tuple[AgentStateData, AgentStateData]:
    """Create a running parent with one running child in its children list.

    Returns (parent_state, child_state).
    """
    child_id = "inst_child_001"
    parent_id = "inst_parent"

    # Create child state (running)
    child_state = AgentStateData(
        instance_id=child_id,
        trace_id="tr_child",
        persona_name="worker",
        state=AgentState.running,
        lifecycle=LifecycleType.ephemeral,
        parent_id=parent_id,
        depth=1,
        current_turn=3,
    )
    write_state(child_id, child_state)

    # Create parent state with child in children list
    parent_state = AgentStateData(
        instance_id=parent_id,
        trace_id="tr_parent",
        persona_name="parent",
        state=AgentState.running,
        lifecycle=LifecycleType.ephemeral,
        depth=0,
        max_depth=3,
        children=[child_id],
    )
    write_state(parent_id, parent_state)

    return parent_state, child_state


# ---------------------------------------------------------------------------
# ctl_poll tests
# ---------------------------------------------------------------------------


class TestCtlPoll:
    """Tests for ctl_poll tool."""

    @pytest.mark.asyncio
    async def test_poll_returns_running_status(
        self, parent_with_child: tuple[AgentStateData, AgentStateData]
    ) -> None:
        """ctl_poll returns status envelope for a running child."""
        parent, child = parent_with_child

        result = await ctl_poll(parent.instance_id, child.instance_id)

        assert isinstance(result, Success)
        envelope = json.loads(result.unwrap())
        assert envelope["child_id"] == child.instance_id
        assert envelope["status"] == "running"
        assert envelope["current_turn"] == 3
        assert envelope["error"] is None
        assert "summary" in envelope

    @pytest.mark.asyncio
    async def test_poll_returns_completed_status(
        self, parent_with_child: tuple[AgentStateData, AgentStateData]
    ) -> None:
        """ctl_poll returns completed status for a finished child."""
        parent, child = parent_with_child

        # Transition child to completed
        completed_child = child.model_copy(
            update={"state": AgentState.completed, "current_turn": 5}
        )
        write_state(child.instance_id, completed_child)

        result = await ctl_poll(parent.instance_id, child.instance_id)

        assert isinstance(result, Success)
        envelope = json.loads(result.unwrap())
        assert envelope["child_id"] == child.instance_id
        assert envelope["status"] == "completed"
        assert envelope["current_turn"] == 5
        assert envelope["error"] is None

    @pytest.mark.asyncio
    async def test_poll_returns_failed_status_with_error(
        self, parent_with_child: tuple[AgentStateData, AgentStateData]
    ) -> None:
        """ctl_poll returns failed status with error field populated."""
        parent, child = parent_with_child

        # Transition child to failed with error
        failed_child = child.model_copy(
            update={
                "state": AgentState.failed,
                "current_turn": 4,
                "error": "max_turns exceeded",
            }
        )
        write_state(child.instance_id, failed_child)

        result = await ctl_poll(parent.instance_id, child.instance_id)

        assert isinstance(result, Success)
        envelope = json.loads(result.unwrap())
        assert envelope["child_id"] == child.instance_id
        assert envelope["status"] == "failed"
        assert envelope["error"] == "max_turns exceeded"
        assert envelope["current_turn"] == 4

    @pytest.mark.asyncio
    async def test_poll_rejects_invalid_child_id(
        self, parent_with_child: tuple[AgentStateData, AgentStateData]
    ) -> None:
        """ctl_poll rejects a child_id not in parent's children list."""
        parent, _child = parent_with_child

        result = await ctl_poll(parent.instance_id, "inst_not_my_child")

        assert isinstance(result, Failure)
        error = result.failure()
        assert isinstance(error, InvalidInputError)
        assert "inst_not_my_child" in str(error)

    @pytest.mark.asyncio
    async def test_poll_rejects_nonexistent_child(
        self, tmp_state_dir: Path
    ) -> None:
        """ctl_poll fails when child's state.json doesn't exist."""
        parent_id = "inst_parent_orphan"
        ghost_child_id = "inst_ghost"

        # Parent lists a child whose state dir doesn't exist
        parent_state = AgentStateData(
            instance_id=parent_id,
            trace_id="tr_orphan",
            persona_name="parent",
            state=AgentState.running,
            lifecycle=LifecycleType.ephemeral,
            children=[ghost_child_id],
        )
        write_state(parent_id, parent_state)

        result = await ctl_poll(parent_id, ghost_child_id)

        assert isinstance(result, Failure)


# ---------------------------------------------------------------------------
# ctl_cancel tests
# ---------------------------------------------------------------------------


class TestCtlCancel:
    """Tests for ctl_cancel tool."""

    @pytest.mark.asyncio
    async def test_cancel_kills_child(
        self,
        parent_with_child: tuple[AgentStateData, AgentStateData],
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """ctl_cancel kills a running child (mock kill_agent_tree)."""
        parent, child = parent_with_child

        kill_called_with: list[str] = []

        def _mock_kill(instance_id: str, error: str = "", **kwargs: object) -> Success:
            kill_called_with.append(instance_id)
            return Success(None)

        monkeypatch.setattr(
            "anima.shell.ctl_child.kill_agent_tree", _mock_kill
        )

        result = await ctl_cancel(parent.instance_id, child.instance_id)

        assert isinstance(result, Success)
        envelope = json.loads(result.unwrap())
        assert envelope["child_id"] == child.instance_id
        assert envelope["status"] == "cancelled"
        assert "summary" in envelope
        assert child.instance_id in kill_called_with

    @pytest.mark.asyncio
    async def test_cancel_rejects_invalid_child_id(
        self, parent_with_child: tuple[AgentStateData, AgentStateData]
    ) -> None:
        """ctl_cancel rejects a child_id not in parent's children list."""
        parent, _child = parent_with_child

        result = await ctl_cancel(parent.instance_id, "inst_not_my_child")

        assert isinstance(result, Failure)
        error = result.failure()
        assert isinstance(error, InvalidInputError)
        assert "inst_not_my_child" in str(error)


# ---------------------------------------------------------------------------
# ctl_child_logs tests
# ---------------------------------------------------------------------------


class TestCtlChildLogs:
    """Tests for ctl_child_logs tool."""

    @pytest.mark.asyncio
    async def test_child_logs_returns_filtered_events(
        self, parent_with_child: tuple[AgentStateData, AgentStateData]
    ) -> None:
        """ctl_child_logs returns only conversation events, filtering infra noise."""
        parent, child = parent_with_child

        # Write mixed log events to child's log.jsonl
        conversation_events = [
            ("user_message", {"text": "Analyze auth module"}),
            ("tool_call", {"tool": "read_file", "args": "src/auth.py"}),
            ("response", {"text": "Found 3 issues"}),
            ("error", {"text": "max_turns exceeded"}),
        ]
        infra_events = [
            ("created", {"persona": "worker"}),
            ("state_transition", {"from": "pending", "to": "running"}),
        ]

        # Write infra events first, then conversation events
        for event, details in infra_events:
            append_log(child.instance_id, event, details)
        for event, details in conversation_events:
            append_log(child.instance_id, event, details)

        result = await ctl_child_logs(
            parent.instance_id, child.instance_id, last_n=50
        )

        assert isinstance(result, Success)
        envelope = json.loads(result.unwrap())
        assert envelope["child_id"] == child.instance_id
        # Should only contain conversation events, not infra
        assert envelope["returned_events"] == len(conversation_events)
        assert len(envelope["events"]) == len(conversation_events)
        event_types = [e["event"] for e in envelope["events"]]
        assert "created" not in event_types
        assert "state_transition" not in event_types
        assert "user_message" in event_types
        assert "response" in event_types

    @pytest.mark.asyncio
    async def test_child_logs_respects_last_n(
        self, parent_with_child: tuple[AgentStateData, AgentStateData]
    ) -> None:
        """ctl_child_logs with last_n=5 returns only 5 events."""
        parent, child = parent_with_child

        # Write 20 conversation events
        for i in range(20):
            append_log(
                child.instance_id,
                "response",
                {"text": f"Response {i}", "turn": i},
            )

        result = await ctl_child_logs(
            parent.instance_id, child.instance_id, last_n=5
        )

        assert isinstance(result, Success)
        envelope = json.loads(result.unwrap())
        assert envelope["returned_events"] == 5
        assert len(envelope["events"]) == 5
        assert envelope["total_events"] == 20

    @pytest.mark.asyncio
    async def test_child_logs_default_last_n(
        self, parent_with_child: tuple[AgentStateData, AgentStateData]
    ) -> None:
        """ctl_child_logs defaults to last_n=10."""
        parent, child = parent_with_child

        # Write 20 conversation events
        for i in range(20):
            append_log(
                child.instance_id,
                "user_message",
                {"text": f"Message {i}"},
            )

        # Call without last_n — should default to 10
        result = await ctl_child_logs(
            parent.instance_id, child.instance_id
        )

        assert isinstance(result, Success)
        envelope = json.loads(result.unwrap())
        assert envelope["returned_events"] == 10
        assert len(envelope["events"]) == 10
        assert envelope["total_events"] == 20

    @pytest.mark.asyncio
    async def test_child_logs_rejects_invalid_child_id(
        self, parent_with_child: tuple[AgentStateData, AgentStateData]
    ) -> None:
        """ctl_child_logs rejects a child_id not in parent's children list."""
        parent, _child = parent_with_child

        result = await ctl_child_logs(
            parent.instance_id, "inst_not_my_child"
        )

        assert isinstance(result, Failure)
        error = result.failure()
        assert isinstance(error, InvalidInputError)
        assert "inst_not_my_child" in str(error)
