#!/usr/bin/env python3
"""test_patch_semantics.py — H4: prove the hi-fi mock models REAL PATCH semantics.

These are exactly the behaviors the shipped naive mock cannot reproduce (it appends
everything at EOF), so without this the section-replace / section-append / missing-
heading / frontmatter-replace paths are untested. Drives the real echo.py against
mock_olrapi_hifi.py.

Run:  python test_patch_semantics.py [--port 8820]
"""
from __future__ import annotations

import argparse
import os
import subprocess
import sys
import tempfile
import time
import urllib.request
from pathlib import Path

HERE = Path(__file__).resolve().parent
ECHO = HERE.parent / "echo-memory.plugin.src" / "skills" / "echo-memory" / "scripts" / "echo.py"
KEY = "test-key-not-a-real-secret"

failures = []


def check(name, cond, detail=""):
    print(f"{'ok  ' if cond else 'FAIL'} {name}" + (f"  -- {detail}" if not cond else ""))
    if not cond:
        failures.append(name)


def http(method, url, body=None, headers=None):
    data = body.encode() if isinstance(body, str) else body
    req = urllib.request.Request(url, data=data, method=method,
                                 headers={"Authorization": f"Bearer {KEY}", **(headers or {})})
    try:
        with urllib.request.urlopen(req, timeout=10) as r:
            return r.status, r.read().decode("utf-8", "replace")
    except Exception as e:  # noqa: BLE001
        return getattr(e, "code", 0), ""


def tmp(content):
    f = tempfile.NamedTemporaryFile("w", suffix=".md", delete=False, encoding="utf-8")
    f.write(content)
    f.close()
    return f.name


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--port", type=int, default=8820)
    a = ap.parse_args()
    base = f"http://127.0.0.1:{a.port}"
    srv = subprocess.Popen([sys.executable, str(HERE / "mock_olrapi_hifi.py"), "--port", str(a.port)],
                           stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)

    def echo(*args):
        import tempfile
        env = dict(os.environ, ECHO_BASE=base, ECHO_KEY=KEY, ECHO_VERIFY="0",
                   ECHO_STATE_DIR=globals().setdefault("_STATE_DIR", tempfile.mkdtemp()))
        return subprocess.run([sys.executable, str(ECHO), *args], capture_output=True, text=True, env=env)

    def ground(path):
        _, body = http("GET", f"{base}/__debug__?path={path}")
        return None if body == "<<MISSING>>" else body

    try:
        for _ in range(50):
            try:
                urllib.request.urlopen(f"{base}/__debug__reset", data=b"", timeout=1)
                break
            except Exception:
                time.sleep(0.1)

        doc = "doc.md"
        seed = ("---\ntype: note\nupdated: 2026-01-01\n---\n\n"
                "# Doc\n\n## Scope\nold scope\n\n## Other\nkeep me\n")

        # replace: Scope body becomes new; Other untouched.
        http("PUT", f"{base}/vault/{doc}", seed)
        echo("patch", doc, "replace", "heading", "Doc::Scope", tmp("new scope"))
        g = ground(doc) or ""
        check("heading replace swaps section body", "new scope" in g and "old scope" not in g, g)
        check("heading replace leaves sibling section intact", "## Other\nkeep me" in g, g)

        # append: lands INSIDE the section (before '## Other'), not at EOF.
        echo("patch", doc, "append", "heading", "Doc::Scope", tmp("appended line"))
        g = ground(doc) or ""
        check("heading append inserts within the section (not EOF)",
              "appended line" in g and g.index("appended line") < g.index("## Other"), g)

        # prepend: lands at the TOP of the section body.
        echo("patch", doc, "prepend", "heading", "Doc::Scope", tmp("first line"))
        g = ground(doc) or ""
        check("heading prepend inserts at section top",
              "first line" in g and g.index("first line") < g.index("new scope"), g)

        # missing heading -> 400 -> echo.py exits non-zero (the silent-loss guard).
        r = echo("patch", doc, "append", "heading", "Doc::Nope", tmp("lost?"))
        check("missing heading fails loud (non-zero exit)", r.returncode != 0, r.stderr)
        check("missing heading does not write", "lost?" not in (ground(doc) or ""))

        # frontmatter replace on an existing field updates it.
        echo("fm", doc, "updated", "2026-06-22")
        g = ground(doc) or ""
        check("frontmatter replace updates existing field", "updated: 2026-06-22" in g, g)

        # fm on a MISSING field: the server still 400s the raw PATCH (matches real API),
        # but cmd_fm (v1.5) falls back to a surgical create — the key is inserted and
        # every other line is preserved. (The old contract failed loud on exactly the
        # notes that needed repair.)
        before = ground(doc) or ""
        r = echo("fm", doc, "nonexistent_field", "x")
        check("fm creates a missing field (create-or-replace)", r.returncode == 0,
              r.stderr or r.stdout)
        g = ground(doc) or ""
        check("fm-created key holds the value", "nonexistent_field: x" in g, g)
        check("fm create preserves every existing line",
              all(line in g for line in before.splitlines() if line.strip()), g)

        # raw `patch replace frontmatter` (no fallback) still fails loud on a missing key.
        r = echo("patch", doc, "replace", "frontmatter", "still_missing", tmp('"x"'))
        check("raw frontmatter PATCH of a missing field fails loud", r.returncode != 0, r.stderr)

        print(f"\n{len(failures)} failure(s)" if failures else "\nall PATCH-semantics tests passed")
        return 1 if failures else 0
    finally:
        srv.terminate()


if __name__ == "__main__":
    raise SystemExit(main())
