"""Class-P projector tests — architecture graph + federation coupling from MGF_LIBRARIES.json."""

from __future__ import annotations

import json
import sys
from pathlib import Path

import pytest

from mgf.fedres import SyncState, validate_payload
from mgf.fedres.generate import GenerateError, generate
from mgf.fedres.provider import DiskProvider
from mgf.fedres.sync import deep_check

LIBS = {
    "cornerstone": "mgf-common",
    "count": 3,
    "libraries": [
        {"repo": "mgf-common", "lang": "python", "version": "0.49.1", "deps": {}, "used_by": []},
        {
            "repo": "mgf-fedres",
            "lang": "python",
            "version": "0.1.0",
            "deps": {"mgf-common": ">=0.49,<0.50", "jsonschema": ">=4.18"},
            "used_by": [],
        },
        {
            "repo": "mgf-test-web",
            "lang": "python",
            "version": "0.1.0",
            "deps": {"mgf-fedres": ">=0.1,<0.2", "mgf-test-supervisor": ">=0.1.3,<0.2"},
            "used_by": [],
        },
    ],
}


def _project_toml(scope: str | None = None) -> str:
    scope_line = f'scope = "{scope}"\n' if scope else ""
    return (
        '[project]\nid = "mgf-fedres"\nname = "mgf-fedres"\nkind = "library"\n\n'
        '[projectors.architecture]\nlibraries_json = "MGF_LIBRARIES.json"\n'
        f"{scope_line}\n"
        '[projectors.federation]\nlibraries_json = "MGF_LIBRARIES.json"\n'
    )


PROJECT = _project_toml()


def _setup(root: Path, scope: str | None = None) -> DiskProvider:
    root.mkdir(parents=True, exist_ok=True)
    (root / "MGF_LIBRARIES.json").write_text(json.dumps(LIBS), encoding="utf-8")
    src = root / "fed-resources" / "source"
    src.mkdir(parents=True, exist_ok=True)
    (src / "project.toml").write_text(_project_toml(scope), encoding="utf-8")
    return DiskProvider(root)


def test_architecture_graph_projects(tmp_path: Path) -> None:
    """Default scope is THIS project: itself + its transitive sibling deps."""
    provider = _setup(tmp_path)
    generate(provider, generated_at="2026-01-01T00:00:00Z", write=True)
    arch = json.loads(provider.generated_bytes("architecture.json"))
    assert arch["mode"] == "graph"
    # mgf-test-web depends ON mgf-fedres, not the reverse — a dependent is not part of
    # this project's architecture, so it is absent.
    assert {n["id"] for n in arch["graph"]["nodes"]} == {"mgf-common", "mgf-fedres"}
    edges = {(e["from"], e["to"]) for e in arch["graph"]["edges"]}
    assert ("mgf-fedres", "mgf-common") in edges
    # third-party deps (jsonschema, mgf-test-supervisor) are not nodes -> no edges to them
    assert not any(dst in {"jsonschema", "mgf-test-supervisor"} for _, dst in edges)
    assert validate_payload(arch) == []


def test_architecture_federation_scope_projects_the_whole_roster(tmp_path: Path) -> None:
    """`scope = "federation"` — for the registry's OWNER, where the roster IS the subject."""
    provider = _setup(tmp_path, scope="federation")
    generate(provider, generated_at="2026-01-01T00:00:00Z", write=True)
    arch = json.loads(provider.generated_bytes("architecture.json"))
    assert {n["id"] for n in arch["graph"]["nodes"]} == {"mgf-common", "mgf-fedres", "mgf-test-web"}
    edges = {(e["from"], e["to"]) for e in arch["graph"]["edges"]}
    assert ("mgf-test-web", "mgf-fedres") in edges  # a dependent IS in the federation view
    assert validate_payload(arch) == []


