#!/usr/bin/env python3
"""Self-test for refine_round.py. Run: python test_refine_round.py"""

import json
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path

SCRIPT = Path(__file__).with_name("refine_round.py")
failures = []

INPUTS = "# 输入\n- 初稿：refine-logs/FINAL_PROPOSAL.md\nF1 [评价] “方法太复杂”\nF2 [奇思妙想] “换成 LoRA 会怎样”\nP1 LoRA → papers/lora.md\n"
NOTE = "# 笔记\n创新点：低秩更新（第 4 节）\n"
REV = ("# 修订\n方案：合并模块\n"
       "- [ ] C01 [依据] 合并三个模块 ｜目标：refine-logs/FINAL_PROPOSAL.md#方法 ｜出处：F1\n"
       "- [ ] C02 [验证] 适配器换成 LoRA ｜目标：refine-logs/FINAL_PROPOSAL.md#方法 ｜出处：F2, P1\n"
       "- [ ] C03 [否决] 去掉基线 ｜出处：F1 ｜理由：失去对照\n")
LINKED = REV.replace("｜出处：F2, P1", "｜出处：F2, P1 ｜验证：lora-swap")


def run(root, *args):
    proc = subprocess.run([sys.executable, str(SCRIPT), *args, "--root", str(root)],
                          capture_output=True, text=True, encoding="utf-8")
    try:
        return proc.returncode, json.loads(proc.stdout)
    except ValueError:
        return proc.returncode, {"stdout": proc.stdout, "stderr": proc.stderr}


def check(label, cond, detail=""):
    print(("  PASS  " if cond else "  FAIL  ") + label + ("" if cond else f" — {detail}"))
    if not cond:
        failures.append(label)


def fill(path, text):
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(text, encoding="utf-8")


def phase(root, rid):
    return run(root, "check", rid)[1].get("phase")


def fake_verify(root, slug, status, verdict):
    fill(root / "_verify-idea-stage" / slug / "STATUS.json",
         json.dumps({"slug": slug, "status": status, "verdict": verdict}))


