"""Unit tests for SPEC §6.1 (edge routing) and §11.1 (human-gate edges).

Pure-logic tests: build small `ValidGraph` fixtures and assert which
edge `pick_*` selects for each routing mode.
"""

from __future__ import annotations

from attractor.engine import OutcomeStatus
from attractor.engine.routing import (
    human_gate_choices,
    outgoing_edges,
    pick_choice_edge,
    pick_edge_for_outcome,
    pick_failure_edge,
    pick_partial_success_edge,
    pick_success_edge,
)
from attractor.workflow import Edge, Node, NodeKind, Span, ValidGraph


def _e(
    src: str, dst: str, label: str | None, *, condition: str | None = None
) -> Edge:
    attrs: dict[str, str] = {}
    if label:
        attrs["label"] = label
    if condition:
        attrs["condition"] = condition
    return Edge(
        src=src,
        dst=dst,
        label=label,
        attributes=attrs,
        span=Span(line=0, column=0),
    )


def _n(node_id: str, kind: NodeKind = NodeKind.AGENT) -> Node:
    return Node(
        id=node_id,
        kind=kind,
        label=None,
        class_=None,
        attributes={},
        span=Span(line=0, column=0),
    )


def _g(nodes: list[Node], edges: list[Edge]) -> ValidGraph:
    return ValidGraph(name="t", attributes={}, nodes=nodes, edges=edges)


# [unit->REQ-EXEC-EDGE-ROUTING]
def test_pick_success_prefers_labelled_success_edge() -> None:
    """`label="SUCCESS"` wins over an unlabeled fallback."""
    g = _g(
        nodes=[_n("a"), _n("b"), _n("c")],
        edges=[_e("a", "b", None), _e("a", "c", "SUCCESS")],
    )
    edge = pick_success_edge(g, "a")
    assert edge is not None
    assert edge.dst == "c"


# [unit->REQ-EXEC-EDGE-ROUTING]
def test_pick_success_falls_back_to_unlabeled() -> None:
    """SPEC §6.1: unlabeled-default applies to SUCCESS."""
    g = _g(
        nodes=[_n("a"), _n("b")],
        edges=[_e("a", "b", None)],
    )
    edge = pick_success_edge(g, "a")
    assert edge is not None
    assert edge.dst == "b"
    assert edge.label is None


# [unit->REQ-EXEC-EDGE-ROUTING]
def test_pick_success_returns_none_when_no_match() -> None:
    """No SUCCESS and no unlabeled → no routable edge."""
    g = _g(
        nodes=[_n("a"), _n("b")],
        edges=[_e("a", "b", "FAILURE")],
    )
    assert pick_success_edge(g, "a") is None


# [unit->REQ-EXEC-EDGE-ROUTING]
def test_pick_failure_requires_explicit_label() -> None:
    """FAILURE must match `label="FAILURE"` exactly; no unlabeled fallback."""
    g = _g(
        nodes=[_n("a"), _n("b"), _n("c")],
        edges=[_e("a", "b", None), _e("a", "c", "FAILURE")],
    )
    edge = pick_failure_edge(g, "a")
    assert edge is not None
    assert edge.dst == "c"


# [unit->REQ-EXEC-EDGE-ROUTING]
def test_pick_failure_no_unlabeled_fallback() -> None:
    """An unlabeled edge does NOT serve as a FAILURE fallback."""
    g = _g(
        nodes=[_n("a"), _n("b")],
        edges=[_e("a", "b", None)],
    )
    assert pick_failure_edge(g, "a") is None


# [unit->REQ-HUMAN-GATE]
def test_pick_choice_exact_match() -> None:
    """Human-gate choice matches a labeled outgoing edge."""
    g = _g(
        nodes=[_n("a", NodeKind.HUMAN), _n("b"), _n("c")],
        edges=[_e("a", "b", "approve"), _e("a", "c", "revise")],
    )
    edge = pick_choice_edge(g, "a", "approve")
    assert edge is not None
    assert edge.dst == "b"


# [unit->REQ-HUMAN-GATE]
def test_pick_choice_no_unlabeled_fallback() -> None:
    """§11.1 rule 1: unlabeled edges from hexagons are unreachable."""
    g = _g(
        nodes=[_n("a", NodeKind.HUMAN), _n("b")],
        edges=[_e("a", "b", None)],
    )
    assert pick_choice_edge(g, "a", "anything") is None


# [unit->REQ-HUMAN-GATE]
def test_human_gate_choices_excludes_unlabeled_edges() -> None:
    """§11.1 rule 1: only labeled edges appear in `choices`."""
    g = _g(
        nodes=[_n("a", NodeKind.HUMAN), _n("b"), _n("c")],
        edges=[_e("a", "b", None), _e("a", "c", "approve")],
    )
    assert human_gate_choices(g, "a") == ["approve"]


