"""anu_v2.tests.test_auto_gemini_triage_2538 — 9 회귀 (회장 §명시 1:1 박제).

회귀 케이스 (회장 §명시):
  1. false_positive dismiss              — 회귀 fixture 매칭 시 dismiss
  2. style_only dismiss                  — 코드 동작 무관 style 권고 dismiss
  3. minor_fix_in_scope auto_apply       — expected_files 내부 minor fix 자동 적용
  4. scope_expansion escalate            — expected_files 밖 수정 요구 → Critical
  5. Security-High in scope              — Security 권고는 minor 무관 적용 (단 scope 내부)
  6. Security-High out of scope          — escalate
  7. interface contract                  — applied/dismissed/escalated 3 키 제공
  8. token raw 0                         — finding 처리 후 raw 토큰 노출 0
  9. chat=6937032012 격리                 — 다른 chat record 노출 0

본 회귀는 anu_v2/* 모듈만 import 한다 (one-way isolation).
"""

from __future__ import annotations

import sys
from pathlib import Path
from typing import Any, Mapping

# workspace root → sys.path (anu_v2 패키지를 절대 import 하기 위함)
WORKSPACE_ROOT = Path(__file__).resolve().parents[2]
if str(WORKSPACE_ROOT) not in sys.path:
    sys.path.insert(0, str(WORKSPACE_ROOT))

from anu_v2.auto_gemini_triage import (  # noqa: E402
    ACTION_AUTO_APPLY_MINOR_FIX,
    ACTION_DISMISS_FALSE_POSITIVE,
    ACTION_DISMISS_STYLE_ONLY,
    ACTION_ESCALATE_SCOPE_EXPANSION,
    ACTIONS,
    CRITICAL_GEMINI_SCOPE_EXPANSION,
    DEFAULT_CHAT_ID,
    TOKEN_KEY_HINTS,
    AutoGeminiTriage,
    FalsePositiveFixture,
    TriageResult,
    _redact_tokens,
)


# ─── helpers ────────────────────────────────────────────────────────────────
EXPECTED_FILES_DEFAULT = (
    "anu_v2/auto_gemini_triage.py",
    "anu_v2/tests/test_auto_gemini_triage_2538.py",
)


def _make_triage(
    *,
    audit_calls: list[Mapping[str, Any]] | None = None,
    fix_applier=None,
    fixtures=(),
    chat_id: str = DEFAULT_CHAT_ID,
    task_id: str = "task-2538",
) -> AutoGeminiTriage:
    if audit_calls is None:
        audit_calls = []

    def audit_writer(rec):
        audit_calls.append(dict(rec))

    return AutoGeminiTriage(
        audit_writer=audit_writer,
        fix_applier=fix_applier,
        false_positive_fixtures=fixtures,
        chat_id=chat_id,
        task_id=task_id,
    )


# ─── 1. false_positive dismiss ──────────────────────────────────────────────
def test_1_false_positive_dismiss_via_fixture() -> None:
    audit: list[Mapping[str, Any]] = []
    fixtures = (
        FalsePositiveFixture(rule_id="unused-import", reason="vendored stub"),
    )
    triage = _make_triage(audit_calls=audit, fixtures=fixtures)

    finding = {
        "rule_id": "unused-import",
        "severity": "Low",
        "category": "style",
        "path": "anu_v2/auto_gemini_triage.py",
        "title": "F401 unused import",
        "body": "module-level import not used",
    }
    action, details = triage.classify_evidence(finding, EXPECTED_FILES_DEFAULT)
    assert action == ACTION_DISMISS_FALSE_POSITIVE
    assert details["rule_id"] == "unused-import"
    assert details["fixture_reason"] == "vendored stub"