def main():
    tmp = Path(tempfile.mkdtemp(prefix="refine-round-test-"))
    try:
        root = tmp / "proj"
        fill(root / "idea-stage" / "IDEA_REPORT.md", "# idea\n")
        proposal = root / "refine-logs" / "FINAL_PROPOSAL.md"
        fill(proposal, "# proposal v1\n")
        original = proposal.read_bytes()

        code, out = run(root, "new", "--title", "融入 LoRA 并简化方法")
        check("new starts round r01 in the collect phase",
              code == 0 and out.get("round") == "r01" and out.get("phase") == "collect", out)
        rd = root / "_refine" / "r01"
        check("inputs and revision templates render",
              all((rd / n).is_file() and "{{" not in (rd / n).read_text(encoding="utf-8") for n in ("INPUTS.md", "REVISION.md")))
        check("no warning when the mainline exists", "warning" not in out, out)
        code, out = run(root, "new")
        check("the next round is r02", code == 0 and out.get("round") == "r02", out)

        code, out = run(root, "paper", "r01", "--slug", "lora", "--source", "arXiv:2106.09685")
        note = rd / "papers" / "lora.md"
        check("paper renders a note", code == 0 and note.is_file() and "{{" not in note.read_text(encoding="utf-8"), out)
        code, out = run(root, "paper", "r01", "--slug", "lora", "--source", "x")
        check("a duplicate paper note is refused", code == 1, out)
        code, out = run(root, "paper", "r01", "--slug", "Bad Slug", "--source", "x")
        check("an invalid paper slug is refused", code == 1, out)

        fill(rd / "INPUTS.md", INPUTS)
        check("an unfilled paper note keeps the phase at collect", phase(root, "r01") == "collect")
        fill(note, NOTE)
        check("filled inputs and notes move to triage", phase(root, "r01") == "triage")
        fill(rd / "REVISION.md", "# 修订\n方案：同初稿\n")
        check("a revision without changes stays at triage", phase(root, "r01") == "triage")
        fill(rd / "REVISION.md", REV)
        code, out = run(root, "check", "r01")
        check("a triaged revision waits for the user's review",
              out.get("phase") == "review" and out["counts"]["total"] == 3 and out["counts"]["验证"] == 1, out)

        code, out = run(root, "approve", "r01", "--stage", "landing")
        check("landing cannot be approved during the review", code == 1, out)
        code, out = run(root, "approve", "r01", "--stage", "triage")
        check("approving the triage moves to verify", code == 0 and out.get("phase") == "verify", out)

        fill(rd / "REVISION.md", LINKED)
        code, out = run(root, "check", "r01")
        check("a linked but missing verification is flagged",
              out.get("phase") == "reconcile" and any("not found" in i["do"] for i in out.get("issues", [])), out)
        fake_verify(root, "lora-swap", "open", None)
        check("an open verification keeps the phase at verify", phase(root, "r01") == "verify")
        fake_verify(root, "lora-swap", "concluded", "supported")
        code, out = run(root, "check", "r01")
        check("a supported verdict joins the other pending items for reconciliation",
              out.get("phase") == "reconcile" and sorted(i["change"] for i in out.get("issues", [])) == ["C01", "C02", "C03"], out)

        fill(rd / "REVISION.md", LINKED.replace("- [ ] C01", "- [x] C01").replace("- [ ] C02", "- [x] C02")
             .replace("- [ ] C03", "- [-] C03"))
        code, out = run(root, "check", "r01")
        check("a reconciled revision waits for landing confirmation",
              out.get("phase") == "confirm" and out.get("mainline_touched") is False, out)
        code, out = run(root, "land", "r01", "--to", "research-refine")
        check("landing without the user's confirmation is refused", code == 1, out)
        code, out = run(root, "approve", "r01", "--stage", "landing")
        check("approving the landing moves to land", code == 0 and out.get("phase") == "land", out)
        code, out = run(root, "land", "r01", "--to", "direct")
        check("a direct landing requires backups of its targets",
              code == 1 and "FINAL_PROPOSAL.md" in out.get("error", ""), out)

        code, out = run(root, "backup", "r01", "../outside.md")
        check("backup refuses paths outside the project", code == 1, out)
        code, out = run(root, "backup", "r01", "_verify-idea-stage/lora-swap/STATUS.json")
        check("backup refuses verification files", code == 1, out)
        code, out = run(root, "backup", "r01", "refine-logs/FINAL_PROPOSAL.md")
        bak = rd / "backup" / "refine-logs" / "FINAL_PROPOSAL.md"
        check("backup copies the original", code == 0 and bak.read_bytes() == original, out)
        fill(proposal, "# proposal v2\n")
        code, out = run(root, "backup", "r01", "refine-logs/FINAL_PROPOSAL.md")
        check("a second backup keeps the first original",
              code == 0 and bak.read_bytes() == original and out.get("already_backed_up"), out)
        code, out = run(root, "land", "r01", "--to", "direct")
        check("a direct landing is recorded once its targets are backed up",
              code == 0 and out.get("landed_to") == ["direct"], out)
        check("a landed round is closed", phase(root, "r01") == "closed")

        r2 = root / "_refine" / "r02"
        fill(r2 / "INPUTS.md", INPUTS)
        fill(r2 / "REVISION.md", "# 修订\n方案\n- [ ] C01 [验证] 想法甲 ｜验证：idea-a\n- [x] C02 [验证] 想法乙 ｜验证：idea-b\n")
        run(root, "approve", "r02", "--stage", "triage")
        fake_verify(root, "idea-a", "concluded", "inconclusive")
        fake_verify(root, "idea-b", "concluded", "refuted")
        code, out = run(root, "check", "r02")
        issues = {i["change"]: i["do"] for i in out.get("issues", [])}
        check("an inconclusive verdict is handed to the user", "inconclusive" in issues.get("C01", ""), out)
        check("an adopted change whose verification was refuted is flagged", "refuted" in issues.get("C02", ""), out)
        check("mainline changes since the round began are reported",
              out.get("mainline_touched") is True and "refine-logs/FINAL_PROPOSAL.md" in out.get("mainline_changes", []), out)

        code, out = run(root, "list")
        rounds = {r["round"]: r for r in out.get("rounds", [])}
        check("list reports each round's phase",
              rounds.get("r01", {}).get("phase") == "closed" and rounds.get("r02", {}).get("phase") == "reconcile", out)
        run(root, "close", "r02", "--reason", "改走下一轮")
        code, out = run(root, "list")
        check("list hides closed rounds", "r02" not in [r["round"] for r in out.get("rounds", [])], out)
        code, out = run(root, "list", "--all")
        check("list --all includes closed rounds", "r02" in [r["round"] for r in out.get("rounds", [])], out)

        code, out = run(root, "new", "--title", "real templates")
        rid = out.get("round")
        r3 = root / "_refine" / rid
        run(root, "paper", rid, "--slug", "p1", "--source", "local.pdf")

        def fill_placeholders(path):
            lines = path.read_text(encoding="utf-8").splitlines()
            fill(path, "\n".join("已填写" if "【TODO】" in line else line for line in lines) + "\n")

        seen = []
        for path in (r3 / "INPUTS.md", r3 / "papers" / "p1.md", r3 / "REVISION.md"):
            fill_placeholders(path)
            seen.append(phase(root, rid))
        fill(r3 / "REVISION.md", (r3 / "REVISION.md").read_text(encoding="utf-8")
             + "- [x] C01 [依据] 合并模块 ｜目标：refine-logs/FINAL_PROPOSAL.md ｜出处：F1\n")
        seen.append(phase(root, rid))
        check("real templates clear phase by phase once their placeholders are filled",
              rid == "r03" and seen == ["collect", "triage", "triage", "review"], [rid, seen])

        written = [p.relative_to(root).as_posix() for p in root.rglob("*") if p.is_file()]
        stray = [p for p in written if not p.startswith(("_refine/", "_verify-"))
                 and p not in ("idea-stage/IDEA_REPORT.md", "refine-logs/FINAL_PROPOSAL.md")]
        check("nothing is written outside _refine/ and the test's own fixtures", not stray, stray)

        aris = tmp / "aris"
        (aris / "skills").mkdir(parents=True)
        (aris / "tools").mkdir()
        fill(aris / "AGENT_GUIDE.md", "x")
        code, out = run(aris, "new")
        check("refuses to run inside the ARIS repository", code == 1 and "ARIS repository" in out.get("error", ""), out)

        bare = tmp / "bare"
        bare.mkdir()
        code, out = run(bare, "new")
        check("warns when no ARIS mainline exists", code == 0 and "warning" in out, out)
    finally:
        shutil.rmtree(tmp, ignore_errors=True)
    print("OK" if not failures else f"{len(failures)} check(s) failed")
    return 0 if not failures else 1


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