"""Tests for the federation release-consistency gate (REL-13).

The gate lives in ``scripts/gen_mgf_libraries.py`` (a supervisor tool,
not shipped in the wheel). These pin the load-bearing logic: the
pin-window acceptance check that decides whether the federation is
consistent, plus the zon-name normalization.
"""

from __future__ import annotations

import importlib.util
import sys
from pathlib import Path

import pytest

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


def _load_module():
    spec = importlib.util.spec_from_file_location("gen_mgf_libraries", _GEN)
    assert spec and spec.loader
    mod = importlib.util.module_from_spec(spec)
    # Register before exec: the module's @dataclass uses string
    # annotations (from __future__ import annotations), which
    # dataclasses resolves via sys.modules[cls.__module__].
    sys.modules[spec.name] = mod
    spec.loader.exec_module(mod)
    return mod


gen = _load_module()


class TestWindowAccepts:
    @pytest.mark.parametrize(
        ("pin", "version", "expected"),
        [
            # Accepts: current version inside the window.
            (">=0.40,<0.42", "0.41.11", True),
            (">=0.5,<0.6", "0.5.1", True),
            (">=0.41,<0.42", "0.41.0", True),
            # Excludes: version above the upper bound (the drift the gate catches).
            (">=0.1,<0.2", "0.5.1", False),
            (">=0.4,<0.5", "0.5.1", False),
            (">=0.38,<0.39", "0.41.11", False),
            # Excludes: version below the lower bound.
            (">=0.41,<0.42", "0.40.0", False),
        ],
    )
    def test_window(self, pin: str, version: str, expected: bool) -> None:
        assert gen._window_accepts(pin, version) is expected

    def test_non_window_pins_return_none(self) -> None:
        # Exact / git pins aren't windows — the window check abstains.
        assert gen._window_accepts("(any)", "1.0.0") is None
        assert gen._window_accepts("==0.1.0 (git-pinned)", "0.1.0") is None


class TestVerTuple:
    def test_parses_dotted(self) -> None:
        assert gen._ver_tuple("0.41.11") == (0, 41, 11)
        assert gen._ver_tuple("1.0") == (1, 0)

    def test_empty_is_zero(self) -> None:
        assert gen._ver_tuple("") == (0,)


class TestZonName:
    def test_underscore_to_hyphen(self) -> None:
        assert gen._zon_name_to_repo("mgf_hrb_model") == "mgf-hrb-model"


class TestConsistencyGate:
    """A dependent pinned to a window that excludes a current dependency
    is a hard failure (the gate goes RED)."""

    def test_excluding_pin_is_a_problem(self) -> None:
        libs = {
            "mgf-dep": gen.Library(repo="mgf-dep", lang="python", version="0.5.1", purpose="x"),
            "mgf-consumer": gen.Library(
                repo="mgf-consumer",
                lang="python",
                version="0.1.0",
                purpose="y",
                deps={"mgf-dep": ">=0.1,<0.2"},
            ),
        }
        problems = gen.check_consistency(libs)
        assert any("mgf-consumer" in p and "mgf-dep" in p for p in problems)

    def test_accepting_pin_is_clean(self) -> None:
        libs = {
            "mgf-dep": gen.Library(repo="mgf-dep", lang="python", version="0.5.1", purpose="x"),
            "mgf-consumer": gen.Library(
                repo="mgf-consumer",
                lang="python",
                version="0.1.0",
                purpose="y",
                deps={"mgf-dep": ">=0.5,<0.6"},
            ),
        }
        assert gen.check_consistency(libs) == []

    def test_lagging_python_git_ref_is_flagged(self) -> None:
        """A `@ git+...@vX` direct ref that lags the current version is a
        hard failure (the gate's blind-spot fix)."""
        libs = {
            "mgf-dep": gen.Library(repo="mgf-dep", lang="python", version="0.1.6", purpose="x"),
            "mgf-consumer": gen.Library(
                repo="mgf-consumer",
                lang="python",
                version="0.1.0",
                purpose="y",
                deps={"mgf-dep": "@ git+ssh://git@codeberg.org/o/mgf-dep.git@v0.1.1"},
            ),
        }
        problems = gen.check_consistency(libs)
        assert any("LAGS" in p and "mgf-dep" in p for p in problems)

    def test_current_git_ref_is_clean(self) -> None:
        """A git ref pinned at the current version is fine."""
        libs = {
            "mgf-dep": gen.Library(repo="mgf-dep", lang="python", version="0.1.6", purpose="x"),
            "mgf-consumer": gen.Library(
                repo="mgf-consumer",
                lang="python",
                version="0.1.0",
                purpose="y",
                deps={"mgf-dep": "@ git+ssh://git@codeberg.org/o/mgf-dep.git@v0.1.6"},
            ),
        }
        assert gen.check_consistency(libs) == []