def test_1_false_positive_signature_regex_match() -> None:
    fixtures = (
        FalsePositiveFixture(
            rule_id="redundant-cast",
            signature=r"int\(int_value\)",
            reason="Python 3 idiom",
        ),
    )
    triage = _make_triage(fixtures=fixtures)

    matching = {"rule_id": "redundant-cast", "body": "int(int_value) is redundant"}
    non_matching = {"rule_id": "redundant-cast", "body": "different body content"}

    a1, _ = triage.classify_evidence(matching, EXPECTED_FILES_DEFAULT)
    a2, _ = triage.classify_evidence(non_matching, EXPECTED_FILES_DEFAULT)
    assert a1 == ACTION_DISMISS_FALSE_POSITIVE
    # signature 미매칭 → 일반 경로로 떨어짐 (in-scope 가 아닌 path 면 escalate)
    assert a2 != ACTION_DISMISS_FALSE_POSITIVE


# ─── 2. style_only dismiss ──────────────────────────────────────────────────
def test_2_style_only_dismiss() -> None:
    triage = _make_triage()
    finding = {
        "rule_id": "indent-not-multiple-of-four",
        "severity": "Low",
        "category": "style",
        "path": "anu_v2/auto_gemini_triage.py",
        "title": "indentation",
        "body": "use 4-space indent",
    }
    action, details = triage.classify_evidence(finding, EXPECTED_FILES_DEFAULT)
    assert action == ACTION_DISMISS_STYLE_ONLY
    assert details["category"] == "style"


def test_2_style_only_dismiss_even_when_out_of_scope() -> None:
    """style 권고는 path 가 scope 밖이어도 dismiss (코드 동작 무관)."""
    triage = _make_triage()
    finding = {
        "rule_id": "trailing-whitespace",
        "severity": "Low",
        "category": "style",
        "path": "utils/some_other.py",  # scope 밖
        "title": "trailing whitespace",
    }
    action, _ = triage.classify_evidence(finding, EXPECTED_FILES_DEFAULT)
    assert action == ACTION_DISMISS_STYLE_ONLY


# ─── 3. minor_fix_in_scope auto_apply ───────────────────────────────────────
def test_3_minor_fix_in_scope_auto_apply() -> None:
    audit: list[Mapping[str, Any]] = []
    triage = _make_triage(audit_calls=audit)
    finding = {
        "rule_id": "missing-type-annotation",
        "severity": "Medium",
        "category": "bug",
        "path": "anu_v2/auto_gemini_triage.py",
        "title": "add type hint",
    }
    action, details = triage.classify_evidence(finding, EXPECTED_FILES_DEFAULT)
    assert action == ACTION_AUTO_APPLY_MINOR_FIX
    assert details["path"] == "anu_v2/auto_gemini_triage.py"

    rec = triage.apply_minor_fix(finding, "anu_v2/auto_gemini_triage.py")
    assert rec["applied"] is True
    assert rec["escalated"] is False
    # audit 기록되어야 한다
    assert len(audit) == 1
    assert audit[0]["kind"] == "auto_apply_minor_fix"


def test_3_minor_fix_apply_failure_escalates() -> None:
    """fix_applier 가 False 반환 시 record 가 escalate 로 전환되어야 한다."""
    audit: list[Mapping[str, Any]] = []

    def failing_applier(finding: Mapping[str, Any], target_file: str) -> bool:
        _ = (finding, target_file)
        return False

    triage = _make_triage(audit_calls=audit, fix_applier=failing_applier)
    finding = {
        "rule_id": "x",
        "severity": "Medium",
        "category": "bug",
        "path": "anu_v2/auto_gemini_triage.py",
    }
    rec = triage.apply_minor_fix(finding, "anu_v2/auto_gemini_triage.py")
    assert rec["applied"] is False
    assert rec["escalated"] is True
    assert audit[0]["kind"] == "auto_apply_failed_escalated"


