#!/usr/bin/env python3
"""헤임달(개발2팀 테스터): codex_gate_check 테스트 선행 작성 — TDD RED 단계.

대상: /home/jay/workspace/scripts/codex_gate_check.py
함수: codex_gate_check(task_file, affected_files, workspace_root) -> dict
"""

import json
import subprocess
import sys
from pathlib import Path
from unittest.mock import MagicMock, patch

# scripts 디렉토리를 sys.path에 추가
sys.path.insert(0, str(Path(__file__).parent.parent))

from codex_gate_check import codex_gate_check, _maat_fallback_check  # type: ignore[import-untyped]  # noqa: E402

# ---------------------------------------------------------------------------
# 헬퍼: subprocess.run 모킹용 가짜 응답 생성
# ---------------------------------------------------------------------------


def _make_codex_result(risks: list, suggestions: list | None = None) -> MagicMock:
    """Codex CLI가 정상 반환하는 JSON stdout을 모킹한다."""
    payload = {
        "risks": risks,
        "suggestions": suggestions or [],
    }
    mock_proc = MagicMock()
    mock_proc.returncode = 0
    mock_proc.stdout = json.dumps(payload)
    mock_proc.stderr = ""
    return mock_proc


def _make_maat_result(risks: list, suggestions: list | None = None) -> MagicMock:
    """마아트 폴백이 반환하는 JSON stdout을 모킹한다."""
    payload = {
        "risks": risks,
        "suggestions": suggestions or [],
    }
    mock_proc = MagicMock()
    mock_proc.returncode = 0
    mock_proc.stdout = json.dumps(payload)
    mock_proc.stderr = ""
    return mock_proc


# ---------------------------------------------------------------------------
# 1. critical 리스크 0개 → pass=True
# ---------------------------------------------------------------------------
class TestCodexPassNoCritical:
    """critical severity 리스크가 없으면 gate PASS."""

    def test_codex_pass_no_critical(self, tmp_path):
        task_file = str(tmp_path / "task.md")
        Path(task_file).write_text("# 설계 문서")

        risks = [
            {"severity": "high", "description": "성능 저하 가능성"},
            {"severity": "medium", "description": "로그 누락 우려"},
            {"severity": "low", "description": "주석 부족"},
        ]
        mock_proc = _make_codex_result(risks, suggestions=["캐시 레이어 추가 검토"])

        with patch("subprocess.run", return_value=mock_proc):
            result = codex_gate_check(
                task_file=task_file,
                affected_files=["src/api.py"],
                workspace_root="/home/jay/workspace",
            )

        assert result["pass"] is True, "critical 없으면 PASS여야 함"

    def test_codex_pass_empty_risks(self, tmp_path):
        """리스크가 아예 없어도 PASS."""
        task_file = str(tmp_path / "task.md")
        Path(task_file).write_text("# 빈 설계")

        mock_proc = _make_codex_result(risks=[], suggestions=[])

        with patch("subprocess.run", return_value=mock_proc):
            result = codex_gate_check(
                task_file=task_file,
                affected_files=[],
                workspace_root="/home/jay/workspace",
            )

        assert result["pass"] is True


