"""Unit tests for the SPEC §6.1 step 2 / §10 cascade resolver.

The cascade is pure logic — no I/O, no async. These exercise the
three-layer ordering (graph → stylesheet → per-node) without touching
the engine class itself.
"""

from __future__ import annotations

from attractor.engine.cascade import resolve_effective_config
from attractor.workflow import (
    Node,
    NodeKind,
    Span,
    Stylesheet,
    ValidGraph,
)


def _node(
    node_id: str = "n",
    *,
    class_: str | None = None,
    attrs: dict[str, str] | None = None,
) -> Node:
    return Node(
        id=node_id,
        kind=NodeKind.AGENT,
        label=None,
        class_=class_,
        attributes=attrs or {},
        span=Span(line=0, column=0),
    )


def _graph(attrs: dict[str, str] | None = None) -> ValidGraph:
    return ValidGraph(
        name="t",
        attributes=attrs or {},
        nodes=[],
        edges=[],
    )


# [unit->REQ-EXEC-CONFIG-CASCADE]
def test_graph_defaults_appear_in_effective_config() -> None:
    """Graph-level attrs flow through as the lowest layer."""
    g = _graph({"default_max_visits": "7", "goal": "ship"})
    n = _node()
    eff = resolve_effective_config(g, None, n)
    assert eff["default_max_visits"] == "7"
    assert eff["goal"] == "ship"


# [unit->REQ-EXEC-CONFIG-CASCADE]
def test_model_stylesheet_attr_is_not_propagated() -> None:
    """`model_stylesheet` is the source of the cascade, not a layer of it.

    Including it would dump the raw stylesheet text into every node's
    effective config — useless and noisy.
    """
    g = _graph({"model_stylesheet": "* { model: haiku; }"})
    n = _node()
    eff = resolve_effective_config(g, None, n)
    assert "model_stylesheet" not in eff


# [unit->REQ-EXEC-CONFIG-CASCADE]
# [unit->REQ-ROUTING-STYLESHEET]
def test_stylesheet_layer_overrides_graph_default() -> None:
    """Universal stylesheet rules override graph attributes on the
    overlapping declaration names."""
    g = _graph({"reasoning_effort": "low"})  # mock graph-default
    sheet = Stylesheet.parse("* { reasoning_effort: high; }")
    n = _node()
    eff = resolve_effective_config(g, sheet, n)
    assert eff["reasoning_effort"] == "high"


# [unit->REQ-EXEC-CONFIG-CASCADE]
def test_per_node_attrs_win_over_stylesheet() -> None:
    """Per-node attrs are the top layer — SPEC §6.1 step 2."""
    sheet = Stylesheet.parse("* { model: haiku; }")
    n = _node(attrs={"model": "opus"})
    eff = resolve_effective_config(_graph(), sheet, n)
    assert eff["model"] == "opus"


# [unit->REQ-EXEC-CONFIG-CASCADE]
def test_class_selector_specificity_beats_universal() -> None:
    """`.heavy { model: sonnet }` beats `* { model: haiku }`."""
    sheet = Stylesheet.parse("""
        * { model: haiku; }
        .heavy { model: sonnet; }
    """)
    n = _node(class_="heavy")
    eff = resolve_effective_config(_graph(), sheet, n)
    assert eff["model"] == "sonnet"


# [unit->REQ-EXEC-CONFIG-CASCADE]
def test_per_declaration_last_wins_within_bucket() -> None:
    """SPEC §10 ¶2: same-class rule overrides only the declarations it sets.

    `.heavy { model: X }` after `.heavy { model: Y; reasoning_effort: high }`
    leaves `reasoning_effort: high` intact.
    """
    sheet = Stylesheet.parse("""
        .heavy { model: sonnet; reasoning_effort: high; }
        .heavy { model: opus; }
    """)
    n = _node(class_="heavy")
    eff = resolve_effective_config(_graph(), sheet, n)
    assert eff["model"] == "opus"
    assert eff["reasoning_effort"] == "high"


# [unit->REQ-EXEC-CONFIG-CASCADE]
def test_empty_stylesheet_is_safe() -> None:
    """`None` stylesheet returns graph + per-node only."""
    g = _graph({"goal": "g"})
    n = _node(attrs={"prompt": "p"})
    eff = resolve_effective_config(g, None, n)
    assert eff == {"goal": "g", "prompt": "p"}
