"""카마소츠(테스터): AST 통합 테스트 — worktree_manager & codex_gate_check.

대상 함수:
  - worktree_manager._get_blast_radius_summary
  - codex_gate_check._get_callers_context
"""

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

sys.path.insert(0, str(Path(__file__).parent.parent))

from worktree_manager import _get_blast_radius_summary  # type: ignore[import-not-found]
from codex_gate_check import _get_callers_context  # type: ignore[import-not-found]


# ---------------------------------------------------------------------------
# 헬퍼
# ---------------------------------------------------------------------------


def _make_proc(returncode: int = 0, stdout: str = "", stderr: str = "") -> MagicMock:
    """subprocess.run 반환값 Mock 생성."""
    mock = MagicMock()
    mock.returncode = returncode
    mock.stdout = stdout
    mock.stderr = stderr
    return mock


# ---------------------------------------------------------------------------
# TestGetBlastRadiusSummary
# ---------------------------------------------------------------------------


class TestGetBlastRadiusSummary:
    """worktree_manager._get_blast_radius_summary 유닛 테스트."""

    def test_git_diff_failure_returns_empty(self):
        """git diff returncode != 0 이면 빈 문자열 반환."""
        git_fail = _make_proc(returncode=1, stderr="fatal: not a git repo")

        with patch("subprocess.run", return_value=git_fail):
            result = _get_blast_radius_summary("/fake/path", "feature", "main")

        assert result == ""

    def test_no_changed_files_returns_empty(self):
        """변경된 .py 파일이 없으면 빈 문자열 반환."""
        git_ok = _make_proc(returncode=0, stdout="")

        with patch("subprocess.run", return_value=git_ok):
            result = _get_blast_radius_summary("/fake/path", "feature", "main")

        assert result == ""

    def test_ast_success_returns_blast_radius_markdown(self):
        """AST 성공 시 '## Blast Radius' 마크다운 반환."""
        git_ok = _make_proc(returncode=0, stdout="src/foo.py\nsrc/bar.py\n")
        ast_data = {
            "direct_importers": ["src/baz.py"],
            "test_files": ["tests/test_foo.py"],
        }
        ast_ok = _make_proc(returncode=0, stdout=json.dumps(ast_data))

        with patch("subprocess.run", side_effect=[git_ok, ast_ok]):
            result = _get_blast_radius_summary("/fake/path", "feature", "main")

        assert result.startswith("## Blast Radius")
        assert "변경 파일" in result
        assert "영향받는 파일" in result
        assert "관련 테스트" in result

    def test_ast_success_empty_importers(self):
        """AST 성공이지만 direct_importers/test_files 비어도 마크다운 반환."""
        git_ok = _make_proc(returncode=0, stdout="src/foo.py\n")
        ast_data = {"direct_importers": [], "test_files": []}
        ast_ok = _make_proc(returncode=0, stdout=json.dumps(ast_data))

        with patch("subprocess.run", side_effect=[git_ok, ast_ok]):
            result = _get_blast_radius_summary("/fake/path", "feature", "main")

        assert result.startswith("## Blast Radius")
        assert "없음" in result

    def test_subprocess_timeout_returns_empty(self):
        """subprocess.TimeoutExpired 발생 시 빈 문자열 반환."""
        with patch("subprocess.run", side_effect=subprocess.TimeoutExpired(cmd="git", timeout=30)):
            result = _get_blast_radius_summary("/fake/path", "feature", "main")

        assert result == ""

    def test_json_parse_failure_returns_empty(self):
        """AST stdout이 유효하지 않은 JSON이면 빈 문자열 반환."""
        git_ok = _make_proc(returncode=0, stdout="src/foo.py\n")
        ast_bad = _make_proc(returncode=0, stdout="NOT VALID JSON {{{")

        with patch("subprocess.run", side_effect=[git_ok, ast_bad]):
            result = _get_blast_radius_summary("/fake/path", "feature", "main")

        assert result == ""

    def test_ast_script_failure_returns_empty(self):
        """AST 스크립트 returncode != 0 이면 빈 문자열 반환."""
        git_ok = _make_proc(returncode=0, stdout="src/foo.py\n")
        ast_fail = _make_proc(returncode=1, stderr="Error: script failed")

        with patch("subprocess.run", side_effect=[git_ok, ast_fail]):
            result = _get_blast_radius_summary("/fake/path", "feature", "main")

        assert result == ""


# ---------------------------------------------------------------------------
# TestGetCallersContext
# ---------------------------------------------------------------------------


