#!/usr/bin/env python3
"""
selftest.py -- exercises memory_dream.py against a synthetic fixture store.

Builds a throwaway repo + profile memory store in a temp dir, runs the analyzer
both in report-only and --apply-safe modes (as a subprocess, with
CLAUDETOOLS_ROOT / HOME / CLAUDE_PROJECT_DIR pointed at the fixtures), and
asserts:
  * each detector fires (orphan, missing index target, broken backlink, stale
    referenced path, overlap cluster, profile drift),
  * --apply-safe is strictly additive (no file deleted, no file overwritten,
    orphan index line appended, profile-only file migrated, differing file
    skipped not clobbered).

Stdlib only. Exit 0 on success, 1 on any failed assertion.
"""

from __future__ import annotations

import os
import re
import subprocess
import sys
import tempfile
from pathlib import Path

SCRIPT = Path(__file__).resolve().with_name("memory_dream.py")
PY = sys.executable or "python"

FAILURES: list[str] = []


def check(cond: bool, msg: str) -> None:
    status = "[OK]" if cond else "[ERROR]"
    print(f"{status} {msg}")
    if not cond:
        FAILURES.append(msg)


def write(path: Path, text: str) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(text, encoding="utf-8")


def build_fixture(root: Path):
    """Create repo + profile fixture stores. Returns (repo_root, project_dir, home)."""
    repo_root = root / "repo"
    mem = repo_root / ".claude" / "memory"
    mem.mkdir(parents=True, exist_ok=True)

    # A real script the memory can reference (exists -> must NOT be flagged).
    write(repo_root / ".claude" / "scripts" / "real.sh", "#!/bin/sh\necho hi\n")

    # --- memory files ---
    # indexed + clean
    write(mem / "reference_alpha.md",
          "---\nname: Alpha\ndescription: alpha thing\nmetadata:\n  type: reference\n---\n"
          "Uses `.claude/scripts/real.sh` which exists.\n")
    # orphan (no index line) + broken backlink + stale referenced path
    write(mem / "feedback_orphan.md",
          "---\nname: Orphan Feedback\ndescription: an orphan\ntype: feedback\n---\n"
          "See [[no_such_memory]] and `scripts/ghost_missing.py` which is gone.\n")
    # two overlapping feedback memories (same slug prefix -> cluster)
    write(mem / "feedback_syncro_aaa.md",
          "---\nname: Syncro AAA\ndescription: syncro billing rule one\ntype: feedback\n---\nbody\n")
    write(mem / "feedback_syncro_bbb.md",
          "---\nname: Syncro BBB\ndescription: syncro billing rule two\ntype: feedback\n---\nbody\n")
    # stale dated project fact
    write(mem / "project_old.md",
          "---\nname: Old Project\ndescription: ancient\ntype: project\n---\n"
          "Migration completed as of 2019-01-01 and never touched since.\n")

    # --- MEMORY.md index ---
    # references reference_alpha + a MISSING target; omits feedback_orphan (orphan)
    write(mem / "MEMORY.md",
          "# Memory Index\n\n"
          "## Reference\n"
          "- [Alpha](reference_alpha.md) -- alpha thing\n"
          "- [Ghost](reference_ghost.md) -- points at a missing file\n\n"
          "## Feedback\n"
          "- [Syncro AAA](feedback_syncro_aaa.md) -- syncro billing rule one\n"
          "- [Syncro BBB](feedback_syncro_bbb.md) -- syncro billing rule two\n\n"
          "## Project\n"
          "- [Old Project](project_old.md) -- ancient\n")

    # --- profile store ---
    # slug derivation mirrors memory_dream.profile_memory_dir
    project_dir = repo_root  # we set CLAUDE_PROJECT_DIR to repo_root
    home = root / "home"
    slug = re.sub(r"[^A-Za-z0-9]+", "-", str(project_dir.resolve()))
    prof = home / ".claude" / "projects" / slug / "memory"
    prof.mkdir(parents=True, exist_ok=True)

    # profile-only file (candidate to migrate INTO repo)
    write(prof / "feedback_profile_only.md",
          "---\nname: Profile Only\ndescription: lives only in profile\ntype: feedback\n---\nkeep me\n")
    # same-named in BOTH but DIFFERING content (must be skipped, not overwritten)
    write(prof / "reference_alpha.md",
          "---\nname: Alpha\ndescription: alpha thing\nmetadata:\n  type: reference\n---\n"
          "PROFILE VERSION -- different content.\n")

    return repo_root, project_dir, home, prof


