"""Tests for tool policy enforcement.

Tests for:
- is_read_only_tool: Read-only tool name heuristic
- check_tool_policy: Policy decision logic
- ToolBlockedError / ApprovalRequiredError: Error classes
"""

from inspect import signature
from typing import Any, cast

import pytest

from anima.core.schemas import SideEffectPolicy, ToolPosture
from anima.core.schemas.enums import POSTURE_RANK
from anima.core.tool_policy import (
    CTL_TOOL_PREFIX,
    READ_ONLY_PREFIXES,
    ToolDecision,
    check_tool_policy,
    is_read_only_tool,
    is_read_only_tool_with_annotations,
    most_restrictive,
    resolve_tool_policy,
)
from anima.shell.tool_policy import (
    ApprovalRequiredError,
    ToolBlockedError,
    create_policy_wrapped_tool,
)


class TestIsReadOnlyTool:
    """Tests for is_read_only_tool function."""

    def test_read_prefix_tools_are_read_only(self) -> None:
        """Tools starting with read_ are read-only."""
        assert is_read_only_tool("read_file") is True
        assert is_read_only_tool("read_config") is True
        assert is_read_only_tool("read_state") is True

    def test_get_prefix_tools_are_read_only(self) -> None:
        """Tools starting with get_ are read-only."""
        assert is_read_only_tool("get_status") is True
        assert is_read_only_tool("get_config") is True
        assert is_read_only_tool("get_user") is True

    def test_list_prefix_tools_are_read_only(self) -> None:
        """Tools starting with list_ are read-only."""
        assert is_read_only_tool("list_agents") is True
        assert is_read_only_tool("list_files") is True
        assert is_read_only_tool("list_jobs") is True

    def test_search_prefix_tools_are_read_only(self) -> None:
        """Tools starting with search_ are read-only."""
        assert is_read_only_tool("search_index") is True
        assert is_read_only_tool("search_files") is True
        assert is_read_only_tool("search_web") is True

    def test_find_prefix_tools_are_read_only(self) -> None:
        """Tools starting with find_ are read-only."""
        assert is_read_only_tool("find_user") is True
        assert is_read_only_tool("find_file") is True
        assert is_read_only_tool("find_record") is True

    def test_show_prefix_tools_are_read_only(self) -> None:
        """Tools starting with show_ are read-only."""
        assert is_read_only_tool("show_config") is True
        assert is_read_only_tool("show_status") is True
        assert is_read_only_tool("show_log") is True

    def test_status_is_read_only(self) -> None:
        """Tool named 'status' is read-only."""
        assert is_read_only_tool("status") is True
        assert is_read_only_tool("status_check") is True

    def test_ctl_tools_are_read_only(self) -> None:
        """CTL lifecycle tools are read-only for policy checks."""
        assert is_read_only_tool("ctl_finish") is True
        assert is_read_only_tool("ctl_fail") is True
        assert is_read_only_tool("ctl_request_approval") is True
        assert is_read_only_tool("ctl_spawn_subagent") is True
        assert is_read_only_tool("ctl_suspend_until") is True

    def test_write_tools_are_not_read_only(self) -> None:
        """Tools with write semantics are not read-only."""
        assert is_read_only_tool("write_file") is False
        assert is_read_only_tool("create_user") is False
        assert is_read_only_tool("update_record") is False
        assert is_read_only_tool("delete_file") is False
        assert is_read_only_tool("send_message") is False

    def test_case_insensitive_prefix_matching(self) -> None:
        """Prefix matching is case-insensitive."""
        assert is_read_only_tool("READ_file") is True
        assert is_read_only_tool("Get_Status") is True
        assert is_read_only_tool("LIST_AGENTS") is True
        assert is_read_only_tool("Search_Index") is True

    def test_empty_string_raises_error(self) -> None:
        """Empty string raises precondition error."""
        with pytest.raises(Exception):  # deal.pre raises PreContractError
            is_read_only_tool("")


class TestIsReadOnlyToolWithAnnotations:
    """Tests for annotation-aware read-only checks."""

    def test_read_only_hint_overrides_write_name(self) -> None:
        """readOnlyHint=True forces read-only even for mutating names."""
        assert (
            is_read_only_tool_with_annotations("write_file", {"readOnlyHint": True})
            is True
        )

    def test_destructive_hint_overrides_read_name(self) -> None:
        """destructiveHint=True forces non-read-only even for read names."""
        assert (
            is_read_only_tool_with_annotations("read_file", {"destructiveHint": True})
            is False
        )

    def test_no_annotations_falls_back_to_name_heuristic(self) -> None:
        """None annotations preserve existing name-based behavior."""
        assert is_read_only_tool_with_annotations("write_file", None) is False

    def test_read_only_hint_precedence_when_both_hints_true(self) -> None:
        """readOnlyHint takes precedence over destructiveHint when both are true."""
        assert (
            is_read_only_tool_with_annotations(
                "write_file",
                {"readOnlyHint": True, "destructiveHint": True},
            )
            is True
        )


