"""Tests for the drift checker."""

from __future__ import annotations

import json
import shutil
from pathlib import Path

import pytest

from mgf.fed._drift import check
from mgf.fed._roster import members
from mgf.fed._template import load_policy

_REAL_TEMPLATES = Path(__file__).resolve().parents[3] / "mgf-standard" / "templates"

# Federation-INTEGRATION tests: read the REAL mgf-standard/templates, present only
# when the sibling repo is checked out alongside (federation-root layout). A
# single-repo CI clone has no sibling mgf-standard, so skip there — drift is still
# validated by `mgf-fed drift-check` (supervisor) and federation-root/local runs.
pytestmark = pytest.mark.skipif(
    not _REAL_TEMPLATES.exists(),
    reason="needs sibling mgf-standard/templates (federation-root); validated by drift-check",
)

_POLICY = """\
schema = 1
present = ["scripts/verify_codeberg.py", "SCOPE.md"]
py_typed = true
woodpecker_required_steps = ["lint", "publish"]
[exact]
"scripts/verify_codeberg.py" = "verify_codeberg.py"
[pyproject]
mypy_strict = true
ruff_spec = "ruff>=0.4,<0.16"
standards_extra = true
"""


def _root(tmp_path: Path) -> Path:
    reg = {
        "release_waves": [["mgf-clean", "mgf-drift"]],
        "libraries": [
            {
                "repo": "mgf-clean",
                "lang": "python",
                "version": "1.0.0",
                "extras": ["dev", "standards"],
            },
            {"repo": "mgf-drift", "lang": "python", "version": "0.1.0", "extras": ["dev"]},
        ],
    }
    std = tmp_path / "mgf-standard"
    (std / "templates").mkdir(parents=True)
    (std / "MGF_LIBRARIES.json").write_text(json.dumps(reg), encoding="utf-8")
    (std / "templates" / "policy.toml").write_text(_POLICY, encoding="utf-8")
    (std / "templates" / "verify_codeberg.py").write_text("# canonical\n", encoding="utf-8")

    # mgf-clean: satisfies the policy
    clean = tmp_path / "mgf-clean"
    (clean / "scripts").mkdir(parents=True)
    (clean / "src" / "mgf" / "clean").mkdir(parents=True)
    (clean / "src" / "mgf" / "clean" / "py.typed").touch()
    (clean / "scripts" / "verify_codeberg.py").write_text("# canonical\n", encoding="utf-8")
    (clean / "SCOPE.md").write_text("scope\n")
    (clean / ".woodpecker.yml").write_text("steps:\n  - name: lint\n  - name: publish\n")
    (clean / "pyproject.toml").write_text(
        '[project]\nname="mgf-clean"\nversion="1.0.0"\n'
        '[project.optional-dependencies]\ndev=["ruff>=0.4,<0.16"]\nstandards=["mgf-common[standards]"]\n'
        "[tool.mypy]\nstrict=true\n"
    )
    # mgf-drift: violates several rules
    drift = tmp_path / "mgf-drift"
    (drift / "scripts").mkdir(parents=True)
    (drift / "scripts" / "verify_codeberg.py").write_text("# DIFFERENT\n", encoding="utf-8")
    (drift / ".woodpecker.yml").write_text("steps:\n  - name: lint\n")  # missing 'publish'
    (drift / "pyproject.toml").write_text(
        '[project]\nname="mgf-drift"\nversion="0.1.0"\n'
        '[project.optional-dependencies]\ndev=["ruff>=0.4"]\n'  # no upper bound, no standards
        "[tool.mypy]\nstrict=false\n"
    )
    return tmp_path


def test_clean_repo_has_no_drift(tmp_path: Path) -> None:
    root = _root(tmp_path)
    m = next(x for x in members(root) if x.repo == "mgf-clean")
    assert check(m, load_policy(root), root).ok


