"""
test_quality_gates_integration.py — Dispatch Quality Gates 파이프라인 통합 테스트 (벨레스)

8개 시나리오로 개별 컴포넌트들이 함께 올바르게 동작하는지 검증.
- Scenario 1: "확인 불가" 보고서 → l1_smoketest FAIL
- Scenario 2: 수정 파일 심볼 역추적 → 미수정 파일 WARN
- Scenario 3: pytest FAIL → ci_preflight FAIL
- Scenario 4: goal_assertions 전부 PASS → GOAL-GATE PASS
- Scenario 5: goal_assertions 1개 FAIL → GOAL-GATE BLOCKED
- Scenario 6: 미해결 이슈 4개 → unresolved_gate BLOCK
- Scenario 7: gate-config mode=warn → WARN만 (block 없음)
- Scenario 8: 백틱 토큰 자동 추출 → affected_files 자동 주입
"""

import json
import os
import re
import subprocess
import sys
from pathlib import Path

import pytest

# ---------------------------------------------------------------------------
# sys.path 설정 — 워크스페이스 루트와 scripts 디렉터리 우선 삽입
# ---------------------------------------------------------------------------
WORKSPACE_ROOT = "/home/jay/workspace"

if WORKSPACE_ROOT not in sys.path:
    sys.path.insert(0, WORKSPACE_ROOT)
if "/home/jay/workspace/scripts" not in sys.path:
    sys.path.insert(0, "/home/jay/workspace/scripts")
# l1_smoketest_check 등 verifier 모듈 직접 임포트를 위한 경로
if "/home/jay/workspace/teams/shared/verifiers" not in sys.path:
    sys.path.insert(0, "/home/jay/workspace/teams/shared/verifiers")

# ---------------------------------------------------------------------------
# 모듈 임포트
# ---------------------------------------------------------------------------
from impact_scanner import scan  # noqa: E402
from l1_smoketest_check import verify as l1_verify  # noqa: E402
from dispatch import _auto_inject_affected_files  # noqa: E402
from utils.gate_config_loader import load_gate_config, is_gate_enabled, get_gate_mode  # noqa: E402

# ---------------------------------------------------------------------------
# 상수
# ---------------------------------------------------------------------------
GATE_CONFIG_PATH = os.path.join(WORKSPACE_ROOT, "config", "gate-config.json")
CI_PREFLIGHT_SCRIPT = os.path.join(WORKSPACE_ROOT, "scripts", "ci_preflight.sh")

ALLOWED_COMMANDS = {"grep", "curl", "pytest", "python3", "tsc", "cat", "jq", "npx", "npm"}


# ---------------------------------------------------------------------------
# 공통 픽스처
# ---------------------------------------------------------------------------

@pytest.fixture()
def gate_config_backup():
    """gate-config.json 내용을 백업하고, 테스트 종료 후 원본으로 복원한다."""
    original_content = Path(GATE_CONFIG_PATH).read_text(encoding="utf-8")
    yield
    Path(GATE_CONFIG_PATH).write_text(original_content, encoding="utf-8")


# ---------------------------------------------------------------------------
# 게이트 로직 헬퍼 (finish-task.sh 로직 파이썬 미러)
# ---------------------------------------------------------------------------