# ---------------------------------------------------------------------------
# 2. critical 리스크 1개 이상 → pass=False
# ---------------------------------------------------------------------------
class TestCodexFailCriticalExists:
    """critical severity 리스크가 1개 이상이면 gate FAIL."""

    def test_codex_fail_single_critical(self, tmp_path):
        task_file = str(tmp_path / "task.md")
        Path(task_file).write_text("# 설계 문서")

        risks = [
            {"severity": "critical", "description": "인증 우회 취약점"},
            {"severity": "medium", "description": "로그 누락"},
        ]
        mock_proc = _make_codex_result(risks)

        with patch("subprocess.run", return_value=mock_proc):
            result = codex_gate_check(
                task_file=task_file,
                affected_files=["src/auth.py"],
                workspace_root="/home/jay/workspace",
            )

        assert result["pass"] is False, "critical 존재 시 FAIL이어야 함"

    def test_codex_fail_multiple_criticals(self, tmp_path):
        """critical 2개: 여전히 FAIL."""
        task_file = str(tmp_path / "task.md")
        Path(task_file).write_text("# 설계")

        risks = [
            {"severity": "critical", "description": "SQL 인젝션"},
            {"severity": "critical", "description": "권한 상승 가능"},
        ]
        mock_proc = _make_codex_result(risks)

        with patch("subprocess.run", return_value=mock_proc):
            result = codex_gate_check(
                task_file=task_file,
                affected_files=["src/db.py", "src/auth.py"],
                workspace_root="/home/jay/workspace",
            )

        assert result["pass"] is False
        critical_count = sum(1 for r in result["risks"] if r["severity"] == "critical")
        assert critical_count == 2


# ---------------------------------------------------------------------------
# 3. Codex 타임아웃 → source="maat_fallback"
# ---------------------------------------------------------------------------
class TestCodexTimeoutFallbackToMaat:
    """subprocess.run이 TimeoutExpired를 발생시키면 마아트 폴백을 사용한다."""

    def test_codex_timeout_fallback_to_maat(self, tmp_path):
        task_file = str(tmp_path / "task.md")
        Path(task_file).write_text("# 설계 문서")

        # 마아트 폴백 응답 (두 번째 subprocess.run 호출)
        maat_risks = [{"severity": "low", "description": "폴백 경고"}]
        maat_proc = _make_maat_result(maat_risks, suggestions=["마아트 제안"])

        def side_effect(*args, **kwargs):
            cmd = args[0] if args else kwargs.get("args", [])
            # codex 호출 시 타임아웃 발생
            if any("codex" in str(c) for c in cmd):
                raise subprocess.TimeoutExpired(cmd=cmd, timeout=30)
            return maat_proc

        with patch("subprocess.run", side_effect=side_effect):
            result = codex_gate_check(
                task_file=task_file,
                affected_files=["src/service.py"],
                workspace_root="/home/jay/workspace",
            )

        assert result["source"] == "maat_fallback"

    def test_codex_timeout_result_is_valid(self, tmp_path):
        """타임아웃 폴백 결과도 유효한 딕셔너리 구조를 가져야 한다."""
        task_file = str(tmp_path / "task.md")
        Path(task_file).write_text("# 설계")

        maat_proc = _make_maat_result([], suggestions=[])

        def side_effect(*args, **kwargs):
            cmd = args[0] if args else kwargs.get("args", [])
            if any("codex" in str(c) for c in cmd):
                raise subprocess.TimeoutExpired(cmd=cmd, timeout=30)
            return maat_proc

        with patch("subprocess.run", side_effect=side_effect):
            result = codex_gate_check(
                task_file=task_file,
                affected_files=[],
                workspace_root="/home/jay/workspace",
            )

        required_keys = {"pass", "risks", "suggestions", "source", "error"}
        assert required_keys.issubset(result.keys())