class TestPinnedVersion:
    @pytest.mark.parametrize(
        ("pin", "expected"),
        [
            ("==0.1.0 (git-pinned)", "0.1.0"),
            ("@ git+ssh://git@codeberg.org/o/x.git@v0.1.1", "0.1.1"),
            ("@ git+https://codeberg.org/o/x.git@1.2.3", "1.2.3"),
            ("@ git+https://codeberg.org/o/x.git#v2.0.0", "2.0.0"),
            (">=0.5,<0.6", None),  # window — not an exact pin
            ("@ git+https://codeberg.org/o/x.git#deadbeef", None),  # bare sha
        ],
    )
    def test_pinned_version(self, pin: str, expected) -> None:
        assert gen._pinned_version(pin) == expected


class TestDiscoverMembership:
    """A federation member carries a recognised manifest. ``mgf-*``
    dirs without one (e.g. ``mgf-*-review`` audit deliverables) MUST
    NOT land in the registry."""

    @staticmethod
    def _repo(parent: Path, name: str, files: dict[str, str]) -> None:
        d = parent / name
        (d / ".git").mkdir(parents=True)
        for fname, body in files.items():
            (d / fname).write_text(body, encoding="utf-8")

    def test_python_zig_and_template_members_are_discovered(self, tmp_path: Path) -> None:
        self._repo(
            tmp_path,
            "mgf-pylib",
            {"pyproject.toml": '[project]\nname="mgf-pylib"\nversion="1.0.0"\n'},
        )
        self._repo(tmp_path, "mgf-ziglib", {"build.zig.zon": '.{ .version = "0.2.0" }'})
        self._repo(
            tmp_path,
            "mgf-template",
            {
                "cookiecutter.json": "{}",
                "README.md": "# mgf-template\nA template.",
                "CHANGELOG.md": "# Changelog\n\n## [Unreleased]\n\n## [0.3.0] — 2026-01-01\n",
            },
        )
        libs = gen.discover(tmp_path)
        assert set(libs) == {"mgf-pylib", "mgf-ziglib", "mgf-template"}
        assert libs["mgf-pylib"].lang == "python"
        assert libs["mgf-ziglib"].lang == "zig"
        # A cookiecutter member is parsed for real: lang "template", version
        # from the latest released CHANGELOG heading, purpose from the README
        # (skipping the name-only H1).
        assert libs["mgf-template"].lang == "template"
        assert libs["mgf-template"].version == "0.3.0"
        assert libs["mgf-template"].purpose == "A template."

    def test_manifestless_repos_are_excluded(self, tmp_path: Path) -> None:
        # A review deliverable: git repo + docs, but no build manifest.
        self._repo(
            tmp_path,
            "mgf-pylib",
            {"pyproject.toml": '[project]\nname="mgf-pylib"\nversion="1.0.0"\n'},
        )
        self._repo(
            tmp_path, "mgf-pylib-review", {"README.md": "# mgf-pylib review\n", "FINDINGS.md": "x"}
        )
        libs = gen.discover(tmp_path)
        assert "mgf-pylib-review" not in libs
        assert set(libs) == {"mgf-pylib"}

    def test_non_git_dirs_are_excluded(self, tmp_path: Path) -> None:
        # Manifest but no .git — not a tracked member.
        (tmp_path / "mgf-loose").mkdir()
        (tmp_path / "mgf-loose" / "pyproject.toml").write_text('[project]\nname="x"\nversion="1"\n')
        assert gen.discover(tmp_path) == {}


