"""Tests for context graph Pydantic schema models."""

from __future__ import annotations

from datetime import UTC, datetime

import pytest
from pydantic import ValidationError

from koan.graph.schema import (
    ActionNode,
    AgentNode,
    DecisionNode,
    EntityRef,
    ExceptionNode,
    OutcomeNode,
    PolicyNode,
    RunNode,
)


class TestDecisionNode:
    def test_valid_decision(self) -> None:
        node = DecisionNode(
            decision_id="d-1",
            run_id="r-1",
            timestamp=datetime.now(UTC),
            description="Classified request as billing issue",
            confidence=0.95,
            reasoning="Keyword match: 'invoice', 'charge'",
            decision_type="classification",
        )
        assert node.decision_id == "d-1"
        assert node.decision_type == "classification"
        assert node.inputs == {}
        assert node.metadata == {}

    def test_all_decision_types(self) -> None:
        valid_types = [
            "classification",
            "routing",
            "tool_selection",
            "policy_eval",
            "exception",
            "approval",
            "output",
        ]
        for dt in valid_types:
            node = DecisionNode(
                decision_id="d-1",
                run_id="r-1",
                timestamp=datetime.now(UTC),
                description="test",
                confidence=0.5,
                reasoning="test",
                decision_type=dt,
            )
            assert node.decision_type == dt

    def test_invalid_decision_type(self) -> None:
        with pytest.raises(ValidationError, match="decision_type"):
            DecisionNode(
                decision_id="d-1",
                run_id="r-1",
                timestamp=datetime.now(UTC),
                description="test",
                confidence=0.5,
                reasoning="test",
                decision_type="invalid_type",  # type: ignore[arg-type]
            )

    def test_confidence_bounds(self) -> None:
        with pytest.raises(ValidationError, match="confidence"):
            DecisionNode(
                decision_id="d-1",
                run_id="r-1",
                timestamp=datetime.now(UTC),
                description="test",
                confidence=1.5,
                reasoning="test",
                decision_type="output",
            )

        with pytest.raises(ValidationError, match="confidence"):
            DecisionNode(
                decision_id="d-1",
                run_id="r-1",
                timestamp=datetime.now(UTC),
                description="test",
                confidence=-0.1,
                reasoning="test",
                decision_type="output",
            )

    def test_with_inputs_and_metadata(self) -> None:
        node = DecisionNode(
            decision_id="d-1",
            run_id="r-1",
            timestamp=datetime.now(UTC),
            description="test",
            confidence=1.0,
            reasoning="test",
            decision_type="routing",
            inputs={"user_query": "help me"},
            metadata={"source": "api"},
        )
        assert node.inputs["user_query"] == "help me"
        assert node.metadata["source"] == "api"


class TestActionNode:
    def test_valid_tool_call(self) -> None:
        node = ActionNode(
            action_id="a-1",
            action_type="tool_call",
            tool_name="web_search",
            input={"query": "KOAN framework"},
            output={"results": ["result1"]},
            latency_ms=340,
        )
        assert node.action_type == "tool_call"
        assert node.tool_name == "web_search"
        assert node.error is None

    def test_action_with_error(self) -> None:
        node = ActionNode(
            action_id="a-2",
            action_type="api_call",
            error="Connection timeout",
        )
        assert node.error == "Connection timeout"
        assert node.tool_name is None

    def test_invalid_action_type(self) -> None:
        with pytest.raises(ValidationError, match="action_type"):
            ActionNode(action_id="a-1", action_type="invalid")  # type: ignore[arg-type]


class TestPolicyNode:
    def test_valid_policy(self) -> None:
        node = PolicyNode(
            policy_id="p-1",
            name="Refund Policy",
            version="2.0",
            description="Governs refund decisions",
            rules=["Refund if < 30 days", "Manager approval if > $500"],
        )
        assert len(node.rules) == 2
        assert node.source is None

    def test_empty_rules(self) -> None:
        node = PolicyNode(
            policy_id="p-2",
            name="Empty",
            version="1.0",
            description="No rules yet",
        )
        assert node.rules == []


class TestExceptionNode:
    def test_valid_exception(self) -> None:
        now = datetime.now(UTC)
        node = ExceptionNode(
            exception_id="ex-1",
            reason="Customer is VIP — override standard policy",
            approved_by="manager@example.com",
            approved_at=now,
            precedent_ref="d-old-123",
        )
        assert node.approved_by == "manager@example.com"
        assert node.precedent_ref == "d-old-123"

    def test_minimal_exception(self) -> None:
        node = ExceptionNode(exception_id="ex-2", reason="Emergency override")
        assert node.approved_by is None
        assert node.approved_at is None


class TestOutcomeNode:
    def test_valid_outcome(self) -> None:
        node = OutcomeNode(
            outcome_id="o-1",
            run_id="r-1",
            status="success",
            result="Refund processed",
            evaluation_score=0.92,
        )
        assert node.status == "success"

    def test_invalid_status(self) -> None:
        with pytest.raises(ValidationError, match="status"):
            OutcomeNode(
                outcome_id="o-1",
                run_id="r-1",
                status="unknown",  # type: ignore[arg-type]
                result=None,
            )

    def test_all_statuses(self) -> None:
        for status in ("success", "failure", "partial", "pending_review"):
            node = OutcomeNode(outcome_id="o-1", run_id="r-1", status=status)
            assert node.status == status


class TestEntityRef:
    def test_valid_entity(self) -> None:
        node = EntityRef(
            entity_id="e-1",
            entity_type="account",
            source_system="salesforce",
            external_id="SF-12345",
            properties={"tier": "enterprise"},
        )
        assert node.entity_type == "account"
        assert node.properties["tier"] == "enterprise"

    def test_minimal_entity(self) -> None:
        node = EntityRef(entity_id="e-2", entity_type="ticket")
        assert node.source_system == ""
        assert node.properties == {}


class TestRunNode:
    def test_valid_run(self) -> None:
        now = datetime.now(UTC)
        node = RunNode(
            run_id="r-1",
            thread_id="t-1",
            agent_id="agent-research",
            started_at=now,
            status="running",
        )
        assert node.status == "running"
        assert node.ended_at is None

    def test_completed_run(self) -> None:
        now = datetime.now(UTC)
        node = RunNode(
            run_id="r-1",
            thread_id="t-1",
            agent_id="agent-1",
            started_at=now,
            ended_at=now,
            status="completed",
        )
        assert node.status == "completed"

    def test_invalid_status(self) -> None:
        with pytest.raises(ValidationError, match="status"):
            RunNode(
                run_id="r-1",
                thread_id="t-1",
                agent_id="a-1",
                started_at=datetime.now(UTC),
                status="paused",  # type: ignore[arg-type]
            )


class TestAgentNode:
    def test_valid_agent(self) -> None:
        node = AgentNode(
            agent_id="agent-1",
            name="Research Agent",
            agent_type="react",
            model="gpt-4o",
            version="1.0",
        )
        assert node.name == "Research Agent"

    def test_minimal_agent(self) -> None:
        node = AgentNode(agent_id="a-1", name="Simple")
        assert node.agent_type == ""
        assert node.model == ""
