#!/usr/bin/env python3
"""task-3077 변이(봉인) 하네스.

★ 원칙 3가지 — 과거 사고에서 나온 것들이다.
  1) **대조군 먼저.** 변이를 넣기 전에 무변경 상태로 테스트를 돌려 GREEN 을 확인한다.
     이걸 건너뛰면 환경 문제로 인한 실패를 "변이가 잡혔다"로 오독한다.
  2) **git worktree 제자리에서** 돌린다. /tmp 로 복사하면 상대경로·패키지 해석이
     달라져 전건 거짓 KILL 이 난다.
  3) **주입 전 `assert 원문 in 소스`.** 치환이 no-op 이면 변이가 안 들어간 채로
     테스트가 통과하고, 그걸 "SURVIVED" 로 기록하면 정반대 결론이 나온다.
     여기서는 no-op 이면 그 변이를 ERROR 로 표시하고 SURVIVED 로 세지 않는다.
"""
import subprocess, sys, shutil, json
from pathlib import Path

WT = Path("/home/jay/workspace/.worktrees/task-3077-dev1")
MAIN = WT / "server" / "main.py"
METRICS = WT / "server" / "scripts" / "bench_harness" / "metrics.py"
GENOPTS = WT / "src" / "data" / "generateOptions.ts"
GENPAGE = WT / "src" / "pages" / "Generate.tsx"

PY_TESTS = [
    "tests/test_task3077_seo_mode.py",
    "tests/test_task3064_benchmark_grounding.py",
    "tests/test_task3070_image_section_and_timeout.py",
    "tests/test_task3074_image_instruction_unification.py",
]
HARNESS_TESTS = ["scripts/bench_harness/tests"]
TS_TESTS = [
    "src/components/generate/__tests__/SeoModeSelector.test.tsx",
    "src/pages/__tests__/Generate.seoMode.test.tsx",
]


def run_py(paths):
    r = subprocess.run(
        [sys.executable, "-m", "pytest", "-q", "-p", "no:randomly", *paths],
        cwd=WT / "server", capture_output=True, text=True,
    )
    return r.returncode, (r.stdout + r.stderr).strip().splitlines()[-1] if r.stdout else ""


def run_ts(paths):
    r = subprocess.run(
        ["./node_modules/.bin/vitest", "run", *paths],
        cwd=WT, capture_output=True, text=True,
    )
    return r.returncode, "vitest"


MUTATIONS = [
    # (id, 설명, 파일, 원문, 치환, 테스트러너, 테스트경로)
    ("M1", "모드 분기 전면 무력화 (organic 항상 False)", MAIN,
     "    organic = seo_mode_is_organic(seo_mode, benchmark_prompt)",
     "    organic = False", "py", PY_TESTS),
    ("M2", "★채널 프롬프트 변환만 무력화 (이 태스크 최대 함정)", MAIN,
     "        if organic:\n            channel_prompt = apply_seo_mode_to_channel_prompt(channel_prompt, seo_mode)",
     "        if False:\n            channel_prompt = apply_seo_mode_to_channel_prompt(channel_prompt, seo_mode)",
     "py", PY_TESTS),
    ("M3", "SEO 스킬(naver_seo/geo) 제외 무력화", MAIN,
     "        if organic and skill in SEO_SKILL_IDS:\n            continue",
     "        if False and skill in SEO_SKILL_IDS:\n            continue", "py", PY_TESTS),
    ("M4", "base/pro_blog 줄 필터 무력화 (no-op 반환)", MAIN,
     "    drop = set(SEO_SKILL_PROMPT_LINES_TO_DROP)",
     "    drop = set()", "py", PY_TESTS),
    ("M5", "기본값을 상위노출용으로 바꿈 (회장 확정 위반)", MAIN,
     "SEO_MODE_DEFAULT = SEO_MODE_ORGANIC_STRUCTURED",
     "SEO_MODE_DEFAULT = SEO_MODE_SEO", "py", PY_TESTS),
    ("M6", "최종 지시(⑦) 리마인더 제거", MAIN,
     "    if organic:\n        final_parts += build_organic_final_reminder_lines(seo_mode)",
     "    if False:\n        final_parts += build_organic_final_reminder_lines(seo_mode)",
     "py", PY_TESTS),
    ("M7", "모드 3 을 모드 2 와 동일하게 (소제목 금지 소멸)", MAIN,
     "        if mode == SEO_MODE_ORGANIC_PROSE\n        else _ORGANIC_SUBHEADING_STRUCTURED",
     "        if False\n        else _ORGANIC_SUBHEADING_STRUCTURED", "py", PY_TESTS),
    ("M8", "불가침 위반: organic 모드에서 환각 가드 제거", MAIN,
     "    parts: list[str] = [_BENCHMARK_HALLUCINATION_GUARD]",
     "    parts: list[str] = [] if normalize_seo_mode(seo_mode) in ORGANIC_SEO_MODES else [_BENCHMARK_HALLUCINATION_GUARD]",
     "py", PY_TESTS),
    ("M9", "키워드 배치 횟수 강제 복귀 (organic 분기 무력화)", MAIN,
     "    if normalize_seo_mode(seo_mode) in ORGANIC_SEO_MODES:\n        parts.append(\n            _BENCHMARK_PRIORITY_RULE_ORGANIC.format(",
     "    if False:\n        parts.append(\n            _BENCHMARK_PRIORITY_RULE_ORGANIC.format(",
     "py", PY_TESTS),
    ("M10", "카페 도입문 오탐 회귀 (goal 판정 → 단어 판정)", MAIN,
     "    if preamble.strip() and not _SEO_GOAL_DECLARATION_RE.search(preamble):",
     "    if preamble.strip() and not _SEO_SECTION_MARKER_RE.search(preamble):", "py", PY_TESTS),
    ("M11", "볼드 결함 되돌리기 (strip_emphasis no-op)", METRICS,
     "    prev = None\n    cur = line.strip()",
     "    return line.strip()\n    prev = None\n    cur = line.strip()", "py", HARNESS_TESTS),
    ("M12", "프론트 기본값을 상위노출용으로", GENOPTS,
     "export const DEFAULT_SEO_MODE: SeoMode = SEO_MODE_ORGANIC_STRUCTURED;",
     "export const DEFAULT_SEO_MODE: SeoMode = SEO_MODE_SEO;", "ts", TS_TESTS),
    ("M13", "★페이로드에서 seoMode 제거 (t3014 유형: 화면만 바뀌고 서버 미전달)", GENPAGE,
     "...(benchmarkPayload ? { benchmark: benchmarkPayload, seoMode } : {}),",
     "...(benchmarkPayload ? { benchmark: benchmarkPayload } : {}),", "ts", TS_TESTS),
    ("M14", "프론트 모드 값을 서버와 어긋나게 (조용한 계약 파손)", GENOPTS,
     'export const SEO_MODE_ORGANIC_PROSE = "organic_prose";',
     'export const SEO_MODE_ORGANIC_PROSE = "organic-prose";', "ts", TS_TESTS),
]