def test_architecture_project_scope_ignores_an_unrelated_new_member(tmp_path: Path) -> None:
    """THE COUPLING FIX (2026-07-17). A member joining the roster that this project does
    not depend on must leave this project's architecture BYTE-IDENTICAL.

    Before scoping, every member carried a copy of the whole roster, so publishing the
    registry with one new leaf turned ~35 repos' `fedres check` RED through no change of
    their own — and no registry release could avoid it.
    """
    provider = _setup(tmp_path)
    generate(provider, generated_at="2026-01-01T00:00:00Z", write=True)
    before = provider.generated_bytes("architecture.json")

    libs = json.loads(json.dumps(LIBS))
    libs["count"] = 4
    libs["libraries"].append(
        {
            "repo": "mgf-uitest-web",
            "lang": "python",
            "version": "0.1.0",
            "deps": {"mgf-common": ">=0.49,<0.50"},
            "used_by": [],
        }
    )
    (tmp_path / "MGF_LIBRARIES.json").write_text(json.dumps(libs), encoding="utf-8")
    generate(provider, generated_at="2026-01-01T00:00:00Z", write=True)
    assert provider.generated_bytes("architecture.json") == before

    # ...and the federation scope DOES see it — the roster owner still tracks the roster.
    fed = _setup(tmp_path / "owner", scope="federation")
    (tmp_path / "owner" / "MGF_LIBRARIES.json").write_text(json.dumps(libs), encoding="utf-8")
    generate(fed, generated_at="2026-01-01T00:00:00Z", write=True)
    arch = json.loads(fed.generated_bytes("architecture.json"))
    assert "mgf-uitest-web" in {n["id"] for n in arch["graph"]["nodes"]}


def test_architecture_rejects_an_unknown_scope(tmp_path: Path) -> None:
    provider = _setup(tmp_path, scope="galaxy")
    with pytest.raises(GenerateError, match="unknown scope"):
        generate(provider, generated_at="2026-01-01T00:00:00Z", write=True)


def test_federation_coupling_projects(tmp_path: Path) -> None:
    provider = _setup(tmp_path)
    generate(provider, generated_at="2026-01-01T00:00:00Z", write=True)
    fed = json.loads(provider.generated_bytes("federation.json"))
    assert {c["from"] for c in fed["consumes"]} == {"mgf-common"}  # internal dep only
    assert validate_payload(fed) == []


def test_projected_in_sync_and_deterministic(tmp_path: Path) -> None:
    provider = _setup(tmp_path)
    generate(provider, generated_at="2026-01-01T00:00:00Z", write=True)
    assert deep_check(provider).state is SyncState.IN_SYNC
    a = generate(provider, generated_at="A", write=False).files
    b = generate(provider, generated_at="B", write=False).files
    assert a == b


def test_architecture_reads_registry_from_mgf_standard_package(tmp_path: Path) -> None:
    """With no ``libraries_json`` the projector reads the registry shipped as
    mgf.standard package data — the hermetic path that lets any fleet repo project
    its graph in a single-repo CI checkout (no sibling mgf-standard on disk)."""
    src = tmp_path / "fed-resources" / "source"
    src.mkdir(parents=True)
    # No libraries_json key under the projectors → exercises the package-data read.
    (src / "project.toml").write_text(
        '[project]\nid = "mgf-fedres"\nname = "mgf-fedres"\nkind = "library"\n\n'
        "[projectors.architecture]\n[projectors.federation]\n",
        encoding="utf-8",
    )
    provider = DiskProvider(tmp_path)
    generate(provider, generated_at="2026-01-01T00:00:00Z", write=True)
    arch = json.loads(provider.generated_bytes("architecture.json"))
    ids = {n["id"] for n in arch["graph"]["nodes"]}
    assert "mgf-common" in ids  # the cornerstone is always in the registry
    assert validate_payload(arch) == []
    assert deep_check(provider).state is SyncState.IN_SYNC


# --- PAPER-01: the RegistryAdapter seam — a second family registers, not forks ---


