"""Tests for PolicyManager — seed, discover, promote, and query policies."""

from __future__ import annotations

from unittest.mock import AsyncMock

import pytest

from koan.graph.client import GraphRecord
from koan.graph.policies import DiscoveredPattern, PolicyManager


def _mock_client() -> AsyncMock:
    return AsyncMock()


class TestPolicyManager:
    async def test_seed_policy_creates_node(self) -> None:
        client = _mock_client()
        client.execute_write.return_value = []

        pm = PolicyManager(client)
        pid = await pm.seed_policy(
            name="max-credit",
            description="Credits above $50 require approval.",
            rules=["credit_amount <= 50"],
            policy_id="pol-1",
        )

        assert pid == "pol-1"
        client.execute_write.assert_called_once()
        call_args = client.execute_write.call_args
        assert "MERGE (p:Policy" in call_args[0][0]
        assert call_args[0][1]["name"] == "max-credit"

    async def test_seed_policy_generates_id_when_not_provided(self) -> None:
        client = _mock_client()
        client.execute_write.return_value = []

        pm = PolicyManager(client)
        pid = await pm.seed_policy(
            name="test-policy",
            description="Test.",
        )

        assert pid  # Should be a UUID string
        assert len(pid) == 36  # UUID format

    async def test_list_policies(self) -> None:
        client = _mock_client()
        client.execute.return_value = [
            GraphRecord(data={"policy_id": "p1", "name": "policy-a", "version": "1.0"}),
            GraphRecord(data={"policy_id": "p2", "name": "policy-b", "version": "2.0"}),
        ]

        pm = PolicyManager(client)
        policies = await pm.list_policies()

        assert len(policies) == 2
        assert policies[0]["name"] == "policy-a"

    async def test_get_policy_found(self) -> None:
        client = _mock_client()
        client.execute.return_value = [
            GraphRecord(data={"policy_id": "p1", "name": "max-credit", "version": "1.0"}),
        ]

        pm = PolicyManager(client)
        policy = await pm.get_policy("p1")

        assert policy is not None
        assert policy["name"] == "max-credit"

    async def test_get_policy_not_found(self) -> None:
        client = _mock_client()
        client.execute.return_value = []

        pm = PolicyManager(client)
        policy = await pm.get_policy("nonexistent")

        assert policy is None

    async def test_discover_patterns(self) -> None:
        client = _mock_client()
        client.execute.return_value = [
            GraphRecord(data={
                "dtype": "classification",
                "tool": "lookup_account",
                "freq": 5,
                "statuses": ["success", "success", "success", "success", "failure"],
                "entities": ["acct-1001", "acct-1002"],
            }),
        ]

        pm = PolicyManager(client)
        patterns = await pm.discover_patterns(min_frequency=3, min_success_rate=0.7)

        assert len(patterns) == 1
        assert patterns[0].decision_type == "classification"
        assert patterns[0].tool_sequence == ["lookup_account"]
        assert patterns[0].frequency == 5
        assert patterns[0].success_rate == pytest.approx(0.8)

    async def test_discover_filters_low_success_rate(self) -> None:
        client = _mock_client()
        client.execute.return_value = [
            GraphRecord(data={
                "dtype": "classification",
                "tool": "bad_tool",
                "freq": 10,
                "statuses": ["failure", "failure", "success"],
                "entities": [],
            }),
        ]

        pm = PolicyManager(client)
        patterns = await pm.discover_patterns(min_frequency=3, min_success_rate=0.7)

        assert len(patterns) == 0  # ~33% success rate, below threshold

    async def test_promote_pattern(self) -> None:
        client = _mock_client()
        client.execute_write.return_value = []

        pm = PolicyManager(client)
        pattern = DiscoveredPattern(
            decision_type="classification",
            tool_sequence=["lookup_account"],
            frequency=10,
            success_rate=0.9,
            description="Auto-discovered pattern.",
        )

        pid = await pm.promote_pattern(pattern)

        assert pid  # UUID
        client.execute_write.assert_called_once()
        call_args = client.execute_write.call_args
        assert call_args[0][1]["source"] == "auto-discovered"

    async def test_auto_evolve_promotes_new_patterns(self) -> None:
        client = _mock_client()

        # discover_patterns query returns one qualifying pattern
        # _get_existing_policy_names query returns no existing policies
        client.execute.side_effect = [
            # discover_patterns call
            [GraphRecord(data={
                "dtype": "classification",
                "tool": "lookup_account",
                "freq": 5,
                "statuses": ["success", "success", "success", "success", "failure"],
                "entities": ["acct-1001"],
            })],
            # _get_existing_policy_names call
            [],
        ]
        client.execute_write.return_value = []

        pm = PolicyManager(client, min_frequency=3, min_success_rate=0.7)
        promoted = await pm.auto_evolve()

        assert len(promoted) == 1
        assert promoted[0] == "auto:classification:lookup_account"
        client.execute_write.assert_called_once()

    async def test_auto_evolve_skips_existing_policies(self) -> None:
        client = _mock_client()

        client.execute.side_effect = [
            # discover_patterns call
            [GraphRecord(data={
                "dtype": "classification",
                "tool": "lookup_account",
                "freq": 5,
                "statuses": ["success", "success", "success"],
                "entities": [],
            })],
            # _get_existing_policy_names call — already exists
            [GraphRecord(data={"name": "auto:classification:lookup_account"})],
        ]

        pm = PolicyManager(client)
        promoted = await pm.auto_evolve()

        assert len(promoted) == 0
        client.execute_write.assert_not_called()

    async def test_auto_evolve_returns_empty_when_no_patterns(self) -> None:
        client = _mock_client()
        client.execute.return_value = []  # No patterns found

        pm = PolicyManager(client)
        promoted = await pm.auto_evolve()

        assert promoted == []

    async def test_link_decision_to_policy(self) -> None:
        client = _mock_client()
        client.execute_write.return_value = []

        pm = PolicyManager(client)
        await pm.link_decision_to_policy(
            decision_id="d-1",
            policy_id="p-1",
        )

        client.execute_write.assert_called_once()
        assert "GOVERNED_BY" in client.execute_write.call_args[0][0]

    async def test_match_policies_by_decision_type(self) -> None:
        client = _mock_client()
        client.execute.return_value = [
            GraphRecord(data={
                "policy_id": "p1",
                "name": "auto:classification:lookup_account",
                "description": "Use lookup_account",
                "source": "auto-discovered",
                "rules": ["For classification decisions, use: lookup_account"],
            }),
        ]

        pm = PolicyManager(client)
        matched = await pm.match_policies(decision_type="classification")

        assert len(matched) == 1
        assert matched[0]["policy_id"] == "p1"
        # Verify query uses the right prefix
        call_args = client.execute.call_args
        assert call_args[0][1]["prefix"] == "auto:classification:"

    async def test_link_decision_to_matching_policies(self) -> None:
        client = _mock_client()
        # match_policies query
        client.execute.return_value = [
            GraphRecord(data={"policy_id": "p1", "name": "auto:classification:lookup", "source": "auto-discovered"}),
            GraphRecord(data={"policy_id": "p2", "name": "manual-policy", "source": "manual"}),
        ]
        client.execute_write.return_value = []

        pm = PolicyManager(client)
        linked = await pm.link_decision_to_matching_policies(
            decision_id="d-1", decision_type="classification",
        )

        assert len(linked) == 2
        assert "p1" in linked
        assert "p2" in linked
        # Should have created 2 GOVERNED_BY edges
        assert client.execute_write.call_count == 2

    async def test_retire_underperforming_deletes_bad_policies(self) -> None:
        client = _mock_client()
        client.execute.return_value = [
            GraphRecord(data={
                "policy_id": "p-bad",
                "name": "auto:classification:bad_tool",
                "total_decisions": 5,
                "failures": 4,  # 80% failure rate
            }),
        ]
        client.execute_write.return_value = []

        pm = PolicyManager(client)
        retired = await pm.retire_underperforming(max_failure_rate=0.5, min_governed_decisions=3)

        assert len(retired) == 1
        assert retired[0] == "auto:classification:bad_tool"
        client.execute_write.assert_called_once()
        assert "DETACH DELETE" in client.execute_write.call_args[0][0]

    async def test_retire_skips_healthy_policies(self) -> None:
        client = _mock_client()
        client.execute.return_value = [
            GraphRecord(data={
                "policy_id": "p-ok",
                "name": "auto:classification:good_tool",
                "total_decisions": 10,
                "failures": 2,  # 20% failure rate — below threshold
            }),
        ]

        pm = PolicyManager(client)
        retired = await pm.retire_underperforming(max_failure_rate=0.5, min_governed_decisions=3)

        assert len(retired) == 0

    async def test_retire_skips_insufficient_data(self) -> None:
        client = _mock_client()
        client.execute.return_value = [
            GraphRecord(data={
                "policy_id": "p-new",
                "name": "auto:classification:new_tool",
                "total_decisions": 1,  # Not enough data
                "failures": 1,
            }),
        ]

        pm = PolicyManager(client)
        # min_governed_decisions=3, but policy only has 1 — should not retire
        retired = await pm.retire_underperforming(max_failure_rate=0.5, min_governed_decisions=3)

        assert len(retired) == 0


class TestDiscoveredPattern:
    def test_suggested_policy_name(self) -> None:
        pattern = DiscoveredPattern(
            decision_type="billing_inquiry",
            tool_sequence=["check_balance", "apply_credit"],
            frequency=5,
            success_rate=0.9,
        )
        assert pattern.suggested_policy_name == "auto:billing_inquiry:check_balance → apply_credit"

    def test_suggested_policy_name_no_tools(self) -> None:
        pattern = DiscoveredPattern(
            decision_type="classification",
            tool_sequence=[],
            frequency=5,
            success_rate=0.9,
        )
        assert pattern.suggested_policy_name == "auto:classification:no-tool"