# ─── 4. scope_expansion escalate ────────────────────────────────────────────
def test_4_scope_expansion_escalate_critical() -> None:
    audit: list[Mapping[str, Any]] = []
    triage = _make_triage(audit_calls=audit)
    finding = {
        "rule_id": "missing-validation",
        "severity": "Medium",
        "category": "bug",
        "path": "utils/external_module.py",  # expected_files 밖
        "title": "add input validation",
    }
    action, details = triage.classify_evidence(finding, EXPECTED_FILES_DEFAULT)
    assert action == ACTION_ESCALATE_SCOPE_EXPANSION
    assert details["reason"] == "scope_expansion_required"

    rec = triage.escalate_scope_expansion(finding)
    assert rec["critical_code"] == CRITICAL_GEMINI_SCOPE_EXPANSION
    assert rec["kind"] == "scope_expansion_critical"
    assert audit[0]["critical_code"] == CRITICAL_GEMINI_SCOPE_EXPANSION


# ─── 5. Security-High in scope ──────────────────────────────────────────────
def test_5_security_high_in_scope_auto_applies() -> None:
    """Security-High 권고는 style/minor 분류와 무관하게 우선 적용 (scope 내부)."""
    triage = _make_triage()
    finding = {
        "rule_id": "owasp-a02-injection",
        "severity": "High",
        "category": "security",
        "path": "anu_v2/auto_gemini_triage.py",
        "title": "SQL-like string concatenation",
    }
    action, details = triage.classify_evidence(finding, EXPECTED_FILES_DEFAULT)
    assert action == ACTION_AUTO_APPLY_MINOR_FIX
    assert details["reason"] == "security_high_in_scope"


# ─── 6. Security-High out of scope ──────────────────────────────────────────
def test_6_security_high_out_of_scope_escalates() -> None:
    """Security-High 라도 scope 밖이면 절대 자동 적용 X — 회장 보고."""
    audit: list[Mapping[str, Any]] = []
    triage = _make_triage(audit_calls=audit)
    finding = {
        "rule_id": "owasp-a01-broken-access",
        "severity": "High",
        "category": "security",
        "path": "utils/auth.py",   # scope 밖
        "title": "auth bypass",
    }
    action, details = triage.classify_evidence(finding, EXPECTED_FILES_DEFAULT)
    assert action == ACTION_ESCALATE_SCOPE_EXPANSION
    assert details["reason"] == "security_high_out_of_scope"

    rec = triage.escalate_scope_expansion(finding)
    assert rec["critical_code"] == CRITICAL_GEMINI_SCOPE_EXPANSION


# ─── 7. interface contract — applied/dismissed/escalated 3 키 ──────────────
def test_7_triage_batch_interface_contract_keys() -> None:
    triage = _make_triage(
        fixtures=(FalsePositiveFixture(rule_id="known-fp", reason="fixture"),),
    )
    findings = [
        # false_positive
        {"rule_id": "known-fp", "severity": "Low", "category": "style",
         "path": "anu_v2/auto_gemini_triage.py", "body": "x"},
        # style_only
        {"rule_id": "indent", "severity": "Low", "category": "style",
         "path": "anu_v2/auto_gemini_triage.py"},
        # minor_fix_in_scope
        {"rule_id": "type-hint", "severity": "Medium", "category": "bug",
         "path": "anu_v2/auto_gemini_triage.py"},
        # scope_expansion
        {"rule_id": "perf-x", "severity": "Medium", "category": "performance",
         "path": "utils/perf.py"},
    ]
    out = triage.triage_batch(findings, EXPECTED_FILES_DEFAULT)

    # 정확히 3 키 (그 외 키 노출 금지)
    assert set(out.keys()) == {"applied", "dismissed", "escalated"}
    assert len(out["dismissed"]) == 2  # false_positive + style_only
    assert len(out["applied"]) == 1
    assert len(out["escalated"]) == 1
    assert out["escalated"][0]["critical_code"] == CRITICAL_GEMINI_SCOPE_EXPANSION


def test_7_triage_batch_empty_findings_returns_empty_lists() -> None:
    triage = _make_triage()
    out = triage.triage_batch([], EXPECTED_FILES_DEFAULT)
    assert out == {"applied": [], "dismissed": [], "escalated": []}


def test_7_triage_result_has_escalation_flag() -> None:
    """has_escalation 은 Critical 보고 대상 존재 여부를 즉시 반환."""
    r = TriageResult()
    assert r.has_escalation is False
    r.escalated.append({"critical_code": CRITICAL_GEMINI_SCOPE_EXPANSION})
    assert r.has_escalation is True