def test_second_family_registers_its_own_adapter(tmp_path: Path) -> None:
    """A registry with a completely different shape projects through a registered
    adapter — no copy of the projector module, no MGF keys."""
    from mgf.fedres.projectors import RegistryEntry, register_registry_adapter

    sooth_registry = {
        "family": "sooth",
        "members": {
            "sooth-core": {"language": "zig", "needs": []},
            "soothwatch": {"language": "zig", "needs": ["sooth-core"]},
        },
    }
    (tmp_path / "SOOTH_REGISTRY.json").write_text(json.dumps(sooth_registry), encoding="utf-8")

    def sooth_adapter(provider: DiskProvider, cfg: dict) -> list[RegistryEntry]:
        raw = json.loads(
            (provider.project_root / str(cfg["registry_json"])).read_text(encoding="utf-8")
        )
        return [
            RegistryEntry(repo=name, lang=info["language"], deps=dict.fromkeys(info["needs"], "*"))
            for name, info in raw["members"].items()
        ]

    register_registry_adapter("sooth-test", sooth_adapter)
    try:
        src = tmp_path / "fed-resources" / "source"
        src.mkdir(parents=True)
        (src / "project.toml").write_text(
            '[project]\nid = "soothwatch"\nname = "soothwatch"\nkind = "library"\n\n'
            '[projectors.architecture]\nregistry_adapter = "sooth-test"\n'
            'registry_json = "SOOTH_REGISTRY.json"\n\n'
            '[projectors.federation]\nregistry_adapter = "sooth-test"\n'
            'registry_json = "SOOTH_REGISTRY.json"\n',
            encoding="utf-8",
        )
        provider = DiskProvider(tmp_path)
        generate(provider, generated_at="2026-01-01T00:00:00Z", write=True)
        arch = json.loads(provider.generated_bytes("architecture.json"))
        assert {n["id"] for n in arch["graph"]["nodes"]} == {"sooth-core", "soothwatch"}
        assert arch["graph"]["layers"] == ["zig"]
        assert {(e["from"], e["to"]) for e in arch["graph"]["edges"]} == {
            ("soothwatch", "sooth-core")
        }
        fed = json.loads(provider.generated_bytes("federation.json"))
        assert {c["from"] for c in fed["consumes"]} == {"sooth-core"}
        assert validate_payload(arch) == [] and validate_payload(fed) == []
    finally:
        from mgf.fedres.projectors import REGISTRY_ADAPTERS

        REGISTRY_ADAPTERS.pop("sooth-test", None)


def test_unknown_registry_adapter_refuses_loudly(tmp_path: Path) -> None:
    import pytest

    from mgf.fedres import GenerateError

    src = tmp_path / "fed-resources" / "source"
    src.mkdir(parents=True)
    (src / "project.toml").write_text(
        '[project]\nid = "x"\nname = "x"\nkind = "library"\n\n'
        '[projectors.architecture]\nregistry_adapter = "nope"\n',
        encoding="utf-8",
    )
    with pytest.raises(GenerateError, match="unknown registry_adapter 'nope'"):
        generate(DiskProvider(tmp_path), generated_at="2026-01-01T00:00:00Z", write=False)


# --- CIR-26: the family-adapter seam — blessed slot overrides + MGF_FEDRES_PROJECTORS ---


def test_register_projector_replaces_a_builtin_slot(tmp_path: Path) -> None:
    """CIR-26 contract: overriding a BUILT-IN slot is SUPPORTED, last-write-wins.

    This is the seam the Sooth family uses (a different registry shape needs a whole
    new projector BODY, not just a registry adapter). A future refactor that made
    ``register_projector`` refuse to replace a built-in would silently break the sooth
    adapter pattern — this test is the tripwire.
    """
    from mgf.fedres.generate import PROJECTORS, register_projector

    saved = dict(PROJECTORS)
    assert "architecture" in PROJECTORS  # a built-in is already registered on import
    sentinel = b'{"kind": "sentinel"}'

    def fake_arch(provider: DiskProvider, project_id: str, cfg: dict) -> bytes:
        return sentinel

    try:
        register_projector("architecture", fake_arch)
        assert PROJECTORS["architecture"] is fake_arch  # replaced in place, not appended
        src = tmp_path / "fed-resources" / "source"
        src.mkdir(parents=True)
        (src / "project.toml").write_text(
            '[project]\nid = "x"\nname = "x"\nkind = "library"\n\n[projectors.architecture]\n',
            encoding="utf-8",
        )
        result = generate(DiskProvider(tmp_path), generated_at="2026-01-01T00:00:00Z", write=False)
        assert result.files["architecture.json"] == sentinel  # the override drove the engine
    finally:
        PROJECTORS.clear()
        PROJECTORS.update(saved)