class TestCheckToolPolicy:
    """Tests for check_tool_policy function."""

    def test_allow_policy_allows_all_tools(self) -> None:
        """Allow policy permits all tools."""
        assert (
            check_tool_policy(SideEffectPolicy.allow, "write_file")
            == ToolDecision.ALLOW
        )
        assert (
            check_tool_policy(SideEffectPolicy.allow, "delete_file")
            == ToolDecision.ALLOW
        )
        assert (
            check_tool_policy(SideEffectPolicy.allow, "read_file") == ToolDecision.ALLOW
        )
        assert (
            check_tool_policy(SideEffectPolicy.allow, "create_user")
            == ToolDecision.ALLOW
        )

    def test_read_only_policy_allows_read_tools(self) -> None:
        """Read_only policy allows read-only tools."""
        assert (
            check_tool_policy(SideEffectPolicy.read_only, "read_file")
            == ToolDecision.ALLOW
        )
        assert (
            check_tool_policy(SideEffectPolicy.read_only, "get_status")
            == ToolDecision.ALLOW
        )
        assert (
            check_tool_policy(SideEffectPolicy.read_only, "list_agents")
            == ToolDecision.ALLOW
        )
        assert (
            check_tool_policy(SideEffectPolicy.read_only, "ctl_finish")
            == ToolDecision.ALLOW
        )

    def test_read_only_policy_blocks_write_tools(self) -> None:
        """Read_only policy blocks non-read-only tools."""
        assert (
            check_tool_policy(SideEffectPolicy.read_only, "write_file")
            == ToolDecision.BLOCK
        )
        assert (
            check_tool_policy(SideEffectPolicy.read_only, "delete_file")
            == ToolDecision.BLOCK
        )
        assert (
            check_tool_policy(SideEffectPolicy.read_only, "create_user")
            == ToolDecision.BLOCK
        )
        assert (
            check_tool_policy(SideEffectPolicy.read_only, "send_message")
            == ToolDecision.BLOCK
        )
        assert (
            check_tool_policy(SideEffectPolicy.read_only, "write_file")
            == ToolDecision.BLOCK
        )

    def test_approval_required_policy_allows_read_tools(self) -> None:
        """Approval_required policy allows read-only tools."""
        assert (
            check_tool_policy(SideEffectPolicy.approval_required, "read_file")
            == ToolDecision.ALLOW
        )
        assert (
            check_tool_policy(SideEffectPolicy.approval_required, "get_status")
            == ToolDecision.ALLOW
        )
        assert (
            check_tool_policy(SideEffectPolicy.approval_required, "list_agents")
            == ToolDecision.ALLOW
        )
        assert (
            check_tool_policy(SideEffectPolicy.approval_required, "ctl_finish")
            == ToolDecision.ALLOW
        )

    def test_approval_required_policy_requires_approval_for_write_tools(self) -> None:
        """Approval_required policy requires approval for non-read-only tools."""
        assert (
            check_tool_policy(SideEffectPolicy.approval_required, "write_file")
            == ToolDecision.APPROVAL
        )
        assert (
            check_tool_policy(SideEffectPolicy.approval_required, "delete_file")
            == ToolDecision.APPROVAL
        )
        assert (
            check_tool_policy(SideEffectPolicy.approval_required, "create_user")
            == ToolDecision.APPROVAL
        )
        assert (
            check_tool_policy(SideEffectPolicy.approval_required, "send_message")
            == ToolDecision.APPROVAL
        )


