"""worktree_manager.py 통합 테스트.

임시 git repo를 생성하여 create/finish/list/status 명령어를 검증한다.
"""

import json
import os
import subprocess
import sys
import unittest.mock
from pathlib import Path
from typing import Any

import pytest

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

from worktree_manager import (  # type: ignore[import-not-found]  # noqa: E402
    _auto_fix_high_comments,
    _classify_medium_comments,
    _log_medium_comments,
    _parse_gemini_comments,
    _resolve_task_level,
    cmd_create,
    cmd_finish,
)

_WORKSPACE_ROOT = Path(__file__).resolve().parent.parent.parent
SCRIPT = str(_WORKSPACE_ROOT / "scripts" / "worktree_manager.py")


def _run_wt(args: list[str]) -> tuple[dict[str, Any], int]:
    """worktree_manager.py를 실행하고 JSON 결과를 반환."""
    result = subprocess.run(
        ["python3", SCRIPT, *args],
        capture_output=True,
        text=True,
    )
    return json.loads(result.stdout), result.returncode


@pytest.fixture()
def git_repo(tmp_path: Path) -> Path:
    """초기 커밋이 있는 임시 git repo 생성."""
    repo = tmp_path / "repo"
    repo.mkdir()
    subprocess.run(["git", "init"], cwd=str(repo), check=True, capture_output=True)
    subprocess.run(["git", "checkout", "-b", "main"], cwd=str(repo), check=True, capture_output=True)
    readme = repo / "README.md"
    readme.write_text("hello")
    subprocess.run(["git", "add", "."], cwd=str(repo), check=True, capture_output=True)
    subprocess.run(
        ["git", "commit", "-m", "init"],
        cwd=str(repo),
        check=True,
        capture_output=True,
        env={
            **os.environ,
            "GIT_AUTHOR_NAME": "test",
            "GIT_COMMITTER_NAME": "test",
            "GIT_AUTHOR_EMAIL": "t@t",
            "GIT_COMMITTER_EMAIL": "t@t",
        },
    )
    return repo


class TestCreate:
    def test_create_new(self, git_repo: Path) -> None:
        data, rc = _run_wt(["create", str(git_repo), "task-330.1", "dev2"])
        assert rc == 0
        assert data["status"] == "created"
        assert data["branch"] == "task/task-330.1-dev2"
        assert Path(data["worktree_path"]).is_dir()

    def test_create_reuse(self, git_repo: Path) -> None:
        _run_wt(["create", str(git_repo), "task-330.1", "dev2"])
        data, rc = _run_wt(["create", str(git_repo), "task-330.1", "dev2"])
        assert rc == 0
        assert data["status"] == "reused"

    def test_create_invalid_path(self) -> None:
        data, rc = _run_wt(["create", "/nonexistent/path", "task-999", "dev3"])
        assert rc == 1
        assert data["status"] == "error"


class TestList:
    def test_list_empty(self, git_repo: Path) -> None:
        data, _ = _run_wt(["list", str(git_repo)])
        assert data["worktrees"] == []

    def test_list_with_worktree(self, git_repo: Path) -> None:
        _run_wt(["create", str(git_repo), "task-330.1", "dev2"])
        data, _ = _run_wt(["list", str(git_repo)])
        assert len(data["worktrees"]) == 1
        wt = data["worktrees"][0]
        assert wt["task_id"] == "task-330.1"
        assert wt["team_id"] == "dev2"


class TestStatus:
    def test_status_changed_files(self, git_repo: Path) -> None:
        _run_wt(["create", str(git_repo), "task-330.1", "dev2"])
        wt_path = git_repo / ".worktrees" / "task-330.1-dev2"
        (wt_path / "new.txt").write_text("test")
        data, _ = _run_wt(["status", str(git_repo), "task-330.1"])
        assert data["worktrees"][0]["changed_files"] >= 1

    def test_status_no_match(self, git_repo: Path) -> None:
        data, _ = _run_wt(["status", str(git_repo), "task-nonexistent"])
        assert data["worktrees"] == []