def _run_goal_assertions_gate(task_file_path: str) -> dict:
    """
    finish-task.sh의 goal_assertions 게이트 로직을 Python으로 미러링.

    Returns:
        dict with keys:
            - "result": "PASS" | "FAIL" | "BLOCKED" | "DISABLED"
            - "gate_mode": "warn" | "fail"
            - "commands_run": list of (cmd, exit_code)
    """
    if not is_gate_enabled("goal_assertions"):
        return {"result": "DISABLED", "gate_mode": get_gate_mode("goal_assertions"), "commands_run": []}

    gate_mode = get_gate_mode("goal_assertions")
    allowed_cmds = set(load_gate_config("goal_assertions").get("allowed_commands", list(ALLOWED_COMMANDS)))

    with open(task_file_path, "r", encoding="utf-8") as f:
        content = f.read()

    # goal_assertions 섹션에서 백틱 명령 추출 (finish-task.sh 파이썬 인라인 로직과 동일)
    m = re.search(r"## goal_assertions.*?\n(.*?)(?=\n##|\Z)", content, re.S)
    commands_run = []
    goal_fail = False

    if m:
        section_text = m.group(1).strip()
        for line in section_text.split("\n"):
            cmd_match = re.search(r"`([^`]+)`", line)
            if not cmd_match:
                continue
            cmd = cmd_match.group(1).strip()
            if not cmd:
                continue

            first_word = cmd.split()[0]
            if first_word not in allowed_cmds:
                # 화이트리스트 미해당 — SKIP
                continue

            proc = subprocess.run(
                cmd, shell=True, capture_output=True, text=True
            )
            commands_run.append((cmd, proc.returncode))

            if proc.returncode != 0:
                goal_fail = True

    if goal_fail:
        blocked = gate_mode == "fail"
        return {
            "result": "BLOCKED" if blocked else "FAIL",
            "gate_mode": gate_mode,
            "commands_run": commands_run,
        }

    return {"result": "PASS", "gate_mode": gate_mode, "commands_run": commands_run}


def _run_unresolved_gate(report_file_path: str) -> dict:
    """
    finish-task.sh의 unresolved_gate 로직을 Python으로 미러링.

    Returns:
        dict with keys:
            - "result": "PASS" | "WARN" | "BLOCKED" | "DISABLED"
            - "count": int
            - "max": int
            - "gate_mode": str
    """
    if not is_gate_enabled("unresolved_gate"):
        return {"result": "DISABLED", "count": 0, "max": 0, "gate_mode": get_gate_mode("unresolved_gate")}

    gate_mode = get_gate_mode("unresolved_gate")
    max_unresolved = load_gate_config("unresolved_gate").get("max_in_scope_unresolved", 3)

    count = 0
    if os.path.isfile(report_file_path):
        with open(report_file_path, "r", encoding="utf-8") as f:
            for line in f:
                if re.search(r"범위 내 미해결|in.scope.*unresolved", line, re.IGNORECASE):
                    count += 1

    if count > max_unresolved:
        blocked = gate_mode == "fail"
        return {
            "result": "BLOCKED" if blocked else "WARN",
            "count": count,
            "max": max_unresolved,
            "gate_mode": gate_mode,
        }
    elif count > 0:
        return {"result": "WARN", "count": count, "max": max_unresolved, "gate_mode": gate_mode}

    return {"result": "PASS", "count": count, "max": max_unresolved, "gate_mode": gate_mode}


# ===========================================================================
# Scenario 1: "확인 불가" 보고서 → l1_smoketest FAIL
# ===========================================================================

