"""
Unit tests for dependency tracking models.

Tests EdgeType, DependencyEdge, DependencyGraph operations,
cycle detection, and topological sort.
"""

import pytest
from datetime import datetime

from markitect.prompts.dependencies.models import (
    EdgeType,
    DependencyEdge,
    DependencyGraph,
    CircularDependencyError,
)


class TestEdgeType:
    """Tests for EdgeType enum."""

    def test_edge_type_values(self):
        """Test edge type enum values."""
        assert EdgeType.REQUIRES.value == "requires"
        assert EdgeType.GENERATES.value == "generates"
        assert EdgeType.INCLUDES.value == "includes"

    def test_edge_type_from_value(self):
        """Test creating edge type from string value."""
        assert EdgeType("requires") == EdgeType.REQUIRES
        assert EdgeType("generates") == EdgeType.GENERATES
        assert EdgeType("includes") == EdgeType.INCLUDES

    def test_edge_type_invalid_value(self):
        """Test invalid edge type raises ValueError."""
        with pytest.raises(ValueError):
            EdgeType("invalid")


class TestDependencyEdge:
    """Tests for persistent DependencyEdge."""

    def test_create_edge(self):
        """Test creating a dependency edge via factory method."""
        edge = DependencyEdge.create(
            source_artifact_id="artifact-1",
            target_artifact_id="artifact-2",
            run_id="run-1",
            edge_type=EdgeType.REQUIRES,
        )
        assert edge.id  # UUID generated
        assert edge.source_artifact_id == "artifact-1"
        assert edge.target_artifact_id == "artifact-2"
        assert edge.run_id == "run-1"
        assert edge.edge_type == EdgeType.REQUIRES
        assert isinstance(edge.created_at, datetime)

    def test_create_edge_unique_ids(self):
        """Test each created edge gets a unique ID."""
        edge1 = DependencyEdge.create(
            source_artifact_id="a",
            target_artifact_id="b",
            run_id="r1",
            edge_type=EdgeType.REQUIRES,
        )
        edge2 = DependencyEdge.create(
            source_artifact_id="a",
            target_artifact_id="b",
            run_id="r1",
            edge_type=EdgeType.REQUIRES,
        )
        assert edge1.id != edge2.id

    def test_edge_to_dict(self):
        """Test edge serialization to dict."""
        edge = DependencyEdge.create(
            source_artifact_id="artifact-1",
            target_artifact_id="artifact-2",
            run_id="run-1",
            edge_type=EdgeType.GENERATES,
        )
        data = edge.to_dict()

        assert data["id"] == edge.id
        assert data["source_artifact_id"] == "artifact-1"
        assert data["target_artifact_id"] == "artifact-2"
        assert data["run_id"] == "run-1"
        assert data["edge_type"] == "generates"
        assert "created_at" in data

    def test_edge_from_dict(self):
        """Test edge deserialization from dict."""
        data = {
            "id": "edge-123",
            "source_artifact_id": "artifact-1",
            "target_artifact_id": "artifact-2",
            "run_id": "run-1",
            "edge_type": "includes",
            "created_at": "2026-01-01T00:00:00",
        }
        edge = DependencyEdge.from_dict(data)

        assert edge.id == "edge-123"
        assert edge.source_artifact_id == "artifact-1"
        assert edge.target_artifact_id == "artifact-2"
        assert edge.run_id == "run-1"
        assert edge.edge_type == EdgeType.INCLUDES
        assert edge.created_at == datetime(2026, 1, 1)

    def test_edge_roundtrip(self):
        """Test serialization roundtrip preserves data."""
        original = DependencyEdge.create(
            source_artifact_id="src",
            target_artifact_id="tgt",
            run_id="run",
            edge_type=EdgeType.REQUIRES,
        )
        restored = DependencyEdge.from_dict(original.to_dict())

        assert restored.id == original.id
        assert restored.source_artifact_id == original.source_artifact_id
        assert restored.target_artifact_id == original.target_artifact_id
        assert restored.run_id == original.run_id
        assert restored.edge_type == original.edge_type


