"""Tests for graph query functions and PrecedentSearch."""

from __future__ import annotations

from unittest.mock import AsyncMock

from koan.graph.client import GraphClient, GraphRecord
from koan.graph.queries import (
    detect_policy_gaps,
    find_cross_entity_decisions,
    find_exceptions,
    find_precedents,
    get_run_trace,
    trace_causal_chain,
)
from koan.graph.search import PrecedentResult, PrecedentSearch


def _mock_client(records: list[GraphRecord] | None = None) -> GraphClient:
    client = AsyncMock(spec=GraphClient)
    client.execute = AsyncMock(return_value=records or [])
    client.execute_write = AsyncMock(return_value=[])
    return client


# ---------------------------------------------------------------------------
# get_run_trace
# ---------------------------------------------------------------------------


class TestGetRunTrace:
    async def test_sends_correct_query(self) -> None:
        client = _mock_client()
        await get_run_trace(client, "r-1")

        client.execute.assert_awaited_once()
        cypher = client.execute.call_args[0][0]
        params = client.execute.call_args[0][1]
        assert "MATCH (r:Run {run_id: $run_id})" in cypher
        assert "CONTAINS" in cypher
        assert "ORDER BY d.timestamp" in cypher
        assert params["run_id"] == "r-1"

    async def test_returns_records(self) -> None:
        records = [
            GraphRecord(data={"decision_id": "d-1", "description": "First"}),
            GraphRecord(data={"decision_id": "d-2", "description": "Second"}),
        ]
        client = _mock_client(records)
        result = await get_run_trace(client, "r-1")
        assert len(result) == 2
        assert result[0].data["decision_id"] == "d-1"


# ---------------------------------------------------------------------------
# find_precedents
# ---------------------------------------------------------------------------


class TestFindPrecedents:
    async def test_basic_query(self) -> None:
        client = _mock_client()
        await find_precedents(
            client, entity_id="acct-1", decision_type="exception"
        )

        cypher = client.execute.call_args[0][0]
        params = client.execute.call_args[0][1]
        assert "entity_id: $entity_id" in cypher
        assert "decision_type = $decision_type" in cypher
        assert params["entity_id"] == "acct-1"
        assert params["decision_type"] == "exception"
        assert params["limit"] == 5

    async def test_with_before_filter(self) -> None:
        client = _mock_client()
        await find_precedents(
            client,
            entity_id="acct-1",
            decision_type="exception",
            before="2026-01-01T00:00:00Z",
        )

        cypher = client.execute.call_args[0][0]
        params = client.execute.call_args[0][1]
        assert "d.timestamp < $before" in cypher
        assert params["before"] == "2026-01-01T00:00:00Z"

    async def test_without_before_filter(self) -> None:
        client = _mock_client()
        await find_precedents(
            client, entity_id="acct-1", decision_type="routing"
        )

        cypher = client.execute.call_args[0][0]
        assert "$before" not in cypher

    async def test_custom_limit(self) -> None:
        client = _mock_client()
        await find_precedents(
            client, entity_id="acct-1", decision_type="routing", limit=10
        )

        params = client.execute.call_args[0][1]
        assert params["limit"] == 10


# ---------------------------------------------------------------------------
# find_exceptions
# ---------------------------------------------------------------------------


class TestFindExceptions:
    async def test_sends_correct_query(self) -> None:
        client = _mock_client()
        await find_exceptions(client, "pol-1")

        cypher = client.execute.call_args[0][0]
        params = client.execute.call_args[0][1]
        assert "GOVERNED_BY" in cypher
        assert "GRANTED" in cypher
        assert "Exception" in cypher
        assert params["policy_id"] == "pol-1"
        assert params["limit"] == 10

    async def test_custom_limit(self) -> None:
        client = _mock_client()
        await find_exceptions(client, "pol-1", limit=3)

        params = client.execute.call_args[0][1]
        assert params["limit"] == 3


# ---------------------------------------------------------------------------
# trace_causal_chain
# ---------------------------------------------------------------------------