class TestScenario1_L1SmoketestBlockPattern:
    """BLOCK 패턴이 포함된 보고서는 l1_smoketest가 FAIL을 반환해야 한다."""

    def test_ui_block_pattern_triggers_fail(self, tmp_path):
        """'UI 직접 확인 불가' 문자열이 포함된 보고서 파일이 있으면 status == FAIL."""
        # 임시 workspace 구조 생성
        reports_dir = tmp_path / "memory" / "reports"
        reports_dir.mkdir(parents=True)
        task_id = "test-scenario-1"
        report_file = reports_dir / f"{task_id}.md"
        report_file.write_text(
            "# 작업 보고서\n\nUI 직접 확인 불가\n\n테스트 진행 불가 환경.\n",
            encoding="utf-8",
        )

        result = l1_verify(task_id, str(tmp_path))

        assert result["status"] == "FAIL", (
            f"BLOCK 패턴 'UI 직접 확인 불가' 포함 시 FAIL 이어야 하지만 실제: {result}"
        )
        # BLOCK 패턴 매칭 메시지가 details에 포함됨을 확인
        assert any("BLOCK 패턴" in d for d in result["details"]), (
            f"details에 BLOCK 패턴 언급 없음: {result['details']}"
        )

    def test_block_pattern_in_details(self, tmp_path):
        """details 필드에 매칭된 패턴 정보가 포함되어야 한다."""
        reports_dir = tmp_path / "memory" / "reports"
        reports_dir.mkdir(parents=True)
        task_id = "test-scenario-1b"
        report_file = reports_dir / f"{task_id}.md"
        report_file.write_text(
            "UI 직접 확인 불가 — 스크린샷 첨부 불가\n",
            encoding="utf-8",
        )

        result = l1_verify(task_id, str(tmp_path))

        assert result["status"] == "FAIL"
        # 패턴 문자열이 details에 반영
        details_combined = " ".join(result["details"])
        assert "UI 직접 확인 불가" in details_combined, (
            f"details에 패턴이 포함되어야 함: {details_combined}"
        )

    def test_no_report_file_returns_skip(self, tmp_path):
        """보고서 파일이 없으면 SKIP을 반환해야 한다 (경계 조건 확인)."""
        result = l1_verify("nonexistent-task-9999", str(tmp_path))
        assert result["status"] == "SKIP"


# ===========================================================================
# Scenario 2: 수정 파일 심볼 역추적 → 미수정 파일 WARN
# ===========================================================================

class TestScenario2_ImpactScannerWarn:
    """수정된 A.py의 심볼을 B.py가 참조하고, B.py는 미수정이면 WARN이어야 한다."""

    def test_unmodified_reference_causes_warn(self, tmp_path):
        """A.py 수정, B.py 미수정일 때 B.py에서 심볼 참조 감지 → WARN."""
        a_py = tmp_path / "A.py"
        b_py = tmp_path / "B.py"

        a_py.write_text(
            "def calculate_premium(base_rate, multiplier):\n"
            "    \"\"\"보험료 계산.\"\"\"\n"
            "    return base_rate * multiplier\n",
            encoding="utf-8",
        )
        b_py.write_text(
            "from A import calculate_premium\n\n"
            "monthly = calculate_premium(100, 1.5)\n",
            encoding="utf-8",
        )

        result = scan(str(tmp_path), ["A.py"])

        assert result["gate_result"] == "WARN", (
            f"미수정 파일 참조 감지 시 WARN 이어야 함. 실제: {result['gate_result']}"
        )
        # B.py가 unmodified_references에 포함되어야 함
        ref_files = [ref["file"] for ref in result["unmodified_references"]]
        b_py_abs = str(b_py.resolve())
        assert any(b_py_abs in rf or str(b_py) in rf for rf in ref_files), (
            f"B.py가 unmodified_references에 없음. refs: {ref_files}"
        )

    def test_symbols_checked_contains_function_name(self, tmp_path):
        """scan() 결과의 symbols_checked에 추출된 심볼명이 포함되어야 한다."""
        a_py = tmp_path / "A.py"
        b_py = tmp_path / "B.py"

        a_py.write_text(
            "def calculate_premium(x):\n    return x\n",
            encoding="utf-8",
        )
        b_py.write_text(
            "from A import calculate_premium\n",
            encoding="utf-8",
        )

        result = scan(str(tmp_path), ["A.py"])

        assert "calculate_premium" in result["symbols_checked"], (
            f"symbols_checked에 'calculate_premium' 없음: {result['symbols_checked']}"
        )

    def test_no_unmodified_references_is_pass(self, tmp_path):
        """미수정 파일에 참조가 없으면 PASS여야 한다."""
        a_py = tmp_path / "A.py"
        a_py.write_text(
            "def unique_isolated_function_xyz():\n    return 42\n",
            encoding="utf-8",
        )

        result = scan(str(tmp_path), ["A.py"])

        assert result["gate_result"] == "PASS", (
            f"참조 없으면 PASS 이어야 함. 실제: {result['gate_result']}"
        )


