"""Tests for ``mgf-common doctor --standards``.

Builds a tiny project tree per test (pyproject.toml + src/<pkg>/)
under ``tmp_path``, then runs each check against it. Avoids
shelling out via the CLI — exercises the audit functions directly
so failures point at the right line.

Covers:
  - Project shape discovery (flat + namespace layouts).
  - AP-09 py.typed marker presence / absence.
  - AP-13 __version__ ↔ pyproject sync (match / mismatch /
    missing).
  - TS-04 filterwarnings escalation (configured / missing /
    not-error-first).
  - DP-12 naive datetime detection.
  - End-to-end ``doctor_standards()`` exit code.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

from mgf.fed.cli._doctor_standards import (
    _check_commit_hook,
    _check_conformance_ledger_complete,
    _check_filterwarnings,
    _check_gitignore_my_stuff,
    _check_lesson_learned_present,
    _check_memory_dir,
    _check_memory_store_vcs,
    _check_naive_datetime,
    _check_propagation_headers,
    _check_py_typed_marker,
    _check_version_sync,
    _corpus_must_ids,
    _discover_project,
    doctor_standards,
)

if TYPE_CHECKING:
    from collections.abc import Mapping
    from pathlib import Path

    import pytest


def _write_project(
    root: Path,
    *,
    package_name: str = "myapp",
    declared_version: str = "1.0.0",
    runtime_version: str | None = "1.0.0",
    py_typed: bool = True,
    filterwarnings: list[str] | None = ("error",),
    extra_pyproject: Mapping[str, object] | None = None,
    package_files: Mapping[str, str] | None = None,
    complete_ledger: bool = True,
    lesson_learned: bool = True,
    memory_md: bool = True,
    gitignore_my_stuff: bool = True,
    prepare_commit_hook: bool = True,
) -> Path:
    """Build a minimal project tree at ``root``; return ``root``.

    By default also writes a COMPLETE ``MGF_STANDARDS_CONFORMANCE.md`` (every
    corpus MUST as an N/A row) so the DOC-15 ledger-completeness check passes —
    pass ``complete_ledger=False`` to omit it.

    Defaults to a fully conformant project (AG-17 docs/memory/MEMORY.md +
    AG-18 my_stuff/-ignoring .gitignore both seeded) so callers that build a
    clean tree stay green; pass ``memory_md=False`` / ``gitignore_my_stuff=False``
    to omit each.
    """
    underscore = package_name.replace("-", "_")
    (root / "src" / underscore).mkdir(parents=True, exist_ok=True)
    if py_typed:
        (root / "src" / underscore / "py.typed").touch()
    init = root / "src" / underscore / "__init__.py"
    if runtime_version is not None:
        init.write_text(f'__version__ = "{runtime_version}"\n')
    else:
        init.write_text("")
    for rel, content in (package_files or {}).items():
        (root / "src" / underscore / rel).write_text(content)

    pyproject_text = f'[project]\nname = "{package_name}"\n'
    if declared_version:
        pyproject_text += f'version = "{declared_version}"\n'
    if filterwarnings is not None:
        # Render as a TOML array literal.
        items = ", ".join(f'"{f}"' for f in filterwarnings)
        pyproject_text += f"\n[tool.pytest.ini_options]\nfilterwarnings = [{items}]\n"
    (root / "pyproject.toml").write_text(pyproject_text)
    if complete_ledger:
        _write_complete_ledger(root)
    if lesson_learned:
        # DOC-19 (MUST since standards v2.21) — a clean project ships LESSON_LEARNED.md.
        (root / "LESSON_LEARNED.md").write_text("# LESSON_LEARNED\n", encoding="utf-8")
    if memory_md:
        # AG-17 (MUST since standards v2.22) — committed in-repo agent memory.
        mem = root / "docs" / "memory"
        mem.mkdir(parents=True, exist_ok=True)
        (mem / "MEMORY.md").write_text("# Memory index\n", encoding="utf-8")
    if gitignore_my_stuff:
        # AG-18 (MUST since standards v2.22) — .gitignore ignores the local secrets dir.
        (root / ".gitignore").write_text("my_stuff/\n", encoding="utf-8")
    if prepare_commit_hook:
        # AG-18 (commit-trailer enforcement, MUST since standards v2.24) — the
        # committed prepare-commit-msg hook. Presence is the bar (doctor checks
        # `.githooks/prepare-commit-msg` exists).
        hooks = root / ".githooks"
        hooks.mkdir(parents=True, exist_ok=True)
        (hooks / "prepare-commit-msg").write_text("#!/bin/sh\n", encoding="utf-8")
    return root


def _write_complete_ledger(root: Path) -> None:
    """Write a conformance ledger that accounts for every corpus MUST (N/A
    rows) so the DOC-15 completeness check is satisfied. Ids are derived from
    the same source the check reads, so the fixture tracks the corpus."""
    ids = _corpus_must_ids() or set()
    rows = "\n".join(f"| {rid} | N/A | fixture |" for rid in sorted(ids))
    led = root / "docs" / "inprogress"
    led.mkdir(parents=True, exist_ok=True)
    (led / "MGF_STANDARDS_CONFORMANCE.md").write_text(
        "# MGF standards conformance\n\n| ID | Status | Notes |\n|---|---|---|\n" + rows + "\n",
        encoding="utf-8",
    )


# ---------------------------------------------------------------------------
# Project shape discovery
# ---------------------------------------------------------------------------


class TestProjectDiscovery:
    def test_flat_layout(self, tmp_path: Path) -> None:
        _write_project(tmp_path, package_name="myapp")
        shape = _discover_project(tmp_path)
        assert shape is not None
        assert shape.package_name == "myapp"
        assert shape.package_root.name == "myapp"

    def test_namespace_layout(self, tmp_path: Path) -> None:
        # Distribution name "mgf-common" → import path "mgf/common".
        (tmp_path / "src" / "mgf" / "common").mkdir(parents=True)
        (tmp_path / "src" / "mgf" / "common" / "__init__.py").write_text(
            '__version__ = "0.1.0"\n',
        )
        (tmp_path / "src" / "mgf" / "common" / "py.typed").touch()
        (tmp_path / "pyproject.toml").write_text(
            '[project]\nname = "mgf-common"\nversion = "0.1.0"\n',
        )
        shape = _discover_project(tmp_path)
        assert shape is not None
        assert shape.package_name == "mgf-common"
        assert shape.package_root == tmp_path / "src" / "mgf" / "common"

    def test_no_pyproject_returns_none(self, tmp_path: Path) -> None:
        # Empty dir.
        assert _discover_project(tmp_path) is None

    def test_flat_layout_returns_shape_no_package(self, tmp_path: Path) -> None:
        # Flat / multi-package layout (e.g. a Django project: config/ + apps/ at
        # the repo root) — still a shape (root-level checks run), but no single
        # import package, so package_root is None.
        (tmp_path / "pyproject.toml").write_text('[project]\nname = "myapp"\n')
        (tmp_path / "config").mkdir()
        (tmp_path / "config" / "__init__.py").touch()
        (tmp_path / "apps").mkdir()
        (tmp_path / "apps" / "__init__.py").touch()
        shape = _discover_project(tmp_path)
        assert shape is not None
        assert shape.package_root is None
        assert shape.src_dir is None

    def test_descend_single_child_namespace(self, tmp_path: Path) -> None:
        # dist `vm-vmanager-agent` → package src/vmanager/agent (vmanager is a
        # bare namespace whose only child is the real package).
        pkg = tmp_path / "src" / "vmanager" / "agent"
        pkg.mkdir(parents=True)
        (pkg / "__init__.py").write_text('__version__ = "0.1.0"\n')
        (tmp_path / "pyproject.toml").write_text(
            '[project]\nname = "vm-vmanager-agent"\nversion = "0.1.0"\n'
        )
        shape = _discover_project(tmp_path)
        assert shape is not None
        assert shape.package_root == pkg

    def test_no_pyproject_still_none(self, tmp_path: Path) -> None:
        (tmp_path / "config").mkdir()
        assert _discover_project(tmp_path) is None


# ---------------------------------------------------------------------------
# Individual checks
# ---------------------------------------------------------------------------


class TestPyTypedCheck:
    def test_present(self, tmp_path: Path) -> None:
        _write_project(tmp_path, py_typed=True)
        shape = _discover_project(tmp_path)
        assert shape is not None
        assert _check_py_typed_marker(shape).tier == "ok"

    def test_missing(self, tmp_path: Path) -> None:
        _write_project(tmp_path, py_typed=False)
        shape = _discover_project(tmp_path)
        assert shape is not None
        finding = _check_py_typed_marker(shape)
        assert finding.tier == "fail"
        assert "py.typed" in finding.message


class TestVersionSyncCheck:
    def test_match(self, tmp_path: Path) -> None:
        _write_project(
            tmp_path,
            declared_version="1.2.3",
            runtime_version="1.2.3",
        )
        shape = _discover_project(tmp_path)
        assert shape is not None
        assert _check_version_sync(shape).tier == "ok"

    def test_mismatch(self, tmp_path: Path) -> None:
        _write_project(
            tmp_path,
            declared_version="1.2.3",
            runtime_version="9.9.9",
        )
        shape = _discover_project(tmp_path)
        assert shape is not None
        finding = _check_version_sync(shape)
        assert finding.tier == "fail"
        assert "1.2.3" in finding.message
        assert "9.9.9" in finding.message

    def test_missing_runtime_version(self, tmp_path: Path) -> None:
        _write_project(
            tmp_path,
            declared_version="1.2.3",
            runtime_version=None,
        )
        shape = _discover_project(tmp_path)
        assert shape is not None
        finding = _check_version_sync(shape)
        assert finding.tier == "warn"

    def test_missing_declared_version(self, tmp_path: Path) -> None:
        _write_project(
            tmp_path,
            declared_version="",
            runtime_version="1.0.0",
        )
        shape = _discover_project(tmp_path)
        assert shape is not None
        finding = _check_version_sync(shape)
        assert finding.tier == "warn"


class TestFilterwarningsCheck:
    def test_configured_with_error(self, tmp_path: Path) -> None:
        _write_project(tmp_path, filterwarnings=["error"])
        shape = _discover_project(tmp_path)
        assert shape is not None
        assert _check_filterwarnings(shape).tier == "ok"

    def test_missing(self, tmp_path: Path) -> None:
        _write_project(tmp_path, filterwarnings=None)
        shape = _discover_project(tmp_path)
        assert shape is not None
        finding = _check_filterwarnings(shape)
        assert finding.tier == "fail"

    def test_no_error_entry(self, tmp_path: Path) -> None:
        _write_project(
            tmp_path,
            filterwarnings=["ignore::DeprecationWarning"],
        )
        shape = _discover_project(tmp_path)
        assert shape is not None
        finding = _check_filterwarnings(shape)
        assert finding.tier == "fail"
        assert "error" in finding.message


class TestNaiveDatetimeCheck:
    def test_no_violations(self, tmp_path: Path) -> None:
        _write_project(
            tmp_path,
            package_files={
                "module.py": ("from datetime import UTC, datetime\nx = datetime.now(UTC)\n"),
            },
        )
        shape = _discover_project(tmp_path)
        assert shape is not None
        assert _check_naive_datetime(shape).tier == "ok"

    def test_utcnow_flagged(self, tmp_path: Path) -> None:
        _write_project(
            tmp_path,
            package_files={
                "module.py": ("from datetime import datetime\nx = datetime.utcnow()\n"),
            },
        )
        shape = _discover_project(tmp_path)
        assert shape is not None
        finding = _check_naive_datetime(shape)
        assert finding.tier == "fail"
        assert "utcnow" in finding.message

    def test_naive_now_flagged(self, tmp_path: Path) -> None:
        _write_project(
            tmp_path,
            package_files={
                "module.py": ("from datetime import datetime\nx = datetime.now()\n"),
            },
        )
        shape = _discover_project(tmp_path)
        assert shape is not None
        finding = _check_naive_datetime(shape)
        assert finding.tier == "fail"

    def test_now_with_tz_kwarg_passes(self, tmp_path: Path) -> None:
        _write_project(
            tmp_path,
            package_files={
                "module.py": ("from datetime import UTC, datetime\nx = datetime.now(tz=UTC)\n"),
            },
        )
        shape = _discover_project(tmp_path)
        assert shape is not None
        # Currently, we accept positional too; the AST check looks
        # for either kwarg `tz=...` OR positional args.
        assert _check_naive_datetime(shape).tier == "ok"


# ---------------------------------------------------------------------------
# End-to-end exit code
# ---------------------------------------------------------------------------


class TestDoctorStandardsExitCode:
    def test_clean_project_exits_zero(
        self,
        tmp_path: Path,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        _write_project(tmp_path)
        rc = doctor_standards(root=tmp_path)
        assert rc == 0

    def test_failing_project_exits_nonzero(
        self,
        tmp_path: Path,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        _write_project(tmp_path, py_typed=False)  # AP-09 will FAIL
        rc = doctor_standards(root=tmp_path)
        assert rc != 0

    def test_no_project_exits_nonzero(
        self,
        tmp_path: Path,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        rc = doctor_standards(root=tmp_path)
        assert rc != 0


class TestPropagationHeaders:
    """DOC-18 — owned rule-defining standards docs MUST carry a
    Propagation header."""

    @staticmethod
    def _std(root: Path, name: str, body: str) -> None:
        d = root / "docs" / "standards"
        d.mkdir(parents=True, exist_ok=True)
        (d / name).write_text(body, encoding="utf-8")

    def test_na_when_no_standards_dir(self, tmp_path: Path) -> None:
        _write_project(tmp_path)
        shape = _discover_project(tmp_path)
        assert shape is not None
        f = _check_propagation_headers(shape)
        assert f.tier == "ok"
        assert "N/A" in f.message

    def test_ok_when_rule_doc_has_header(self, tmp_path: Path) -> None:
        _write_project(tmp_path)
        self._std(
            tmp_path,
            "X.md",
            "# X\n\n## Rules box\n\n> **Propagation (DOC-18):** foo → bar\n",
        )
        shape = _discover_project(tmp_path)
        assert shape is not None
        assert _check_propagation_headers(shape).tier == "ok"

    def test_fail_when_rule_doc_missing_header(self, tmp_path: Path) -> None:
        _write_project(tmp_path)
        self._std(tmp_path, "X.md", "# X\n\n## Rules box\n\n| ID | Rule |\n")
        shape = _discover_project(tmp_path)
        assert shape is not None
        f = _check_propagation_headers(shape)
        assert f.tier == "fail"
        assert "X.md" in f.message

    def test_pointer_only_dir_is_na(self, tmp_path: Path) -> None:
        # A doc with no Rules box (e.g. a pointer README to the cornerstone)
        # does not define rules → not bound by DOC-18.
        _write_project(tmp_path)
        self._std(tmp_path, "README.md", "# Standards\n\nSee mgf-common.\n")
        self._std(tmp_path, "POINTER.md", "# Pointer\n\nLinks only.\n")
        shape = _discover_project(tmp_path)
        assert shape is not None
        f = _check_propagation_headers(shape)
        assert f.tier == "ok"
        assert "N/A" in f.message


class TestConformanceLedgerComplete:
    """WU6 — the partial-sync gate: MGF_STANDARDS_CONFORMANCE.md must account
    for every current MUST in the corpus, else doctor --standards FAILS."""

    def test_corpus_must_ids_nonempty(self) -> None:
        ids = _corpus_must_ids()
        assert ids, "Index of MUSTs parsed empty"
        assert {"DP-01", "DOC-18", "EH-07"} <= ids

    def test_complete_ledger_passes(self, tmp_path: Path) -> None:
        _write_project(tmp_path)  # complete ledger written by default
        shape = _discover_project(tmp_path)
        assert shape is not None
        f = _check_conformance_ledger_complete(shape)
        assert f.tier == "ok", f.message

    def test_missing_ledger_fails(self, tmp_path: Path) -> None:
        _write_project(tmp_path, complete_ledger=False)
        shape = _discover_project(tmp_path)
        assert shape is not None
        f = _check_conformance_ledger_complete(shape)
        assert f.tier == "fail"
        assert "not found" in f.message

    def test_partial_ledger_fails_listing_count(self, tmp_path: Path) -> None:
        _write_project(tmp_path, complete_ledger=False)
        led = tmp_path / "docs" / "inprogress"
        led.mkdir(parents=True, exist_ok=True)
        (led / "MGF_STANDARDS_CONFORMANCE.md").write_text(
            "| DP-01 | N/A | x |\n| DP-02 | N/A | x |\n", encoding="utf-8"
        )
        shape = _discover_project(tmp_path)
        assert shape is not None
        f = _check_conformance_ledger_complete(shape)
        assert f.tier == "fail"
        assert "unaccounted" in f.message


class TestLessonLearnedPresent:
    """DOC-19 / PAPER-99 — the LESSON_LEARNED.md presence gate. Corpus-aware:
    inert unless the resolved Index of MUSTs carries DOC-19 (promoted to a MUST
    in standards v2.21), so a project pinning an older corpus is unaffected."""

    _MOD = "mgf.fed.cli._doctor_standards._corpus_must_ids"

    def test_inert_when_doc19_not_in_corpus(self, tmp_path, monkeypatch) -> None:
        # Older pinned corpus (DOC-19 absent / still a SHOULD) → not required.
        monkeypatch.setattr(self._MOD, lambda: {"DP-01", "DOC-18"})
        _write_project(tmp_path, lesson_learned=False)
        shape = _discover_project(tmp_path)
        assert shape is not None
        f = _check_lesson_learned_present(shape)  # no file created
        assert f.tier == "ok"
        assert "not required" in f.message

    def test_fails_when_required_and_absent(self, tmp_path, monkeypatch) -> None:
        monkeypatch.setattr(self._MOD, lambda: {"DP-01", "DOC-19"})
        _write_project(tmp_path, lesson_learned=False)
        shape = _discover_project(tmp_path)
        assert shape is not None
        f = _check_lesson_learned_present(shape)
        assert f.tier == "fail"
        assert "missing" in f.message

    def test_passes_when_required_and_present(self, tmp_path, monkeypatch) -> None:
        monkeypatch.setattr(self._MOD, lambda: {"DP-01", "DOC-19"})
        _write_project(tmp_path)
        (tmp_path / "LESSON_LEARNED.md").write_text("# LESSON_LEARNED\n", encoding="utf-8")
        shape = _discover_project(tmp_path)
        assert shape is not None
        f = _check_lesson_learned_present(shape)
        assert f.tier == "ok"
        assert "present" in f.message

    def test_warn_when_corpus_unresolvable(self, tmp_path, monkeypatch) -> None:
        monkeypatch.setattr(self._MOD, lambda: None)
        _write_project(tmp_path)
        shape = _discover_project(tmp_path)
        assert shape is not None
        f = _check_lesson_learned_present(shape)
        assert f.tier == "warn"


class TestMemoryDir:
    """AG-17 — the docs/memory/MEMORY.md presence gate. Corpus-aware: inert
    unless the resolved Index of MUSTs carries AG-17 (a MUST since standards
    v2.22), so a project pinning an older corpus is unaffected."""

    _MOD = "mgf.fed.cli._doctor_standards._corpus_must_ids"

    def test_inert_when_ag17_not_in_corpus(self, tmp_path, monkeypatch) -> None:
        # Older pinned corpus (AG-17 absent) → not required.
        monkeypatch.setattr(self._MOD, lambda: {"DP-01", "DOC-18"})
        _write_project(tmp_path, memory_md=False)
        shape = _discover_project(tmp_path)
        assert shape is not None
        f = _check_memory_dir(shape)  # no file created
        assert f.tier == "ok"
        assert "not required" in f.message

    def test_fails_when_required_and_absent(self, tmp_path, monkeypatch) -> None:
        monkeypatch.setattr(self._MOD, lambda: {"DP-01", "AG-17"})
        _write_project(tmp_path, memory_md=False)
        shape = _discover_project(tmp_path)
        assert shape is not None
        f = _check_memory_dir(shape)
        assert f.tier == "fail"
        assert "missing" in f.message

    def test_passes_when_required_and_present(self, tmp_path, monkeypatch) -> None:
        monkeypatch.setattr(self._MOD, lambda: {"DP-01", "AG-17"})
        _write_project(tmp_path)  # memory_md seeded by default
        shape = _discover_project(tmp_path)
        assert shape is not None
        f = _check_memory_dir(shape)
        assert f.tier == "ok"
        assert "present" in f.message

    def test_warn_when_corpus_unresolvable(self, tmp_path, monkeypatch) -> None:
        monkeypatch.setattr(self._MOD, lambda: None)
        _write_project(tmp_path)
        shape = _discover_project(tmp_path)
        assert shape is not None
        f = _check_memory_dir(shape)
        assert f.tier == "warn"


class TestMemoryStoreVcs:
    """AG-19 — the runtime auto-memory store must be git-tracked (allowlist
    .gitignore + off-machine remote). Corpus-aware: inert unless the resolved
    Index of MUSTs carries AG-19 (a MUST since standards v2.25). The store lives
    outside the repo, so the path is injected via _runtime_memory_dir."""

    _CORPUS = "mgf.fed.cli._doctor_standards._corpus_must_ids"
    _MEMDIR = "mgf.fed.cli._doctor_standards._runtime_memory_dir"

    def _shape(self, tmp_path, store):
        _write_project(tmp_path)
        shape = _discover_project(tmp_path)
        assert shape is not None
        return shape

    def _conformant_store(self, store) -> None:
        (store / ".git").mkdir(parents=True)
        (store / ".gitignore").write_text("*\n!*.md\n!.gitignore\n", encoding="utf-8")
        (store / ".git" / "config").write_text(
            '[remote "origin"]\n\turl = ssh://git@codeberg.org/x/proj-memory.git\n',
            encoding="utf-8",
        )

    def test_warn_when_corpus_unresolvable(self, tmp_path, monkeypatch) -> None:
        monkeypatch.setattr(self._CORPUS, lambda: None)
        shape = self._shape(tmp_path, tmp_path / "store")
        assert _check_memory_store_vcs(shape).tier == "warn"

    def test_inert_when_ag19_not_in_corpus(self, tmp_path, monkeypatch) -> None:
        monkeypatch.setattr(self._CORPUS, lambda: {"DP-01", "AG-17"})
        shape = self._shape(tmp_path, tmp_path / "store")
        f = _check_memory_store_vcs(shape)
        assert f.tier == "ok"
        assert "not required" in f.message

    def test_ok_when_required_but_no_store_yet(self, tmp_path, monkeypatch) -> None:
        monkeypatch.setattr(self._CORPUS, lambda: {"DP-01", "AG-19"})
        store = tmp_path / "store"  # never created
        monkeypatch.setattr(self._MEMDIR, lambda _root: store)
        shape = self._shape(tmp_path, store)
        f = _check_memory_store_vcs(shape)
        assert f.tier == "ok"
        assert "nothing to track" in f.message

    def test_fails_when_store_untracked(self, tmp_path, monkeypatch) -> None:
        monkeypatch.setattr(self._CORPUS, lambda: {"DP-01", "AG-19"})
        store = tmp_path / "store"
        store.mkdir()  # exists but no .git / .gitignore / remote
        monkeypatch.setattr(self._MEMDIR, lambda _root: store)
        shape = self._shape(tmp_path, store)
        f = _check_memory_store_vcs(shape)
        assert f.tier == "fail"
        assert "not under version control" in f.message
        assert "allowlist .gitignore" in f.message
        assert "off-machine git remote" in f.message

    def test_passes_when_store_conformant(self, tmp_path, monkeypatch) -> None:
        monkeypatch.setattr(self._CORPUS, lambda: {"DP-01", "AG-19"})
        store = tmp_path / "store"
        self._conformant_store(store)
        monkeypatch.setattr(self._MEMDIR, lambda _root: store)
        shape = self._shape(tmp_path, store)
        f = _check_memory_store_vcs(shape)
        assert f.tier == "ok"
        assert "git-tracked" in f.message


class TestGitignoreMyStuff:
    """AG-18 — the .gitignore-must-ignore-my_stuff/ gate. Corpus-aware: inert
    unless the resolved Index of MUSTs carries AG-18 (a MUST since standards
    v2.22), so a project pinning an older corpus is unaffected."""

    _MOD = "mgf.fed.cli._doctor_standards._corpus_must_ids"

    def test_inert_when_ag18_not_in_corpus(self, tmp_path, monkeypatch) -> None:
        # Older pinned corpus (AG-18 absent) → not required.
        monkeypatch.setattr(self._MOD, lambda: {"DP-01", "DOC-18"})
        _write_project(tmp_path, gitignore_my_stuff=False)
        shape = _discover_project(tmp_path)
        assert shape is not None
        f = _check_gitignore_my_stuff(shape)  # no my_stuff/ ignore
        assert f.tier == "ok"
        assert "not required" in f.message

    def test_fails_when_required_and_absent(self, tmp_path, monkeypatch) -> None:
        monkeypatch.setattr(self._MOD, lambda: {"DP-01", "AG-18"})
        _write_project(tmp_path, gitignore_my_stuff=False)
        shape = _discover_project(tmp_path)
        assert shape is not None
        f = _check_gitignore_my_stuff(shape)
        assert f.tier == "fail"
        assert "my_stuff/" in f.message

    def test_passes_when_required_and_present(self, tmp_path, monkeypatch) -> None:
        monkeypatch.setattr(self._MOD, lambda: {"DP-01", "AG-18"})
        _write_project(tmp_path)  # .gitignore with my_stuff/ seeded by default
        shape = _discover_project(tmp_path)
        assert shape is not None
        f = _check_gitignore_my_stuff(shape)
        assert f.tier == "ok"
        assert "git-ignored" in f.message

    def test_warn_when_corpus_unresolvable(self, tmp_path, monkeypatch) -> None:
        monkeypatch.setattr(self._MOD, lambda: None)
        _write_project(tmp_path)
        shape = _discover_project(tmp_path)
        assert shape is not None
        f = _check_gitignore_my_stuff(shape)
        assert f.tier == "warn"


class TestCheckCommitHook:
    """AG-18 — the prepare-commit-msg hook gate. Self-gating: inert until the
    pinned corpus's AGENTS text references the hook (tightened AG-18, PAPER-102),
    so a project on an older corpus is unaffected."""

    _RS = "mgf.standard.read_standard"

    def test_inert_when_corpus_lacks_hook_clause(self, tmp_path, monkeypatch) -> None:
        monkeypatch.setattr(self._RS, lambda name: "AG-18 wires .gitmessage via commit.template.")
        _write_project(tmp_path)
        shape = _discover_project(tmp_path)
        assert shape is not None
        f = _check_commit_hook(shape)
        assert f.tier == "ok"
        assert "not required" in f.message

    def test_fails_when_required_and_hook_absent(self, tmp_path, monkeypatch) -> None:
        monkeypatch.setattr(self._RS, lambda name: "AG-18 ... a prepare-commit-msg hook ...")
        _write_project(tmp_path, prepare_commit_hook=False)
        shape = _discover_project(tmp_path)
        assert shape is not None
        f = _check_commit_hook(shape)  # project has no .githooks/
        assert f.tier == "fail"
        assert "prepare-commit-msg" in f.message

    def test_passes_when_required_and_hook_present(self, tmp_path, monkeypatch) -> None:
        monkeypatch.setattr(self._RS, lambda name: "AG-18 ... a prepare-commit-msg hook ...")
        _write_project(tmp_path)
        hook = tmp_path / ".githooks" / "prepare-commit-msg"
        hook.parent.mkdir(parents=True, exist_ok=True)
        hook.write_text("#!/bin/sh\n", encoding="utf-8")
        shape = _discover_project(tmp_path)
        assert shape is not None
        f = _check_commit_hook(shape)
        assert f.tier == "ok"
        assert "present" in f.message

    def test_warn_when_corpus_unreadable(self, tmp_path, monkeypatch) -> None:
        def _raise(name: str) -> str:
            raise FileNotFoundError(name)

        monkeypatch.setattr(self._RS, _raise)
        _write_project(tmp_path)
        shape = _discover_project(tmp_path)
        assert shape is not None
        f = _check_commit_hook(shape)
        assert f.tier == "warn"


class TestPackageRootFallback:
    """dist-name ≠ import-package (common for apps, e.g. `vm-vmanager-cli` →
    package `vmanager`): _discover_project must still find the single package
    under src/ rather than bailing with 'cannot detect shape'."""

    def test_single_package_when_name_does_not_derive_it(self, tmp_path) -> None:
        (tmp_path / "src" / "vmanager").mkdir(parents=True)
        (tmp_path / "src" / "vmanager" / "__init__.py").write_text('__version__ = "0.3.1"\n')
        (tmp_path / "src" / "vmanager" / "py.typed").touch()
        (tmp_path / "pyproject.toml").write_text(
            '[project]\nname = "vm-vmanager-cli"\nversion = "0.3.1"\n', encoding="utf-8"
        )
        shape = _discover_project(tmp_path)
        assert shape is not None
        assert shape.package_root.name == "vmanager"


class TestDeepNamespacePackageRoot:
    """Deep PEP-420 namespace (no __init__.py): mgf-vm-core → src/mgf/vm/core."""

    def test_deep_namespace_pep420(self, tmp_path) -> None:
        pkg = tmp_path / "src" / "mgf" / "vm" / "core"
        pkg.mkdir(parents=True)
        (pkg / "py.typed").touch()  # PEP-420: no __init__.py anywhere
        (pkg / "ids.py").write_text("X = 1\n")
        (tmp_path / "pyproject.toml").write_text(
            '[project]\nname = "mgf-vm-core"\nversion = "0.1.10"\n', encoding="utf-8"
        )
        shape = _discover_project(tmp_path)
        assert shape is not None
        assert shape.package_root == pkg

    def test_test_supervisor_not_misresolved_to_nested(self, tmp_path) -> None:
        # mgf-test-supervisor → src/mgf/test_supervisor, NOT src/mgf/test/supervisor.
        pkg = tmp_path / "src" / "mgf" / "test_supervisor"
        pkg.mkdir(parents=True)
        (pkg / "__init__.py").write_text("")
        (tmp_path / "pyproject.toml").write_text(
            '[project]\nname = "mgf-test-supervisor"\nversion = "0.1.11"\n', encoding="utf-8"
        )
        shape = _discover_project(tmp_path)
        assert shape is not None
        assert shape.package_root == pkg