class TestFinish:
    def test_finish_keep(self, git_repo: Path) -> None:
        _run_wt(["create", str(git_repo), "task-330.1", "dev2"])
        data, rc = _run_wt(["finish", str(git_repo), "task-330.1", "dev2", "--action", "keep"])
        assert rc == 0
        assert data["status"] == "kept"

    def test_finish_merge(self, git_repo: Path) -> None:
        _run_wt(["create", str(git_repo), "task-330.1", "dev2"])
        wt_path = git_repo / ".worktrees" / "task-330.1-dev2"
        (wt_path / "merged.txt").write_text("merged")
        subprocess.run(["git", "add", "."], cwd=str(wt_path), check=True, capture_output=True)
        subprocess.run(
            ["git", "commit", "-m", "add merged.txt"],
            cwd=str(wt_path),
            check=True,
            capture_output=True,
            env={
                **os.environ,
                "GIT_AUTHOR_NAME": "test",
                "GIT_COMMITTER_NAME": "test",
                "GIT_AUTHOR_EMAIL": "t@t",
                "GIT_COMMITTER_EMAIL": "t@t",
            },
        )
        data, rc = _run_wt(["finish", str(git_repo), "task-330.1", "dev2", "--action", "merge"])
        assert rc == 0
        assert data["status"] == "merged"
        assert not (git_repo / ".worktrees" / "task-330.1-dev2").exists()
        assert (git_repo / "merged.txt").exists()

    def test_finish_discard(self, git_repo: Path) -> None:
        _run_wt(["create", str(git_repo), "task-330.2", "dev1"])
        wt_path = git_repo / ".worktrees" / "task-330.2-dev1"
        (wt_path / "discard.txt").write_text("discard")
        data, rc = _run_wt(["finish", str(git_repo), "task-330.2", "dev1", "--action", "discard"])
        assert rc == 0
        assert data["status"] == "discarded"
        assert not (git_repo / ".worktrees" / "task-330.2-dev1").exists()


class TestFinishPr:
    def test_finish_pr_no_remote(self, git_repo: Path) -> None:
        """pr 액션에서 remote가 없으면 gh 실패로 에러 처리."""
        _run_wt(["create", str(git_repo), "task-330.3", "dev1"])
        data, rc = _run_wt(["finish", str(git_repo), "task-330.3", "dev1", "--action", "pr"])
        # gh가 없거나 remote가 없으면 에러가 발생해야 함
        assert rc != 0 or data["status"] == "error"

    def test_finish_pr_cli_args(self, git_repo: Path) -> None:
        """pr 액션 CLI 인자 파싱 검증."""
        result = subprocess.run(
            [
                "python3",
                SCRIPT,
                "finish",
                str(git_repo),
                "task-330.3",
                "dev1",
                "--action",
                "pr",
                "--pr-title",
                "Test PR",
                "--pr-body",
                "Test body",
                "--gemini-timeout",
                "60",
            ],
            capture_output=True,
            text=True,
        )
        # gh가 없어도 argparse 파싱 자체는 성공해야 함
        # (실행 중 gh 실패로 에러가 날 수 있지만, argparse 에러와는 구분)
        assert "unrecognized arguments" not in result.stderr

    def test_finish_pr_invalid_action(self, git_repo: Path) -> None:
        """잘못된 action은 argparse 에러."""
        result = subprocess.run(
            [
                "python3",
                SCRIPT,
                "finish",
                str(git_repo),
                "task-330.3",
                "dev1",
                "--action",
                "invalid",
            ],
            capture_output=True,
            text=True,
        )
        assert result.returncode != 0
        assert "invalid choice" in result.stderr


# git 환경변수 헬퍼
_GIT_ENV = {
    **os.environ,
    "GIT_AUTHOR_NAME": "test",
    "GIT_COMMITTER_NAME": "test",
    "GIT_AUTHOR_EMAIL": "t@t",
    "GIT_COMMITTER_EMAIL": "t@t",
}


