"""Tests for subagent approval propagation V1: child polling + command routing.

Spec source: user requirement for child approval propagation.
Tests cover:
- _find_children_awaiting_approval (talk_runtime) — child discovery for polling
- _resolve_child_target (talk_session) — prefix resolution
- _handle_child_approval (talk_session) — direct state write path
- _find_children_awaiting (talk_session) — auto-targeting discovery
- handle_inline_approval_via_mcp with child routing
- Help text update verification
"""

from __future__ import annotations

import asyncio
import io
import json
from dataclasses import replace
from typing import Any

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

from anima.core.lifecycle import LifecycleEvent
from anima.core.schemas import AgentState, AgentStateData, InternalError
from anima.shell.cli_commands.talk_deps import TalkDeps, default_deps
from anima.shell.cli_commands.talk_display import SLASH_HELP_LINES
from anima.shell.cli_commands.talk_runtime import _find_children_awaiting_approval
from anima.shell.cli_commands.talk_session import (
    _find_children_awaiting,
    _handle_child_approval,
    _resolve_child_target,
    handle_inline_approval_via_mcp,
)


# ---------------------------------------------------------------------------
# Fixtures / helpers
# ---------------------------------------------------------------------------


def _make_state(
    instance_id: str,
    *,
    state: AgentState = AgentState.running,
    persona_name: str = "worker",
    children: list[str] | None = None,
) -> AgentStateData:
    """Construct a minimal AgentStateData for tests."""
    return AgentStateData(
        instance_id=instance_id,
        persona_name=persona_name,
        trace_id="tr_test",
        state=state,
        children=children or [],
    )


def _make_log_with_approval_reason(reason: str) -> list[dict[str, Any]]:
    """Create a log entry list containing an approval_requested event."""
    return [
        {"event": "start", "details": {}},
        {
            "event": "approval_requested",
            "details": {"reason": reason},
        },
    ]


def _make_deps(
    *,
    states: dict[str, AgentStateData] | None = None,
    logs: dict[str, list[dict[str, Any]]] | None = None,
    transition_results: dict[str, AgentStateData] | None = None,
    approval_write_results: dict[str, bool] | None = None,
) -> TalkDeps:
    """Build a TalkDeps with mocked read_state, read_log, transition_state, write_approval_response."""
    _states = states or {}
    _logs = logs or {}
    _transition_results = transition_results or {}
    _approval_write_results = approval_write_results or {}

    def _read_state(instance_id: str):
        if instance_id in _states:
            return Success(_states[instance_id])
        return Failure(InternalError(f"Agent not found: {instance_id}"))

    def _read_log(instance_id: str):
        if instance_id in _logs:
            return Success(_logs[instance_id])
        return Success([])

    def _transition_state(instance_id, event, error=None, result_ref=None):
        if instance_id in _transition_results:
            return Success(_transition_results[instance_id])
        if instance_id in _states:
            updated = _states[instance_id].model_copy(
                update={"state": AgentState.running}
            )
            return Success(updated)
        return Failure(InternalError(f"Agent not found: {instance_id}"))

    def _write_approval_response(instance_id, approved, note):
        if instance_id in _approval_write_results:
            return Success(None) if _approval_write_results[instance_id] else Failure(
                InternalError("Write failed")
            )
        return Success(None)

    base = default_deps()
    return replace(
        base,
        read_state=_read_state,
        read_log=_read_log,
        transition_state=_transition_state,
        write_approval_response=_write_approval_response,
    )


def _console_and_stream() -> tuple[Console, io.StringIO]:
    stream = io.StringIO()
    return Console(file=stream, force_terminal=True, width=200), stream


# ---------------------------------------------------------------------------
# Tests: _find_children_awaiting_approval (talk_runtime)
# ---------------------------------------------------------------------------