# ─── 8. token raw 0 ─────────────────────────────────────────────────────────
def test_8_token_raw_zero_in_audit_and_result() -> None:
    """finding 안에 BOT_GITHUB_TOKEN raw 가 박혀 들어와도 audit/결과에 노출되지 않는다."""
    audit: list[Mapping[str, Any]] = []
    triage = _make_triage(audit_calls=audit)
    leaked_finding = {
        "rule_id": "leak-x",
        "severity": "Medium",
        "category": "bug",
        "path": "anu_v2/auto_gemini_triage.py",
        "title": "Token leak in log",
        "body": "found GH_TOKEN=ghs_LEAKED1234567890",
        "BOT_GITHUB_TOKEN": "ghs_should_be_redacted_xxxxx",
        "evidence": {"github_token": "ghp_owner_pat_should_be_redacted"},
    }
    out = triage.triage_batch([leaked_finding], EXPECTED_FILES_DEFAULT)

    # 결과 dump 안에 raw 토큰 prefix 가 어디에도 등장 0 회
    blob = repr(out) + repr(audit)
    assert "ghs_should_be_redacted" not in blob
    assert "ghp_owner_pat" not in blob
    assert "ghs_LEAKED1234567890" not in blob
    # raw 0: redaction sentinel 만 노출
    assert "***REDACTED***" in blob


def test_8_redact_tokens_helper_handles_nested_structures() -> None:
    raw = {
        "ok": True,
        "GITHUB_TOKEN": "ghs_RAW_TOKEN_xxxx",
        "nested": {"x-api-key": "ghp_xxxxxxxxxxxxxxxxxxxx"},
        "list_field": ["fine", "ghs_inside_string_value"],
    }
    redacted = _redact_tokens(raw)
    s = repr(redacted)
    assert "ghs_RAW_TOKEN" not in s
    assert "ghp_xxxxxxxxxxxxxxxxxxxx" not in s
    assert "ghs_inside_string_value" not in s
    assert redacted["GITHUB_TOKEN"] == "***REDACTED***"


# ─── 9. chat=6937032012 격리 ────────────────────────────────────────────────
def test_9_chat_isolation_filters_other_chat_records() -> None:
    """list_chat_audit 가 본 인스턴스 chat_id 외 record 를 0건 노출."""
    triage = _make_triage(chat_id="6937032012")
    mixed_records = [
        {"chat_id": "6937032012", "kind": "auto_apply_minor_fix", "rule_id": "a"},
        {"chat_id": "9999999999", "kind": "auto_apply_minor_fix", "rule_id": "b"},
        {"chat_id": "6937032012", "kind": "scope_expansion_critical", "rule_id": "c"},
        # chat_id 누락은 차단
        {"kind": "auto_apply_minor_fix", "rule_id": "d"},
    ]
    visible = triage.list_chat_audit(mixed_records)
    assert len(visible) == 2
    assert {r["rule_id"] for r in visible} == {"a", "c"}
    # 다른 chat record 의 rule_id 가 결과에 등장 0
    blob = repr(visible)
    assert '"b"' not in blob
    assert "'b'" not in blob


def test_9_audit_records_tagged_with_chat_id_at_write_time() -> None:
    """audit_writer 호출 시점에 chat_id 가 박혀 있어야 list_chat_audit 가 격리 가능."""
    audit: list[Mapping[str, Any]] = []
    triage = _make_triage(audit_calls=audit, chat_id="6937032012")
    finding = {
        "rule_id": "z", "severity": "Medium", "category": "bug",
        "path": "anu_v2/auto_gemini_triage.py",
    }
    triage.triage_batch([finding], EXPECTED_FILES_DEFAULT)
    # 모든 audit record 에 동일 chat_id 박힘
    assert all(r.get("chat_id") == "6937032012" for r in audit)


def test_9_default_chat_id_is_chairman_chat() -> None:
    """회장 §명시: default chat=6937032012."""
    assert DEFAULT_CHAT_ID == "6937032012"