def _merge_worktree_to_main(git_repo: Path, task_id: str, team_id: str) -> None:
    """워크트리 브랜치를 main에 머지하는 헬퍼 (cleanup 테스트용)."""
    wt_path = git_repo / ".worktrees" / f"{task_id}-{team_id}"
    # 워크트리에 파일 추가 + 커밋
    (wt_path / f"{task_id}.txt").write_text(f"work from {task_id}")
    subprocess.run(["git", "add", "."], cwd=str(wt_path), check=True, capture_output=True)
    subprocess.run(
        ["git", "commit", "-m", f"work on {task_id}"],
        cwd=str(wt_path),
        check=True,
        capture_output=True,
        env=_GIT_ENV,
    )
    # main 브랜치에서 머지 (아누의 수동 머지를 시뮬레이션)
    subprocess.run(["git", "checkout", "main"], cwd=str(git_repo), check=True, capture_output=True)
    subprocess.run(
        ["git", "merge", "--no-ff", f"task/{task_id}-{team_id}"],
        cwd=str(git_repo),
        check=True,
        capture_output=True,
        env=_GIT_ENV,
    )


class TestCleanup:
    def test_cleanup_removes_merged(self, git_repo: Path) -> None:
        """머지된 워크트리만 정리되는지 확인."""
        _run_wt(["create", str(git_repo), "task-400.1", "dev1"])
        _merge_worktree_to_main(git_repo, "task-400.1", "dev1")

        data, rc = _run_wt(["cleanup", str(git_repo)])
        assert rc == 0
        assert len(data["cleaned"]) == 1
        assert data["cleaned"][0]["task_id"] == "task-400.1"
        assert not (git_repo / ".worktrees" / "task-400.1-dev1").exists()

    def test_cleanup_skips_unmerged(self, git_repo: Path) -> None:
        """머지되지 않은 워크트리는 건드리지 않는지 확인."""
        _run_wt(["create", str(git_repo), "task-400.2", "dev2"])
        # 커밋은 하되 main에 머지하지 않음
        wt_path = git_repo / ".worktrees" / "task-400.2-dev2"
        (wt_path / "wip.txt").write_text("work in progress")
        subprocess.run(["git", "add", "."], cwd=str(wt_path), check=True, capture_output=True)
        subprocess.run(
            ["git", "commit", "-m", "wip"],
            cwd=str(wt_path),
            check=True,
            capture_output=True,
            env=_GIT_ENV,
        )

        data, rc = _run_wt(["cleanup", str(git_repo)])
        assert rc == 0
        assert len(data["cleaned"]) == 0
        assert len(data["skipped"]) == 1
        assert data["skipped"][0]["reason"] == "not_merged"
        assert (git_repo / ".worktrees" / "task-400.2-dev2").exists()

    def test_cleanup_mixed(self, git_repo: Path) -> None:
        """머지된 것은 정리, 미머지는 보존 (혼합 시나리오)."""
        # 머지할 워크트리
        _run_wt(["create", str(git_repo), "task-400.3", "dev1"])
        _merge_worktree_to_main(git_repo, "task-400.3", "dev1")
        # 미머지 워크트리
        _run_wt(["create", str(git_repo), "task-400.4", "dev2"])
        wt_path = git_repo / ".worktrees" / "task-400.4-dev2"
        (wt_path / "active.txt").write_text("active")
        subprocess.run(["git", "add", "."], cwd=str(wt_path), check=True, capture_output=True)
        subprocess.run(
            ["git", "commit", "-m", "active work"],
            cwd=str(wt_path),
            check=True,
            capture_output=True,
            env=_GIT_ENV,
        )

        data, rc = _run_wt(["cleanup", str(git_repo)])
        assert rc == 0
        assert len(data["cleaned"]) == 1
        assert len(data["skipped"]) == 1
        assert not (git_repo / ".worktrees" / "task-400.3-dev1").exists()
        assert (git_repo / ".worktrees" / "task-400.4-dev2").exists()

    def test_cleanup_no_targets(self, git_repo: Path) -> None:
        """정리 대상이 없을 때 정상 동작."""
        data, rc = _run_wt(["cleanup", str(git_repo)])
        assert rc == 0
        assert data["cleaned"] == []
        assert data["skipped"] == []


