#!/usr/bin/env python3
"""selftest.py: reproducible proof that grounding_pass.py is correct and read-only.

Builds the exact 7-note vault the article walks through, runs the tool in both modes,
and asserts the output matches the article byte-for-byte. Also proves the tool never
writes to a note. No arguments, no network, no deps. Exits 0 on pass, 1 on fail.

    python3 selftest.py
"""
import os, sys, glob, hashlib, tempfile, shutil, subprocess

TOOL = os.path.join(os.path.dirname(os.path.abspath(__file__)), "grounding_pass.py")

# The article's worked example. Two sources (one by folder, one by frontmatter url),
# four derived notes at increasing distance from a source, one navigation note.
FIXTURE = {
    "Sources/attention-is-all-you-need.md": "# Attention Is All You Need\nThe transformer paper.\n",
    "wiki/rag-survey.md":  "---\nurl: https://arxiv.org/abs/2312.10997\n---\n# RAG survey\n",
    "wiki/attention.md":   "# Attention\nSee [[attention-is-all-you-need]].\n",   # source at 1 hop
    "wiki/tokenizer.md":   "# Tokenizer\nGrounded in [[rag-survey]].\n",          # source at 1 hop
    "wiki/context-rot.md": "# Context rot\nRelated to [[attention]].\n",           # source at 2 hops
    "wiki/compaction.md":  "# Compaction\nBuilds on [[context-rot]] only.\n",      # source at 3 hops
    "Daily/2026-08-21.md": "# Daily\nTouched [[compaction]] and [[context-rot]].\n",  # navigation
}

# What the article prints (the-ungrounded-note.md, §"The grounding pass"
# and §"What it catches"). These are the assertions.
EXPECT_CENSUS = "7 notes · 2 sources · 4 derived · 1 navigation (excluded) · 0 unreadable"
EXPECT_DEFAULT_ORPHANS = ["wiki/compaction.md"]
EXPECT_STRICT_ORPHANS = ["wiki/compaction.md", "wiki/context-rot.md"]


def build(root):
    for rel, txt in FIXTURE.items():
        p = os.path.join(root, rel)
        os.makedirs(os.path.dirname(p), exist_ok=True)
        open(p, "w", encoding="utf-8").write(txt)


def note_hashes(root):
    return {p: hashlib.md5(open(p, "rb").read()).hexdigest()
            for p in sorted(glob.glob(os.path.join(root, "**", "*.md"), recursive=True))}


def run(root, *extra):
    log = os.path.join(root, ".selftest-log.jsonl")
    r = subprocess.run([sys.executable, TOOL, root, "--log", log, *extra],
                       capture_output=True, text=True)
    return r.stdout


def orphans_from(out):
    return [ln.strip()[2:] for ln in out.splitlines() if ln.strip().startswith("- ")]


def main():
    root = tempfile.mkdtemp(prefix="grounding-selftest-")
    fails = []
    try:
        build(root)
        before = note_hashes(root)

        d = run(root)
        if EXPECT_CENSUS not in d:
            fails.append(f"census mismatch\n  expected: {EXPECT_CENSUS}\n  got: {d!r}")
        if orphans_from(d) != EXPECT_DEFAULT_ORPHANS:
            fails.append(f"default orphans {orphans_from(d)} != {EXPECT_DEFAULT_ORPHANS}")

        s = run(root, "--strict")
        if orphans_from(s) != EXPECT_STRICT_ORPHANS:
            fails.append(f"strict orphans {orphans_from(s)} != {EXPECT_STRICT_ORPHANS}")

        # regression cases the census fixture does not cover (found in review)
        edge = tempfile.mkdtemp(prefix="grounding-selftest-edge-")
        try:
            def w(rel, txt):
                p = os.path.join(edge, rel)
                os.makedirs(os.path.dirname(p), exist_ok=True)
                open(p, "w", encoding="utf-8").write(txt)
            w("Sources/paper.md", "# Paper\n")
            # a blank `url:` stub followed by another key must stay DERIVED, not become a source
            w("wiki/stub.md", "---\nurl:\ntitle: Made up\ntags: [ai]\n---\n# Stub\nCites [[nothing-real]].\n")
            # a citation via an #anchor markdown link must count as reaching the source
            w("wiki/anchored.md", "# Anchored\n[the paper](../Sources/paper.md#results)\n")
            eo = orphans_from(run(edge))
            if "wiki/stub.md" not in eo:
                fails.append(f"blank `url:` stub misclassified as a source (must be a source-orphan); orphans={eo}")
            if "wiki/anchored.md" in eo:
                fails.append(f"#anchor markdown link not followed; anchored.md wrongly flagged; orphans={eo}")
        finally:
            shutil.rmtree(edge, ignore_errors=True)

        after = note_hashes(root)
        mutated = [os.path.relpath(p, root) for p in before if before[p] != after.get(p)]
        if mutated:
            fails.append(f"tool MUTATED note files (must be read-only): {mutated}")
    finally:
        shutil.rmtree(root, ignore_errors=True)

    if fails:
        print("FAIL")
        for f in fails:
            print("  - " + f)
        return 1
    print("PASS · article census reproduced · default=1 orphan (compaction) · "
          "strict=2 (compaction, context-rot) · blank-url + anchor-link regressions · notes unmodified")
    return 0


if __name__ == "__main__":
    sys.exit(main())