# ─── 통합 보너스: 전체 ACTIONS v0 4종 + task-2558 §명시 확장 ────────────────
def test_actions_set_is_exactly_four() -> None:
    """회장 §명시 v0 4 분류는 보존 (제거/이름 변경 금지).

    task-2558 §명시 (2026-05-12) 가 ACTIONS 를 5번째 분류 (minor_in_expected_files) +
    cascade reply+resolve 용으로 확장. v0 doctrine 4 action 자체는 변형 0 이어야 함.
    """
    v0_actions = frozenset({
        ACTION_DISMISS_FALSE_POSITIVE,
        ACTION_DISMISS_STYLE_ONLY,
        ACTION_AUTO_APPLY_MINOR_FIX,
        ACTION_ESCALATE_SCOPE_EXPANSION,
    })
    # v0 4종은 1:1 보존
    assert v0_actions.issubset(ACTIONS)
    # ACTIONS 는 v0 외 추가 entry 를 허용 (task-2558 §명시 확장)
    assert len(ACTIONS) >= 4


def test_is_in_scope_handles_glob_patterns() -> None:
    """expected_files 가 glob 패턴인 경우도 매칭 정확 (`anu_v2/**`)."""
    triage = _make_triage()
    expected = {"anu_v2/**", "tests/**/*.py"}
    assert triage.is_in_scope("anu_v2/auto_gemini_triage.py", expected) is True
    assert triage.is_in_scope("anu_v2/sub/x.py", expected) is True
    assert triage.is_in_scope("tests/regression/test_x.py", expected) is True
    assert triage.is_in_scope("utils/forbidden.py", expected) is False
    # 빈 path → False (자동 적용 거부)
    assert triage.is_in_scope("", expected) is False


def test_classify_evidence_returns_only_known_actions() -> None:
    """모든 분류 결과가 ACTIONS 집합 내부 값만 반환 (typo 방지)."""
    triage = _make_triage()
    findings = [
        {"rule_id": "x", "severity": "Low", "category": "style", "path": "a/b.py"},
        {"rule_id": "y", "severity": "Medium", "category": "bug",
         "path": "anu_v2/auto_gemini_triage.py"},
        {"rule_id": "z", "severity": "High", "category": "security",
         "path": "anu_v2/auto_gemini_triage.py"},
        {"rule_id": "w", "severity": "Medium", "category": "performance",
         "path": "external/x.py"},
    ]
    for f in findings:
        action, _ = triage.classify_evidence(f, EXPECTED_FILES_DEFAULT)
        assert action in ACTIONS


def test_executor_contract_dict_signature_matches_task_2531() -> None:
    """task-2531 executor 가 기대하는 dict 형식 (3 키) 1:1 박제."""
    triage = _make_triage()
    out = triage.triage_batch([], EXPECTED_FILES_DEFAULT)
    # task-2531 finalize §8 호출부가 기대하는 정확한 키 셋
    assert "applied" in out
    assert "dismissed" in out
    assert "escalated" in out
    # 3 키 외 키 없음
    assert len(out) == 3
    # 모두 list 타입 (json 직렬화 안전)
    for v in out.values():
        assert isinstance(v, list)


# ─── Gemini 1차 리뷰 박제 (3 medium 자동 수용) ─────────────────────────────
def test_gemini_medium_token_hints_includes_github_pat() -> None:
    """Gemini medium #1: TOKEN_KEY_HINTS 에 `github_pat_` 포함 박제.

    회귀 박제: dict 키 기반 마스킹과 문자열 값 기반 마스킹의 일관성.
    Gemini 6차 Security-High 이후로 key 자체에도 redact 적용됨.
    """
    assert "github_pat_" in TOKEN_KEY_HINTS
    # 안전한 key 이름으로 hint 매칭 — value 가 마스킹되는지 확인
    raw = {"my_github_token_field": "github_pat_LEAKED_xxxxxxxxxxxx"}
    redacted = _redact_tokens(raw)
    # key 매칭 hint ("github_token") 으로 value 자동 마스킹
    assert redacted["my_github_token_field"] == "***REDACTED***"


