"""Acceptance contract for generated/atlas.render.json (what the viewer renders)."""
from __future__ import annotations

import json
from pathlib import Path

import pytest

yaml = pytest.importorskip("yaml")

_TOPO = Path(__file__).resolve().parents[1]
_OUT = _TOPO / "generated" / "atlas.render.json"
_OVERLAY = _TOPO / "nodes" / "_render_overlay.yaml"
_SHAPES = {"chip", "rect", "diamond", "diamond_person", "hex", "cylinder"}
_SWIMLANES = {"narrative", "prepro", "production", "review_export", "infra"}
_SENTINELS = {"external", "ssot_layer", "workflow_step", "finish_compile"}


@pytest.fixture(scope="module")
def render():
    assert _OUT.exists(), "atlas.render.json missing — run build_atlas_graph.py --write"
    return json.loads(_OUT.read_text())


def test_counts(render):
    assert len(render["nodes"]) == render["meta"]["node_count"]
    assert len(render["edges"]) == render["meta"]["edge_count"]


def test_every_node_has_shape_and_swimlane(render):
    for n in render["nodes"]:
        assert n["shape"] in _SHAPES, f"{n['id']}: bad shape {n['shape']}"
        assert n["swimlane"] in _SWIMLANES, f"{n['id']}: bad swimlane {n['swimlane']}"


def test_exactly_six_human_glyphs(render):
    humans = sorted(n["id"] for n in render["nodes"] if n["is_human_review"])
    assert humans == sorted(render["meta"]["human_review_nodes"])
    assert len(humans) == 6, f"honesty guardrail: expected 6 human gates, got {humans}"


def test_substrate_nodes_flagged(render):
    subs = sorted(n["id"] for n in render["nodes"] if n["is_substrate"])
    assert subs == sorted(render["meta"]["substrate_only_nodes"])
    assert set(subs) == {"modality_eval", "eval_panel", "strategy_score_bridge"}


def test_deprecated_nodes_flagged(render):
    """Dead-path nodes must carry is_deprecated so the viewer can dim them and
    dash their edges — a deprecated stage must never read as a live one."""
    deps = sorted(n["id"] for n in render["nodes"] if n["is_deprecated"])
    assert deps == sorted(render["meta"]["deprecated_nodes"])
    assert set(deps) == {"previz", "previz_review"}, f"deprecated set drift: {deps}"
    # non-deprecated nodes are explicitly False (no missing/None)
    for n in render["nodes"]:
        assert n["is_deprecated"] == (n["id"] in {"previz", "previz_review"})
    # every edge touching a deprecated node is flagged is_deprecated
    for e in render["edges"]:
        touches = e["from"] in set(deps) or e["to"] in set(deps)
        assert e["is_deprecated"] == touches, f"edge {e['from']}->{e['to']} dep flag wrong"


def test_edges_reference_known_nodes(render):
    ids = {n["id"] for n in render["nodes"]} | _SENTINELS
    for e in render["edges"]:
        assert e["from"] in ids and e["to"] in ids, f"dangling edge {e}"


def test_node_order_is_curated_not_alphabetical(render):
    """The render must preserve the overlay's authored flow order within each
    swimlane (left→right), NOT sort by id — alphabetical order scrambles the lanes."""
    overlay = yaml.safe_load(_OVERLAY.read_text())
    lane_order = render["meta"]["swimlanes"]
    # 1) per-swimlane, the render's id sequence == the overlay's id sequence for that lane
    for lane in lane_order:
        want = [n["id"] for n in overlay["nodes"] if n.get("phase_group", "infra") == lane]
        got = [n["id"] for n in render["nodes"] if n["swimlane"] == lane]
        assert got == want, f"swimlane {lane}: render order {got} != overlay order {want}"
    # 2) swimlanes appear contiguously in left→right order (no interleaving)
    seen_lanes = [n["swimlane"] for n in render["nodes"]]
    collapsed = [lane for i, lane in enumerate(seen_lanes) if i == 0 or lane != seen_lanes[i - 1]]
    assert collapsed == [l for l in lane_order if l in collapsed], f"swimlanes interleaved: {collapsed}"
    # 3) regression guard: the global id sequence is NOT alphabetical
    all_ids = [n["id"] for n in render["nodes"]]
    assert all_ids != sorted(all_ids), "nodes are alphabetically sorted — curated flow order lost"