def test_drift_repo_flags_every_violation(tmp_path: Path) -> None:
    root = _root(tmp_path)
    m = next(x for x in members(root) if x.repo == "mgf-drift")
    items = check(m, load_policy(root), root).items
    kinds = {(i.kind, i.detail) for i in items}
    details = " ".join(i.detail for i in items)
    assert any(k == "missing" and "SCOPE.md" in d for k, d in kinds)
    assert "py.typed" in details
    assert any(k == "exact" for k, _ in kinds)  # verify_codeberg.py differs
    assert any(k == "ci-step" for k, _ in kinds)  # missing publish
    assert "!= required" in details and "[standards]" in details and "strict is not true" in details


# --------------------------------------------------------------------------------
# [exempt].woodpecker — bespoke pipelines skip generation-currency (WF-13),
# falling back to the step-name list instead.
# --------------------------------------------------------------------------------

# Generation-on policy with one bespoke repo excused from generation-currency.
_POLICY_GEN_EXEMPT = """\
schema = 1
present = []
woodpecker_generated = true
woodpecker_required_steps = ["lint", "publish"]
[exact]
[pyproject]
[exempt]
woodpecker = ["mgf-bespoke"]
"""


def _gen_exempt_root(tmp_path: Path, *, pipeline: str) -> Path:
    """Root whose policy generates pipelines but excuses ``mgf-bespoke``."""
    reg = {
        "release_waves": [["mgf-bespoke"]],
        "libraries": [
            {"repo": "mgf-bespoke", "lang": "python", "version": "1.0.0", "extras": ["dev"]}
        ],
    }
    std = tmp_path / "mgf-standard"
    std.mkdir(parents=True)
    (std / "MGF_LIBRARIES.json").write_text(json.dumps(reg), encoding="utf-8")
    shutil.copytree(_REAL_TEMPLATES, std / "templates")
    (std / "templates" / "policy.toml").write_text(_POLICY_GEN_EXEMPT, encoding="utf-8")
    repo = tmp_path / "mgf-bespoke"
    (repo / "src" / "mgf" / "bespoke").mkdir(parents=True)
    (repo / "src" / "mgf" / "bespoke" / "__init__.py").write_text("__version__ = '1.0.0'\n")
    (repo / "pyproject.toml").write_text(
        '[project]\nname = "mgf-bespoke"\nversion = "1.0.0"\n'
        "[project.optional-dependencies]\ndev = []\n"
    )
    (repo / ".woodpecker.yml").write_text(pipeline, encoding="utf-8")
    return tmp_path


def test_exempt_repo_not_flagged_ci_generated(tmp_path: Path) -> None:
    """A bespoke (off-render) pipeline with all required steps is clean — name-list fallback."""
    # Deliberately NOT a fresh render (a generation-currency check would flag it),
    # but it carries every required step name + a bespoke extra step.
    pipeline = (
        "steps:\n"
        "  - name: lint\n"
        "  - name: zig-build-and-test\n"  # bespoke extra
        "  - name: publish\n"
    )
    root = _gen_exempt_root(tmp_path, pipeline=pipeline)
    m = next(x for x in members(root) if x.repo == "mgf-bespoke")
    items = check(m, load_policy(root), root).items
    kinds = {i.kind for i in items}
    assert "ci-generated" not in kinds  # generation-currency skipped for the exempt repo
    assert "ci-step" not in kinds  # name-list fallback satisfied
    assert not items  # fully clean


def test_exempt_repo_uses_name_list_fallback(tmp_path: Path) -> None:
    """The exempt repo is still gated by the step-name list: a missing step flags ci-step."""
    pipeline = "steps:\n  - name: lint\n"  # missing 'publish'
    root = _gen_exempt_root(tmp_path, pipeline=pipeline)
    m = next(x for x in members(root) if x.repo == "mgf-bespoke")
    items = check(m, load_policy(root), root).items
    kinds = {i.kind for i in items}
    assert "ci-generated" not in kinds  # never the generation-currency check
    assert "ci-step" in kinds  # the name-list fallback caught the missing step
