"""Integration tests for MCP tool policy over real stdio transport.

Spec source: contract-alignment-integration.fix-seam-purity-tool-policy-blockers (B3).
These tests use a live MCP subprocess (stdio transport), then enforce
policy via PolicyWrappedToolset without seam mocks.
"""

from __future__ import annotations

import textwrap
import uuid
from pathlib import Path

import pytest
from returns.result import Success

from anima.core.schemas import JobSpec, SideEffectPolicy
from anima.shell.mcp_servers import build_mcp_server_stdio
from anima.shell.mcp_toolset_policy import (
    MAX_TOOL_RESULT_TEXT_CHARS,
    PolicyWrappedToolset,
    TRUNCATED_MARKER,
)
from anima.shell.state import create_agent_state
from tests.fixtures.launch_control_services import (
    make_control_request_fixture_set,
    make_mcp_control_resolution_regression_fixture,
    control_request_name_for_mcp,
)


def _write_policy_probe_server(tmp_path: Path) -> Path:
    """Create a tiny MCP stdio server exposing read/write tools."""
    script_path = tmp_path / "policy_probe_server.py"
    script_path.write_text(
        textwrap.dedent(
            f"""
            from mcp.server.fastmcp import FastMCP

            mcp = FastMCP("policy-probe")
            OVERSIZED = {MAX_TOOL_RESULT_TEXT_CHARS + 257}

            @mcp.tool()
            def read_file(path: str) -> str:
                return f"read:{{path}}"

            @mcp.tool()
            def write_file(path: str, content: str) -> str:
                return f"write:{{path}}:{{content}}"

            @mcp.tool()
            def oversized_payload() -> dict[str, object]:
                return {{
                    "strategy": "paginate_then_excerpt",
                    "payload": "x" * OVERSIZED,
                }}

            @mcp.tool()
            def structured_summary_excerpt() -> dict[str, object]:
                return {{
                    "strategy": "paginate_then_excerpt",
                    "summary": {{
                        "title": "Tree Summary",
                        "metadata": {{"count": 2, "source": "policy-probe"}},
                        "description": "s" * OVERSIZED,
                    }},
                    "excerpt": {{
                        "content": "e" * OVERSIZED,
                        "lines": ["line1", "line2"],
                        "truncated": True,
                    }},
                }}

            @mcp.tool()
            def summary_without_metadata() -> dict[str, object]:
                return {{
                    "strategy": "paginate_then_excerpt",
                    "summary": {{
                        "title": "Summary Without Metadata",
                        "description": "m" * OVERSIZED,
                    }},
                    "excerpt": {{
                        "content": "z" * OVERSIZED,
                        "truncated": True,
                        "metadata": {{"source": "excerpt-only"}},
                    }},
                }}

            if __name__ == "__main__":
                mcp.run(transport="stdio")
            """
        ).strip()
        + "\n"
    )
    return script_path


def _create_agent_state_for_policy(agent_id: str) -> None:
    created = create_agent_state(
        agent_id,
        "policy-persona",
        JobSpec(persona_ref="policy@test", instance_id=agent_id),
    )
    assert isinstance(created, Success)


@pytest.mark.asyncio
async def test_read_only_policy_blocks_mutating_tool_over_real_stdio_transport(
    tmp_path: Path,
) -> None:
    """read_only permits read tool and blocks write tool through live MCP server."""
    server_script = _write_policy_probe_server(tmp_path)
    toolset = build_mcp_server_stdio(command="python", args=[str(server_script)])

    agent_id = f"inst_policy_{uuid.uuid4().hex[:8]}"
    _create_agent_state_for_policy(agent_id)

    wrapped = PolicyWrappedToolset(toolset, SideEffectPolicy.read_only, agent_id)
    tools = await wrapped.get_tools(None)

    read_result = await wrapped.call_tool(
        "read_file",
        {"path": "/tmp/demo.txt"},
        None,
        tools["read_file"],
    )
    assert read_result == "read:/tmp/demo.txt"

    blocked = await wrapped.call_tool(
        "write_file",
        {"path": "/tmp/demo.txt", "content": "payload"},
        None,
        tools["write_file"],
    )
    assert isinstance(blocked, dict)
    assert blocked["error"]["code"] == "TOOL_BLOCKED"
    assert blocked["error"]["numeric_code"] == 330
    assert blocked["error"]["details"]["tool_name"] == "write_file"
    assert blocked["error"]["details"]["source"] == "side_effect_policy"


@pytest.mark.asyncio
async def test_allow_policy_allows_mutating_tool_over_real_stdio_transport(
    tmp_path: Path,
) -> None:
    """allow policy lets mutating tool execute against live MCP stdio server."""
    server_script = _write_policy_probe_server(tmp_path)
    toolset = build_mcp_server_stdio(command="python", args=[str(server_script)])

    agent_id = f"inst_policy_allow_{uuid.uuid4().hex[:8]}"
    _create_agent_state_for_policy(agent_id)

    wrapped = PolicyWrappedToolset(toolset, SideEffectPolicy.allow, agent_id)
    tools = await wrapped.get_tools(None)

    write_result = await wrapped.call_tool(
        "write_file",
        {"path": "/tmp/demo.txt", "content": "ok"},
        None,
        tools["write_file"],
    )
    assert write_result == "write:/tmp/demo.txt:ok"