class TestFindChildrenAwaitingApproval:
    """Tests for _find_children_awaiting_approval in talk_runtime."""

    def test_no_children_returns_empty(self):
        parent = _make_state("inst_parent", children=[])
        deps = _make_deps(states={"inst_parent": parent})
        result = _find_children_awaiting_approval("inst_parent", deps)
        assert result == []

    def test_child_not_awaiting_returns_empty(self):
        parent = _make_state("inst_parent", children=["inst_child1"])
        child = _make_state("inst_child1", state=AgentState.running)
        deps = _make_deps(states={"inst_parent": parent, "inst_child1": child})
        result = _find_children_awaiting_approval("inst_parent", deps)
        assert result == []

    def test_child_awaiting_returned_with_reason(self):
        parent = _make_state("inst_parent", children=["inst_child1"])
        child = _make_state(
            "inst_child1",
            state=AgentState.awaiting_approval,
            persona_name="researcher",
        )
        deps = _make_deps(
            states={"inst_parent": parent, "inst_child1": child},
            logs={
                "inst_child1": _make_log_with_approval_reason(
                    "Need permission to delete files"
                )
            },
        )
        result = _find_children_awaiting_approval("inst_parent", deps)
        assert len(result) == 1
        child_id, persona, reason = result[0]
        assert child_id == "inst_child1"
        assert persona == "researcher"
        assert reason == "Need permission to delete files"

    def test_child_awaiting_no_log_uses_default_reason(self):
        parent = _make_state("inst_parent", children=["inst_child1"])
        child = _make_state(
            "inst_child1",
            state=AgentState.awaiting_approval,
            persona_name="worker",
        )
        deps = _make_deps(
            states={"inst_parent": parent, "inst_child1": child},
        )
        result = _find_children_awaiting_approval("inst_parent", deps)
        assert len(result) == 1
        _, _, reason = result[0]
        assert reason == "no reason provided"

    def test_multiple_children_mixed_states(self):
        parent = _make_state(
            "inst_parent",
            children=["inst_child1", "inst_child2", "inst_child3"],
        )
        child1 = _make_state("inst_child1", state=AgentState.running)
        child2 = _make_state(
            "inst_child2",
            state=AgentState.awaiting_approval,
            persona_name="analyst",
        )
        child3 = _make_state(
            "inst_child3",
            state=AgentState.awaiting_approval,
            persona_name="coder",
        )
        deps = _make_deps(
            states={
                "inst_parent": parent,
                "inst_child1": child1,
                "inst_child2": child2,
                "inst_child3": child3,
            },
            logs={
                "inst_child2": _make_log_with_approval_reason("Run tests?"),
                "inst_child3": _make_log_with_approval_reason("Deploy code?"),
            },
        )
        result = _find_children_awaiting_approval("inst_parent", deps)
        assert len(result) == 2
        ids = [r[0] for r in result]
        assert "inst_child2" in ids
        assert "inst_child3" in ids

    def test_parent_not_found_returns_empty(self):
        deps = _make_deps(states={})
        result = _find_children_awaiting_approval("inst_nonexistent", deps)
        assert result == []

    def test_child_state_unreadable_skipped(self):
        parent = _make_state(
            "inst_parent", children=["inst_child1", "inst_child2"]
        )
        child2 = _make_state(
            "inst_child2",
            state=AgentState.awaiting_approval,
            persona_name="worker",
        )
        # inst_child1 not in states -> will return Failure
        deps = _make_deps(
            states={"inst_parent": parent, "inst_child2": child2},
        )
        result = _find_children_awaiting_approval("inst_parent", deps)
        assert len(result) == 1
        assert result[0][0] == "inst_child2"


# ---------------------------------------------------------------------------
# Tests: _resolve_child_target (talk_session)
# ---------------------------------------------------------------------------


class TestResolveChildTarget:
    """Tests for _resolve_child_target prefix resolution."""

    def test_no_argument_returns_none(self):
        deps = _make_deps(states={})
        child_id, note = _resolve_child_target(
            argument=None, agent_id="inst_parent", deps=deps
        )
        assert child_id is None
        assert note is None

    def test_non_inst_prefix_returns_none_with_argument(self):
        deps = _make_deps(states={})
        child_id, note = _resolve_child_target(
            argument="looks good", agent_id="inst_parent", deps=deps
        )
        assert child_id is None
        assert note == "looks good"

    def test_inst_prefix_resolves_single_match(self):
        parent = _make_state(
            "inst_parent",
            children=["inst_abc123", "inst_def456"],
        )
        deps = _make_deps(states={"inst_parent": parent})
        child_id, note = _resolve_child_target(
            argument="inst_abc looks good", agent_id="inst_parent", deps=deps
        )
        assert child_id == "inst_abc123"
        assert note == "looks good"

    def test_inst_prefix_no_remaining_note(self):
        parent = _make_state(
            "inst_parent",
            children=["inst_abc123"],
        )
        deps = _make_deps(states={"inst_parent": parent})
        child_id, note = _resolve_child_target(
            argument="inst_abc", agent_id="inst_parent", deps=deps
        )
        assert child_id == "inst_abc123"
        assert note is None

    def test_inst_prefix_no_match_returns_none(self):
        parent = _make_state(
            "inst_parent",
            children=["inst_abc123"],
        )
        deps = _make_deps(states={"inst_parent": parent})
        child_id, note = _resolve_child_target(
            argument="inst_zzz", agent_id="inst_parent", deps=deps
        )
        assert child_id is None
        assert note == "inst_zzz"

    def test_inst_prefix_ambiguous_returns_none(self):
        parent = _make_state(
            "inst_parent",
            children=["inst_abc111", "inst_abc222"],
        )
        deps = _make_deps(states={"inst_parent": parent})
        child_id, note = _resolve_child_target(
            argument="inst_abc my note", agent_id="inst_parent", deps=deps
        )
        assert child_id is None
        assert note == "inst_abc my note"

    def test_inst_prefix_exact_match_among_ambiguous(self):
        parent = _make_state(
            "inst_parent",
            children=["inst_abc", "inst_abc123"],
        )
        deps = _make_deps(states={"inst_parent": parent})
        child_id, note = _resolve_child_target(
            argument="inst_abc note", agent_id="inst_parent", deps=deps
        )
        assert child_id == "inst_abc"
        assert note == "note"