class TestResolveToolPolicy:
    """Tests for side_effect_policy resolution in resolve_tool_policy."""

    def test_allow_policy_resolves_to_side_effect_policy_source(self) -> None:
        """Allow policy still reports side_effect_policy source."""
        resolved = resolve_tool_policy(
            SideEffectPolicy.allow,
            "write_file",
        )
        assert resolved.decision == ToolDecision.ALLOW
        assert resolved.source == "side_effect_policy"
        assert resolved.posture is None

    def test_read_only_policy_blocks_write_with_side_effect_source(self) -> None:
        """Read-only policy blocks write tools via side_effect_policy."""
        resolved = resolve_tool_policy(
            SideEffectPolicy.read_only,
            "write_file",
        )
        assert resolved.decision == ToolDecision.BLOCK
        assert resolved.source == "side_effect_policy"
        assert resolved.posture is None

    def test_approval_required_returns_approval_with_side_effect_source(self) -> None:
        """Approval policy for write tools resolves to approval decision."""
        resolved = resolve_tool_policy(
            SideEffectPolicy.approval_required,
            "write_file",
        )
        assert resolved.decision == ToolDecision.APPROVAL
        assert resolved.source == "side_effect_policy"
        assert resolved.posture is None

    def test_annotations_are_preserved_in_side_effect_fallback(self) -> None:
        """Annotation-aware fallback still applies when no per-tool posture exists."""
        resolved = resolve_tool_policy(
            SideEffectPolicy.read_only,
            "write_file",
            {"readOnlyHint": True},
        )
        assert resolved.decision == ToolDecision.ALLOW
        assert resolved.source == "side_effect_policy"
        assert resolved.posture is None


class TestToolBlockedError:
    """Tests for ToolBlockedError."""

    def test_error_message(self) -> None:
        """Error message is formatted correctly."""
        error = ToolBlockedError("write_file", "read_only")

        assert error.code == 330
        assert error.message == "Tool write_file blocked by read_only policy"
        assert error.details == {"tool_name": "write_file", "policy": "read_only"}

    def test_str_representation(self) -> None:
        """String representation shows code and message."""
        error = ToolBlockedError("delete_user", "read_only")

        assert str(error) == "[330] Tool delete_user blocked by read_only policy"


class TestApprovalRequiredError:
    """Tests for ApprovalRequiredError."""

    def test_error_message(self) -> None:
        """Error message is formatted correctly."""
        error = ApprovalRequiredError("create_user")

        assert error.code == 331
        assert error.message == "Tool create_user requires approval"
        assert error.details == {"tool_name": "create_user"}

    def test_str_representation(self) -> None:
        """String representation shows code and message."""
        error = ApprovalRequiredError("send_message")

        assert str(error) == "[331] Tool send_message requires approval"


class TestReadOnlyPrefixes:
    """Tests for READ_ONLY_PREFIXES constant."""

    def test_prefixes_are_complete(self) -> None:
        """All documented prefixes are present."""
        expected = {"read_", "get_", "list_", "search_", "find_", "show_", "status"}
        assert READ_ONLY_PREFIXES == expected

    def test_ctl_prefix(self) -> None:
        """CTL tool prefix is correct."""
        assert CTL_TOOL_PREFIX == "ctl_"


class TestToolPostureRank:
    """Tests for ToolPosture enum values and ordering map."""

    def test_tool_posture_values(self) -> None:
        """ToolPosture values match schema contract."""
        assert ToolPosture.none.value == "none"
        assert ToolPosture.read_only.value == "read_only"
        assert ToolPosture.read_write.value == "read_write"
        assert ToolPosture.destructive.value == "destructive"

    def test_posture_rank_ordering(self) -> None:
        """POSTURE_RANK preserves strict ascending restrictiveness order."""
        assert POSTURE_RANK[ToolPosture.none] == 0
        assert POSTURE_RANK[ToolPosture.read_only] == 1
        assert POSTURE_RANK[ToolPosture.read_write] == 2
        assert POSTURE_RANK[ToolPosture.destructive] == 3
        assert POSTURE_RANK[ToolPosture.none] < POSTURE_RANK[ToolPosture.read_only]
        assert (
            POSTURE_RANK[ToolPosture.read_only] < POSTURE_RANK[ToolPosture.read_write]
        )
        assert (
            POSTURE_RANK[ToolPosture.read_write] < POSTURE_RANK[ToolPosture.destructive]
        )