def main():
    results = []
    # ── 대조군 ──────────────────────────────────────────────
    print("=== 대조군 (무변경) ===")
    for label, runner, paths in (("py", run_py, PY_TESTS), ("harness", run_py, HARNESS_TESTS), ("ts", run_ts, TS_TESTS)):
        rc, last = runner(paths)
        print(f"  control[{label}] rc={rc} {last}")
        if rc != 0:
            print(f"  ★ 대조군이 실패했다 — 변이 판정을 진행하면 안 된다({label}).")
            return 1

    for mid, desc, path, orig, repl, kind, paths in MUTATIONS:
        src = path.read_text(encoding="utf-8")
        backup = src
        if src.count(orig) != 1:
            results.append((mid, desc, "ERROR", f"원문 매치 {src.count(orig)}건 — no-op 위험, 판정 안 함"))
            print(f"[{mid}] ERROR: 원문 미일치({src.count(orig)}건)")
            continue
        mutated = src.replace(orig, repl, 1)
        assert mutated != src, "치환 후 동일 = no-op"
        path.write_text(mutated, encoding="utf-8")
        try:
            rc, last = (run_py if kind in ("py",) else run_ts)(paths)
        finally:
            path.write_text(backup, encoding="utf-8")
        verdict = "KILLED" if rc != 0 else "SURVIVED"
        results.append((mid, desc, verdict, last))
        print(f"[{mid}] {verdict:9s} {desc}")

    print("\n=== 요약 ===")
    killed = sum(1 for r in results if r[2] == "KILLED")
    survived = [r for r in results if r[2] == "SURVIVED"]
    errored = [r for r in results if r[2] == "ERROR"]
    print(f"KILLED {killed}/{len(results)} · SURVIVED {len(survived)} · ERROR {len(errored)}")
    for r in survived: print(f"  SURVIVED: {r[0]} {r[1]}")
    for r in errored: print(f"  ERROR: {r[0]} {r[1]} — {r[3]}")
    Path("/tmp/t3077_mutation_result.json").write_text(
        json.dumps([{"id": a, "desc": b, "verdict": c, "note": d} for a, b, c, d in results],
                   ensure_ascii=False, indent=2), encoding="utf-8")
    # 복원 검증
    r = subprocess.run(["git", "status", "--porcelain"], cwd=WT, capture_output=True, text=True)
    print("\n복원 후 git status(변이 잔존 없어야 정상 diff 만 남는다):")
    print(r.stdout.strip()[:800])
    return 0


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