class TestDependencyGraph:
    """Tests for DependencyGraph."""

    def test_empty_graph(self):
        """Test empty graph properties."""
        graph = DependencyGraph()
        assert len(graph.nodes) == 0
        assert graph.edge_count == 0
        assert not graph.has_cycle()

    def test_add_edge(self):
        """Test adding an edge to the graph."""
        graph = DependencyGraph()
        graph.add_edge("A", "B", EdgeType.REQUIRES)

        assert "A" in graph.nodes
        assert "B" in graph.nodes
        assert graph.edge_count == 1
        assert graph.has_edge("A", "B")
        assert not graph.has_edge("B", "A")

    def test_add_multiple_edges(self):
        """Test adding multiple edges."""
        graph = DependencyGraph()
        graph.add_edge("A", "B")
        graph.add_edge("B", "C")
        graph.add_edge("A", "C")

        assert len(graph.nodes) == 3
        assert graph.edge_count == 3

    def test_get_successors(self):
        """Test getting direct successors."""
        graph = DependencyGraph()
        graph.add_edge("A", "B")
        graph.add_edge("A", "C")
        graph.add_edge("B", "C")

        assert graph.get_successors("A") == {"B", "C"}
        assert graph.get_successors("B") == {"C"}
        assert graph.get_successors("C") == set()

    def test_get_predecessors(self):
        """Test getting direct predecessors."""
        graph = DependencyGraph()
        graph.add_edge("A", "B")
        graph.add_edge("A", "C")
        graph.add_edge("B", "C")

        assert graph.get_predecessors("A") == set()
        assert graph.get_predecessors("B") == {"A"}
        assert graph.get_predecessors("C") == {"A", "B"}

    def test_get_successors_nonexistent_node(self):
        """Test getting successors of non-existent node."""
        graph = DependencyGraph()
        assert graph.get_successors("X") == set()

    def test_get_predecessors_nonexistent_node(self):
        """Test getting predecessors of non-existent node."""
        graph = DependencyGraph()
        assert graph.get_predecessors("X") == set()

    def test_get_edge_type(self):
        """Test getting edge type between nodes."""
        graph = DependencyGraph()
        graph.add_edge("A", "B", EdgeType.REQUIRES)
        graph.add_edge("B", "C", EdgeType.GENERATES)

        assert graph.get_edge_type("A", "B") == EdgeType.REQUIRES
        assert graph.get_edge_type("B", "C") == EdgeType.GENERATES
        assert graph.get_edge_type("A", "C") is None

    def test_has_edge(self):
        """Test edge existence check."""
        graph = DependencyGraph()
        graph.add_edge("A", "B")

        assert graph.has_edge("A", "B")
        assert not graph.has_edge("B", "A")
        assert not graph.has_edge("A", "C")


class TestDependencyGraphCycleDetection:
    """Tests for DependencyGraph cycle detection."""

    def test_no_cycle_linear(self):
        """Test no cycle in linear graph A -> B -> C."""
        graph = DependencyGraph()
        graph.add_edge("A", "B")
        graph.add_edge("B", "C")

        assert not graph.has_cycle()
        assert graph.detect_cycles() == []

    def test_no_cycle_diamond(self):
        """Test no cycle in diamond graph A -> B,C -> D."""
        graph = DependencyGraph()
        graph.add_edge("A", "B")
        graph.add_edge("A", "C")
        graph.add_edge("B", "D")
        graph.add_edge("C", "D")

        assert not graph.has_cycle()

    def test_simple_cycle(self):
        """Test detecting simple cycle A -> B -> A."""
        graph = DependencyGraph()
        graph.add_edge("A", "B")
        graph.add_edge("B", "A")

        assert graph.has_cycle()
        cycles = graph.detect_cycles()
        assert len(cycles) > 0

    def test_three_node_cycle(self):
        """Test detecting 3-node cycle A -> B -> C -> A."""
        graph = DependencyGraph()
        graph.add_edge("A", "B")
        graph.add_edge("B", "C")
        graph.add_edge("C", "A")

        assert graph.has_cycle()
        cycles = graph.detect_cycles()
        assert len(cycles) > 0
        # The cycle should contain A, B, C
        cycle_nodes = set(cycles[0][:-1])  # Last element repeats the start
        assert cycle_nodes == {"A", "B", "C"}

    def test_self_loop(self):
        """Test detecting self-loop A -> A."""
        graph = DependencyGraph()
        graph.add_edge("A", "A")

        assert graph.has_cycle()

    def test_cycle_with_acyclic_parts(self):
        """Test cycle detection in graph with both cyclic and acyclic parts."""
        graph = DependencyGraph()
        # Acyclic part
        graph.add_edge("X", "Y")
        # Cyclic part
        graph.add_edge("A", "B")
        graph.add_edge("B", "C")
        graph.add_edge("C", "A")

        assert graph.has_cycle()