def test_gemini_medium_no_unused_audit_dir_attribute() -> None:
    """Gemini medium #2: audit_dir 인자 제거 박제 — 미사용 속성 0건.

    회귀: AutoGeminiTriage 인스턴스에 _audit_dir 속성이 존재하지 않음을 확인.
    """
    triage = _make_triage()
    assert not hasattr(triage, "_audit_dir")


def test_gemini_medium_is_in_scope_skips_renormalize_on_set() -> None:
    """Gemini medium #3: is_in_scope 가 이미 set 인 expected_files 재정규화 생략.

    회귀: 동일 set 인스턴스를 반복 전달해도 복사가 일어나지 않음 (id 비교).
    `triage_batch` 핫패스 최적화의 박제.
    """
    triage = _make_triage()
    expected_set: set[str] = {"anu_v2/auto_gemini_triage.py"}

    # 모듈 함수 호출 카운트 증가가 없도록, 직접 결과만 검증
    assert triage.is_in_scope("anu_v2/auto_gemini_triage.py", expected_set) is True
    assert triage.is_in_scope("utils/x.py", expected_set) is False
    # tuple/list 입력은 정규화 대상 — 문자열 strip 후 일치 확인
    assert triage.is_in_scope(
        "anu_v2/auto_gemini_triage.py",
        ("  anu_v2/auto_gemini_triage.py  ",),
    ) is True


# ─── Gemini 2차 리뷰 박제 (2 medium 자동 수용) ─────────────────────────────
def test_gemini_medium_docs_category_dismissed_as_style_only() -> None:
    """Gemini 2차 medium #1: STYLE_ONLY_CATEGORIES 에 `docs` 포함 박제.

    docs 는 코드 동작 무관 → low/medium severity 일 때 dismiss.
    Security-High 우선 분기가 high docs 케이스를 먼저 차단하므로 안전.
    """
    triage = _make_triage()
    # low severity docs → dismiss
    docs_finding = {
        "rule_id": "missing-docstring",
        "severity": "Low",
        "category": "docs",
        "path": "anu_v2/auto_gemini_triage.py",
        "title": "module-level docstring missing",
    }
    action, _ = triage.classify_evidence(docs_finding, EXPECTED_FILES_DEFAULT)
    assert action == ACTION_DISMISS_STYLE_ONLY

    # medium severity docs → 여전히 dismiss
    docs_med = {**docs_finding, "severity": "Medium"}
    action, _ = triage.classify_evidence(docs_med, EXPECTED_FILES_DEFAULT)
    assert action == ACTION_DISMISS_STYLE_ONLY


def test_gemini_medium_classify_evidence_skips_renormalize_on_set() -> None:
    """Gemini 2차 medium #2: classify_evidence 도 set 입력 재정규화 생략 박제.

    `triage_batch` 가 한 번 정규화한 set 을 매 finding 처리에 그대로 전달하므로
    재정규화 비용 0 — 핫패스 최적화.
    """
    triage = _make_triage()
    pre_normalized: set[str] = {"anu_v2/auto_gemini_triage.py"}
    finding = {
        "rule_id": "y", "severity": "Medium", "category": "bug",
        "path": "anu_v2/auto_gemini_triage.py",
    }
    # 정상 동작: 동일 set 입력으로도 분류 정확
    action, _ = triage.classify_evidence(finding, pre_normalized)
    assert action == ACTION_AUTO_APPLY_MINOR_FIX

    # 같은 set 을 두 번 더 사용해도 분류 결과 변화 없음 (set 가 mutate 되지 않음)
    action2, _ = triage.classify_evidence(finding, pre_normalized)
    action3, _ = triage.classify_evidence(finding, pre_normalized)
    assert action2 == action3 == ACTION_AUTO_APPLY_MINOR_FIX
    assert pre_normalized == {"anu_v2/auto_gemini_triage.py"}


