"""Tests for the federation release-wave computer (``scripts/fed_waves.py``).

The wave order is the invariant walk order for any federation-wide update
(``docs/recipes/federation-update-strategy.md``), and ``gen_mgf_libraries.py``
embeds it into the generated registry. These pin the load-bearing behaviour:
roots-first layering, the dev/test-edge carve-out that keeps the graph acyclic,
and the loud failure on a genuine runtime cycle.
"""

from __future__ import annotations

import importlib.util
import sys
from pathlib import Path

import pytest

_REPO_ROOT = Path(__file__).resolve().parents[2]
_FED_WAVES = _REPO_ROOT / "scripts" / "fed_waves.py"


def _load_module():
    spec = importlib.util.spec_from_file_location("fed_waves", _FED_WAVES)
    assert spec and spec.loader
    mod = importlib.util.module_from_spec(spec)
    sys.modules[spec.name] = mod
    spec.loader.exec_module(mod)
    return mod


fw = _load_module()


class TestComputeWaves:
    def test_roots_first(self) -> None:
        waves = fw.compute_waves(
            {"mgf-common": [], "mgf-http": ["mgf-common"], "mgf-fastapi": ["mgf-http"]}
        )
        assert waves == [["mgf-common"], ["mgf-http"], ["mgf-fastapi"]]

    def test_siblings_share_a_wave(self) -> None:
        # Two repos with the same single dep land in the same wave (no
        # inter-dep => parallelizable).
        waves = fw.compute_waves(
            {"mgf-common": [], "mgf-http": ["mgf-common"], "mgf-auth": ["mgf-common"]}
        )
        assert waves == [["mgf-common"], ["mgf-auth", "mgf-http"]]

    def test_wave_members_sorted(self) -> None:
        waves = fw.compute_waves({"mgf-z": [], "mgf-a": [], "mgf-m": []})
        assert waves == [["mgf-a", "mgf-m", "mgf-z"]]

    def test_dev_test_edge_is_not_a_constraint(self) -> None:
        # mgf-test-supervisor runtime-deps on mgf-common AND everyone
        # dev-deps on it: a naive graph cycles. The carve-out drops the
        # *incoming* test-supervisor edges, so the graph stays acyclic and
        # the supervisor itself sits one wave above the cornerstone.
        graph = {
            "mgf-common": ["mgf-test-supervisor"],
            "mgf-test-supervisor": ["mgf-common"],
            "mgf-http": ["mgf-common", "mgf-test-supervisor"],
        }
        waves = fw.compute_waves(graph)
        assert waves[0] == ["mgf-common"]
        assert "mgf-test-supervisor" in waves[1]
        assert "mgf-http" in waves[1]

    def test_standard_edge_is_not_a_constraint(self) -> None:
        # mgf-standard is the [standards] extra of mgf-common, not a
        # runtime constraint — so mgf-common stays a root even though it
        # "depends on" standard.
        waves = fw.compute_waves({"mgf-common": ["mgf-standard"], "mgf-standard": []})
        assert set(waves[0]) == {"mgf-common", "mgf-standard"}

    def test_self_and_unknown_deps_ignored(self) -> None:
        waves = fw.compute_waves({"mgf-common": ["mgf-common", "mgf-not-in-roster"]})
        assert waves == [["mgf-common"]]

    def test_runtime_cycle_raises_with_members(self) -> None:
        with pytest.raises(ValueError, match=r"cycle.*mgf-a.*mgf-b|cycle.*mgf-b.*mgf-a"):
            fw.compute_waves({"mgf-a": ["mgf-b"], "mgf-b": ["mgf-a"]})

    def test_empty_graph(self) -> None:
        assert fw.compute_waves({}) == []


class TestRegistryIntegration:
    def test_matches_live_registry(self) -> None:
        # The real registry must produce a clean, acyclic, roots-first set of
        # waves whose union is exactly the roster.
        waves = fw.waves_from_registry(_REPO_ROOT / "MGF_LIBRARIES.json")
        flat = [r for w in waves for r in w]
        assert len(flat) == len(set(flat)), "a repo appears in two waves"
        assert waves[0], "wave 0 must be non-empty (the roots)"
        # mgf-common + mgf-standard are always roots.
        assert "mgf-common" in waves[0]
        assert "mgf-standard" in waves[0]