# ---------------------------------------------------------------------------
# 4. Codex API 오류 → source="maat_fallback"
# ---------------------------------------------------------------------------
class TestCodexApiErrorFallbackToMaat:
    """Codex CLI가 비정상 종료(returncode != 0)하면 마아트 폴백을 사용한다."""

    def test_codex_api_error_fallback_to_maat(self, tmp_path):
        task_file = str(tmp_path / "task.md")
        Path(task_file).write_text("# 설계")

        # Codex 오류 응답
        error_proc = MagicMock()
        error_proc.returncode = 1
        error_proc.stdout = ""
        error_proc.stderr = "API rate limit exceeded"

        # AST callers 호출용 빈 응답 (정상 반환)
        ast_proc = MagicMock()
        ast_proc.returncode = 0
        ast_proc.stdout = json.dumps({"blast_radius": {"callers": []}})
        ast_proc.stderr = ""

        def side_effect(*args, **kwargs):
            cmd = args[0] if args else kwargs.get("args", [])
            # codex CLI 호출은 에러 반환
            if any("codex" in str(c) for c in cmd):
                return error_proc
            # AST 스크립트 호출은 정상 반환
            return ast_proc

        with patch("subprocess.run", side_effect=side_effect):
            result = codex_gate_check(
                task_file=task_file,
                affected_files=["src/api.py"],
                workspace_root="/home/jay/workspace",
            )

        assert result["source"] == "maat_fallback"

    def test_codex_api_error_sets_error_field(self, tmp_path):
        """API 오류 시 마아트 폴백으로 전환되며 source='maat_fallback'이어야 한다.

        구현체는 returncode != 0이면 즉시 _maat_fallback_check()를 호출한다.
        마아트 폴백 결과의 error 필드는 None으로 설정된다(폴백 자체는 성공).
        대신 source='maat_fallback'으로 오류 전환을 알린다.
        """
        task_file = str(tmp_path / "task.md")
        Path(task_file).write_text("# 설계")

        error_proc = MagicMock()
        error_proc.returncode = 1
        error_proc.stdout = ""
        error_proc.stderr = "connection refused"

        # 마아트 폴백은 subprocess.run을 호출하지 않으므로 단일 mock으로 충분
        with patch("subprocess.run", return_value=error_proc):
            result = codex_gate_check(
                task_file=task_file,
                affected_files=[],
                workspace_root="/home/jay/workspace",
            )

        # Codex API 오류 시 마아트 폴백으로 전환 확인
        assert result["source"] == "maat_fallback"
        # 폴백 자체는 성공적으로 완료되므로 error=None
        assert result["error"] is None


