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

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

import pytest

from hangar.services.beads import BeadsService

FIXTURES = Path(__file__).parent / "fixtures"


@pytest.fixture
def service(tmp_path: Path) -> BeadsService:
    """Create a BeadsService with a temporary project path."""
    return BeadsService(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_list_tasks(service: BeadsService) -> None:
    """list_tasks should run bd list --all --json and parse output."""
    fixture_data = (FIXTURES / "bd_list.json").read_text()

    with patch("asyncio.create_subprocess_exec", return_value=_mock_process(fixture_data)) as mock_exec:
        result = await service.list_tasks()

        mock_exec.assert_called_once_with(
            "bd", "list", "--all", "--json",
            stdout=-1, stderr=-1, cwd=service.project_path,
        )
        assert len(result.tasks) > 0
        assert all(t.id.startswith("hangar-") for t in result.tasks)


@pytest.mark.asyncio
async def test_ready_tasks(service: BeadsService) -> None:
    """ready_tasks should run bd ready --json and parse output."""
    fixture_data = (FIXTURES / "bd_ready.json").read_text()

    with patch("asyncio.create_subprocess_exec", return_value=_mock_process(fixture_data)) as mock_exec:
        result = await service.ready_tasks()

        mock_exec.assert_called_once_with(
            "bd", "ready", "--json",
            stdout=-1, stderr=-1, cwd=service.project_path,
        )
        assert len(result.tasks) > 0


@pytest.mark.asyncio
async def test_show_task(service: BeadsService) -> None:
    """show_task should run bd show <id> --json and return a single task."""
    fixture_data = (FIXTURES / "bd_show.json").read_text()

    with patch("asyncio.create_subprocess_exec", return_value=_mock_process(fixture_data)) as mock_exec:
        result = await service.show_task("hangar-910")

        mock_exec.assert_called_once_with(
            "bd", "show", "hangar-910", "--json",
            stdout=-1, stderr=-1, cwd=service.project_path,
        )
        assert result.id == "hangar-910"
        assert result.title


@pytest.mark.asyncio
async def test_claim_task(service: BeadsService) -> None:
    """claim_task should run bd update <id> --claim."""
    with patch("asyncio.create_subprocess_exec", return_value=_mock_process("")) as mock_exec:
        await service.claim_task("hangar-abc")

        mock_exec.assert_called_once_with(
            "bd", "update", "hangar-abc", "--claim",
            stdout=-1, stderr=-1, cwd=service.project_path,
        )


@pytest.mark.asyncio
async def test_close_task(service: BeadsService) -> None:
    """close_task should run bd close <id> --reason."""
    with patch("asyncio.create_subprocess_exec", return_value=_mock_process("")) as mock_exec:
        await service.close_task("hangar-abc", "Done implementing feature")

        mock_exec.assert_called_once_with(
            "bd", "close", "hangar-abc", "--reason", "Done implementing feature",
            stdout=-1, stderr=-1, cwd=service.project_path,
        )


@pytest.mark.asyncio
async def test_bd_command_failure(service: BeadsService) -> None:
    """Service should raise RuntimeError when bd command fails."""
    proc = AsyncMock()
    proc.communicate.return_value = (b"", b"Error: issue not found")
    proc.returncode = 1

    with patch("asyncio.create_subprocess_exec", return_value=proc):
        with pytest.raises(RuntimeError, match="bd show .* failed"):
            await service.show_task("hangar-nonexistent")