# ===========================================================================
# Scenario 3: pytest FAIL → ci_preflight FAIL
# ===========================================================================

class TestScenario3_CIPreflight:
    """항상 실패하는 pytest 테스트가 있는 디렉터리에 대해 ci_preflight.sh가 exit 1을 반환해야 한다."""

    def test_failing_pytest_causes_exit_1(self, tmp_path):
        """test_always_fail.py가 존재하는 프로젝트에서 exit code 1, overall=FAIL 확인."""
        # pytest 런너를 감지시키기 위한 requirements.txt
        (tmp_path / "requirements.txt").write_text("pytest\n", encoding="utf-8")
        # 항상 실패하는 테스트 파일
        (tmp_path / "test_always_fail.py").write_text(
            "def test_always_fail():\n    assert False, '항상 실패하는 테스트'\n",
            encoding="utf-8",
        )

        result = subprocess.run(
            ["bash", CI_PREFLIGHT_SCRIPT, str(tmp_path)],
            capture_output=True,
            text=True,
            timeout=60,
        )

        assert result.returncode == 1, (
            f"pytest FAIL 시 exit code 1 이어야 함. 실제: {result.returncode}\n"
            f"stdout: {result.stdout}\nstderr: {result.stderr}"
        )
        assert "overall=FAIL" in result.stdout, (
            f"stdout에 'overall=FAIL' 없음: {result.stdout}"
        )

    def test_no_test_files_causes_pass(self, tmp_path):
        """테스트 파일이 없으면 pytest가 SKIP되고 overall=PASS 또는 No runners detected가 출력되어야 한다."""
        # pyproject.toml로 pytest 런너 감지
        (tmp_path / "pyproject.toml").write_text(
            "[tool.pytest.ini_options]\n",
            encoding="utf-8",
        )
        # 테스트 파일 없음 — pytest exit code 5 (no tests collected) → SKIP → PASS

        result = subprocess.run(
            ["bash", CI_PREFLIGHT_SCRIPT, str(tmp_path)],
            capture_output=True,
            text=True,
            timeout=60,
        )

        assert result.returncode == 0, (
            f"테스트 파일 없을 때 exit 0 이어야 함. 실제: {result.returncode}\n"
            f"stdout: {result.stdout}"
        )

    def test_invalid_project_root_exits_1(self, tmp_path):
        """존재하지 않는 디렉터리를 PROJECT_ROOT로 전달하면 exit 1이어야 한다."""
        nonexistent = str(tmp_path / "does_not_exist")

        result = subprocess.run(
            ["bash", CI_PREFLIGHT_SCRIPT, nonexistent],
            capture_output=True,
            text=True,
            timeout=10,
        )

        assert result.returncode == 1, (
            f"존재하지 않는 PROJECT_ROOT는 exit 1 이어야 함. 실제: {result.returncode}"
        )


# ===========================================================================
# Scenario 4: goal_assertions 전부 PASS → GOAL-GATE PASS
# ===========================================================================