# ---------------------------------------------------------------------------
# 5. 반환 딕셔너리 구조 검증 (필수 키 존재)
# ---------------------------------------------------------------------------
class TestOutputJsonFormat:
    """반환 딕셔너리가 명세된 스키마를 충족해야 한다."""

    REQUIRED_KEYS = {"pass", "risks", "suggestions", "source", "error"}

    def test_all_required_keys_present(self, tmp_path):
        task_file = str(tmp_path / "task.md")
        Path(task_file).write_text("# 설계")

        mock_proc = _make_codex_result([])

        with patch("subprocess.run", return_value=mock_proc):
            result = codex_gate_check(
                task_file=task_file,
                affected_files=["src/main.py"],
                workspace_root="/home/jay/workspace",
            )

        assert self.REQUIRED_KEYS.issubset(result.keys()), f"누락 키: {self.REQUIRED_KEYS - result.keys()}"

    def test_pass_is_bool(self, tmp_path):
        task_file = str(tmp_path / "task.md")
        Path(task_file).write_text("# 설계")

        mock_proc = _make_codex_result([])

        with patch("subprocess.run", return_value=mock_proc):
            result = codex_gate_check(
                task_file=task_file,
                affected_files=[],
                workspace_root="/home/jay/workspace",
            )

        assert isinstance(result["pass"], bool)

    def test_risks_is_list(self, tmp_path):
        task_file = str(tmp_path / "task.md")
        Path(task_file).write_text("# 설계")

        mock_proc = _make_codex_result([{"severity": "high", "description": "경고"}])

        with patch("subprocess.run", return_value=mock_proc):
            result = codex_gate_check(
                task_file=task_file,
                affected_files=["src/x.py"],
                workspace_root="/home/jay/workspace",
            )

        assert isinstance(result["risks"], list)

    def test_suggestions_is_list(self, tmp_path):
        task_file = str(tmp_path / "task.md")
        Path(task_file).write_text("# 설계")

        mock_proc = _make_codex_result([], suggestions=["제안 1", "제안 2"])

        with patch("subprocess.run", return_value=mock_proc):
            result = codex_gate_check(
                task_file=task_file,
                affected_files=[],
                workspace_root="/home/jay/workspace",
            )

        assert isinstance(result["suggestions"], list)

    def test_source_is_codex_on_success(self, tmp_path):
        """Codex 정상 응답 시 source는 codex_companion."""
        task_file = str(tmp_path / "task.md")
        Path(task_file).write_text("# 설계")

        mock_proc = _make_codex_result([])

        with patch("subprocess.run", return_value=mock_proc):
            result = codex_gate_check(
                task_file=task_file,
                affected_files=[],
                workspace_root="/home/jay/workspace",
            )

        assert result["source"] == "codex_companion"

    def test_error_is_none_on_success(self, tmp_path):
        """정상 응답 시 error=None."""
        task_file = str(tmp_path / "task.md")
        Path(task_file).write_text("# 설계")

        mock_proc = _make_codex_result([])

        with patch("subprocess.run", return_value=mock_proc):
            result = codex_gate_check(
                task_file=task_file,
                affected_files=[],
                workspace_root="/home/jay/workspace",
            )

        assert result["error"] is None

    def test_risk_item_has_severity_and_description(self, tmp_path):
        """리스크 항목은 severity, description 키를 가져야 한다."""
        task_file = str(tmp_path / "task.md")
        Path(task_file).write_text("# 설계")

        risks = [
            {"severity": "high", "description": "고위험 항목"},
            {"severity": "low", "description": "저위험 항목"},
        ]
        mock_proc = _make_codex_result(risks)

        with patch("subprocess.run", return_value=mock_proc):
            result = codex_gate_check(
                task_file=task_file,
                affected_files=["src/y.py"],
                workspace_root="/home/jay/workspace",
            )

        for item in result["risks"]:
            assert "severity" in item, "risk 항목에 severity 키 없음"
            assert "description" in item, "risk 항목에 description 키 없음"

    def test_severity_values_are_valid(self, tmp_path):
        """severity는 critical|high|medium|low 중 하나여야 한다."""
        task_file = str(tmp_path / "task.md")
        Path(task_file).write_text("# 설계")

        valid_severities = {"critical", "high", "medium", "low"}
        risks = [
            {"severity": "critical", "description": "치명"},
            {"severity": "high", "description": "높음"},
            {"severity": "medium", "description": "중간"},
            {"severity": "low", "description": "낮음"},
        ]
        mock_proc = _make_codex_result(risks)

        with patch("subprocess.run", return_value=mock_proc):
            result = codex_gate_check(
                task_file=task_file,
                affected_files=["src/z.py"],
                workspace_root="/home/jay/workspace",
            )

        for item in result["risks"]:
            assert item["severity"] in valid_severities, f"허용되지 않는 severity 값: {item['severity']}"