# [unit->REQ-HUMAN-GATE]
def test_human_gate_choices_preserves_source_order() -> None:
    """§11.1 rule 2: duplicates allowed; first-declared wins on resolve."""
    g = _g(
        nodes=[_n("a", NodeKind.HUMAN), _n("b"), _n("c")],
        edges=[
            _e("a", "b", "approve"),
            _e("a", "c", "approve"),
        ],
    )
    choices = human_gate_choices(g, "a")
    assert choices == ["approve", "approve"]
    # And on resolve, the FIRST one wins.
    edge = pick_choice_edge(g, "a", "approve")
    assert edge is not None
    assert edge.dst == "b"


# [unit->REQ-EXEC-EDGE-ROUTING]
def test_outgoing_edges_filtered_by_source() -> None:
    """`outgoing_edges` returns only the edges leaving `node_id`."""
    g = _g(
        nodes=[_n("a"), _n("b"), _n("c")],
        edges=[_e("a", "b", None), _e("c", "b", None)],
    )
    assert [(e.src, e.dst) for e in outgoing_edges(g, "a")] == [("a", "b")]
    assert [(e.src, e.dst) for e in outgoing_edges(g, "c")] == [("c", "b")]


# ─── PARTIAL_SUCCESS routing (REQ-EXEC-OUTCOME-STATUSES) ────────────────


# [unit->REQ-EXEC-OUTCOME-STATUSES]
def test_pick_partial_success_prefers_explicit_label() -> None:
    """label="PARTIAL_SUCCESS" wins when present, before SUCCESS fallback."""
    g = _g(
        nodes=[_n("a"), _n("b"), _n("c"), _n("d")],
        edges=[
            _e("a", "b", "SUCCESS"),
            _e("a", "c", "PARTIAL_SUCCESS"),
            _e("a", "d", None),
        ],
    )
    edge = pick_partial_success_edge(g, "a")
    assert edge is not None
    assert edge.dst == "c"


# [unit->REQ-EXEC-OUTCOME-STATUSES]
def test_pick_partial_success_falls_back_to_success_edge() -> None:
    """No explicit PARTIAL_SUCCESS edge → use the SUCCESS edge (Option A)."""
    g = _g(
        nodes=[_n("a"), _n("b"), _n("c")],
        edges=[_e("a", "b", "SUCCESS"), _e("a", "c", "FAILURE")],
    )
    edge = pick_partial_success_edge(g, "a")
    assert edge is not None
    assert edge.dst == "b"


# [unit->REQ-EXEC-OUTCOME-STATUSES]
def test_pick_partial_success_falls_back_to_unlabeled() -> None:
    """No explicit PARTIAL_SUCCESS, no SUCCESS edge → unlabeled wins."""
    g = _g(
        nodes=[_n("a"), _n("b"), _n("c")],
        edges=[_e("a", "b", None), _e("a", "c", "FAILURE")],
    )
    edge = pick_partial_success_edge(g, "a")
    assert edge is not None
    assert edge.dst == "b"


# [unit->REQ-EXEC-OUTCOME-STATUSES]
def test_pick_partial_success_no_match_returns_none() -> None:
    """Only FAILURE edge → no PARTIAL_SUCCESS-compatible route."""
    g = _g(
        nodes=[_n("a"), _n("b")],
        edges=[_e("a", "b", "FAILURE")],
    )
    assert pick_partial_success_edge(g, "a") is None


# ─── NO_OP routing (REQ-EXEC-OUTCOME-NO-OP) ─────────────────────────────


# [unit->REQ-EXEC-OUTCOME-NO-OP]
def test_pick_no_op_prefers_explicit_label() -> None:
    g = _g(
        nodes=[_n("a"), _n("b"), _n("c")],
        edges=[_e("a", "b", None), _e("a", "c", "NO_OP")],
    )
    edge = pick_edge_for_outcome(g, "a", OutcomeStatus.NO_OP)
    assert edge is not None
    assert edge.dst == "c"


# [unit->REQ-EXEC-OUTCOME-NO-OP]
def test_pick_no_op_falls_back_to_unlabeled_not_success() -> None:
    g = _g(
        nodes=[_n("a"), _n("b"), _n("c")],
        edges=[_e("a", "b", "SUCCESS"), _e("a", "c", None)],
    )
    edge = pick_edge_for_outcome(g, "a", OutcomeStatus.NO_OP)
    assert edge is not None
    assert edge.dst == "c"