#: A self-contained Sooth-family adapter, written to a tmp module and loaded through the
#: public ``$MGF_FEDRES_PROJECTORS`` hook. It reproduces the SOOTH SHAPE end-to-end: a
#: ``repos[]`` registry with ``slug``/``ring``/list-``deps`` (nothing like the MGF shape),
#: graph-mode architecture keyed by ``ring-*`` layers, and coupling with a sooth-specific
#: ``why`` and NO ``pinned_at`` — every one of which the mgf built-ins cannot emit, so the
#: output itself proves the override (not the built-in) ran.
_SOOTH_ADAPTER_SRC = '''
"""In-test Sooth-family adapter — the SOOTH SHAPE, via the public register_projector seam."""
from __future__ import annotations

import json
from pathlib import Path
from typing import Any

from mgf.fedres import canonical, validate_payload
from mgf.fedres.generate import GenerateError, register_projector

SOOTH_WHY = "declared dependency (SOOTH_LIBRARIES.json)"


def _repos(provider: Any, cfg: dict[str, Any]) -> list[dict[str, Any]]:
    rel = cfg.get("libraries_json", "SOOTH_LIBRARIES.json")
    path = Path(rel)
    if not path.is_absolute():
        path = provider.project_root / rel
    if not path.is_file():
        raise GenerateError(f"sooth projector: registry not found at {path}")
    data = json.loads(path.read_text(encoding="utf-8"))
    repos = data.get("repos")
    if not isinstance(repos, list):
        raise GenerateError(f"sooth projector: {path} has no repos[] array")
    return repos


def _node_id(repo: dict[str, Any]) -> str:
    return str(repo.get("slug") or repo["name"])


def sooth_architecture(provider: Any, project_id: str, cfg: dict[str, Any]) -> bytes:
    repos = _repos(provider, cfg)
    ids = {_node_id(r) for r in repos}
    nodes = sorted(
        (
            {"id": _node_id(r), "label": str(r.get("name", _node_id(r))), "layer": f"ring-{r.get('ring', 'TBD')}"}
            for r in repos
        ),
        key=lambda n: n["id"],
    )
    edges = sorted(
        (
            {"from": _node_id(r), "to": dep, "kind": "depends_on"}
            for r in repos
            for dep in r.get("deps", [])
            if dep in ids
        ),
        key=lambda e: (e["from"], e["to"]),
    )
    layers = sorted({n["layer"] for n in nodes})
    payload = {
        "schema_version": "0.1",
        "kind": "architecture",
        "project": project_id,
        "mode": "graph",
        "graph": {"layers": layers, "nodes": nodes, "edges": edges},
    }
    errors = validate_payload(payload)
    if errors:
        raise GenerateError(f"sooth architecture invalid: {errors}")
    return canonical.dumpb(payload)


def sooth_federation(provider: Any, project_id: str, cfg: dict[str, Any]) -> bytes:
    repos = _repos(provider, cfg)
    ids = {_node_id(r) for r in repos}
    entry = next((r for r in repos if _node_id(r) == project_id), None)
    consumes: list[dict[str, Any]] = []
    if entry:
        for dep in sorted(entry.get("deps", [])):
            if dep in ids:
                consumes.append({"from": dep, "what": dep, "why": SOOTH_WHY, "on_upstream_change": "notify"})
    payload = {
        "schema_version": "0.1",
        "kind": "federation",
        "project": project_id,
        "consumes": consumes,
        "roadmap_external_deps": [],
    }
    errors = validate_payload(payload)
    if errors:
        raise GenerateError(f"sooth federation invalid: {errors}")
    return canonical.dumpb(payload)


register_projector("architecture", sooth_architecture)
register_projector("federation", sooth_federation)
'''

_SOOTH_REGISTRY = {
    "family": "sooth",
    "repos": [
        {"slug": "sooth-core", "name": "sooth-core", "ring": 0, "deps": []},
        {"slug": "soothwatch", "name": "soothwatch", "ring": 1, "deps": ["sooth-core"]},
    ],
}


