"""PR-* per-rule pin tests (SDK side) — PR-* test plan T1.

One test per applicable PR-NN rule for the mgf-hrb SDK side of
the standard. PR-03 / PR-05 are app-side (N/A in the SDK) and
NOT pinned here; they live in hrb-app's mirror file.

These pins are the test-side complement to the
``MGF_STANDARDS_CONFORMANCE.md`` §2.13 PR section: the tracker
declares which rules apply ✅ and how; this file proves the
declarations are still true in CI.
"""

from __future__ import annotations

import ast
import re
import shutil
import subprocess
import sys
from pathlib import Path

import pytest

pytestmark = pytest.mark.conformance

REPO = Path(__file__).resolve().parents[2]
SDK_ROOT = REPO / "src/mgf/hrb"
TRACKER = REPO / "docs/inprogress/MGF_STANDARDS_CONFORMANCE.md"
# Resolve from PATH first — a global `pip install` (CI / container) puts
# lint-imports on PATH, not under a project `.venv/`; fall back to the venv
# layout for local `uv sync` dev.
LINT_IMPORTS = Path(shutil.which("lint-imports") or (REPO / ".venv/bin/lint-imports"))


# ---------------------------------------------------------------------------
# PR-01 — SDK MUST NOT import any UI framework
# ---------------------------------------------------------------------------


def test_pr_01_sdk_forbids_ui_frameworks_via_import_linter() -> None:
    """The PR-01 contract in pyproject.toml MUST currently be KEPT.
    Identical signal to lint-imports' own gate, surfaced as a
    test so the failure has a named home if the gate is bypassed."""
    proc = subprocess.run(  # noqa: S603 — controlled
        [str(LINT_IMPORTS), "--config", "pyproject.toml"],
        cwd=REPO,
        capture_output=True,
        text=True,
        timeout=30,
    )
    assert proc.returncode == 0, f"PR-01 / PR-06 import-linter contract BROKEN: {proc.stdout}"


def test_pr_01_forbidden_module_list_meets_floor() -> None:
    """The PR-01 contract's forbidden_modules list MUST cover at
    minimum: PySide6 + qasync (Qt + async bridge) + textual + rich
    (TUI) + click (CLI framework). This is the federation floor;
    additional siblings extend as their domain warrants (web /
    templating). See PRESENTATION_LAYER.md §9.5."""
    pyproject = REPO / "pyproject.toml"
    content = pyproject.read_text(encoding="utf-8")
    # Crude but effective — find the PR-01 contract block and grep.
    match = re.search(
        r"PR-01.*?forbidden_modules\s*=\s*\[(.*?)\]",
        content,
        re.DOTALL,
    )
    assert match, "PR-01 contract block not found in pyproject.toml"
    forbidden = match.group(1)
    floor = {"PySide6", "qasync", "textual", "rich", "click"}
    missing = [f for f in floor if f'"{f}"' not in forbidden]
    assert not missing, (
        f"PR-01 contract missing floor frameworks: {missing}. "
        f"See PRESENTATION_LAYER.md §9.5 — federation floor is "
        f"Qt + TUI + CLI-framework."
    )


# ---------------------------------------------------------------------------
# PR-04 — import-linter contracts present + executable
# ---------------------------------------------------------------------------


def test_pr_04_pyproject_declares_required_contracts() -> None:
    """The two SDK-side contracts MUST be present by name in
    pyproject.toml. A future contributor accidentally deleting one
    (e.g. during a refactor) gets a named failure here."""
    content = (REPO / "pyproject.toml").read_text(encoding="utf-8")
    assert "PR-01 — SDK forbids UI framework imports" in content, (
        "PR-01 contract missing from pyproject.toml"
    )
    assert "PR-06 — mgf.hrb.fmt forbids UI frameworks" in content, (
        "PR-06 contract missing from pyproject.toml"
    )