# ─── Gemini 3차 리뷰 박제 (2 medium 자동 수용) ─────────────────────────────
def test_gemini_medium_redact_lowers_value_once() -> None:
    """Gemini 3차 medium #1: _redact_tokens 가 value.lower() 를 루프당 1회만 호출.

    회귀 fixture: prefix 검출 결과는 동일하되, 동일 결과를 보장.
    """
    # 정확성 회귀 — 다양한 케이스에서 redaction 결과 정확
    assert _redact_tokens("ghp_TEST_abc123") == "***REDACTED***"
    assert _redact_tokens("GHP_UPPERCASE_xx") == "***REDACTED***"  # lowercase 변환 정확
    assert _redact_tokens("github_pat_TEST") == "***REDACTED***"
    assert _redact_tokens("normal text") == "normal text"
    assert _redact_tokens("ghs_BOT_session_") == "***REDACTED***"


def test_gemini_security_high_dict_key_itself_is_redacted() -> None:
    """Gemini 6차 Security-High #1: dict key 자체에 박힌 raw token 도 redact.

    예전 동작: 끼='ghp_TOKEN' 인 경우 끼는 raw 그대로 노출 (token leak).
    수정 후: key 자체에 _redact_tokens 재귀 → '***REDACTED***' 로 마스킹.
    """
    raw = {"ghp_LEAKED_KEY_NAME": "ok-value"}
    redacted = _redact_tokens(raw)
    # key 가 더 이상 raw 토큰으로 노출되지 않음
    assert "ghp_LEAKED_KEY_NAME" not in redacted
    assert "***REDACTED***" in redacted
    blob = repr(redacted)
    assert "ghp_LEAKED_KEY_NAME" not in blob


def test_gemini_security_high_supports_ordereddict_mapping() -> None:
    """Gemini 6차 Security-High #2: dict 외 Mapping (OrderedDict 등) 도 동일 처리."""
    from collections import OrderedDict

    raw = OrderedDict()
    raw["GITHUB_TOKEN"] = "ghs_LEAKED_xxx"
    raw["safe_field"] = "ok"
    redacted = _redact_tokens(raw)
    # OrderedDict 입력도 정상 처리 (이전엔 isinstance(value, dict) 만 검사 → bypass 됐음)
    assert redacted["GITHUB_TOKEN"] == "***REDACTED***"
    assert redacted["safe_field"] == "ok"


def test_gemini_medium_redact_uses_compiled_regex_ignorecase() -> None:
    """Gemini 5차 medium: _TOKEN_VALUE_RE 컴파일 IGNORECASE 정규식 박제.

    re.IGNORECASE + compiled regex 로 lower() 복사본 생성 + substring 루프 제거.
    동작 회귀: case-insensitive 매칭 결과는 동일.
    """
    from anu_v2.auto_gemini_triage import _TOKEN_VALUE_RE
    # 컴파일된 정규식 자체 검증
    assert _TOKEN_VALUE_RE.search("ghp_xxx") is not None
    assert _TOKEN_VALUE_RE.search("GHP_UPPER_xxx") is not None
    assert _TOKEN_VALUE_RE.search("Ghs_Mixed_Case") is not None
    assert _TOKEN_VALUE_RE.search("github_PAT_xxx") is not None
    assert _TOKEN_VALUE_RE.search("normal text") is None
    # 큰 body 입력에서도 redact 정확성 회귀 (대소문자 혼합)
    big_body = "PREFIX " * 1000 + "GHP_LEAKED_xxx" + " SUFFIX " * 1000
    assert _redact_tokens(big_body) == "***REDACTED***"


def test_gemini_medium_match_fp_lazy_body_init_with_no_signature_fixtures() -> None:
    """Gemini 4차 medium: signature=None fixture 만 있을 때 body 문자열 생성 생략 박제.

    body 가 lazy init 라도 분류 결과는 정확해야 한다.
    """
    fixtures = (
        FalsePositiveFixture(rule_id="rule-only-1", reason="signature 없음"),
        FalsePositiveFixture(rule_id="rule-only-2", reason="signature 없음 2"),
    )
    triage = _make_triage(fixtures=fixtures)
    finding = {
        "rule_id": "rule-only-1",
        "severity": "Medium",
        "category": "bug",
        "path": "anu_v2/auto_gemini_triage.py",
        # body / title 누락 — lazy init 시 생성되지 않으므로 KeyError 없음
    }
    action, _ = triage.classify_evidence(finding, EXPECTED_FILES_DEFAULT)
    assert action == ACTION_DISMISS_FALSE_POSITIVE