# ---------------------------------------------------------------------------
# 6. 마아트 폴백 시에도 유효한 결과 반환
# ---------------------------------------------------------------------------
class TestMaatFallbackReturnsValidResult:
    """마아트 폴백 경로에서도 완전한 result 딕셔너리를 반환해야 한다."""

    REQUIRED_KEYS = {"pass", "risks", "suggestions", "source", "error"}

    def _force_maat_fallback(self, tmp_path, maat_risks, maat_suggestions=None):
        """Codex 오류를 유발하여 마아트 폴백을 강제하는 헬퍼.

        마아트 폴백은 파일 존재 여부를 직접 검사하므로 affected_files는
        실제 존재하는 파일만 넘기거나 빈 목록을 사용한다.
        task_file은 tmp_path 안에 생성하여 실제로 존재하게 한다.
        """
        task_file = str(tmp_path / "task.md")
        Path(task_file).write_text("# 설계")

        error_proc = MagicMock()
        error_proc.returncode = 1
        error_proc.stdout = ""
        error_proc.stderr = "codex unavailable"

        with patch("subprocess.run", return_value=error_proc):
            with patch(
                "codex_gate_check._maat_fallback_check",
                return_value={
                    "pass": not any(r["severity"] == "critical" for r in maat_risks),
                    "risks": maat_risks,
                    "suggestions": maat_suggestions or [],
                    "source": "maat_fallback",
                    "error": None,
                },
            ):
                return codex_gate_check(
                    task_file=task_file,
                    affected_files=[],
                    workspace_root="/home/jay/workspace",
                )

    def test_maat_fallback_has_all_required_keys(self, tmp_path):
        result = self._force_maat_fallback(tmp_path, maat_risks=[])
        assert self.REQUIRED_KEYS.issubset(result.keys())

    def test_maat_fallback_source_is_maat_fallback(self, tmp_path):
        result = self._force_maat_fallback(tmp_path, maat_risks=[])
        assert result["source"] == "maat_fallback"

    def test_maat_fallback_pass_true_when_no_critical(self, tmp_path):
        """마아트 폴백에서도 critical 없으면 PASS."""
        maat_risks = [{"severity": "high", "description": "마아트 고위험"}]
        result = self._force_maat_fallback(tmp_path, maat_risks=maat_risks)
        assert result["pass"] is True

    def test_maat_fallback_pass_false_when_critical(self, tmp_path):
        """마아트 폴백에서 critical 있으면 FAIL."""
        maat_risks = [{"severity": "critical", "description": "마아트 치명 오류"}]
        result = self._force_maat_fallback(tmp_path, maat_risks=maat_risks)
        assert result["pass"] is False

    def test_maat_fallback_risks_preserved(self, tmp_path):
        """마아트 폴백 리스크 목록이 결과에 그대로 담겨야 한다."""
        maat_risks = [
            {"severity": "medium", "description": "마아트 중간 위험"},
            {"severity": "low", "description": "마아트 낮은 위험"},
        ]
        result = self._force_maat_fallback(tmp_path, maat_risks=maat_risks)
        assert len(result["risks"]) == 2

    def test_maat_fallback_suggestions_preserved(self, tmp_path):
        """마아트 폴백 제안 목록이 결과에 담겨야 한다."""
        maat_risks = []
        maat_suggestions = ["마아트 제안 A", "마아트 제안 B"]
        result = self._force_maat_fallback(tmp_path, maat_risks=maat_risks, maat_suggestions=maat_suggestions)
        assert len(result["suggestions"]) == 2