def run_analyzer(repo_root: Path, project_dir: Path, home: Path, *extra) -> str:
    env = dict(os.environ)
    env["CLAUDETOOLS_ROOT"] = str(repo_root)
    env["CLAUDE_PROJECT_DIR"] = str(project_dir)
    env["HOME"] = str(home)
    env["PYTHONIOENCODING"] = "utf-8"
    cmd = [PY, str(SCRIPT), "--no-file", *extra]
    res = subprocess.run(cmd, env=env, capture_output=True, text=True,
                         encoding="utf-8", errors="replace")
    return res.stdout + "\n" + res.stderr


def main() -> int:
    with tempfile.TemporaryDirectory() as td:
        root = Path(td)
        repo_root, project_dir, home, prof = build_fixture(root)
        mem = repo_root / ".claude" / "memory"

        # ---- report-only run ----
        out = run_analyzer(repo_root, project_dir, home)

        check("Mode: REPORT-ONLY" in out, "default run is report-only")
        check("feedback_orphan.md" in out and "Orphan files (no index line): 1" in out,
              "detects the orphan file")
        check("reference_ghost.md" in out and "missing files: 1" in out,
              "detects index line pointing at missing file")
        check("[[no_such_memory]]" in out, "detects broken backlink")
        check("ghost_missing.py" in out, "flags stale referenced path")
        check("real.sh" not in out.split("REFERENCED-ARTIFACT")[-1].split("##")[0]
              if "REFERENCED-ARTIFACT" in out else True,
              "does NOT flag an existing referenced path (real.sh)")
        check("feedback_syncro_aaa.md" in out and "feedback_syncro_bbb.md" in out
              and "CLUSTER" in out.upper(), "detects overlap cluster")
        check("project_old.md" in out and "2019-01-01" in out,
              "detects stale dated project fact")
        check("feedback_profile_only.md" in out
              and "MIGRATE INTO repo" in out, "detects profile-only drift")
        check("reference_alpha.md" in out and "differs between repo and profile" in out,
              "detects repo<->profile content conflict")
        check("PROPOSED (needs human approval" in out, "emits PROPOSED section")

        # ---- snapshot repo state before apply-safe ----
        before = {p.name: p.read_text(encoding="utf-8") for p in mem.glob("*.md")}

        # ---- apply-safe run (additive only) ----
        out2 = run_analyzer(repo_root, project_dir, home, "--apply-safe")

        after = {p.name: p.read_text(encoding="utf-8") for p in mem.glob("*.md")}

        # No file deleted.
        check(set(before).issubset(set(after)), "apply-safe deleted no repo file")
        # Orphan index line appended (file content for non-index unchanged).
        for fn, content in before.items():
            if fn == "MEMORY.md":
                continue
            check(after.get(fn) == content,
                  f"apply-safe did not alter memory body: {fn}")
        # MEMORY.md grew (orphan appended) and kept all old lines.
        idx_before = before["MEMORY.md"]
        idx_after = after["MEMORY.md"]
        check("feedback_orphan.md" in idx_after,
              "apply-safe appended orphan index line")
        check(all(line in idx_after for line in idx_before.splitlines() if line.strip()),
              "apply-safe preserved every existing index line")
        # Profile-only migrated INTO repo.
        check("feedback_profile_only.md" in after,
              "apply-safe migrated profile-only file into repo")
        # Differing same-named file was SKIPPED, not overwritten.
        check(after["reference_alpha.md"] == before["reference_alpha.md"],
              "apply-safe did NOT overwrite differing repo file (skipped)")
        # The differing same-named file is surfaced as a drift conflict, not a
        # copy target -- apply-safe leaves it for human review.
        check("reference_alpha.md" in out2
              and "differs between repo and profile" in out2,
              "apply-safe reported the differing file as a conflict (not overwritten)")
        # Profile store itself untouched by dream (dream only writes repo side).
        check((prof / "feedback_profile_only.md").exists(),
              "profile-only source still present after migration")

    print()
    if FAILURES:
        print(f"[ERROR] {len(FAILURES)} self-test assertion(s) failed:")
        for f in FAILURES:
            print(f"  - {f}")
        return 1
    print("[SUCCESS] all self-test assertions passed")
    return 0


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