"""Coverage gap tests for kernel/world_dag.py — lines 42, 85, 256-257."""

from __future__ import annotations

import pytest

from orxaq.kernel import CausalEdge, CausalRole, Entity, VariableType, WorldDAG


def _entity(name: str) -> Entity:
    return Entity(
        entity_id=name,
        name=name,
        causal_role=CausalRole.EXOGENOUS,
        variable_type=VariableType.STATIC,
    )


def _build_dag(*names: str) -> WorldDAG:
    dag = WorldDAG()
    for n in names:
        dag.add_entity(_entity(n))
    return dag


class TestRemoveEntityGaps:
    """Line 42: remove_entity on missing entity is a no-op."""

    def test_remove_nonexistent_entity_noop(self) -> None:
        """Line 42: entity_id not in _entities → return early, no error."""
        dag = _build_dag("A", "B")
        dag.remove_entity("DOES_NOT_EXIST")
        # DAG should be unchanged
        assert "A" in dag._entities
        assert "B" in dag._entities

    def test_remove_entity_removes_connected_edges(self) -> None:
        """Removing existing entity also removes connected edges."""
        dag = _build_dag("X", "Y", "Z")
        dag.add_edge(CausalEdge(source="X", target="Y", mechanism="m"))
        dag.add_edge(CausalEdge(source="Y", target="Z", mechanism="m"))
        dag.remove_entity("Y")
        assert "Y" not in dag._entities
        # Both edges involving Y should be gone
        for edge in dag._edges.values():
            assert edge.source != "Y" and edge.target != "Y"


class TestCycleDetectionVisitedContinue:
    """Line 85: visited-node skip inside cycle detection (would_create_cycle)."""

    def test_existing_edge_does_not_create_cycle(self) -> None:
        """Cycle detection on diamond shape visits nodes multiple times."""
        dag = _build_dag("A", "B", "C", "D")
        dag.add_edge(CausalEdge(source="A", target="B", mechanism="m"))
        dag.add_edge(CausalEdge(source="A", target="C", mechanism="m"))
        dag.add_edge(CausalEdge(source="B", target="D", mechanism="m"))
        dag.add_edge(CausalEdge(source="C", target="D", mechanism="m"))
        # Adding D → new_node should not be flagged as a cycle
        dag.add_entity(_entity("E"))
        # Adding D→E should work (no cycle)
        dag.add_edge(CausalEdge(source="D", target="E", mechanism="m"))
        assert "E" in dag._entities

    def test_adding_backward_edge_raises(self) -> None:
        """Cycle detection correctly blocks B→A when A→B exists."""
        from orxaq.kernel import CycleError

        dag = _build_dag("A", "B")
        dag.add_edge(CausalEdge(source="A", target="B", mechanism="m"))
        # B→A would create a cycle
        with pytest.raises(CycleError):
            dag.add_edge(CausalEdge(source="B", target="A", mechanism="back"))


class TestMinimalAdjustmentSetGaps:
    """Lines 256-257: parent whose descendants intersect outcome ancestors."""

    def test_backdoor_adjustment_set_indirect_path(self) -> None:
        """Lines 256-257: parent P where descendants(P) ∩ ancestors(Y) != empty."""
        # Structure: P → X → Y, P → M → Y
        # P is parent of X (treatment), and P's descendants reach Y's ancestors
        dag = _build_dag("P", "X", "M", "Y")
        dag.add_edge(CausalEdge(source="P", target="X", mechanism="conf"))
        dag.add_edge(CausalEdge(source="P", target="M", mechanism="m"))
        dag.add_edge(CausalEdge(source="M", target="Y", mechanism="m"))
        dag.add_edge(CausalEdge(source="X", target="Y", mechanism="direct"))

        adj = dag.backdoor_adjustment_set("X", "Y")
        # P should be in the adjustment set since descendants(P) ∩ ancestors(Y) != {}
        assert isinstance(adj, set)
        assert "P" in adj

    def test_backdoor_adjustment_set_simple_confounder(self) -> None:
        """Standard confounder Z where Z → X and Z → Y."""
        dag = _build_dag("X", "Y", "Z")
        dag.add_edge(CausalEdge(source="Z", target="X", mechanism="conf"))
        dag.add_edge(CausalEdge(source="Z", target="Y", mechanism="conf"))
        dag.add_edge(CausalEdge(source="X", target="Y", mechanism="direct"))
        adj = dag.backdoor_adjustment_set("X", "Y")
        assert "Z" in adj