# ---------------------------------------------------------------------------
# Tests: _handle_child_approval (talk_session)
# ---------------------------------------------------------------------------


class TestHandleChildApproval:
    """Tests for _handle_child_approval direct state write path."""

    def test_approve_child_success(self):
        child = _make_state(
            "inst_child1",
            state=AgentState.awaiting_approval,
            persona_name="worker",
        )
        deps = _make_deps(states={"inst_child1": child})
        console, stream = _console_and_stream()

        result = _handle_child_approval(
            child_id="inst_child1",
            command="approve",
            note="looks good",
            json_output=False,
            console=console,
            deps=deps,
        )
        assert result is True
        output = stream.getvalue()
        assert "Approved" in output

    def test_reject_child_success(self):
        child = _make_state(
            "inst_child1",
            state=AgentState.awaiting_approval,
            persona_name="worker",
        )
        deps = _make_deps(states={"inst_child1": child})
        console, stream = _console_and_stream()

        result = _handle_child_approval(
            child_id="inst_child1",
            command="reject",
            note="not safe",
            json_output=False,
            console=console,
            deps=deps,
        )
        assert result is True
        output = stream.getvalue()
        assert "Rejected" in output

    def test_child_not_awaiting_approval_error(self):
        child = _make_state(
            "inst_child1",
            state=AgentState.running,
            persona_name="worker",
        )
        deps = _make_deps(states={"inst_child1": child})
        console, stream = _console_and_stream()

        result = _handle_child_approval(
            child_id="inst_child1",
            command="approve",
            note=None,
            json_output=False,
            console=console,
            deps=deps,
        )
        assert result is True
        output = stream.getvalue()
        assert "not awaiting approval" in output

    def test_child_not_found_error(self):
        deps = _make_deps(states={})
        console, stream = _console_and_stream()

        result = _handle_child_approval(
            child_id="inst_nonexistent",
            command="approve",
            note=None,
            json_output=False,
            console=console,
            deps=deps,
        )
        assert result is True
        output = stream.getvalue()
        assert "Error" in output

    def test_approve_child_json_output(self):
        child = _make_state(
            "inst_child1",
            state=AgentState.awaiting_approval,
            persona_name="worker",
        )
        deps = _make_deps(states={"inst_child1": child})
        console, stream = _console_and_stream()

        result = _handle_child_approval(
            child_id="inst_child1",
            command="approve",
            note="ok",
            json_output=True,
            console=console,
            deps=deps,
        )
        assert result is True
        output = stream.getvalue()
        payload = json.loads(output.strip())
        assert payload["event"] == "approval_applied"
        assert payload["agent_id"] == "inst_child1"
        assert payload["approved"] is True


# ---------------------------------------------------------------------------
# Tests: _find_children_awaiting (talk_session)
# ---------------------------------------------------------------------------


class TestFindChildrenAwaiting:
    """Tests for _find_children_awaiting auto-targeting discovery."""

    def test_no_children(self):
        parent = _make_state("inst_parent")
        deps = _make_deps(states={"inst_parent": parent})
        assert _find_children_awaiting("inst_parent", deps) == []

    def test_one_awaiting(self):
        parent = _make_state("inst_parent", children=["inst_child1"])
        child = _make_state("inst_child1", state=AgentState.awaiting_approval)
        deps = _make_deps(states={"inst_parent": parent, "inst_child1": child})
        result = _find_children_awaiting("inst_parent", deps)
        assert result == ["inst_child1"]

    def test_multiple_awaiting(self):
        parent = _make_state(
            "inst_parent", children=["inst_child1", "inst_child2"]
        )
        child1 = _make_state("inst_child1", state=AgentState.awaiting_approval)
        child2 = _make_state("inst_child2", state=AgentState.awaiting_approval)
        deps = _make_deps(
            states={
                "inst_parent": parent,
                "inst_child1": child1,
                "inst_child2": child2,
            }
        )
        result = _find_children_awaiting("inst_parent", deps)
        assert set(result) == {"inst_child1", "inst_child2"}