class TestSafetyDecorator:
    def test_gitignore_auto_update(self, git_repo: Path) -> None:
        """.gitignore에 .worktrees/가 없을 때 자동 추가되는지 확인."""

        gitignore_path = git_repo / ".gitignore"
        assert not gitignore_path.exists() or ".worktrees/" not in gitignore_path.read_text()

        result = cmd_create(str(git_repo), "task-safety-1", "dev1")

        assert result.get("gitignore_updated") is True
        assert gitignore_path.exists()
        assert ".worktrees/" in gitignore_path.read_text()

    def test_gitignore_already_present(self, git_repo: Path) -> None:
        """이미 .gitignore에 .worktrees/가 있으면 추가 안 되는지 확인."""

        gitignore_path = git_repo / ".gitignore"
        gitignore_path.write_text(".worktrees/\n")

        result = cmd_create(str(git_repo), "task-safety-2", "dev1")

        assert "gitignore_updated" not in result
        assert gitignore_path.read_text().count(".worktrees/") == 1

    def test_non_git_repo_error(self, tmp_path: Path) -> None:
        """git repo가 아닌 디렉토리에서 에러 반환 확인."""

        non_repo = tmp_path / "not_a_repo"
        non_repo.mkdir()

        result = cmd_create(str(non_repo), "task-safety-3", "dev1")

        assert result["status"] == "error"


class TestFinishPrUnit:
    def test_pr_action_worktree_not_found(self, git_repo: Path) -> None:
        """pr 액션에서 worktree가 없으면 RuntimeError."""
        with pytest.raises(RuntimeError, match="worktree not found"):
            cmd_finish(str(git_repo), "task-999.9", "dev1", "pr")


class TestResolveTaskLevel:
    """_resolve_task_level 함수 단위 테스트."""

    def test_yaml_frontmatter_level(self, tmp_path: Path) -> None:
        """YAML frontmatter에서 level 필드를 정확히 읽는다."""
        import re

        tasks_dir = tmp_path / "memory" / "tasks"
        tasks_dir.mkdir(parents=True)
        task_file = tasks_dir / "task-test-1.md"
        task_file.write_text("---\ntask_id: task-test-1\nlevel: 3\n---\n# Test task\n")

        # 직접 Path 조작이 어려우므로 실제 함수의 tasks_dir를 패치
        def patched_resolve(task_id: str) -> int:
            candidates = list(tasks_dir.glob(f"{task_id}.md")) + list(tasks_dir.glob(f"{task_id}*.md"))
            for p in candidates:
                try:
                    text = p.read_text(encoding="utf-8")
                    if text.startswith("---"):
                        end_idx = text.find("---", 3)
                        if end_idx > 0:
                            m = re.search(r"^level:\s*(\d+)", text[3:end_idx], re.MULTILINE)
                            if m:
                                return int(m.group(1))
                except (OSError, ValueError, TypeError):
                    continue
            return 0

        result = patched_resolve("task-test-1")
        assert result == 3

    def test_no_frontmatter_returns_zero(self, tmp_path: Path) -> None:
        """frontmatter 없는 task 파일은 0을 반환한다."""
        tasks_dir = tmp_path / "memory" / "tasks"
        tasks_dir.mkdir(parents=True)
        task_file = tasks_dir / "task-nolevel.md"
        task_file.write_text("# No frontmatter task\nJust content.\n")

        text = task_file.read_text(encoding="utf-8")
        # frontmatter가 없으면 startswith("---")가 False
        assert not text.startswith("---")

    def test_real_task_with_level(self) -> None:
        """실제 task-1283.1.md에서 level 2를 읽는다."""
        result = _resolve_task_level("task-1283.1")
        assert result == 2

    def test_nonexistent_task_returns_zero(self) -> None:
        """존재하지 않는 task ID는 0을 반환한다."""
        result = _resolve_task_level("task-does-not-exist-99999")
        assert result == 0

    def test_task_without_level_field(self) -> None:
        """level 필드가 없는 task 파일은 0 또는 work_level fallback."""
        result = _resolve_task_level("task-1866")
        assert isinstance(result, int)
        assert result >= 0