def test_pr_04_lint_imports_binary_exists_in_venv() -> None:
    """A missing binary would silently no-op the gate."""
    assert LINT_IMPORTS.exists(), (
        f"lint-imports binary not found at {LINT_IMPORTS} — gate would silent-skip."
    )


# ---------------------------------------------------------------------------
# PR-06 — fmt module stdlib-only + no I/O
# ---------------------------------------------------------------------------


_STDLIB_NAMES = sys.stdlib_module_names


def test_pr_06_fmt_imports_are_stdlib_only() -> None:
    """AST-walk fmt.py and assert every import resolves to a stdlib
    module per ``sys.stdlib_module_names``. This is the structural
    half of PR-06's stdlib-only invariant; the import-linter
    contract is the third-party-frameworks half."""
    src = (SDK_ROOT / "fmt.py").read_text(encoding="utf-8")
    tree = ast.parse(src)
    non_stdlib: list[str] = []
    for node in ast.walk(tree):
        if isinstance(node, ast.Import):
            for alias in node.names:
                top = alias.name.split(".", 1)[0]
                if top not in _STDLIB_NAMES:
                    non_stdlib.append(alias.name)
        elif isinstance(node, ast.ImportFrom):
            if node.module is None:
                continue
            top = node.module.split(".", 1)[0]
            if top in _STDLIB_NAMES:
                continue
            # The TYPE_CHECKING-only import of Severity is allowed
            # because it never executes at runtime. Detect by walking
            # up the AST.
            parent_is_type_checking = any(
                isinstance(p, ast.If)
                and ast.unparse(p.test) in ("TYPE_CHECKING", "typing.TYPE_CHECKING")
                for p in ast.walk(tree)
                if isinstance(p, ast.If) and node in ast.walk(p)
            )
            if parent_is_type_checking:
                continue
            non_stdlib.append(node.module)
    assert not non_stdlib, (
        f"mgf.hrb.fmt imports non-stdlib at runtime: {non_stdlib}. PR-06 requires stdlib-only."
    )


def test_pr_06_fmt_has_no_io_calls() -> None:
    """AST-walk fmt.py and assert no call sites for forbidden I/O
    primitives. Lifts to a structural test the docstring's "no I/O,
    no logging, no state" invariant."""
    src = (SDK_ROOT / "fmt.py").read_text(encoding="utf-8")
    tree = ast.parse(src)
    forbidden_calls = {
        "open",
        "print",
        "input",
        "exec",
        "eval",
        "compile",
    }
    found: list[tuple[str, int]] = []
    for node in ast.walk(tree):
        if isinstance(node, ast.Call):
            func = node.func
            name = ""
            if isinstance(func, ast.Name):
                name = func.id
            elif isinstance(func, ast.Attribute):
                name = func.attr
            if name in forbidden_calls:
                found.append((name, node.lineno))
    assert not found, (
        f"mgf.hrb.fmt has forbidden I/O calls: {found}. "
        f"PR-06 requires no I/O — pure functions only."
    )


# ---------------------------------------------------------------------------
# PR-07 — conformance tracker declares the PR section
# ---------------------------------------------------------------------------


def test_pr_07_tracker_has_pr_section() -> None:
    """The MGF_STANDARDS_CONFORMANCE.md MUST contain a §PR section
    with rows for every applicable PR-* rule."""
    assert TRACKER.exists(), f"conformance tracker missing: {TRACKER}"
    content = TRACKER.read_text(encoding="utf-8")
    assert "PR — PRESENTATION_LAYER" in content, (
        "tracker missing the PR — PRESENTATION_LAYER section heading"
    )
    # SDK side applies: PR-01, -02, -04, -06, -07. PR-03 / PR-05 are
    # app-side (N/A in the SDK). Assert each applicable rule appears
    # as a row.
    for pr_id in ("PR-01", "PR-02", "PR-04", "PR-06", "PR-07"):
        assert pr_id in content, f"tracker missing row for {pr_id} in §PR section"
