#!/usr/bin/env python3
"""ANU 자작 변이 하네스 — task-3078 (봇 하네스 미사용, 독립 작성).

원칙:
 - git worktree 제자리 실행 (/tmp 복사 금지)
 - 주입 전 `원문 in 소스` + count==1 로 no-op 선증명
 - 파이프 없이 returncode 직접 판정
 - 대조군 선행 GREEN, 매 변이 후 원본 복원
"""
import subprocess, sys, shutil, os

WT = "/home/jay/projects/InsuRo/.worktrees/task-3078-dev1"
SERVER = os.path.join(WT, "server")

PY_SCOPE = [
    "tests/test_task3078_endpoint_wiring_seal.py",
    "tests/test_task3078_plan_drift_journal.py",
    "tests/test_task3076_plan_catalog.py",
]

# (id, 파일(WT 상대), 원문, 치환문, 오라클종류, 설명)
MUTATIONS = [
    # --- 봇이 KILL 주장한 것 재현 ---
    ("W1", "server/main.py", "seo_mode = normalize_seo_mode(req.seoMode)",
     'seo_mode = "seo"', "py", "seo_mode 요청값 무시 하드코딩"),
    ("W2", "server/main.py", "build_benchmark_prompt(req.benchmark, seo_mode)",
     "build_benchmark_prompt(req.benchmark)", "py", "벤치마크 프롬프트 seo_mode 인자 제거"),
    ("W3", "server/main.py", "seo_mode=seo_mode,",
     "", "py", "build_content_prompt seo_mode 결선 제거 (t3077 A4)"),
    ("S1", "server/main.py", "skills=filtered_skills",
     "skills=requested_skills", "py", "플랜 스킬 필터 우회"),
    ("S2", "server/main.py", "model_cap = plan_tier.model_cap",
     'model_cap = "opus"', "py", "모델 상한 우회"),
    ("S3", "server/main.py", "cli_model = resolved_model",
     'cli_model = "haiku"', "py", "과금·집계 모델 오기록"),
    # --- ★ ANU 신규 설계 (봇이 시험하지 않은 것) ---
    ("A-C1", "server/main.py", "        plan_drift_journal.record(\n            plan_drift_journal.SEVERITY_ERROR,",
     "        _noop_drift_record(\n            plan_drift_journal.SEVERITY_ERROR,", "py",
     "★신규: 드리프트 ERROR 저널 결선 제거 (작업 C 핵심 한 줄)"),
    ("A-C2", "server/main.py",
     '@app.get("/api/insuro/admin/plan-catalog-drift")\ndef admin_plan_catalog_drift(\n    limit: int = 20,\n    payload: dict = Depends(verify_admin),\n)',
     '@app.get("/api/insuro/admin/plan-catalog-drift")\ndef admin_plan_catalog_drift(\n    limit: int = 20,\n    payload: dict = None,\n)', "py",
     "★신규: 드리프트 조회 API 관리자 인증 제거 (무인증 노출)"),
    ("A-C3", "server/plan_drift_journal.py", "        _memory.appendleft(event)",
     "        pass", "py", "★신규: 메모리 링버퍼 기록 무력화 (DDL 미적용 시 관측 0)"),
    ("A-C4", "server/plan_drift_journal.py",
     '            sb.table(DRIFT_TABLE).insert(row).execute()\n            persisted = True',
     "            persisted = True", "py", "★신규: DB insert 실제 호출 제거 (persisted 거짓보고)"),
    # --- 작업 B (프론트, 오라클 vitest) ---
    ("A-B1", "src/config/planSkillMap.ts",
     '  Max: ["base", "naver_seo", "geo", "viral_hook", "pro_blog", "cro_copy"],',
     '  Max: ["base", "naver_seo", "geo", "viral_hook", "pro_blog", "cro_copy", "infokeyword_analyze"],',
     "vitest", "★신규: 인포키워드를 Max 스킬맵에 되돌림 (작업 B 원복)"),
    ("A-B2", "src/config/planSkillMap.ts",
     '  { id: "benchmark", name: "벤치마킹"',
     '  { id: "infokeyword_analyze", name: "정보성키워드 분석", emoji: "🔬", description: "x", minPlan: "Max" },\n  { id: "benchmark", name: "벤치마킹"',
     "vitest", "★신규: 인포키워드 체크박스를 SKILLS 목록에 되돌림"),
]


def run(cmd, cwd, timeout=2400):
    return subprocess.run(cmd, cwd=cwd, shell=True,
                          stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
                          timeout=timeout).returncode


def oracle(kind):
    if kind == "py":
        return run("python3 -m pytest -q -p no:randomly " + " ".join(PY_SCOPE), SERVER)
    return run("npx vitest run src/config/__tests__ src/components/admin/__tests__ --reporter=dot", WT)


def main():
    print("=== 대조군 (변이 없음) ===", flush=True)
    for k in ("py", "vitest"):
        rc = oracle(k)
        print(f"  {k}: rc={rc} {'GREEN' if rc == 0 else '★RED — 중단'}", flush=True)
        if rc != 0:
            print("대조군이 GREEN 이 아니다. 중단한다.")
            return 1

    results = []
    for mid, relpath, old, new, kind, desc in MUTATIONS:
        path = os.path.join(WT, relpath)
        src = open(path, encoding="utf-8").read()
        # ★ no-op 선증명
        cnt = src.count(old)
        if cnt != 1:
            print(f"[{mid}] ★SKIP — 원문 count={cnt} (선증명 실패): {desc}", flush=True)
            results.append((mid, "SKIP", cnt, desc))
            continue
        bak = path + ".anubak"
        shutil.copy2(path, bak)
        try:
            open(path, "w", encoding="utf-8").write(src.replace(old, new, 1))
            after = open(path, encoding="utf-8").read()
            assert after != src, "치환이 no-op 이다"
            rc = oracle(kind)
            verdict = "KILL" if rc != 0 else "★SURVIVED"
            print(f"[{mid}] {verdict} (rc={rc}) — {desc}", flush=True)
            results.append((mid, verdict, rc, desc))
        finally:
            shutil.copy2(bak, path)
            os.remove(bak)
            assert open(path, encoding="utf-8").read() == src, f"{mid} 복원 실패!"

    print("\n=== 요약 ===")
    for mid, v, rc, desc in results:
        print(f"{mid:6} {v:12} {desc}")
    surv = [r for r in results if r[1] != "KILL"]
    print(f"\nKILL {len(results)-len(surv)}/{len(results)}")
    return 0


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