class TestParseGeminiComments:
    def test_parse_high_severity(self, tmp_path: Path) -> None:
        """HIGH severity 코멘트가 올바르게 파싱되는지 확인."""
        mock_comments = json.dumps(
            [
                {
                    "user": {"login": "gemini-code-assist[bot]"},
                    "body": "severity: high\nSQL injection vulnerability in query parameter",
                    "path": "app/db.py",
                    "line": 42,
                }
            ]
        )
        with unittest.mock.patch("worktree_manager._run") as mock_run:
            mock_run.return_value = subprocess.CompletedProcess(args=[], returncode=0, stdout=mock_comments, stderr="")
            result = _parse_gemini_comments("1", "owner", "repo", str(tmp_path))
        assert len(result) == 1
        assert result[0]["severity"] == "high"
        assert result[0]["path"] == "app/db.py"

    def test_parse_medium_severity(self, tmp_path: Path) -> None:
        """MEDIUM severity 코멘트 파싱."""
        mock_comments = json.dumps(
            [
                {
                    "user": {"login": "gemini-code-assist[bot]"},
                    "body": "severity: medium\n⚠️ Consider adding input validation",
                    "path": "app/api.py",
                    "line": 10,
                }
            ]
        )
        with unittest.mock.patch("worktree_manager._run") as mock_run:
            mock_run.return_value = subprocess.CompletedProcess(args=[], returncode=0, stdout=mock_comments, stderr="")
            result = _parse_gemini_comments("1", "owner", "repo", str(tmp_path))
        assert len(result) == 1
        assert result[0]["severity"] == "medium"

    def test_parse_ignores_non_gemini(self, tmp_path: Path) -> None:
        """Gemini가 아닌 사용자의 코멘트는 무시."""
        mock_comments = json.dumps(
            [
                {
                    "user": {"login": "human-reviewer"},
                    "body": "severity: high\nThis is a big problem",
                    "path": "app/main.py",
                    "line": 1,
                }
            ]
        )
        with unittest.mock.patch("worktree_manager._run") as mock_run:
            mock_run.return_value = subprocess.CompletedProcess(args=[], returncode=0, stdout=mock_comments, stderr="")
            result = _parse_gemini_comments("1", "owner", "repo", str(tmp_path))
        assert len(result) == 0

    def test_parse_api_failure(self, tmp_path: Path) -> None:
        """API 실패 시 빈 리스트 반환."""
        with unittest.mock.patch("worktree_manager._run") as mock_run:
            mock_run.return_value = subprocess.CompletedProcess(args=[], returncode=1, stdout="", stderr="error")
            result = _parse_gemini_comments("1", "owner", "repo", str(tmp_path))
        assert result == []

    def test_parse_mixed_severities(self, tmp_path: Path) -> None:
        """HIGH + MEDIUM + LOW 혼합 코멘트 파싱."""
        mock_comments = json.dumps(
            [
                {
                    "user": {"login": "gemini-code-assist[bot]"},
                    "body": "HIGH severity issue 🔴",
                    "path": "a.py",
                    "line": 1,
                },
                {"user": {"login": "gemini-code-assist[bot]"}, "body": "MEDIUM severity ⚠️", "path": "b.py", "line": 2},
                {"user": {"login": "gemini-code-assist[bot]"}, "body": "Minor suggestion", "path": "c.py", "line": 3},
            ]
        )
        with unittest.mock.patch("worktree_manager._run") as mock_run:
            mock_run.return_value = subprocess.CompletedProcess(args=[], returncode=0, stdout=mock_comments, stderr="")
            result = _parse_gemini_comments("1", "owner", "repo", str(tmp_path))
        assert len(result) == 3
        severities = [c["severity"] for c in result]
        assert severities == ["high", "medium", "low"]


