"""Tests for PolicyGatedTools — structural policy enforcement at the tool layer."""

from unittest.mock import AsyncMock

import pytest

from koan.graph.gated_tools import PolicyGatedTools
from koan.tools.types import ToolDefinition, ToolResult


def _mock_provider(tool_names: list[str]) -> AsyncMock:
    """Create a mock ToolProvider with the given tool names."""
    provider = AsyncMock()
    # definitions() is a sync method on the protocol, so use a plain function
    defs = [
        ToolDefinition(name=n, description=f"Tool {n}", parameters={})
        for n in tool_names
    ]
    provider.definitions = lambda: defs
    provider.execute.return_value = ToolResult(tool_name="mock", output="ok")
    return provider


class TestPolicyGatedToolsDefinitions:
    def test_no_constraints_returns_all(self) -> None:
        provider = _mock_provider(["lookup", "check", "apply"])
        gated = PolicyGatedTools(provider, allowed_tools=None)

        defs = gated.definitions()
        assert len(defs) == 3

    def test_filters_to_allowed_only(self) -> None:
        provider = _mock_provider(["lookup", "check", "apply"])
        gated = PolicyGatedTools(provider, allowed_tools=["lookup", "check"])

        defs = gated.definitions()
        names = [d.name for d in defs]

        assert names == ["lookup", "check"]

    def test_empty_allowed_blocks_all(self) -> None:
        provider = _mock_provider(["lookup", "check"])
        gated = PolicyGatedTools(provider, allowed_tools=[])
        # Empty list means no tools allowed — but we use None for permissive
        # Empty list = explicit "no tools allowed"
        assert gated._allowed == set()

        defs = gated.definitions()
        assert len(defs) == 0


class TestPolicyGatedToolsExecution:
    async def test_allowed_tool_executes(self) -> None:
        provider = _mock_provider(["lookup", "check"])
        gated = PolicyGatedTools(provider, allowed_tools=["lookup"])

        result = await gated.execute("lookup", {"id": "acct-1"})

        assert result.error is None
        provider.execute.assert_called_once_with(
            "lookup", {"id": "acct-1"}, context=None,
        )

    async def test_blocked_tool_returns_error(self) -> None:
        provider = _mock_provider(["lookup", "check", "apply"])
        gated = PolicyGatedTools(provider, allowed_tools=["lookup"])

        result = await gated.execute("apply", {"amount": 100})

        assert result.is_error
        assert "not permitted" in result.error
        assert "apply" in result.error
        provider.execute.assert_not_called()

    async def test_no_constraints_allows_all(self) -> None:
        provider = _mock_provider(["lookup", "apply"])
        gated = PolicyGatedTools(provider, allowed_tools=None)

        result = await gated.execute("apply", {"amount": 50})

        assert result.error is None
        provider.execute.assert_called_once()


class TestExtractToolsFromPolicies:
    def test_extracts_single_tool(self) -> None:
        policies = [
            {"rules": ["For classification decisions, use: lookup_account"]},
        ]
        result = PolicyGatedTools.extract_tools_from_policies(policies)
        assert result == ["lookup_account"]

    def test_extracts_multi_tool_sequence(self) -> None:
        policies = [
            {"rules": ["For billing decisions, use: check_balance then apply_credit"]},
        ]
        result = PolicyGatedTools.extract_tools_from_policies(policies)
        assert result == ["apply_credit", "check_balance"]  # sorted

    def test_merges_across_policies(self) -> None:
        policies = [
            {"rules": ["For classification decisions, use: lookup_account"]},
            {"rules": ["For billing decisions, use: check_balance"]},
        ]
        result = PolicyGatedTools.extract_tools_from_policies(policies)
        assert result == ["check_balance", "lookup_account"]

    def test_returns_none_when_no_tool_rules(self) -> None:
        policies = [
            {"rules": ["Expected success rate >= 80%"]},
        ]
        result = PolicyGatedTools.extract_tools_from_policies(policies)
        assert result is None

    def test_returns_none_for_empty_policies(self) -> None:
        result = PolicyGatedTools.extract_tools_from_policies([])
        assert result is None

    def test_ignores_no_specific_tool(self) -> None:
        policies = [
            {"rules": ["For routing decisions, use: no specific tool"]},
        ]
        result = PolicyGatedTools.extract_tools_from_policies(policies)
        assert result is None

    def test_handles_missing_rules_key(self) -> None:
        policies = [{"name": "some-policy"}]
        result = PolicyGatedTools.extract_tools_from_policies(policies)
        assert result is None
