#!/usr/bin/env python3
"""verify-desc-readers-both-ways.py — prove the desc wall's own readers read both ways.

The wall (verify-desc-claims.py) must distinguish honest from planted, or it
proves nothing — and the instrument's OWN RULE is the newest surface (the
seed Q102 planted, closed this wake): the desc-tag counter once counted with
a pattern that saw a '<desc' inside an XML comment as a tag — the hand's
words about markup, the coda ghost the files' wall closed, found again in
the wall's own counter — and the text reader that reads the drawings' words
stopped at the FIRST '>' it saw, even when that '>' sat inside a quoted
attribute value, the exact boundary the image wall healed for <img> one
surface over, still the hand's in the desc wall's own room. The readers now
scan comment-aware and quote-aware: a '<desc' inside a comment or a quoted
value is words, not a tag; a '>' inside a quoted attribute value is part of
the value, not the end of the tag.

This harness runs the honest estate (must exit 0) and then plants one
corruption at a time (each must exit as expected, named by its file):

  1. honest estate                                   -> 0
  2. two-fixes.svg: a second REAL <desc id="d">      -> 1 (the reverse gate
     planted before </svg>                                 holds: the smuggled
                                                           clause is unaccounted)
  3. two-fixes.svg: a desc-shaped word inside an     -> 0 (words, not a tag —
     XML comment (the coda ghost, found in the             the hand's words about
     wall's own counter)                                    markup are not markup)
  4. the-faithful-sentinel.svg: a '>' inside a       -> 0 (the reading goes
     quoted attribute value of the colon's <text>          through the boundary:
     tag (data-x="a > b")                                   the colon claim still holds)
  5. two-fixes.svg: a claim-word that lives ONLY     -> 1 (a comment is not a
     in a comment (the real 401 text node removed,          drawing word — the
     a fake <text ...>401</text> planted in a               claim cannot be proven)
     comment)
  6. honest estate again (all restorations)          -> 0

Each corruption is written to the REAL file, verified, then restored from the
in-memory snapshot — the harness never leaves the estate dirty.

Deterministic: same files, same bytes, same verdict.
"""

import subprocess
import sys
from pathlib import Path

TOOLS = Path(__file__).resolve().parent
ASSETS = TOOLS.parent / "assets"
GATE = TOOLS / "verify-desc-claims.py"
FILES = [
    "dead-reckoning.svg",
    "liturgy-not-config.svg",
    "the-checkbox-trap.svg",
    "the-faithful-sentinel.svg",
    "two-fixes.svg",
]

passed = 0
failed = 0


def run_gate(label: str, expect: int) -> None:
    global passed, failed
    r = subprocess.run([sys.executable, str(GATE)], capture_output=True, text=True)
    ok = r.returncode == expect
    mark = "PASS" if ok else "FAIL"
    if ok:
        passed += 1
    else:
        failed += 1
    print(f"[{mark}] {label} (exit {r.returncode}, expected {expect})")
    if not ok:
        tail = (r.stdout.strip().splitlines() or r.stderr.strip().splitlines() or [""])[-1]
        print("   ", tail)


def snapshot() -> dict[str, str]:
    return {f: (ASSETS / f).read_text(encoding="utf-8") for f in FILES}


def restore(snap: dict[str, str]) -> None:
    for f, content in snap.items():
        (ASSETS / f).write_text(content, encoding="utf-8")


def main() -> int:
    snap = snapshot()
    try:
        # 1. honest estate
        run_gate("honest estate (5/5 pieces read whole)", 0)

        # 2. a second REAL desc — the reverse gate must hold
        two = (ASSETS / "two-fixes.svg").read_text(encoding="utf-8")
        (ASSETS / "two-fixes.svg").write_text(
            two.replace("</svg>", '<desc id="d">ghost</desc>\n</svg>'), encoding="utf-8")
        run_gate("second real <desc id=\"d\"> planted — the smuggled clause is unaccounted", 1)
        restore(snap)

        # 3. a desc-shaped word inside an XML comment — words, not a tag
        two = (ASSETS / "two-fixes.svg").read_text(encoding="utf-8")
        (ASSETS / "two-fixes.svg").write_text(
            two.replace("</svg>", "<!-- the hand's words about markup: <desc id=\"d\">ghost</desc> -->\n</svg>"),
            encoding="utf-8")
        run_gate("desc-shaped word inside an XML comment — the hand's words about markup are not a tag", 0)
        restore(snap)

        # 4. a '>' inside a quoted attribute value of the colon's <text> tag —
        #    the reading must go through the boundary and the claim must hold
        sent = (ASSETS / "the-faithful-sentinel.svg").read_text(encoding="utf-8")
        colon_tag = '<text x="960" y="110" text-anchor="middle" font-family="ui-monospace, monospace" font-size="14" fill="#7c7cf0" opacity="0.6">:</text>'
        assert colon_tag in sent, "colon tag not found — the harness's plant is stale"
        (ASSETS / "the-faithful-sentinel.svg").write_text(
            sent.replace(colon_tag, '<text x="960" y="110" text-anchor="middle" data-x="a > b" font-family="ui-monospace, monospace" font-size="14" fill="#7c7cf0" opacity="0.6">:</text>'),
            encoding="utf-8")
        run_gate("'>' inside a quoted attribute value of the colon's <text> — the tag is read whole", 0)
        restore(snap)

        # 5. a claim-word that lives ONLY in a comment — not a drawing word
        two = (ASSETS / "two-fixes.svg").read_text(encoding="utf-8")
        real_401 = '<text x="0" y="-55" text-anchor="middle" font-family="ui-monospace, monospace" font-size="11" fill="#4a4a5e" letter-spacing="0.1em" opacity="0.5">401</text>'
        assert real_401 in two, "401 text node not found — the harness's plant is stale"
        (ASSETS / "two-fixes.svg").write_text(
            two.replace(real_401, "<!-- the gauge's reading: <text x=\"0\" y=\"-55\">401</text> -->"),
            encoding="utf-8")
        run_gate("claim-word only inside a comment — a comment is not a drawing word (the claim cannot be proven)", 1)
        restore(snap)

        # 6. honest estate again — the harness never leaves the estate dirty
        run_gate("honest estate after all restorations", 0)
    finally:
        restore(snap)

    print(f"\ndesc-readers both-ways: {passed} passed, {failed} failed")
    return 1 if failed else 0


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