class TestClassifyMediumComments:
    def test_collect_mode_all_defer(self) -> None:
        """수집 모드에서 모든 MEDIUM이 DEFER로 분류."""
        comments = [
            {"severity": "medium", "body": "sql injection risk", "path": "a.py", "line": "1"},
            {"severity": "medium", "body": "naming convention issue", "path": "b.py", "line": "2"},
        ]
        result = _classify_medium_comments(comments, collect_mode=True)
        assert len(result) == 2
        assert all(c["classification"] == "DEFER" for c in result)
        assert all(c["pattern_matched"] == "collect_mode" for c in result)

    def test_fix_classification(self) -> None:
        """FIX 패턴 매칭 확인 (collect_mode=False)."""
        comments = [
            {"severity": "medium", "body": "Possible sql injection in user input", "path": "db.py", "line": "10"},
            {"severity": "medium", "body": "XSS vulnerability in template", "path": "tmpl.py", "line": "20"},
            {"severity": "medium", "body": "race condition in counter update", "path": "counter.py", "line": "30"},
        ]
        result = _classify_medium_comments(comments, collect_mode=False)
        assert len(result) == 3
        assert all(c["classification"] == "FIX" for c in result)
        assert result[0]["pattern_matched"] == "sql injection"
        assert result[1]["pattern_matched"] == "xss"
        assert result[2]["pattern_matched"] == "race condition"

    def test_skip_classification(self) -> None:
        """SKIP 패턴 매칭 확인 (collect_mode=False)."""
        comments = [
            {"severity": "medium", "body": "naming convention: use snake_case", "path": "a.py", "line": "1"},
            {"severity": "medium", "body": "Missing docstring for function", "path": "b.py", "line": "2"},
            {"severity": "medium", "body": "code style: trailing whitespace", "path": "c.py", "line": "3"},
        ]
        result = _classify_medium_comments(comments, collect_mode=False)
        assert len(result) == 3
        assert all(c["classification"] == "SKIP" for c in result)

    def test_defer_classification(self) -> None:
        """패턴 미매칭은 DEFER."""
        comments = [
            {"severity": "medium", "body": "Consider refactoring this loop for clarity", "path": "x.py", "line": "1"},
        ]
        result = _classify_medium_comments(comments, collect_mode=False)
        assert len(result) == 1
        assert result[0]["classification"] == "DEFER"
        assert result[0]["pattern_matched"] == ""

    def test_ignores_non_medium(self) -> None:
        """MEDIUM이 아닌 코멘트는 무시."""
        comments = [
            {"severity": "high", "body": "sql injection", "path": "a.py", "line": "1"},
            {"severity": "low", "body": "minor style", "path": "b.py", "line": "2"},
        ]
        result = _classify_medium_comments(comments, collect_mode=False)
        assert len(result) == 0

    def test_all_fix_patterns(self) -> None:
        """MEDIUM_FIX_PATTERNS 15개 패턴 전체 매칭 확인."""
        fix_patterns = [
            "sql injection", "xss", "csrf", "auth", "permission",
            "null pointer", "race condition", "deadlock",
            "data loss", "corruption",
            "env variable", "config",
            "accessibility", "a11y", "responsive",
        ]
        comments = [
            {"severity": "medium", "body": f"Issue about {p} detected", "path": f"file_{i}.py", "line": str(i)}
            for i, p in enumerate(fix_patterns)
        ]
        result = _classify_medium_comments(comments, collect_mode=False)
        assert len(result) == 15
        assert all(c["classification"] == "FIX" for c in result), (
            f"Expected all FIX, got: {[(c['pattern_matched'], c['classification']) for c in result]}"
        )
        matched_patterns = [c["pattern_matched"] for c in result]
        for p in fix_patterns:
            assert p in matched_patterns, f"Pattern '{p}' not matched"

    def test_all_skip_patterns(self) -> None:
        """MEDIUM_SKIP_PATTERNS 6개 패턴 전체 매칭 확인."""
        skip_patterns = [
            "naming convention", "code style", "formatting", "whitespace",
            "type annotation", "docstring",
        ]
        comments = [
            {"severity": "medium", "body": f"Issue about {p} in code", "path": f"file_{i}.py", "line": str(i)}
            for i, p in enumerate(skip_patterns)
        ]
        result = _classify_medium_comments(comments, collect_mode=False)
        assert len(result) == 6
        assert all(c["classification"] == "SKIP" for c in result), (
            f"Expected all SKIP, got: {[(c['pattern_matched'], c['classification']) for c in result]}"
        )


