#!/usr/bin/env python3
"""task-2973 — comparison_gate.py 봉인 3구멍(A/B/C) 독립 공격 재현 스크립트.

★★★ 이 스크립트는 InsuRo 리포 밖(워크스페이스)에만 존재한다. InsuRo 리포의
어떤 파일도 이 스크립트로 수정하지 않는다 — 대상 server 디렉토리는 sys.path 에
추가해 import 하는 용도로만 사용한다(읽기 전용).

사용법:
    python3 attack_repro.py <server_dir>

    <server_dir> 예:
        /home/jay/projects/InsuRo/.worktrees/task-2973-baseline/server
        (하드닝 적용본을 검증할 때는 하드닝된 worktree 의 server 디렉토리를 넘기면 됨)

공격 페이로드는 origin/main 커밋 a9b1e9c 의
``server/tests/test_grouping_gate_seal_task2970.py`` 에 있는 실제 테스트
fixture 를 그대로 따온 것이다(추측으로 구성하지 않음):
  - A: TestHardeningAxesSchemaValidation.
       test_maat_forged_entry_that_previously_passed_is_now_excluded
       (6축 스키마 손상 axes, axes_verified=True 로 위조)
  - B: TestHardeningModalityCaseInsensitive.
       test_lowercase_single_modality_trigger_detected_and_blocked
       (trigger 값 소문자 "single_modality")
  - B2(대조군): TestHardeningModalityCaseInsensitive.
       test_uppercase_exact_match_still_detected_regression
       (trigger 값 정상 대문자 "SINGLE_MODALITY")
  - C: TestHardeningNoExceptionLeaks.
       test_missing_map_state_key_excluded_without_raising
       (entry 에 map_state 키 자체가 없음)

예외를 삼키지 않는다 — try/except Exception 으로 잡되 타입/메시지를 그대로
출력한다. reason 문자열 판정(하드닝 여부 최종 판단)은 이 스크립트의 책임이
아니다 — 실측 결과를 그대로 출력만 한다.
"""

from __future__ import annotations

import json
import os
import sys
import traceback


def _axes_dict(*, trigger: str = "UNKNOWN", modality: str | None = None) -> dict:
    """test_grouping_gate_seal_task2970.py::_axes_dict 와 동일한 shape."""
    return {
        "trigger": {"value": trigger, "provenance": None},
        "benefit_scope": {"value": "UNKNOWN", "provenance": None},
        "independence": {"value": "UNKNOWN", "provenance": None},
        "payout_form": {"value": "UNKNOWN", "provenance": None},
        "target_scope": {"value": "UNKNOWN", "provenance": None},
        "amount_form": {"value": "UNKNOWN", "provenance": None},
        "detail_count": None,
        "hospital_condition": None,
        "modality": modality,
        "renewal": False,
        "notes": "",
    }


def main() -> int:
    if len(sys.argv) != 2:
        print("usage: python3 attack_repro.py <server_dir>", file=sys.stderr)
        return 2

    server_dir = os.path.abspath(sys.argv[1])
    if not os.path.isdir(server_dir):
        print(f"ERROR: server_dir not found: {server_dir}", file=sys.stderr)
        return 2

    sys.path.insert(0, server_dir)

    try:
        from policy_grouping import comparison_gate as gate_mod
    except Exception as e:  # noqa: BLE001 — import 실패 자체도 실측 결과
        print(f"IMPORT FAILED: {type(e).__name__}: {e}")
        traceback.print_exc()
        print("RESULT_JSON:" + json.dumps({"import_error": f"{type(e).__name__}: {e}"}))
        return 1

    MAJOR_PER_ITEM_GROUP_ID = getattr(gate_mod, "MAJOR_PER_ITEM_GROUP_ID", "TX_MAJOR_PER_ITEM")

    # ── 4개 공격 케이스 페이로드 (test_grouping_gate_seal_task2970.py 원본 그대로) ──
    cases: dict[str, dict] = {
        "A_forged_axes_schema": {
            "map_state": "CONFIRMED",
            "group_id": "TX_MAJOR_PER_ITEM",
            "axes": {"modality": None, "trigger": {"value": "GARBAGE"}},
            "axes_verified": True,
        },
        "B_single_modality_lowercase": {
            "map_state": "CONFIRMED",
            "group_id": MAJOR_PER_ITEM_GROUP_ID,
            "axes": _axes_dict(trigger="single_modality"),
            "axes_verified": True,
        },
        "B2_single_modality_uppercase_control": {
            "map_state": "CONFIRMED",
            "group_id": MAJOR_PER_ITEM_GROUP_ID,
            "axes": _axes_dict(trigger="SINGLE_MODALITY"),
            "axes_verified": True,
        },
        "C_missing_map_state_key": {
            "group_id": "DX_BASIC",
        },
    }

    results: dict[str, dict] = {}

    print(f"server_dir = {server_dir}")
    print(f"comparison_gate module file = {gate_mod.__file__}")
    print("-" * 78)

    for case_name, entry in cases.items():
        record: dict = {
            "case": case_name,
            "entry": entry,
            "exception": None,
            "exception_message": None,
            "decision": None,  # "included" | "excluded" | None(exception)
            "reason": None,
        }
        print(f"[{case_name}]")
        print(f"  entry = {entry}")
        try:
            result = gate_mod.gate_for_official_comparison([entry])
            if result.included:
                record["decision"] = "included"
                record["reason"] = None
                print("  -> included (no exclusion reason)")
            else:
                record["decision"] = "excluded"
                # excluded 는 GateResult.excluded: list[tuple[dict, str]]
                _entry_out, reason = result.excluded[0]
                record["reason"] = reason
                print(f"  -> excluded, reason = {reason!r}")
            print(f"  exception: none")
        except Exception as e:  # noqa: BLE001 — 예외 삼키지 않고 그대로 출력
            record["exception"] = type(e).__name__
            record["exception_message"] = str(e)
            print(f"  -> EXCEPTION LEAKED: {type(e).__name__}: {e}")
            traceback.print_exc()
        results[case_name] = record
        print("-" * 78)

    print("RESULT_JSON:" + json.dumps(results, ensure_ascii=False, default=str))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