def test_sooth_shape_adapter_end_to_end_through_the_cli_env_hook(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    """THE CIR-26 COMPAT TEST. Reproduce the sooth wrapper's behavior through the STOCK
    public path only — no wrapper script, no in-test import-order override — and pin that
    a future mgf-fedres change cannot silently break the sooth adapter pattern.

    Drives ``fedres generate`` then ``fedres check`` (the CLI ``main``) with a sooth-shaped
    registry + a sooth adapter module loaded via ``$MGF_FEDRES_PROJECTORS``. Asserts the run
    is IN_SYNC and that the OUTPUT is unmistakably the sooth override's, not the mgf built-in.
    """
    from mgf.fedres.cli import PROJECTOR_PLUGINS_ENV, main
    from mgf.fedres.generate import PROJECTORS

    # sooth-shaped registry + the project that declares the two projector slots.
    (tmp_path / "SOOTH_LIBRARIES.json").write_text(json.dumps(_SOOTH_REGISTRY), encoding="utf-8")
    src = tmp_path / "fed-resources" / "source"
    src.mkdir(parents=True)
    (src / "project.toml").write_text(
        '[project]\nid = "soothwatch"\nname = "soothwatch"\nkind = "library"\n\n'
        '[projectors.architecture]\nlibraries_json = "SOOTH_LIBRARIES.json"\n\n'
        '[projectors.federation]\nlibraries_json = "SOOTH_LIBRARIES.json"\n',
        encoding="utf-8",
    )

    # The adapter module, importable off tmp_path via the env hook (no wrapper script).
    (tmp_path / "sooth_compat_adapter.py").write_text(_SOOTH_ADAPTER_SRC, encoding="utf-8")

    saved = dict(PROJECTORS)  # built-ins, restored below (the env hook mutates the registry)
    sys.path.insert(0, str(tmp_path))
    # Trailing colon exercises the colon-separated-list parsing (empty segment skipped).
    monkeypatch.setenv(PROJECTOR_PLUGINS_ENV, "sooth_compat_adapter:")
    try:
        assert main(["-C", str(tmp_path), "generate"]) == 0
        # The freshness gate: regenerate -> byte-identical, driven by the same override.
        assert main(["-C", str(tmp_path), "check"]) == 0  # IN_SYNC or it would be nonzero

        arch = json.loads(
            (tmp_path / "fed-resources" / "generated" / "architecture.json").read_bytes()
        )
        fed = json.loads(
            (tmp_path / "fed-resources" / "generated" / "federation.json").read_bytes()
        )
    finally:
        PROJECTORS.clear()
        PROJECTORS.update(saved)
        sys.modules.pop("sooth_compat_adapter", None)
        if str(tmp_path) in sys.path:
            sys.path.remove(str(tmp_path))

    # Provenance: only the sooth override emits ring-* layers + sooth node ids. The mgf
    # built-in would have read mgf.standard package data and emitted language layers /
    # mgf repos — so this asserts the OVERRIDE, not the built-in, produced the file.
    assert arch["mode"] == "graph"
    assert arch["graph"]["layers"] == ["ring-0", "ring-1"]
    assert {n["id"] for n in arch["graph"]["nodes"]} == {"sooth-core", "soothwatch"}
    assert {(e["from"], e["to"]) for e in arch["graph"]["edges"]} == {("soothwatch", "sooth-core")}
    assert validate_payload(arch) == []

    assert {c["from"] for c in fed["consumes"]} == {"sooth-core"}
    # sooth coupling has NO `pinned_at` (the mgf built-in always adds one) — extra proof.
    assert all("pinned_at" not in c for c in fed["consumes"])
    assert {c["why"] for c in fed["consumes"]} == {"declared dependency (SOOTH_LIBRARIES.json)"}
    assert validate_payload(fed) == []


def test_missing_projector_plugin_module_refuses_loudly(
    monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
    """A nonexistent ``$MGF_FEDRES_PROJECTORS`` module is a loud, NAMED failure — never a
    silent fall-back to the built-ins (which would ship the wrong registry's projection)."""
    from mgf.fedres.cli import PROJECTOR_PLUGINS_ENV, main

    monkeypatch.setenv(PROJECTOR_PLUGINS_ENV, "mgf_fedres_no_such_projector_module")
    rc = main(["-C", ".", "status"])
    assert rc == 2
    err = capsys.readouterr().err
    assert PROJECTOR_PLUGINS_ENV in err  # the error names the variable
    assert "mgf_fedres_no_such_projector_module" in err  # ...and the offending module