class TestLogMediumComments:
    def test_log_writes_entries(self) -> None:
        """_log_medium_comments가 실제 로그 파일에 엔트리를 기록하는지 확인."""
        log_path = Path(__file__).resolve().parent.parent.parent / "dashboard" / "data" / "medium-comments-log.json"
        # 기존 로그 백업
        existing_data: list[dict[str, str]] = []
        if log_path.exists():
            try:
                existing_data = json.loads(log_path.read_text())
            except (json.JSONDecodeError, OSError):
                existing_data = []
        original_count = len(existing_data)

        classified = [
            {
                "classification": "DEFER",
                "body": "test entry from unit test",
                "path": "test.py",
                "pattern_matched": "collect_mode",
            },
        ]
        _log_medium_comments(classified)

        updated_data = json.loads(log_path.read_text())
        assert len(updated_data) == original_count + 1
        assert updated_data[-1]["body"] == "test entry from unit test"
        assert updated_data[-1]["classification"] == "DEFER"

        # 클린업: 추가한 엔트리 제거
        log_path.write_text(json.dumps(existing_data, indent=2, ensure_ascii=False))


class TestAutoFixHighComments:
    def test_collect_mode_no_execution(self) -> None:
        """수집 모드에서는 프롬프트만 생성, 실행하지 않음."""
        comments = [
            {"severity": "high", "body": "SQL injection in query", "path": "db.py", "line": "42"},
        ]
        result = _auto_fix_high_comments(comments, "/tmp/fake", collect_mode=True)
        assert result["executed"] is False
        assert len(result["prompts"]) == 1
        assert "db.py" in result["prompts"][0]
        assert result["diff_stat"] == ""

    def test_no_high_comments(self) -> None:
        """HIGH 코멘트가 없으면 빈 결과."""
        comments = [
            {"severity": "medium", "body": "style issue", "path": "a.py", "line": "1"},
        ]
        result = _auto_fix_high_comments(comments, "/tmp/fake", collect_mode=True)
        assert result["prompts"] == []
        assert result["executed"] is False

    def test_multiple_high_comments_prompts(self) -> None:
        """여러 HIGH 코멘트에서 각각 프롬프트 생성."""
        comments = [
            {"severity": "high", "body": "Issue 1", "path": "a.py", "line": "10"},
            {"severity": "high", "body": "Issue 2", "path": "b.py", "line": "20"},
        ]
        result = _auto_fix_high_comments(comments, "/tmp/fake", collect_mode=True)
        assert len(result["prompts"]) == 2

    def test_execution_mode_calls_claude(self) -> None:
        """실행 모드에서 Claude CLI 호출 + git push가 실행되는지 확인."""
        comments = [
            {"severity": "high", "body": "SQL injection in query", "path": "db.py", "line": "42"},
        ]
        with unittest.mock.patch("worktree_manager._run") as mock_run:
            mock_run.return_value = subprocess.CompletedProcess(args=[], returncode=0, stdout="1 file changed", stderr="")
            result = _auto_fix_high_comments(comments, "/tmp/fake", collect_mode=False)
        assert result["executed"] is True
        assert len(result["prompts"]) == 1
        # claude CLI 호출 확인
        call_strs = [str(c) for c in mock_run.call_args_list]
        assert any("claude" in s for s in call_strs), "Claude CLI should be called"
        assert any("push" in s for s in call_strs), "git push should be called"