def test_gemini_medium_empty_signature_does_not_match_all() -> None:
    """Gemini 3차 medium #2: fp.signature == \"\" 가 모든 finding 매칭하지 않게 가드.

    빈 문자열 signature 는 re.search 가 항상 매칭하는 함정 회귀 박제.
    """
    fixtures = (
        FalsePositiveFixture(rule_id="empty-sig-rule", signature="", reason="empty"),
        FalsePositiveFixture(rule_id="real-fp", signature=None, reason="rule-only"),
    )
    triage = _make_triage(fixtures=fixtures)

    # signature="" 인 fixture 는 모든 body 와 매칭 ❌
    candidate = {
        "rule_id": "empty-sig-rule",
        "severity": "Medium",
        "category": "bug",
        "path": "anu_v2/auto_gemini_triage.py",
        "body": "any random body content",
    }
    action, _ = triage.classify_evidence(candidate, EXPECTED_FILES_DEFAULT)
    # 빈 signature → false_positive 매칭되지 않아야 함 → in-scope minor_fix
    assert action == ACTION_AUTO_APPLY_MINOR_FIX

    # signature=None 인 rule-only fixture 는 정상 매칭
    candidate2 = {
        "rule_id": "real-fp",
        "severity": "Medium",
        "category": "bug",
        "path": "anu_v2/auto_gemini_triage.py",
        "body": "anything",
    }
    action2, _ = triage.classify_evidence(candidate2, EXPECTED_FILES_DEFAULT)
    assert action2 == ACTION_DISMISS_FALSE_POSITIVE


# ─── Gemini 7차 회귀 박제 ─────────────────────────────────────────────────────
def test_gemini_7_medium_redact_set_and_frozenset_normalizes_to_list():
    """Gemini 7차 medium #1 박제: set/frozenset 안의 token 도 마스킹되어야 한다."""
    from anu_v2.auto_gemini_triage import _redact_tokens

    result = _redact_tokens({"keys": {"ghp_LEAK", "safe"}})
    redacted_list = result["keys"]
    assert isinstance(redacted_list, list)
    # set 내부 token 마스킹 확인
    assert "***REDACTED***" in redacted_list
    assert "ghp_LEAK" not in redacted_list

    fset_result = _redact_tokens(frozenset({"ghs_TOKEN", "ok"}))
    assert isinstance(fset_result, list)
    assert "***REDACTED***" in fset_result
    assert "ghs_TOKEN" not in fset_result


def test_gemini_7_medium_fixture_regex_error_emits_audit_record():
    """Gemini 7차 medium #2 박제: 잘못된 regex fixture 는 audit 로깅 후 skip."""
    from anu_v2.auto_gemini_triage import AutoGeminiTriage, FalsePositiveFixture

    audit_calls: list[dict] = []

    def audit_writer(rec):
        audit_calls.append(dict(rec))

    triage = AutoGeminiTriage(
        audit_writer=audit_writer,
        chat_id=6937032012,
    )
    # 잘못된 regex (unmatched paren) fixture 등록
    triage._fixtures = (
        FalsePositiveFixture(rule_id="bad-rule", signature="(unclosed"),
    )

    finding = {
        "rule_id": "bad-rule",
        "body": "any body",
        "title": "any title",
        "path": "anu_v2/auto_gemini_triage.py",
    }
    # silent skip 대신 audit 기록되어야 함
    matched = triage._match_false_positive("bad-rule", finding)
    assert matched is None

    # audit 호출 확인
    kinds = [rec.get("kind") for rec in audit_calls]
    assert "fixture_regex_error" in kinds