@pytest.mark.asyncio
async def test_allow_policy_bounds_oversized_payload_over_real_stdio_transport(
    tmp_path: Path,
) -> None:
    """Oversized MCP payload is bounded at PolicyWrappedToolset.call_tool seam."""
    server_script = _write_policy_probe_server(tmp_path)
    toolset = build_mcp_server_stdio(command="python", args=[str(server_script)])

    agent_id = f"inst_policy_large_{uuid.uuid4().hex[:8]}"
    _create_agent_state_for_policy(agent_id)

    wrapped = PolicyWrappedToolset(toolset, SideEffectPolicy.allow, agent_id)
    tools = await wrapped.get_tools(None)

    result = await wrapped.call_tool(
        "oversized_payload",
        {},
        None,
        tools["oversized_payload"],
    )

    assert isinstance(result, dict)
    assert result["strategy"] == "paginate_then_excerpt"
    payload = result["payload"]
    assert isinstance(payload, str)
    assert len(payload) == MAX_TOOL_RESULT_TEXT_CHARS
    assert payload.endswith(TRUNCATED_MARKER)


@pytest.mark.asyncio
async def test_allow_policy_preserves_structured_summary_excerpt_over_real_stdio_transport(
    tmp_path: Path,
) -> None:
    """Structured summary/excerpt stays structured while string leaves are bounded."""
    server_script = _write_policy_probe_server(tmp_path)
    toolset = build_mcp_server_stdio(command="python", args=[str(server_script)])

    agent_id = f"inst_policy_structured_{uuid.uuid4().hex[:8]}"
    _create_agent_state_for_policy(agent_id)

    wrapped = PolicyWrappedToolset(toolset, SideEffectPolicy.allow, agent_id)
    tools = await wrapped.get_tools(None)

    result = await wrapped.call_tool(
        "structured_summary_excerpt",
        {},
        None,
        tools["structured_summary_excerpt"],
    )

    assert isinstance(result, dict)
    assert result["strategy"] == "paginate_then_excerpt"
    summary = result["summary"]
    excerpt = result["excerpt"]
    assert isinstance(summary, dict)
    assert isinstance(excerpt, dict)
    assert summary["metadata"] == {"count": 2, "source": "policy-probe"}
    assert excerpt["lines"] == ["line1", "line2"]
    assert excerpt["truncated"] is True
    assert isinstance(summary["description"], str)
    assert isinstance(excerpt["content"], str)
    assert len(summary["description"]) == MAX_TOOL_RESULT_TEXT_CHARS
    assert len(excerpt["content"]) == MAX_TOOL_RESULT_TEXT_CHARS
    assert summary["description"].endswith(TRUNCATED_MARKER)
    assert excerpt["content"].endswith(TRUNCATED_MARKER)


@pytest.mark.asyncio
async def test_allow_policy_summary_without_metadata_stays_bounded_over_real_stdio_transport(
    tmp_path: Path,
) -> None:
    """Missing summary metadata keeps fallback shape and bounds string leaves."""
    server_script = _write_policy_probe_server(tmp_path)
    toolset = build_mcp_server_stdio(command="python", args=[str(server_script)])

    agent_id = f"inst_policy_no_meta_{uuid.uuid4().hex[:8]}"
    _create_agent_state_for_policy(agent_id)

    wrapped = PolicyWrappedToolset(toolset, SideEffectPolicy.allow, agent_id)
    tools = await wrapped.get_tools(None)

    result = await wrapped.call_tool(
        "summary_without_metadata",
        {},
        None,
        tools["summary_without_metadata"],
    )

    assert isinstance(result, dict)
    assert result["strategy"] == "paginate_then_excerpt"
    summary = result["summary"]
    excerpt = result["excerpt"]
    assert isinstance(summary, dict)
    assert isinstance(excerpt, dict)
    assert "metadata" not in summary
    assert summary["title"] == "Summary Without Metadata"
    assert isinstance(summary["description"], str)
    assert len(summary["description"]) == MAX_TOOL_RESULT_TEXT_CHARS
    assert summary["description"].endswith(TRUNCATED_MARKER)
    assert excerpt["truncated"] is True
    assert excerpt["metadata"] == {"source": "excerpt-only"}
    assert isinstance(excerpt["content"], str)
    assert len(excerpt["content"]) == MAX_TOOL_RESULT_TEXT_CHARS
    assert excerpt["content"].endswith(TRUNCATED_MARKER)


def test_mcp_control_request_fixture_maps_to_expected_tool_names() -> None:
    """Control request fixtures map cleanly to MCP control tool namespace."""
    fixture = make_control_request_fixture_set("inst_mcp_control_fixture")

    assert control_request_name_for_mcp(fixture.status) == "anima.status"
    assert control_request_name_for_mcp(fixture.interact) == "anima.interact"
    assert control_request_name_for_mcp(fixture.approve) == "anima.approve"
    assert control_request_name_for_mcp(fixture.reject) == "anima.reject"
    assert control_request_name_for_mcp(fixture.resume) == "anima.resume"
    assert control_request_name_for_mcp(fixture.kill) == "anima.kill"


def test_mcp_control_resolution_regression_fixture_locks_single_resolution() -> None:
    """Regression fixture for MCP control-path resolution duplication remains strict."""
    fixture = make_mcp_control_resolution_regression_fixture()

    assert fixture.bug_path == "mcp_control"
    assert fixture.expected_resolution_calls == 1
    assert fixture.expected_runtime_resolution_calls == 1