class TestGetCallersContext:
    """codex_gate_check._get_callers_context 유닛 테스트."""

    def test_empty_list_returns_empty(self):
        """빈 affected_files 리스트 → 빈 문자열."""
        result = _get_callers_context([], "/fake/workspace")
        assert result == ""

    def test_ast_script_not_found_returns_empty(self, tmp_path):
        """AST 스크립트 파일이 없으면 빈 문자열 반환."""
        # tmp_path에는 scripts/ast_dependency_map.py 가 없음
        result = _get_callers_context(["src/foo.py"], str(tmp_path))
        assert result == ""

    def test_ast_success_with_callers_returns_info(self, tmp_path):
        """AST 성공 + callers 있음 → '함수 호출자 정보:' 포함 문자열."""
        # AST 스크립트 파일 위치 생성 (내용 불필요, 존재만 확인)
        ast_dir = tmp_path / "scripts"
        ast_dir.mkdir()
        (ast_dir / "ast_dependency_map.py").touch()

        ast_data = {
            "blast_radius": {
                "callers": ["a.py", "b.py", "c.py"],
            }
        }
        ast_ok = _make_proc(returncode=0, stdout=json.dumps(ast_data))

        with patch("subprocess.run", return_value=ast_ok):
            result = _get_callers_context(["src/foo.py"], str(tmp_path))

        assert result.startswith("함수 호출자 정보:")
        assert "src/foo.py" in result
        assert "3곳에서 호출됨" in result

    def test_ast_success_no_callers_returns_empty(self, tmp_path):
        """AST 성공 + callers 비어있음 → 빈 문자열."""
        ast_dir = tmp_path / "scripts"
        ast_dir.mkdir()
        (ast_dir / "ast_dependency_map.py").touch()

        ast_data = {"blast_radius": {"callers": []}}
        ast_ok = _make_proc(returncode=0, stdout=json.dumps(ast_data))

        with patch("subprocess.run", return_value=ast_ok):
            result = _get_callers_context(["src/foo.py"], str(tmp_path))

        assert result == ""

    def test_subprocess_timeout_returns_empty(self, tmp_path):
        """TimeoutExpired 발생 시 빈 문자열."""
        ast_dir = tmp_path / "scripts"
        ast_dir.mkdir()
        (ast_dir / "ast_dependency_map.py").touch()

        with patch("subprocess.run", side_effect=subprocess.TimeoutExpired(cmd="python3", timeout=30)):
            result = _get_callers_context(["src/foo.py"], str(tmp_path))

        assert result == ""

    def test_top_5_callers_only(self, tmp_path):
        """callers가 6개 이상이어도 상위 5개만 표시."""
        ast_dir = tmp_path / "scripts"
        ast_dir.mkdir()
        (ast_dir / "ast_dependency_map.py").touch()

        callers = [f"caller_{i}.py" for i in range(8)]
        ast_data = {"blast_radius": {"callers": callers}}
        ast_ok = _make_proc(returncode=0, stdout=json.dumps(ast_data))

        with patch("subprocess.run", return_value=ast_ok):
            result = _get_callers_context(["src/foo.py"], str(tmp_path))

        # 8곳에서 호출됨 (전체 수는 8)
        assert "8곳에서 호출됨" in result
        # 하지만 나열된 파일은 5개까지만
        caller_part = result.split(": ", 2)[-1]  # "caller_0.py, ..." 부분
        listed = [c.strip() for c in caller_part.split(",")]
        assert len(listed) == 5

    def test_multiple_files_with_callers(self, tmp_path):
        """여러 파일 처리 시 callers 있는 파일만 행 생성."""
        ast_dir = tmp_path / "scripts"
        ast_dir.mkdir()
        (ast_dir / "ast_dependency_map.py").touch()

        def side_effect(cmd, **_kw):
            # cmd 구조: ["python3", ast_script, "--root", root, "--files", file_path, "--json"]
            # file_path는 "--files" 다음 인자
            files_idx = cmd.index("--files") if "--files" in cmd else -2
            file_arg = cmd[files_idx + 1] if files_idx >= 0 else ""
            if "foo.py" in file_arg:
                data = {"blast_radius": {"callers": ["a.py", "b.py"]}}
            else:
                data = {"blast_radius": {"callers": []}}
            return _make_proc(returncode=0, stdout=json.dumps(data))

        with patch("subprocess.run", side_effect=side_effect):
            result = _get_callers_context(["src/foo.py", "src/bar.py"], str(tmp_path))

        assert result.startswith("함수 호출자 정보:")
        assert "foo.py" in result
        assert "bar.py" not in result

    def test_json_parse_failure_skips_file(self, tmp_path):
        """JSON 파싱 실패한 파일은 건너뛰고 나머지 반환."""
        ast_dir = tmp_path / "scripts"
        ast_dir.mkdir()
        (ast_dir / "ast_dependency_map.py").touch()

        def side_effect(cmd, **_kw):
            files_idx = cmd.index("--files") if "--files" in cmd else -2
            file_arg = cmd[files_idx + 1] if files_idx >= 0 else ""
            if "foo.py" in file_arg:
                return _make_proc(returncode=0, stdout="INVALID JSON")
            data = {"blast_radius": {"callers": ["x.py"]}}
            return _make_proc(returncode=0, stdout=json.dumps(data))

        with patch("subprocess.run", side_effect=side_effect):
            result = _get_callers_context(["src/foo.py", "src/bar.py"], str(tmp_path))

        # foo.py는 파싱 실패로 건너뜀, bar.py는 정상
        assert result.startswith("함수 호출자 정보:")
        assert "bar.py" in result