class TestSelfEdgeDrop:
    """discover() must never emit a library that lists itself in deps/used-by.

    The shipped registry carried an ``mgf-fed -> mgf-fed`` edge because its
    pyproject uses the legitimate PEP 508 self-referential-extra idiom
    (``standards = ["mgf-fed[cli]"]`` — a back-compat alias). That is
    packaging, not a dependency edge (F-M6).
    """

    def test_self_referential_extra_is_not_an_edge(self, tmp_path: Path) -> None:
        TestDiscoverMembership._repo(
            tmp_path,
            "mgf-selfy",
            {
                "pyproject.toml": (
                    '[project]\nname="mgf-selfy"\nversion="1.0.0"\n'
                    "[project.optional-dependencies]\n"
                    'cli=["mgf-other>=1,<2"]\nstandards=["mgf-selfy[cli]"]\n'
                )
            },
        )
        TestDiscoverMembership._repo(
            tmp_path,
            "mgf-other",
            {"pyproject.toml": '[project]\nname="mgf-other"\nversion="1.2.0"\n'},
        )
        libs = gen.discover(tmp_path)
        assert "mgf-selfy" not in libs["mgf-selfy"].deps  # the self-edge is dropped
        assert "mgf-selfy" not in libs["mgf-selfy"].used_by
        assert libs["mgf-selfy"].deps == {"mgf-other": ">=1,<2"}  # real edge survives
        assert gen.check_integrity(libs) == []  # and the gate agrees


class TestIntegrityGate:
    """B4 — the deliberate negative tests: ``--check`` (via check_integrity)
    must FAIL on any self-edge or ``version: '?'`` row, so the F-M6 class
    can't return through any future code path."""

    def test_clean_roster_passes(self) -> None:
        assert gen.check_integrity(_libs()) == []

    def test_self_edge_in_deps_fails(self) -> None:
        libs = {
            "mgf-fed": gen.Library(
                repo="mgf-fed",
                lang="python",
                version="0.11.1",
                purpose="x",
                deps={"mgf-fed": "[cli]"},
            ),
        }
        problems = gen.check_integrity(libs)
        assert any("mgf-fed" in p and "self-edge" in p for p in problems)

    def test_self_edge_in_used_by_fails(self) -> None:
        lib = gen.Library(repo="mgf-fed", lang="python", version="0.11.1", purpose="x")
        lib.used_by.append("mgf-fed")
        problems = gen.check_integrity({"mgf-fed": lib})
        assert any("used-by" in p and "self-edge" in p for p in problems)

    def test_unknown_version_fails(self) -> None:
        libs = {
            "mgf-mystery": gen.Library(repo="mgf-mystery", lang="other", version="?", purpose="x"),
        }
        problems = gen.check_integrity(libs)
        assert any("mgf-mystery" in p and "'?'" in p for p in problems)

    def test_template_member_without_changelog_release_is_caught(self, tmp_path: Path) -> None:
        # End-to-end: a cookiecutter member with no released CHANGELOG heading
        # discovers as version '?' and the gate goes RED — the operator signal
        # to give the repo a version source, not a silent '?' row.
        TestDiscoverMembership._repo(
            tmp_path,
            "mgf-tmpl",
            {"cookiecutter.json": "{}", "CHANGELOG.md": "# Changelog\n\n## [Unreleased]\n"},
        )
        libs = gen.discover(tmp_path)
        assert libs["mgf-tmpl"].version == "?"
        problems = gen.check_integrity(libs)
        assert any("mgf-tmpl" in p and "'?'" in p for p in problems)


def _libs(x_pin: str = ">=0.41,<0.43") -> dict:
    """A minimal two-library federation: cornerstone + one consumer."""
    return {
        "mgf-common": gen.Library(
            repo="mgf-common",
            lang="python",
            version="0.42.0",
            purpose="cornerstone",
            requires_python=">=3.11",
        ),
        "mgf-x": gen.Library(
            repo="mgf-x",
            lang="python",
            version="0.1.0",
            purpose="sibling x",
            deps={"mgf-common": x_pin},
            requires_python=">=3.11",
            extras=("dev",),
            third_party_count=2,
        ),
    }