class TestDependencyGraphTopologicalSort:
    """Tests for DependencyGraph topological sort."""

    def test_topological_sort_linear(self):
        """Test topological sort of linear graph."""
        graph = DependencyGraph()
        graph.add_edge("A", "B")
        graph.add_edge("B", "C")

        order = graph.topological_sort()
        assert order.index("A") < order.index("B")
        assert order.index("B") < order.index("C")

    def test_topological_sort_diamond(self):
        """Test topological sort of diamond graph."""
        graph = DependencyGraph()
        graph.add_edge("A", "B")
        graph.add_edge("A", "C")
        graph.add_edge("B", "D")
        graph.add_edge("C", "D")

        order = graph.topological_sort()
        assert order.index("A") < order.index("B")
        assert order.index("A") < order.index("C")
        assert order.index("B") < order.index("D")
        assert order.index("C") < order.index("D")

    def test_topological_sort_single_node(self):
        """Test topological sort with single node."""
        graph = DependencyGraph()
        graph.add_edge("A", "B")

        order = graph.topological_sort()
        assert len(order) == 2
        assert order[0] == "A"
        assert order[1] == "B"

    def test_topological_sort_disconnected(self):
        """Test topological sort with disconnected components."""
        graph = DependencyGraph()
        graph.add_edge("A", "B")
        graph.add_edge("C", "D")

        order = graph.topological_sort()
        assert len(order) == 4
        assert order.index("A") < order.index("B")
        assert order.index("C") < order.index("D")

    def test_topological_sort_with_cycle_raises_error(self):
        """Test topological sort raises error on cycle."""
        graph = DependencyGraph()
        graph.add_edge("A", "B")
        graph.add_edge("B", "C")
        graph.add_edge("C", "A")

        with pytest.raises(CircularDependencyError) as exc_info:
            graph.topological_sort()

        assert exc_info.value.cycle is not None
        assert len(exc_info.value.cycle) > 0

    def test_topological_sort_empty_graph(self):
        """Test topological sort of empty graph."""
        graph = DependencyGraph()
        assert graph.topological_sort() == []


class TestDependencyGraphSubgraph:
    """Tests for DependencyGraph subgraph extraction."""

    def test_subgraph_extraction(self):
        """Test extracting a subgraph."""
        graph = DependencyGraph()
        graph.add_edge("A", "B", EdgeType.REQUIRES)
        graph.add_edge("B", "C", EdgeType.GENERATES)
        graph.add_edge("C", "D", EdgeType.INCLUDES)

        subgraph = graph.get_subgraph({"A", "B", "C"})

        assert subgraph.has_edge("A", "B")
        assert subgraph.has_edge("B", "C")
        assert not subgraph.has_edge("C", "D")
        assert "D" not in subgraph.nodes

    def test_subgraph_preserves_edge_types(self):
        """Test subgraph preserves edge types."""
        graph = DependencyGraph()
        graph.add_edge("A", "B", EdgeType.REQUIRES)
        graph.add_edge("B", "C", EdgeType.GENERATES)

        subgraph = graph.get_subgraph({"A", "B", "C"})

        assert subgraph.get_edge_type("A", "B") == EdgeType.REQUIRES
        assert subgraph.get_edge_type("B", "C") == EdgeType.GENERATES

    def test_subgraph_isolated_nodes(self):
        """Test subgraph includes isolated nodes."""
        graph = DependencyGraph()
        graph.add_edge("A", "B")

        subgraph = graph.get_subgraph({"A", "B", "C"})

        assert "C" in subgraph.nodes
        assert subgraph.get_successors("C") == set()

    def test_subgraph_empty(self):
        """Test extracting empty subgraph."""
        graph = DependencyGraph()
        graph.add_edge("A", "B")

        subgraph = graph.get_subgraph(set())
        assert len(subgraph.nodes) == 0


class TestCircularDependencyError:
    """Tests for CircularDependencyError."""

    def test_error_with_cycle(self):
        """Test error with cycle information."""
        error = CircularDependencyError(
            "Cycle detected",
            cycle=["A", "B", "C", "A"],
        )
        assert str(error) == "Cycle detected"
        assert error.cycle == ["A", "B", "C", "A"]

    def test_error_without_cycle(self):
        """Test error without cycle information."""
        error = CircularDependencyError("Cycle detected")
        assert error.cycle == []

    def test_error_is_exception(self):
        """Test that CircularDependencyError is an Exception."""
        with pytest.raises(CircularDependencyError):
            raise CircularDependencyError("test")