# ---------------------------------------------------------------------------
# 7. AST callers 컨텍스트 통합 테스트
# ---------------------------------------------------------------------------
class TestCallersContext:
    """_get_callers_context가 프롬프트에 통합되는지 검증."""

    def test_callers_context_included_in_prompt(self, tmp_path):
        """AST callers 컨텍스트가 Codex 프롬프트에 포함되어야 한다."""
        task_file = str(tmp_path / "task.md")
        Path(task_file).write_text("# 설계 문서")

        # AST 스크립트 더미 파일 생성 (경로 체크 통과용)
        scripts_dir = tmp_path / "scripts"
        scripts_dir.mkdir()
        (scripts_dir / "ast_dependency_map.py").write_text("# dummy")

        # AST 스크립트 결과를 모킹 (딕셔너리 형태 — _get_callers_context가 data.get("blast_radius") 사용)
        ast_result = MagicMock()
        ast_result.returncode = 0
        ast_result.stdout = json.dumps(
            {
                "changed_file": "src/api.py",
                "blast_radius": {
                    "callers": ["server.py:42", "routes.py:15"],
                    "direct_importers": ["server.py", "routes.py"],
                    "test_files": ["test_api.py"],
                    "total_affected": 3,
                },
            }
        )
        ast_result.stderr = ""

        # Codex 결과를 모킹
        codex_result = _make_codex_result([], suggestions=[])

        call_count = {"n": 0}
        captured_inputs = []

        def side_effect(*args, **kwargs):
            call_count["n"] += 1
            cmd = args[0] if args else kwargs.get("args", [])
            # 첫번째 호출: AST 스크립트 (ast_dependency_map.py 포함)
            if any("ast_dependency_map" in str(c) for c in cmd):
                return ast_result
            # 나머지: Codex CLI — stdin input 캡처
            stdin_input = kwargs.get("input", "")
            captured_inputs.append(stdin_input)
            return codex_result

        with patch("subprocess.run", side_effect=side_effect):
            result = codex_gate_check(
                task_file=task_file,
                affected_files=["src/api.py"],
                workspace_root=str(tmp_path),
            )

        assert result["pass"] is True
        # Codex에 stdin으로 전달된 프롬프트에 callers 정보("호출됨")가 포함되어야 한다
        assert any("호출됨" in str(inp) for inp in captured_inputs)

    def test_callers_context_fallback_on_ast_failure(self, tmp_path):
        """AST 스크립트 실패 시에도 codex_gate_check는 정상 동작해야 한다."""
        task_file = str(tmp_path / "task.md")
        Path(task_file).write_text("# 설계 문서")

        # AST 스크립트가 없으므로 _get_callers_context는 빈 문자열 반환
        codex_result = _make_codex_result([], suggestions=[])

        with patch("subprocess.run", return_value=codex_result):
            result = codex_gate_check(
                task_file=task_file,
                affected_files=["nonexistent.py"],
                workspace_root="/tmp/nonexistent",
            )

        # AST 실패해도 codex_gate_check 자체는 정상 동작
        assert "pass" in result
        assert "source" in result


# ---------------------------------------------------------------------------
# 8. _get_callers_context 단위 테스트
# ---------------------------------------------------------------------------
from codex_gate_check import _get_callers_context  # type: ignore[import-untyped]  # noqa: E402


class TestGetCallersContext:
    """_get_callers_context 헬퍼 함수 단위 테스트."""

    def test_returns_empty_when_no_ast_script(self):
        """AST 스크립트가 없으면 빈 문자열 반환."""
        result = _get_callers_context(["src/api.py"], "/tmp/nonexistent")
        assert result == ""

    def test_returns_context_on_success(self, tmp_path):
        """AST 호출 성공 시 호출 관계 문자열 반환."""
        # AST 스크립트 더미 파일 생성 (경로 체크 통과용)
        scripts_dir = tmp_path / "scripts"
        scripts_dir.mkdir()
        (scripts_dir / "ast_dependency_map.py").write_text("# dummy")

        # 실제 파일 생성 (함수명 추출 대상)
        api_file = tmp_path / "api.py"
        api_file.write_text("def get_data():\n    pass\n")

        # AST 스크립트 결과 모킹
        ast_output = json.dumps(
            {
                "changed_file": "api.py",
                "blast_radius": {
                    "direct_importers": ["server.py"],
                    "callers": ["server.py:10"],
                    "test_files": [],
                    "total_affected": 2,
                },
            }
        )

        mock_proc = MagicMock()
        mock_proc.returncode = 0
        mock_proc.stdout = ast_output
        mock_proc.stderr = ""

        with patch("subprocess.run", return_value=mock_proc):
            result = _get_callers_context([str(api_file)], str(tmp_path))

        assert "호출됨" in result
        assert "server.py:10" in result

    def test_returns_empty_on_ast_error(self, tmp_path):
        """AST 호출 실패 시 빈 문자열 반환."""
        scripts_dir = tmp_path / "scripts"
        scripts_dir.mkdir()
        (scripts_dir / "ast_dependency_map.py").write_text("# dummy")

        mock_proc = MagicMock()
        mock_proc.returncode = 1
        mock_proc.stdout = ""
        mock_proc.stderr = "error"

        with patch("subprocess.run", return_value=mock_proc):
            result = _get_callers_context(["api.py"], str(tmp_path))

        assert result == ""