class TestPolicyWrapper:
    """Tests that policy wrapping is enforced before execution."""

    @pytest.mark.asyncio
    async def test_read_only_blocks_write_before_execution(self) -> None:
        """read_only blocks write-like tools and does not run tool body."""
        from pydantic_ai import Tool

        executed = False

        async def write_file(filename: str, content: str) -> str:
            nonlocal executed
            executed = True
            return f"wrote {filename}:{content}"

        class DummyRunner:
            instance_id = "ag_test"

        wrapped = create_policy_wrapped_tool(
            Tool(write_file, name="write_file"),
            SideEffectPolicy.read_only,
            cast(Any, DummyRunner()),
        )
        wrapped_function = cast(Any, wrapped.function)
        result = await wrapped_function(filename="notes.txt", content="hello")

        assert "blocked by read_only policy" in result
        assert executed is False

    @pytest.mark.asyncio
    async def test_allow_policy_allows_write_execution(self) -> None:
        """allow policy permits write-like tool execution."""
        from pydantic_ai import Tool

        executed = False

        async def write_file(filename: str, content: str) -> str:
            nonlocal executed
            executed = True
            return f"wrote {filename}:{content}"

        class DummyRunner:
            instance_id = "ag_test"

        wrapped = create_policy_wrapped_tool(
            Tool(write_file, name="write_file"),
            SideEffectPolicy.allow,
            cast(Any, DummyRunner()),
        )
        wrapped_function = cast(Any, wrapped.function)
        result = await wrapped_function(filename="notes.txt", content="hello")

        assert result == "wrote notes.txt:hello"
        assert executed is True


class TestMostRestrictive:
    """Tests for most_restrictive policy merge function."""

    def test_allow_read_only_returns_read_only(self) -> None:
        """most_restrictive(allow, read_only) -> read_only per spec invariant."""
        from anima.core.schemas import SideEffectPolicy as SEP

        result = most_restrictive(SEP.allow, SEP.read_only)
        assert result == SEP.read_only

    def test_read_only_allow_returns_read_only(self) -> None:
        """most_restrictive(read_only, allow) -> read_only."""
        from anima.core.schemas import SideEffectPolicy as SEP

        result = most_restrictive(SEP.read_only, SEP.allow)
        assert result == SEP.read_only

    def test_both_none_returns_allow(self) -> None:
        """most_restrictive(None, None) -> allow."""
        from anima.core.schemas import SideEffectPolicy as SEP

        result = most_restrictive(None, None)
        assert result == SEP.allow

    def test_persona_none_returns_job_policy(self) -> None:
        """most_restrictive(None, job_policy) -> job_policy."""
        from anima.core.schemas import SideEffectPolicy as SEP

        result = most_restrictive(None, SEP.approval_required)
        assert result == SEP.approval_required

    def test_job_none_returns_persona_policy(self) -> None:
        """most_restrictive(persona_policy, None) -> persona_policy."""
        from anima.core.schemas import SideEffectPolicy as SEP

        result = most_restrictive(SEP.read_only, None)
        assert result == SEP.read_only


class TestPolicyInvariants:
    """Contract invariants for tool policy as per spec."""

    def test_check_tool_policy_read_only_blocks_write_file(self) -> None:
        """check_tool_policy(read_only, 'write_file') -> BLOCK per spec invariant."""
        from anima.core.schemas import SideEffectPolicy as SEP

        result = check_tool_policy(SEP.read_only, "write_file")
        assert result == ToolDecision.BLOCK

    def test_check_tool_policy_approval_requires_write_file(self) -> None:
        """check_tool_policy(approval_required, 'write_file') -> APPROVAL per spec invariant."""
        from anima.core.schemas import SideEffectPolicy as SEP

        result = check_tool_policy(SEP.approval_required, "write_file")
        assert result == ToolDecision.APPROVAL


class TestWrappedToolSignature:
    """Regression tests for preserving wrapped tool schemas."""

    def test_wrapped_tool_preserves_original_signature(self) -> None:
        """Policy wrappers preserve parameters for tool schema introspection."""
        from pydantic_ai import Tool

        async def ctl_spawn_subagent(
            persona_ref: str | None = None,
            task: str | None = None,
            lifecycle: str | None = None,
            context_mode: str = "isolated",
            max_turns: int | None = None,
            max_tokens: int | None = None,
        ) -> str:
            return "ok"

        class DummyRunner:
            instance_id = "ag_test"

        wrapped = create_policy_wrapped_tool(
            Tool(ctl_spawn_subagent, name="ctl_spawn_subagent"),
            SideEffectPolicy.read_only,
            cast(Any, DummyRunner()),
        )

        wrapped_signature = signature(cast(Any, wrapped.function))
        assert list(wrapped_signature.parameters) == [
            "persona_ref",
            "task",
            "lifecycle",
            "context_mode",
            "max_turns",
            "max_tokens",
        ]
        assert wrapped_signature.parameters["task"].default is None
        assert wrapped_signature.parameters["context_mode"].default == "isolated"
