"""記事B テストの検出力の確認（改訂版 rev1、GPTレビュー R3 対応）

approval_v2.py に故意の誤った変更を1つずつ加えた一時コピー（変異版）を、一時フォルダの中だけに作り、
強化したテストがそれを検知（少なくとも1件失敗）するかを確かめる。
変異版はこのフォルダに残さない。正常版 approval_v2.py は変更しない。

使い方: python3 mutation_check.py
終了コード: 4種すべて検知できれば 0、検知できない変異があれば 1
"""
import os
import shutil
import subprocess
import sys
import tempfile

HERE = os.path.dirname(os.path.abspath(__file__))

MUTANTS = {
    # 1. UPDATE の状態条件と更新件数の検査を削除（保存先での守りを外す）
    "sql_guard_removed": [
        ("\"UPDATE requests SET state = '承認済み' WHERE id = ? AND state = '提出済み'\"",
         "\"UPDATE requests SET state = '承認済み' WHERE id = ?\""),
        ("            if cur.rowcount != 1:\n                raise RuntimeError(\"操作時点で対象の状態が有効でなかった\")\n            if _fault is not None:",
         "            if _fault is not None:"),
    ],
    # 2. 割当不一致で拒否する直前に、対象の既存履歴を削除して確定
    "rejection_deletes_history": [
        ("    if actor_id != approver_id:\n        return None, ApprovalResult(ok=False, reason=\"割り当てられた承認者ではない\")",
         "    if actor_id != approver_id:\n        conn.execute(\"DELETE FROM history WHERE request_id = ?\", (request_id,)); conn.commit()\n        return None, ApprovalResult(ok=False, reason=\"割り当てられた承認者ではない\")"),
    ],
    # 3. 理由なし差戻しを拒否する直前に、余分な履歴を追加して確定
    "empty_reason_adds_history": [
        ("    if not reason or not reason.strip():\n        return ApprovalResult(ok=False, reason=\"差戻しには理由が必要\")",
         "    if not reason or not reason.strip():\n        conn.execute(\"INSERT INTO history (request_id, action, actor_id, at) VALUES (?, '差戻し', ?, ?)\", (request_id, actor_id, _now())); conn.commit()\n        return ApprovalResult(ok=False, reason=\"差戻しには理由が必要\")"),
    ],
    # 4. 差戻し成功時の履歴の操作者を別人、時刻を空文字にする
    "sendback_wrong_actor": [
        ("                (request_id, actor_id, _now(), reason),",
         "                (request_id, \"u-takahashi\", \"\", reason),"),
    ],
}


def make_mutant(src, name, edits, dest_dir):
    text = open(src, encoding="utf-8").read()
    for old, new in edits:
        if old not in text:
            raise SystemExit(f"変異 {name}: 置換元がコードに見つからない（コードが変わった可能性）:\n{old}")
        text = text.replace(old, new, 1)
    path = os.path.join(dest_dir, f"mutant_{name}.py")
    with open(path, "w", encoding="utf-8") as f:
        f.write(f'"""一時的な変異版（故意に誤らせたコード）。教材として配布しない。変異: {name}"""\n' + text)
    return f"mutant_{name}"


def main():
    results = []
    with tempfile.TemporaryDirectory(prefix="sample-b-mutants-") as tmp:
        for fname in ("common.py", "test_approval.py"):
            shutil.copy2(os.path.join(HERE, fname), tmp)
        src = os.path.join(HERE, "approval_v2.py")
        for name, edits in MUTANTS.items():
            mod = make_mutant(src, name, edits, tmp)
            row = {"name": name}
            for suite in ("original", "additional"):
                p = subprocess.run([sys.executable, "test_approval.py", mod, "--suite", suite],
                                   cwd=tmp, capture_output=True, text=True)
                out = p.stdout + p.stderr   # unittest の詳細は標準エラー出力に出る
                summary = [l for l in out.splitlines() if l.startswith("結果:")]
                failed = [l.split()[1] for l in out.splitlines() if l.startswith(("FAIL:", "ERROR:"))]
                row[suite] = (p.returncode, summary[0] if summary else "(出力なし)", failed)
            results.append(row)
        print(f"変異版は一時フォルダ {tmp} に作り、終了時に削除する")

    print()
    print("| 変異（故意の誤変更） | 原版8件（強化後） | 追加4件 | 検知したテスト | 判定 |")
    print("|---|---|---|---|---|")
    all_detected = True
    for r in results:
        o_code, o_sum, o_failed = r["original"]
        a_code, a_sum, a_failed = r["additional"]
        detected = (o_code != 0) or (a_code != 0)
        all_detected &= detected
        names = ", ".join(sorted(set(o_failed + a_failed))) or "なし"
        print(f"| {r['name']} | {o_sum.replace('結果: ', '')} | {a_sum.replace('結果: ', '')} | {names} | {'検知' if detected else '見逃し'} |")
    print()
    print("すべて検知" if all_detected else "見逃しあり")
    sys.exit(0 if all_detected else 1)


if __name__ == "__main__":
    main()