class TestTraceCausalChain:
    async def test_sends_correct_query(self) -> None:
        client = _mock_client()
        await trace_causal_chain(client, "o-1")

        cypher = client.execute.call_args[0][0]
        params = client.execute.call_args[0][1]
        assert "Outcome {outcome_id: $outcome_id}" in cypher
        assert "CAUSED" in cypher
        assert params["outcome_id"] == "o-1"


# ---------------------------------------------------------------------------
# detect_policy_gaps
# ---------------------------------------------------------------------------


class TestDetectPolicyGaps:
    async def test_sends_correct_query(self) -> None:
        client = _mock_client()
        await detect_policy_gaps(client)

        cypher = client.execute.call_args[0][0]
        params = client.execute.call_args[0][1]
        assert "NOT (d)-[:GOVERNED_BY]->(:Policy)" in cypher
        assert "policy_eval" in cypher
        assert params["limit"] == 20

    async def test_custom_limit(self) -> None:
        client = _mock_client()
        await detect_policy_gaps(client, limit=5)

        params = client.execute.call_args[0][1]
        assert params["limit"] == 5


# ---------------------------------------------------------------------------
# find_cross_entity_decisions
# ---------------------------------------------------------------------------


class TestFindCrossEntityDecisions:
    async def test_sends_correct_query(self) -> None:
        client = _mock_client()
        await find_cross_entity_decisions(
            client, entity_a="acct-1", entity_b="ticket-2"
        )

        cypher = client.execute.call_args[0][0]
        params = client.execute.call_args[0][1]
        assert "entity_id: $entity_a" in cypher
        assert "entity_id: $entity_b" in cypher
        assert params["entity_a"] == "acct-1"
        assert params["entity_b"] == "ticket-2"


# ---------------------------------------------------------------------------
# PrecedentSearch
# ---------------------------------------------------------------------------


class TestPrecedentSearch:
    async def test_find_returns_precedent_results(self) -> None:
        records = [
            GraphRecord(data={
                "decision_id": "d-1",
                "description": "Granted exception",
                "reasoning": "VIP customer",
                "confidence": 0.9,
                "timestamp": "2026-01-15T10:00:00Z",
                "outcome_status": "success",
                "evaluation_score": 0.95,
            }),
            GraphRecord(data={
                "decision_id": "d-2",
                "description": "Standard handling",
                "reasoning": "No special case",
                "confidence": 0.7,
                "timestamp": "2026-01-10T10:00:00Z",
                "outcome_status": "success",
                "evaluation_score": None,
            }),
        ]
        client = _mock_client(records)
        search = PrecedentSearch(client)

        results = await search.find(
            entity_id="acct-1", decision_type="exception"
        )

        assert len(results) == 2
        assert isinstance(results[0], PrecedentResult)
        assert results[0].decision_id == "d-1"
        assert results[0].confidence == 0.9
        assert results[0].outcome_status == "success"

    async def test_find_filters_by_min_confidence(self) -> None:
        records = [
            GraphRecord(data={
                "decision_id": "d-1",
                "description": "High conf",
                "reasoning": "r",
                "confidence": 0.9,
                "timestamp": "t",
            }),
            GraphRecord(data={
                "decision_id": "d-2",
                "description": "Low conf",
                "reasoning": "r",
                "confidence": 0.3,
                "timestamp": "t",
            }),
        ]
        client = _mock_client(records)
        search = PrecedentSearch(client)

        results = await search.find(
            entity_id="acct-1",
            decision_type="exception",
            min_confidence=0.5,
        )

        assert len(results) == 1
        assert results[0].decision_id == "d-1"

    async def test_find_empty_results(self) -> None:
        client = _mock_client([])
        search = PrecedentSearch(client)

        results = await search.find(
            entity_id="acct-1", decision_type="exception"
        )

        assert results == []

    async def test_get_trace_delegates_to_query(self) -> None:
        records = [GraphRecord(data={"decision_id": "d-1"})]
        client = _mock_client(records)
        search = PrecedentSearch(client)

        result = await search.get_trace("r-1")

        assert len(result) == 1
        client.execute.assert_awaited_once()