class TestRenderSections:
    def test_has_all_four_sections(self) -> None:
        md = gen.render(_libs())
        for h in (
            "## At a glance",
            "## Release & compatibility",
            "## Per-library detail",
            "## Consistency",
        ):
            assert h in md, f"missing section {h!r}"

    def test_compatibility_matrix_marks_acceptance(self) -> None:
        md = gen.render(_libs())
        assert "✅" in md  # mgf-x accepts mgf-common 0.42.0 (pin <0.43)
        assert "_(cornerstone)_" in md  # mgf-common's own row

    def test_compatibility_marks_exclusion_when_pin_too_low(self) -> None:
        md = gen.render(_libs(x_pin=">=0.41,<0.42"))  # excludes 0.42.0
        assert "❌" in md

    def test_per_library_detail_table(self) -> None:
        md = gen.render(_libs())
        assert "### `mgf-x`" in md
        assert "pip install mgf-x" in md
        assert "Python >=3.11" in md
        assert "`dev`" in md  # extras
        assert "Third-party runtime deps | 2" in md


class TestCurrencyGate:
    def test_current_returns_none(self, tmp_path) -> None:
        libs = _libs()
        p = tmp_path / "MGF_LIBRARIES.md"
        p.write_text(gen.render(libs) + "\n", encoding="utf-8")
        assert gen.check_currency(libs, p) is None

    def test_stale_version_detected(self, tmp_path) -> None:
        libs = _libs()
        p = tmp_path / "MGF_LIBRARIES.md"
        p.write_text(gen.render(libs) + "\n", encoding="utf-8")
        libs["mgf-x"].version = "0.2.0"  # released without regenerating
        msg = gen.check_currency(libs, p)
        assert msg is not None
        assert "STALE" in msg

    def test_date_line_is_ignored(self, tmp_path) -> None:
        import re

        libs = _libs()
        p = tmp_path / "MGF_LIBRARIES.md"
        fresh = gen.render(libs) + "\n"
        altered = re.sub(
            r"_Generated [^\n]*",
            "_Generated 2099-12-31 from the on-disk federation._",
            fresh,
        )
        p.write_text(altered, encoding="utf-8")
        # Only the date differs → still considered current.
        assert gen.check_currency(libs, p) is None

    def test_missing_file_is_stale(self, tmp_path) -> None:
        assert gen.check_currency(_libs(), tmp_path / "nope.md") is not None


class TestRenderJson:
    """The machine-readable registry shipped as wheel data + read by
    ``mgf.standard.libraries()`` (the agent-facing federation map)."""

    def test_payload_shape(self) -> None:
        import json

        data = json.loads(gen.render_json(_libs()))
        assert data["schema"] == 1
        assert data["count"] == 2
        assert data["cornerstone_version"] == "0.42.0"
        assert {lib["repo"] for lib in data["libraries"]} == {"mgf-common", "mgf-x"}

    def test_entry_carries_deps_extras_footprint(self) -> None:
        import json

        data = json.loads(gen.render_json(_libs()))
        mx = next(lib for lib in data["libraries"] if lib["repo"] == "mgf-x")
        assert mx["deps"]["mgf-common"]  # pin string present
        assert mx["extras"] == ["dev"]
        assert mx["third_party_count"] == 2

    def test_deterministic(self) -> None:
        # Two renders of the same libs are byte-identical (sorted keys),
        # so the currency check reacts only to real drift, not ordering.
        assert gen.render_json(_libs()) == gen.render_json(_libs())


class TestJsonCurrencyGate:
    def test_current_returns_none(self, tmp_path) -> None:
        libs = _libs()
        p = tmp_path / "MGF_LIBRARIES.json"
        p.write_text(gen.render_json(libs), encoding="utf-8")
        assert gen.check_json_currency(libs, p) is None

    def test_stale_version_detected(self, tmp_path) -> None:
        libs = _libs()
        p = tmp_path / "MGF_LIBRARIES.json"
        p.write_text(gen.render_json(libs), encoding="utf-8")
        libs["mgf-x"].version = "0.2.0"  # released without regenerating
        assert gen.check_json_currency(libs, p) is not None

    def test_date_ignored(self, tmp_path) -> None:
        import json

        libs = _libs()
        p = tmp_path / "MGF_LIBRARIES.json"
        d = json.loads(gen.render_json(libs))
        d["generated"] = "2099-12-31"
        p.write_text(json.dumps(d, indent=2, sort_keys=True) + "\n", encoding="utf-8")
        assert gen.check_json_currency(libs, p) is None

    def test_missing_file_is_stale(self, tmp_path) -> None:
        assert gen.check_json_currency(_libs(), tmp_path / "nope.json") is not None