# ---------------------------------------------------------------------------
# 9. 3단계 캐스케이드(companion → exec → maat) 동작 검증
# ---------------------------------------------------------------------------
class TestCodexCascade:
    """3단계 캐스케이드(companion → exec → maat) 동작 검증."""

    def test_companion_success_returns_codex_companion_source(self, tmp_path):
        """codex-companion 성공 시 source='codex_companion'."""
        task_file = str(tmp_path / "task.md")
        Path(task_file).write_text("# 설계")
        mock_proc = _make_codex_result([])
        with patch("subprocess.run", return_value=mock_proc):
            with patch("codex_gate_check.os.path.isfile", return_value=True):
                result = codex_gate_check(
                    task_file=task_file, affected_files=[], workspace_root="/home/jay/workspace",
                )
        assert result["source"] == "codex_companion"

    def test_companion_fail_falls_back_to_maat(self, tmp_path):
        """companion 실패 시 마아트 폴백으로 전환."""
        task_file = str(tmp_path / "task.md")
        Path(task_file).write_text("# 설계")

        fail_proc = MagicMock()
        fail_proc.returncode = 1
        fail_proc.stdout = ""
        fail_proc.stderr = "companion error"

        with patch("subprocess.run", return_value=fail_proc):
            result = codex_gate_check(
                task_file=task_file, affected_files=[], workspace_root="/home/jay/workspace",
            )
        assert result["source"] == "maat_fallback"

    def test_both_codex_fail_returns_maat_fallback(self, tmp_path):
        """companion, exec 모두 실패 시 maat 폴백."""
        task_file = str(tmp_path / "task.md")
        Path(task_file).write_text("# 설계")

        fail_proc = MagicMock()
        fail_proc.returncode = 1
        fail_proc.stdout = ""
        fail_proc.stderr = "error"

        with patch("subprocess.run", return_value=fail_proc):
            result = codex_gate_check(
                task_file=task_file, affected_files=[], workspace_root="/home/jay/workspace",
            )
        assert result["source"] == "maat_fallback"


# ---------------------------------------------------------------------------
# 10. 마아트 폴백 강화 기능 검증
# ---------------------------------------------------------------------------
class TestMaatFallbackEnhanced:
    """마아트 폴백 강화 기능 검증."""

    def test_large_scope_warning(self, tmp_path):
        """affected_files 6개 이상이면 high 리스크 경고."""
        task_file = str(tmp_path / "task.md")
        Path(task_file).write_text("# 설계 문서")
        # 6개 파일 생성
        files = []
        for i in range(6):
            f = tmp_path / f"file{i}.py"
            f.write_text(f"# file {i}")
            files.append(str(f))

        result = _maat_fallback_check(task_file, files, str(tmp_path))
        high_risks = [r for r in result["risks"] if r["severity"] == "high" and "변경 범위" in r["description"]]
        assert len(high_risks) >= 1

    def test_security_keyword_detection(self, tmp_path):
        """task_file에 보안 민감 키워드 포함 시 medium 리스크."""
        task_file = str(tmp_path / "task.md")
        Path(task_file).write_text("# 인증 모듈 변경\n보안 취약점 수정 및 API키 갱신")

        result = _maat_fallback_check(task_file, [], str(tmp_path))
        medium_risks = [r for r in result["risks"] if r["severity"] == "medium" and "보안 민감 키워드" in r["description"]]
        assert len(medium_risks) >= 1

    def test_no_false_positive_on_clean_task(self, tmp_path):
        """보안 키워드 없는 일반 task에서는 medium 리스크 미생성."""
        task_file = str(tmp_path / "task.md")
        Path(task_file).write_text("# 로깅 개선\n로그 포맷 변경")

        result = _maat_fallback_check(task_file, [], str(tmp_path))
        medium_risks = [r for r in result["risks"] if r["severity"] == "medium"]
        assert len(medium_risks) == 0
