"""Tests for /api/tasks/{id}/diff router — unit tests AND integration tests."""

import json
from unittest.mock import AsyncMock, patch

import pytest
from fastapi.testclient import TestClient

from hangar.dependencies import get_gitdiff_service
from hangar.main import app
from hangar.services.gitdiff import GitDiffService, TaskCommit, TaskDiff

from tests.helpers import make_mock_process


# ══════════════════════════════════════════════════════════════════
#  UNIT TESTS (mocked service via DI override)
# ══════════════════════════════════════════════════════════════════


class TestGitDiffRouterUnit:
    """Unit tests with DI-overridden GitDiffService."""

    @pytest.fixture(autouse=True)
    def _setup_mocks(self, client: TestClient) -> None:
        self.client = client
        self.mock_service = AsyncMock(spec=GitDiffService)
        app.dependency_overrides[get_gitdiff_service] = lambda: self.mock_service
        yield
        app.dependency_overrides.clear()

    def test_get_task_diff_with_commits(self) -> None:
        """GET /api/tasks/{id}/diff should return commits + diff."""
        self.mock_service.get_task_diff.return_value = TaskDiff(
            task_id="hangar-abc",
            commits=[
                TaskCommit(
                    hash="a1b2c3d4e5f6",
                    short_hash="a1b2c3d",
                    subject="feat: implement task hangar-abc",
                    author="Pyro",
                    date="2026-02-07 10:00:00 +0100",
                ),
                TaskCommit(
                    hash="f6e5d4c3b2a1",
                    short_hash="f6e5d4c",
                    subject="test: add tests for hangar-abc",
                    author="Pyro",
                    date="2026-02-07 11:00:00 +0100",
                ),
            ],
            diff_stat="# a1b2c3d\n 2 files changed, 50 insertions(+)",
            full_diff="diff --git a/foo.py b/foo.py\n+new code",
        )

        response = self.client.get("/api/tasks/hangar-abc/diff")
        assert response.status_code == 200
        data = response.json()
        assert data["task_id"] == "hangar-abc"
        assert len(data["commits"]) == 2
        assert data["commits"][0]["hash"] == "a1b2c3d4e5f6"
        assert data["commits"][0]["short_hash"] == "a1b2c3d"
        assert data["commits"][0]["subject"] == "feat: implement task hangar-abc"
        assert data["diff_stat"] != ""
        assert data["full_diff"] != ""

    def test_get_task_diff_empty(self) -> None:
        """GET /api/tasks/{id}/diff should return empty when no commits."""
        self.mock_service.get_task_diff.return_value = TaskDiff(
            task_id="hangar-xyz",
            commits=[],
            diff_stat="",
            full_diff="",
        )

        response = self.client.get("/api/tasks/hangar-xyz/diff")
        assert response.status_code == 200
        data = response.json()
        assert data["task_id"] == "hangar-xyz"
        assert data["commits"] == []
        assert data["diff_stat"] == ""
        assert data["full_diff"] == ""

    def test_get_task_diff_response_structure(self) -> None:
        """Response should have task_id, commits, diff_stat, full_diff."""
        self.mock_service.get_task_diff.return_value = TaskDiff(
            task_id="hangar-test",
            commits=[],
            diff_stat="",
            full_diff="",
        )

        response = self.client.get("/api/tasks/hangar-test/diff")
        data = response.json()
        assert set(data.keys()) == {"task_id", "commits", "diff_stat", "full_diff"}


# ══════════════════════════════════════════════════════════════════
#  INTEGRATION TESTS (real DI, mock subprocess)
# ══════════════════════════════════════════════════════════════════


class TestGitDiffRouterIntegration:
    """Integration tests — real DI chain, mock at subprocess level."""

    @pytest.mark.integration
    def test_get_task_diff_through_real_di(
        self, integration_client: TestClient
    ) -> None:
        """GET /api/tasks/{id}/diff through real GitDiffService.

        Mocks git subprocess calls but lets real DI wiring run.
        """
        # git log output format: %H|%h|%s|%an|%ai
        git_log_output = (
            "a1b2c3d4e5f6|a1b2c3d|feat: implement hangar-abc|Pyro|2026-02-07 10:00:00 +0100\n"
        )
        # git diff-tree --stat output
        git_stat_output = " foo.py | 10 ++++++++++\n 1 file changed, 10 insertions(+)\n"
        # git diff-tree -p output
        git_diff_output = "diff --git a/foo.py b/foo.py\n+new line\n"

        call_count = 0
        responses = [
            make_mock_process(stdout=git_log_output),   # git log
            make_mock_process(stdout=git_stat_output),   # git diff-tree --stat
            make_mock_process(stdout=git_diff_output),   # git diff-tree -p
        ]

        async def mock_exec(*args, **kwargs):
            nonlocal call_count
            proc = responses[min(call_count, len(responses) - 1)]
            call_count += 1
            return proc

        with patch("asyncio.create_subprocess_exec", side_effect=mock_exec):
            response = integration_client.get("/api/tasks/hangar-abc/diff")

        assert response.status_code == 200
        data = response.json()
        assert data["task_id"] == "hangar-abc"
        assert len(data["commits"]) == 1
        assert data["commits"][0]["author"] == "Pyro"
        assert "foo.py" in data["diff_stat"]

    @pytest.mark.integration
    def test_get_task_diff_no_commits_through_real_di(
        self, integration_client: TestClient
    ) -> None:
        """GET /api/tasks/{id}/diff with no matching commits."""
        # git log returns empty (no commits mentioning this task)
        with patch(
            "asyncio.create_subprocess_exec",
            return_value=make_mock_process(stdout=""),
        ):
            response = integration_client.get("/api/tasks/hangar-nonexistent/diff")

        assert response.status_code == 200
        data = response.json()
        assert data["commits"] == []
        assert data["diff_stat"] == ""

    @pytest.mark.integration
    def test_ready_route_not_captured_by_diff(
        self, integration_client: TestClient
    ) -> None:
        """GET /api/tasks/ready should resolve to tasks router, NOT gitdiff.

        The gitdiff router has /{task_id}/diff, and the tasks router has /ready.
        Both are mounted under /api/tasks. Verify no route collision.
        """
        # Mock bd subprocess for the ready endpoint
        bd_ready_data = [
            {
                "id": "hangar-abc",
                "title": "Test task",
                "status": "open",
                "priority": 2,
                "issue_type": "task",
                "created_at": "2026-02-07T01:00:00+01:00",
                "updated_at": "2026-02-07T01:00:00+01:00",
            }
        ]

        with patch(
            "asyncio.create_subprocess_exec",
            return_value=make_mock_process(stdout=json.dumps(bd_ready_data)),
        ):
            response = integration_client.get("/api/tasks/ready")

        assert response.status_code == 200
        data = response.json()
        # tasks/ready returns {"tasks": [...]}, NOT a diff response
        assert "tasks" in data
        assert "diff_stat" not in data