# ─── pick_edge_for_outcome (REQ-EXEC-OUTCOME-PREFERRED-LABEL) ───────────


# [unit->REQ-EXEC-OUTCOME-PREFERRED-LABEL]
def test_pick_edge_for_outcome_dispatches_to_status_router() -> None:
    """Without a preferred_label hint, falls through to status-based routing."""
    g = _g(
        nodes=[_n("a"), _n("b"), _n("c")],
        edges=[_e("a", "b", "SUCCESS"), _e("a", "c", "FAILURE")],
    )
    succ = pick_edge_for_outcome(g, "a", OutcomeStatus.SUCCESS)
    assert succ is not None and succ.dst == "b"

    fail = pick_edge_for_outcome(g, "a", OutcomeStatus.FAILURE)
    assert fail is not None and fail.dst == "c"

    part = pick_edge_for_outcome(g, "a", OutcomeStatus.PARTIAL_SUCCESS)
    assert part is not None and part.dst == "b", (
        "PARTIAL_SUCCESS falls back to SUCCESS edge per Option A"
    )


# [unit->REQ-EXEC-OUTCOME-PREFERRED-LABEL]
def test_pick_edge_for_outcome_preferred_label_takes_priority() -> None:
    """A matching preferred_label edge wins over the status-name match."""
    g = _g(
        nodes=[_n("a"), _n("b"), _n("c"), _n("d")],
        edges=[
            _e("a", "b", "FAILURE"),
            _e("a", "c", "env-error"),
            _e("a", "d", None),
        ],
    )
    # FAILURE status would normally route to b; preferred_label "env-error"
    # routes to c instead.
    edge = pick_edge_for_outcome(
        g, "a", OutcomeStatus.FAILURE, preferred_label="env-error"
    )
    assert edge is not None
    assert edge.dst == "c"


# ─── suggested_next_ids (REQ-EXEC-OUTCOME-SUGGESTED-IDS) ────────────────


# [unit->REQ-EXEC-OUTCOME-SUGGESTED-IDS]
def test_suggested_next_ids_match_by_target() -> None:
    """An unconditional edge whose target is in the suggested list wins."""
    g = _g(
        nodes=[_n("a"), _n("b"), _n("c"), _n("d")],
        edges=[
            _e("a", "b", "SUCCESS"),
            _e("a", "c", None),
            _e("a", "d", None),
        ],
    )
    edge = pick_edge_for_outcome(
        g, "a", OutcomeStatus.SUCCESS, suggested_next_ids=("c",)
    )
    assert edge is not None
    assert edge.dst == "c"


# [unit->REQ-EXEC-OUTCOME-SUGGESTED-IDS]
def test_suggested_first_in_list_wins() -> None:
    """When multiple suggestions could match, the first in the list wins
    (not the first in source order).
    """
    g = _g(
        nodes=[_n("a"), _n("b"), _n("c")],
        edges=[
            _e("a", "b", None),
            _e("a", "c", None),
        ],
    )
    edge = pick_edge_for_outcome(
        g, "a", OutcomeStatus.SUCCESS, suggested_next_ids=("c", "b")
    )
    assert edge is not None
    assert edge.dst == "c", "list order in suggested_next_ids wins"


# [unit->REQ-EXEC-OUTCOME-SUGGESTED-IDS]
def test_suggested_below_preferred_label_priority() -> None:
    """preferred_label match wins over a suggested_next_ids match."""
    g = _g(
        nodes=[_n("a"), _n("b"), _n("c")],
        edges=[
            _e("a", "b", "env-error"),
            _e("a", "c", None),
        ],
    )
    edge = pick_edge_for_outcome(
        g,
        "a",
        OutcomeStatus.FAILURE,
        preferred_label="env-error",
        suggested_next_ids=("c",),
    )
    assert edge is not None
    assert edge.dst == "b", "preferred_label wins over suggested_next_ids"


# [unit->REQ-EXEC-OUTCOME-SUGGESTED-IDS]
def test_suggested_skips_condition_bearing_edge() -> None:
    """Condition-bearing edges drop out after step 1 — they don't get
    matched by suggested_next_ids in step 3 even if their target is in
    the list.
    """
    g = _g(
        nodes=[_n("a"), _n("b"), _n("c")],
        edges=[
            _e("a", "b", None, condition="outcome=FAILURE"),
            _e("a", "c", None),
        ],
    )
    # outcome=SUCCESS so condition on b doesn't match; suggested is "b"
    edge = pick_edge_for_outcome(
        g, "a", OutcomeStatus.SUCCESS, suggested_next_ids=("b",)
    )
    # b has a condition that didn't match → it drops out of later steps.
    # c is the unconditional fallback.
    assert edge is not None
    assert edge.dst == "c"