class TestScenario4_GoalAssertionsAllPass:
    """모든 goal_assertions 명령이 exit 0을 반환하면 GOAL-GATE PASS여야 한다."""

    def test_all_passing_assertions_result_in_pass(self, tmp_path):
        """grep으로 실제 존재하는 패턴을 검색 → exit 0 → PASS."""
        task_file = tmp_path / "task.md"
        task_file.write_text(
            "# task-integration-4\n\n"
            "## goal_assertions\n"
            f"- `grep -c \"def \" {WORKSPACE_ROOT}/scripts/impact_scanner.py`\n",
            encoding="utf-8",
        )

        result = _run_goal_assertions_gate(str(task_file))

        assert result["result"] == "PASS", (
            f"모든 assertion 통과 시 PASS 이어야 함. 실제: {result}"
        )
        assert len(result["commands_run"]) >= 1, "최소 1개 명령이 실행되어야 함"
        # 모든 명령의 exit code가 0이어야 함
        for cmd, exit_code in result["commands_run"]:
            assert exit_code == 0, f"명령 '{cmd}'의 exit code가 0이어야 함. 실제: {exit_code}"

    def test_multiple_passing_assertions_all_pass(self, tmp_path):
        """여러 assertion 명령이 모두 통과하면 PASS."""
        task_file = tmp_path / "task.md"
        task_file.write_text(
            "# task-integration-4b\n\n"
            "## goal_assertions\n"
            f"- `grep -c \"def \" {WORKSPACE_ROOT}/scripts/impact_scanner.py`\n"
            f"- `python3 -c \"import sys; sys.exit(0)\"`\n",
            encoding="utf-8",
        )

        result = _run_goal_assertions_gate(str(task_file))

        assert result["result"] == "PASS", f"모두 통과하면 PASS: {result}"
        assert len(result["commands_run"]) == 2, (
            f"2개 명령이 실행되어야 함. 실제: {len(result['commands_run'])}"
        )


# ===========================================================================
# Scenario 5: goal_assertions 1개 FAIL → GOAL-GATE BLOCKED
# ===========================================================================

class TestScenario5_GoalAssertionsFail:
    """mode=fail 상태에서 goal_assertions가 실패하면 BLOCKED 반환해야 한다."""

    def test_failing_grep_causes_blocked(self, tmp_path):
        """존재하지 않는 패턴을 grep → exit non-0 → mode=fail → BLOCKED."""
        task_file = tmp_path / "task.md"
        task_file.write_text(
            "# task-integration-5\n\n"
            "## goal_assertions\n"
            f"- `grep -c \"NONEXISTENT_PATTERN_xyz123\" {WORKSPACE_ROOT}/scripts/impact_scanner.py`\n",
            encoding="utf-8",
        )

        # gate-config의 goal_assertions mode가 "fail" 임을 먼저 확인
        mode = get_gate_mode("goal_assertions")
        assert mode == "fail", (
            f"이 시나리오는 mode=fail 전제. 실제 mode: {mode}"
        )

        result = _run_goal_assertions_gate(str(task_file))

        assert result["result"] == "BLOCKED", (
            f"mode=fail + assertion 실패 → BLOCKED 이어야 함. 실제: {result}"
        )

    def test_failing_assertion_exit_code_nonzero(self, tmp_path):
        """실패한 명령의 exit_code가 0이 아니어야 한다."""
        task_file = tmp_path / "task.md"
        task_file.write_text(
            "# task-integration-5b\n\n"
            "## goal_assertions\n"
            f"- `grep -c \"NONEXISTENT_PATTERN_xyz123\" {WORKSPACE_ROOT}/scripts/impact_scanner.py`\n",
            encoding="utf-8",
        )

        result = _run_goal_assertions_gate(str(task_file))

        assert len(result["commands_run"]) >= 1, "최소 1개 명령이 실행되어야 함"
        failing_cmds = [(cmd, ec) for cmd, ec in result["commands_run"] if ec != 0]
        assert len(failing_cmds) >= 1, (
            f"실패한 명령이 최소 1개 있어야 함. commands_run: {result['commands_run']}"
        )


# ===========================================================================
# Scenario 6: 미해결 이슈 4개 → unresolved_gate BLOCK
# ===========================================================================

