"""Tests for GitDiffService with mocked subprocess calls."""

from pathlib import Path
from unittest.mock import AsyncMock, patch

import pytest

from hangar.services.gitdiff import GitDiffService


@pytest.fixture
def service(tmp_path: Path) -> GitDiffService:
    """Create a GitDiffService with a temporary project path."""
    return GitDiffService(project_path=tmp_path)


def _mock_process(stdout: str, returncode: int = 0) -> AsyncMock:
    """Create a mock subprocess process."""
    proc = AsyncMock()
    proc.communicate.return_value = (stdout.encode(), b"")
    proc.returncode = returncode
    return proc


@pytest.mark.asyncio
async def test_find_commits_for_task(service: GitDiffService) -> None:
    """find_commits_for_task should parse git log output."""
    git_log_output = (
        "abc123def456|abc123d|hangar-abc: Implement feature|Dev User|2025-01-15 10:00:00 +0000\n"
        "def789ghi012|def789g|hangar-abc: Fix bug|Dev User|2025-01-16 11:00:00 +0000\n"
    )

    with patch(
        "asyncio.create_subprocess_exec",
        return_value=_mock_process(git_log_output),
    ) as mock_exec:
        commits = await service.find_commits_for_task("hangar-abc")

        mock_exec.assert_called_once_with(
            "git", "log", "--all", "--grep=hangar-abc",
            "--format=%H|%h|%s|%an|%ai",
            stdout=-1, stderr=-1, cwd=service.project_path,
        )
        assert len(commits) == 2
        assert commits[0].hash == "abc123def456"
        assert commits[0].short_hash == "abc123d"
        assert commits[0].subject == "hangar-abc: Implement feature"
        assert commits[1].hash == "def789ghi012"


@pytest.mark.asyncio
async def test_find_commits_empty(service: GitDiffService) -> None:
    """find_commits_for_task should return empty list when no commits found."""
    with patch(
        "asyncio.create_subprocess_exec",
        return_value=_mock_process(""),
    ):
        commits = await service.find_commits_for_task("hangar-nonexistent")
        assert commits == []


@pytest.mark.asyncio
async def test_find_commits_git_failure(service: GitDiffService) -> None:
    """find_commits_for_task should return empty list on git failure."""
    with patch(
        "asyncio.create_subprocess_exec",
        return_value=_mock_process("", returncode=1),
    ):
        # _run_git raises RuntimeError on non-zero exit, find_commits catches it
        commits = await service.find_commits_for_task("hangar-abc")
        assert commits == []


@pytest.mark.asyncio
async def test_get_diff_stat(service: GitDiffService) -> None:
    """get_diff_stat should call git diff-tree --stat for each commit."""
    stat_output = " src/main.py | 10 +++++++---\n 1 file changed, 7 insertions(+), 3 deletions(-)\n"

    with patch(
        "asyncio.create_subprocess_exec",
        return_value=_mock_process(stat_output),
    ):
        result = await service.get_diff_stat(["abc123"])
        assert "src/main.py" in result
        assert "# abc123" in result


@pytest.mark.asyncio
async def test_get_diff_stat_empty(service: GitDiffService) -> None:
    """get_diff_stat should return empty string for no commits."""
    result = await service.get_diff_stat([])
    assert result == ""


@pytest.mark.asyncio
async def test_get_full_diff(service: GitDiffService) -> None:
    """get_full_diff should return combined diff output."""
    diff_output = "diff --git a/file.py b/file.py\n--- a/file.py\n+++ b/file.py\n@@ -1 +1 @@\n-old\n+new\n"

    with patch(
        "asyncio.create_subprocess_exec",
        return_value=_mock_process(diff_output),
    ):
        result = await service.get_full_diff(["abc123"])
        assert "diff --git" in result
        assert "-old" in result
        assert "+new" in result


@pytest.mark.asyncio
async def test_get_full_diff_truncation(service: GitDiffService) -> None:
    """get_full_diff should truncate output at max_lines."""
    long_diff = "\n".join(f"line {i}" for i in range(100))

    with patch(
        "asyncio.create_subprocess_exec",
        return_value=_mock_process(long_diff),
    ):
        result = await service.get_full_diff(["abc123"], max_lines=10)
        assert "truncated" in result


@pytest.mark.asyncio
async def test_get_task_diff_integration(service: GitDiffService) -> None:
    """get_task_diff should combine commits, stats, and diffs."""
    git_log = "abc123def456|abc123d|hangar-abc: feature|Dev|2025-01-15 10:00:00 +0000\n"
    stat = " file.py | 5 +++--\n"
    diff = "diff --git a/file.py b/file.py\n-old\n+new\n"

    call_count = 0

    async def mock_exec(*args, **kwargs):
        nonlocal call_count
        call_count += 1
        if call_count == 1:
            return _mock_process(git_log)
        elif call_count == 2:
            return _mock_process(stat)
        else:
            return _mock_process(diff)

    with patch("asyncio.create_subprocess_exec", side_effect=mock_exec):
        result = await service.get_task_diff("hangar-abc")

        assert result.task_id == "hangar-abc"
        assert len(result.commits) == 1
        assert result.commits[0].short_hash == "abc123d"
        assert "file.py" in result.diff_stat
        assert "diff --git" in result.full_diff