# ─── edge `condition=` integration (REQ-EXEC-EDGE-CONDITION-DSL) ────────


# [unit->REQ-EXEC-EDGE-CONDITION-DSL]
def test_condition_match_beats_status_routing() -> None:
    """An edge whose condition evaluates true wins over status-based routing."""
    g = _g(
        nodes=[_n("a"), _n("b"), _n("c"), _n("d")],
        edges=[
            _e("a", "b", "SUCCESS"),  # would win on status alone
            _e("a", "c", None, condition="outcome=SUCCESS"),
            _e("a", "d", None),
        ],
    )
    edge = pick_edge_for_outcome(g, "a", OutcomeStatus.SUCCESS)
    assert edge is not None
    assert edge.dst == "c", "condition-bearing edge wins step 1"


# [unit->REQ-EXEC-EDGE-CONDITION-DSL]
def test_first_matching_condition_wins() -> None:
    """Among multiple condition-bearing edges that all match, source-order wins."""
    g = _g(
        nodes=[_n("a"), _n("b"), _n("c")],
        edges=[
            _e("a", "b", None, condition="outcome=SUCCESS"),
            _e("a", "c", None, condition="outcome=SUCCESS"),
        ],
    )
    edge = pick_edge_for_outcome(g, "a", OutcomeStatus.SUCCESS)
    assert edge is not None
    assert edge.dst == "b"


# [unit->REQ-EXEC-EDGE-CONDITION-DSL]
def test_unmatching_condition_does_not_fall_back_to_label() -> None:
    """A condition-bearing edge whose condition is false drops out of all
    later routing steps — it must NOT be re-selected via its label= match.
    """
    g = _g(
        nodes=[_n("a"), _n("b"), _n("c")],
        edges=[
            # condition evaluates false for FAILURE; this edge is out
            _e("a", "b", "FAILURE", condition="outcome=SUCCESS"),
            # the actual FAILURE handler
            _e("a", "c", "FAILURE"),
        ],
    )
    edge = pick_edge_for_outcome(g, "a", OutcomeStatus.FAILURE)
    assert edge is not None
    assert edge.dst == "c", (
        "the condition-bearing FAILURE edge dropped out; the real "
        "FAILURE edge should win"
    )


# [unit->REQ-EXEC-EDGE-CONDITION-DSL]
def test_condition_uses_preferred_label_too() -> None:
    """Conditions can match on preferred_label, not just outcome."""
    g = _g(
        nodes=[_n("a"), _n("b"), _n("c")],
        edges=[
            _e("a", "b", None, condition="preferred_label=env-error"),
            _e("a", "c", "FAILURE"),
        ],
    )
    edge = pick_edge_for_outcome(
        g, "a", OutcomeStatus.FAILURE, preferred_label="env-error"
    )
    assert edge is not None
    assert edge.dst == "b"


# [unit->REQ-EXEC-EDGE-CONDITION-DSL]
def test_and_combined_condition() -> None:
    """Multi-clause AND condition: both must match for the edge to win."""
    g = _g(
        nodes=[_n("a"), _n("b"), _n("c")],
        edges=[
            _e(
                "a", "b", None,
                condition="outcome=SUCCESS && preferred_label=approved",
            ),
            _e("a", "c", None),  # unlabeled SUCCESS fallback
        ],
    )
    # Both conditions hold → b wins.
    e_both = pick_edge_for_outcome(
        g, "a", OutcomeStatus.SUCCESS, preferred_label="approved"
    )
    assert e_both is not None and e_both.dst == "b"

    # Only outcome matches; preferred_label doesn't → fallback to c.
    e_partial = pick_edge_for_outcome(
        g, "a", OutcomeStatus.SUCCESS, preferred_label="rejected"
    )
    assert e_partial is not None and e_partial.dst == "c"


# [unit->REQ-EXEC-OUTCOME-PREFERRED-LABEL]
def test_pick_edge_for_outcome_preferred_label_misses_falls_through() -> None:
    """Hint set but no matching edge → fall through to status-based routing.

    Hints are advisory; they don't fail the run if the workflow author
    didn't wire the edge.
    """
    g = _g(
        nodes=[_n("a"), _n("b"), _n("c")],
        edges=[_e("a", "b", "SUCCESS"), _e("a", "c", "FAILURE")],
    )
    edge = pick_edge_for_outcome(
        g, "a", OutcomeStatus.SUCCESS, preferred_label="nonexistent"
    )
    assert edge is not None
    assert edge.dst == "b", "no edge matched the hint, fell through to SUCCESS"