class TestScenario6_UnresolvedGateBlock:
    """max_in_scope_unresolved=3인데 count=4이면 BLOCKED 반환해야 한다."""

    def test_four_unresolved_exceed_max_causes_blocked(self, tmp_path):
        """'범위 내 미해결' 4줄 → count(4) > max(3) → BLOCKED."""
        # gate config에서 max_in_scope_unresolved 확인
        max_val = load_gate_config("unresolved_gate").get("max_in_scope_unresolved", 3)
        assert max_val == 3, f"이 시나리오는 max=3 전제. 실제 max: {max_val}"

        report_file = tmp_path / "report.md"
        report_file.write_text(
            "# 보고서\n\n"
            "- 범위 내 미해결: 이슈 A\n"
            "- 범위 내 미해결: 이슈 B\n"
            "- 범위 내 미해결: 이슈 C\n"
            "- 범위 내 미해결: 이슈 D\n",
            encoding="utf-8",
        )

        # unresolved_gate mode를 일시적으로 fail로 변경하여 BLOCKED 검증
        original_config = Path(GATE_CONFIG_PATH).read_text(encoding="utf-8")
        try:
            config_data = json.loads(original_config)
            config_data["gates"]["unresolved_gate"]["mode"] = "fail"
            Path(GATE_CONFIG_PATH).write_text(
                json.dumps(config_data, indent=2, ensure_ascii=False), encoding="utf-8"
            )

            result = _run_unresolved_gate(str(report_file))
        finally:
            Path(GATE_CONFIG_PATH).write_text(original_config, encoding="utf-8")

        assert result["result"] == "BLOCKED", (
            f"count=4 > max=3 + mode=fail → BLOCKED 이어야 함. 실제: {result}"
        )
        assert result["count"] == 4, f"count는 4여야 함. 실제: {result['count']}"

    def test_three_unresolved_within_max_is_warn_or_pass(self, tmp_path):
        """'범위 내 미해결' 정확히 3줄 → count == max → WARN (초과 아님)."""
        report_file = tmp_path / "report.md"
        report_file.write_text(
            "# 보고서\n\n"
            "- 범위 내 미해결: 이슈 A\n"
            "- 범위 내 미해결: 이슈 B\n"
            "- 범위 내 미해결: 이슈 C\n",
            encoding="utf-8",
        )

        result = _run_unresolved_gate(str(report_file))

        # count == max 이므로 초과 아님 → WARN (not BLOCKED)
        assert result["result"] != "BLOCKED", (
            f"count=3 == max=3은 초과가 아님 → BLOCKED 아니어야 함. 실제: {result}"
        )
        assert result["count"] == 3

    def test_zero_unresolved_is_pass(self, tmp_path):
        """미해결 이슈 없으면 PASS."""
        report_file = tmp_path / "report.md"
        report_file.write_text("# 보고서\n\n모든 이슈 해결됨.\n", encoding="utf-8")

        result = _run_unresolved_gate(str(report_file))

        assert result["result"] == "PASS", f"미해결 없으면 PASS: {result}"
        assert result["count"] == 0


# ===========================================================================
# Scenario 7: gate-config mode=warn → WARN만 (block 없음)
# ===========================================================================