# ---------------------------------------------------------------------------
# Tests: handle_inline_approval_via_mcp with child routing
# ---------------------------------------------------------------------------


class _FakeMcpSession:
    """Fake MCP session for testing main agent approval path."""

    def __init__(self, *, response_payload: dict[str, Any] | None = None):
        self._response = response_payload or {"ok": True}
        self.calls: list[tuple[str, dict[str, Any]]] = []

    async def call_tool(self, tool_name: str, params: dict[str, Any]) -> Any:
        self.calls.append((tool_name, params))

        class _Content:
            def __init__(self, text: str):
                self.text = text

        class _Result:
            def __init__(self, content_list):
                self.content = content_list

        return _Result([_Content(json.dumps(self._response))])


class TestHandleInlineApprovalWithChildRouting:
    """Tests for handle_inline_approval_via_mcp with child routing."""

    def test_approve_main_agent_via_mcp(self):
        """Bare /approve targets main agent when main is awaiting."""
        parent = _make_state(
            "inst_parent", state=AgentState.awaiting_approval
        )
        deps = _make_deps(states={"inst_parent": parent})
        console, stream = _console_and_stream()
        session = _FakeMcpSession()

        result = asyncio.run(
            handle_inline_approval_via_mcp(
                session=session,
                message="/approve looks good",
                agent_id="inst_parent",
                json_output=False,
                console=console,
                deps=deps,
            )
        )
        assert result is True
        assert len(session.calls) == 1
        assert session.calls[0][0] == "anima.approve"
        output = stream.getvalue()
        assert "Approved" in output

    def test_approve_explicit_child_target(self):
        """``/approve inst_chi`` targets child by prefix."""
        parent = _make_state(
            "inst_parent",
            state=AgentState.running,
            children=["inst_child_abc"],
        )
        child = _make_state(
            "inst_child_abc",
            state=AgentState.awaiting_approval,
            persona_name="researcher",
        )
        deps = _make_deps(
            states={"inst_parent": parent, "inst_child_abc": child}
        )
        console, stream = _console_and_stream()
        session = _FakeMcpSession()

        result = asyncio.run(
            handle_inline_approval_via_mcp(
                session=session,
                message="/approve inst_child_abc ok",
                agent_id="inst_parent",
                json_output=False,
                console=console,
                deps=deps,
            )
        )
        assert result is True
        # Should NOT call MCP — direct state write
        assert len(session.calls) == 0
        output = stream.getvalue()
        assert "Approved" in output

    def test_reject_explicit_child_target(self):
        """``/reject inst_chi reason`` targets child by prefix."""
        parent = _make_state(
            "inst_parent",
            state=AgentState.running,
            children=["inst_child_abc"],
        )
        child = _make_state(
            "inst_child_abc",
            state=AgentState.awaiting_approval,
            persona_name="researcher",
        )
        deps = _make_deps(
            states={"inst_parent": parent, "inst_child_abc": child}
        )
        console, stream = _console_and_stream()
        session = _FakeMcpSession()

        result = asyncio.run(
            handle_inline_approval_via_mcp(
                session=session,
                message="/reject inst_child_abc not safe",
                agent_id="inst_parent",
                json_output=False,
                console=console,
                deps=deps,
            )
        )
        assert result is True
        assert len(session.calls) == 0
        output = stream.getvalue()
        assert "Rejected" in output

    def test_bare_approve_auto_targets_single_child(self):
        """Bare /approve when main is running and 1 child awaiting auto-targets child."""
        parent = _make_state(
            "inst_parent",
            state=AgentState.running,
            children=["inst_child1"],
        )
        child = _make_state(
            "inst_child1",
            state=AgentState.awaiting_approval,
            persona_name="worker",
        )
        deps = _make_deps(
            states={"inst_parent": parent, "inst_child1": child}
        )
        console, stream = _console_and_stream()
        session = _FakeMcpSession()

        result = asyncio.run(
            handle_inline_approval_via_mcp(
                session=session,
                message="/approve",
                agent_id="inst_parent",
                json_output=False,
                console=console,
                deps=deps,
            )
        )
        assert result is True
        # Should NOT call MCP — direct state write to child
        assert len(session.calls) == 0
        output = stream.getvalue()
        assert "Approved" in output

    def test_bare_approve_multiple_children_shows_error(self):
        """Bare /approve with multiple children awaiting shows disambiguation error."""
        parent = _make_state(
            "inst_parent",
            state=AgentState.running,
            children=["inst_child1", "inst_child2"],
        )
        child1 = _make_state(
            "inst_child1", state=AgentState.awaiting_approval
        )
        child2 = _make_state(
            "inst_child2", state=AgentState.awaiting_approval
        )
        deps = _make_deps(
            states={
                "inst_parent": parent,
                "inst_child1": child1,
                "inst_child2": child2,
            }
        )
        console, stream = _console_and_stream()
        session = _FakeMcpSession()

        result = asyncio.run(
            handle_inline_approval_via_mcp(
                session=session,
                message="/approve",
                agent_id="inst_parent",
                json_output=False,
                console=console,
                deps=deps,
            )
        )
        assert result is True
        assert len(session.calls) == 0
        output = stream.getvalue()
        assert "Multiple" in output or "Specify" in output

    def test_bare_approve_no_one_awaiting_shows_error(self):
        """Bare /approve when no one is awaiting approval shows error."""
        parent = _make_state("inst_parent", state=AgentState.running)
        deps = _make_deps(states={"inst_parent": parent})
        console, stream = _console_and_stream()
        session = _FakeMcpSession()

        result = asyncio.run(
            handle_inline_approval_via_mcp(
                session=session,
                message="/approve",
                agent_id="inst_parent",
                json_output=False,
                console=console,
                deps=deps,
            )
        )
        assert result is True
        output = stream.getvalue()
        assert "not awaiting approval" in output

    def test_non_approve_reject_returns_false(self):
        """Non-approve/reject commands return False."""
        deps = _make_deps(states={})
        console, stream = _console_and_stream()
        session = _FakeMcpSession()

        result = asyncio.run(
            handle_inline_approval_via_mcp(
                session=session,
                message="/help",
                agent_id="inst_parent",
                json_output=False,
                console=console,
                deps=deps,
            )
        )
        assert result is False

    def test_approve_child_json_output(self):
        """JSON output for child approval includes child_id as agent_id."""
        parent = _make_state(
            "inst_parent",
            state=AgentState.running,
            children=["inst_child1"],
        )
        child = _make_state(
            "inst_child1",
            state=AgentState.awaiting_approval,
        )
        deps = _make_deps(
            states={"inst_parent": parent, "inst_child1": child}
        )
        console, stream = _console_and_stream()
        session = _FakeMcpSession()

        result = asyncio.run(
            handle_inline_approval_via_mcp(
                session=session,
                message="/approve",
                agent_id="inst_parent",
                json_output=True,
                console=console,
                deps=deps,
            )
        )
        assert result is True
        output = stream.getvalue()
        payload = json.loads(output.strip())
        assert payload["agent_id"] == "inst_child1"
        assert payload["approved"] is True

    def test_bare_approve_multiple_children_json_error(self):
        """JSON output for ambiguous child target includes error code."""
        parent = _make_state(
            "inst_parent",
            state=AgentState.running,
            children=["inst_child1", "inst_child2"],
        )
        child1 = _make_state(
            "inst_child1", state=AgentState.awaiting_approval
        )
        child2 = _make_state(
            "inst_child2", state=AgentState.awaiting_approval
        )
        deps = _make_deps(
            states={
                "inst_parent": parent,
                "inst_child1": child1,
                "inst_child2": child2,
            }
        )
        console, stream = _console_and_stream()
        session = _FakeMcpSession()

        result = asyncio.run(
            handle_inline_approval_via_mcp(
                session=session,
                message="/approve",
                agent_id="inst_parent",
                json_output=True,
                console=console,
                deps=deps,
            )
        )
        assert result is True
        output = stream.getvalue()
        payload = json.loads(output.strip())
        assert payload["error"]["code"] == "AMBIGUOUS_TARGET"


# ---------------------------------------------------------------------------
# Tests: Help text update
# ---------------------------------------------------------------------------


class TestHelpTextUpdate:
    """Verify help text includes child_id parameter."""

    def test_approve_help_shows_child_id(self):
        approve_line = [l for l in SLASH_HELP_LINES if "/approve" in l]
        assert len(approve_line) == 1
        assert "child_id" in approve_line[0]

    def test_reject_help_shows_child_id(self):
        reject_line = [l for l in SLASH_HELP_LINES if "/reject" in l]
        assert len(reject_line) == 1
        assert "child_id" in reject_line[0]

    def test_approve_help_mentions_auto_target(self):
        approve_line = [l for l in SLASH_HELP_LINES if "/approve" in l][0]
        assert "auto-targets" in approve_line