class TestScenario7_WarnModeNoBlock:
    """goal_assertions mode=warn이면 assertion 실패 시 BLOCKED가 아닌 FAIL(경고)만 반환해야 한다."""

    @pytest.mark.usefixtures("gate_config_backup")
    def test_warn_mode_failing_assertion_no_block(self, tmp_path):
        """
        gate-config.json goal_assertions mode를 warn으로 변경 후
        failing assertion 실행 → BLOCKED가 아닌 FAIL (경고) 반환.
        gate_config_backup 픽스처가 테스트 후 원본 자동 복원 보장.
        """
        # goal_assertions mode를 warn으로 임시 변경
        config_data = json.loads(Path(GATE_CONFIG_PATH).read_text(encoding="utf-8"))
        config_data["gates"]["goal_assertions"]["mode"] = "warn"
        Path(GATE_CONFIG_PATH).write_text(
            json.dumps(config_data, indent=2, ensure_ascii=False), encoding="utf-8"
        )

        task_file = tmp_path / "task.md"
        task_file.write_text(
            "# task-integration-7\n\n"
            "## goal_assertions\n"
            f"- `grep -c \"NONEXISTENT_PATTERN_xyz123\" {WORKSPACE_ROOT}/scripts/impact_scanner.py`\n",
            encoding="utf-8",
        )

        result = _run_goal_assertions_gate(str(task_file))

        # mode=warn이면 BLOCKED 아닌 FAIL (경고 수준)
        assert result["result"] != "BLOCKED", (
            f"mode=warn + assertion 실패 → BLOCKED 아니어야 함. 실제: {result}"
        )
        assert result["result"] == "FAIL", (
            f"mode=warn + assertion 실패 → FAIL 이어야 함 (블록 없음). 실제: {result}"
        )
        assert result["gate_mode"] == "warn", (
            f"gate_mode가 warn 이어야 함. 실제: {result['gate_mode']}"
        )

    @pytest.mark.usefixtures("gate_config_backup")
    def test_gate_config_restored_after_scenario7(self):
        """
        gate_config_backup 픽스처 동작 검증 — 테스트 후 원본 mode=fail 복원 확인.
        이 테스트는 gate_config_backup을 사용하여 설정을 변경 후 픽스처가 복원하는지 확인.
        """
        # mode를 warn으로 변경
        config_data = json.loads(Path(GATE_CONFIG_PATH).read_text(encoding="utf-8"))
        config_data["gates"]["goal_assertions"]["mode"] = "warn"
        Path(GATE_CONFIG_PATH).write_text(
            json.dumps(config_data, indent=2, ensure_ascii=False), encoding="utf-8"
        )

        # 변경 확인
        changed_mode = get_gate_mode("goal_assertions")
        assert changed_mode == "warn", f"변경 후 mode는 warn 이어야 함. 실제: {changed_mode}"

        # 픽스처가 teardown에서 복원할 것이므로 여기서는 변경된 상태 확인만 수행
        # (픽스처 teardown 후 다른 테스트에서 "fail"임을 확인할 수 있음)


# ===========================================================================
# Scenario 8: 백틱 토큰 자동 추출 → affected_files 자동 주입
# ===========================================================================

class TestScenario8_AutoInjectAffectedFiles:
    """FeatureGate 백틱 토큰이 있는 task_desc에 affected_files 섹션이 자동 주입되어야 한다."""

    def test_backtick_token_triggers_affected_files_injection(self):
        """`FeatureGate` 토큰 → grep → 2개 이상 파일 → ## affected_files (auto-detected) 섹션 생성."""
        task_desc = "다음 클래스를 확인하세요: `FeatureGate` 관련 변경 사항."

        result = _auto_inject_affected_files(task_desc, WORKSPACE_ROOT)

        assert "## affected_files (auto-detected)" in result, (
            f"'## affected_files (auto-detected)' 섹션이 없음.\n결과:\n{result}"
        )

    def test_at_least_two_files_detected(self):
        """FeatureGate는 tests/test_dispatch_auto_inject.py와 scripts/tests/test_impact_scanner.py에 존재 — 2개 이상 감지."""
        task_desc = "클래스 `FeatureGate`를 수정하세요."

        result = _auto_inject_affected_files(task_desc, WORKSPACE_ROOT)

        # affected_files 섹션 파싱
        section_match = re.search(
            r"## affected_files \(auto-detected\)\n(.*?)(?=\n##|\Z)",
            result,
            re.S,
        )
        assert section_match is not None, f"affected_files 섹션 없음:\n{result}"

        files_section = section_match.group(1).strip()
        file_lines = [line for line in files_section.split("\n") if line.startswith("- ")]
        assert len(file_lines) >= 2, (
            f"최소 2개 파일이 감지되어야 함. 감지된 파일:\n{files_section}"
        )

    def test_known_files_in_affected_list(self):
        """알려진 참조 파일(test_dispatch_auto_inject.py, test_impact_scanner.py)이 목록에 포함되어야 한다."""
        task_desc = "클래스 `FeatureGate`를 분석하세요."

        result = _auto_inject_affected_files(task_desc, WORKSPACE_ROOT)

        assert "test_dispatch_auto_inject.py" in result or "test_impact_scanner.py" in result, (
            f"알려진 FeatureGate 참조 파일이 affected_files에 없음.\n결과:\n{result}"
        )

    def test_no_injection_when_already_present(self):
        """이미 ## affected_files 섹션이 있으면 중복 주입하지 않아야 한다."""
        task_desc = (
            "클래스 `FeatureGate`를 수정하세요.\n\n"
            "## affected_files\n"
            "- src/feature_gate.py\n"
        )

        result = _auto_inject_affected_files(task_desc, WORKSPACE_ROOT)

        # 기존 섹션 유지, auto-detected 섹션 추가 없음
        assert "## affected_files (auto-detected)" not in result, (
            f"이미 affected_files 있으면 auto-detected 섹션 추가 없어야 함:\n{result}"
        )
        # 원본 섹션이 보존됨
        assert "## affected_files" in result

    def test_common_filter_tokens_not_injected(self):
        """COMMON_FILTER에 해당하는 토큰(예: `data`, `config`)은 grep 대상에서 제외되어야 한다."""
        # `data`와 `config`만 있으면 grep 대상 토큰이 없어 섹션이 추가되지 않아야 함
        # 단, 실제로 grep 결과가 있을 수도 있으므로 주입 여부보다 예외 없이 동작함을 검증
        task_desc = "다음 `data`와 `config`를 확인하세요."

        # 예외 없이 실행되어야 함
        result = _auto_inject_affected_files(task_desc, WORKSPACE_ROOT)

        assert isinstance(result, str), "반환값은 문자열이어야 함"


# ===========================================================================
# 게이트 설정 일관성 검증 (통합 회귀 방지)
# ===========================================================================

class TestGateConfigConsistency:
    """gate-config.json 설정이 기대값과 일치하는지 확인하는 회귀 방지 테스트."""

    def test_all_required_gates_enabled(self):
        """impact_scanner, ci_preflight, l1_smoketest, goal_assertions, unresolved_gate 모두 활성화."""
        required_gates = [
            "impact_scanner",
            "ci_preflight",
            "l1_smoketest",
            "goal_assertions",
            "unresolved_gate",
        ]
        for gate in required_gates:
            assert is_gate_enabled(gate), f"게이트 '{gate}'가 비활성화 상태"

    def test_goal_assertions_mode_is_fail(self):
        """goal_assertions 기본 mode는 'fail'이어야 한다."""
        mode = get_gate_mode("goal_assertions")
        assert mode == "fail", f"goal_assertions mode는 fail 이어야 함. 실제: {mode}"

    def test_l1_smoketest_mode_is_fail(self):
        """l1_smoketest 기본 mode는 'fail'이어야 한다."""
        mode = get_gate_mode("l1_smoketest")
        assert mode == "fail", f"l1_smoketest mode는 fail 이어야 함. 실제: {mode}"

    def test_unresolved_gate_max_is_three(self):
        """unresolved_gate의 max_in_scope_unresolved는 3이어야 한다."""
        cfg = load_gate_config("unresolved_gate")
        assert cfg.get("max_in_scope_unresolved") == 3, (
            f"max_in_scope_unresolved는 3이어야 함. 실제: {cfg}"
        )

    def test_goal_assertions_allowed_commands_whitelist(self):
        """goal_assertions allowed_commands에 핵심 명령어가 포함되어야 한다."""
        cfg = load_gate_config("goal_assertions")
        allowed = set(cfg.get("allowed_commands", []))
        expected = {"grep", "curl", "pytest", "python3"}
        missing = expected - allowed
        assert not missing, f"allowed_commands에 없는 명령: {missing}"
