# tests/test_dispatcher.py
import asyncio
import json
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from dobby.core.dispatcher import Dispatcher
from dobby.models import Project, Task


@pytest.fixture
def dispatcher(tmp_path):
    return Dispatcher(
        max_concurrent=2,
        default_timeout=60,
        results_dir=str(tmp_path / "results"),
    )


def _make_readline_mock(output: bytes):
    """Return an async callable that yields lines from output then b''."""
    lines = [line + b"\n" for line in output.split(b"\n") if line]
    lines.append(b"")  # EOF sentinel
    idx = 0

    async def readline():
        nonlocal idx
        val = lines[idx] if idx < len(lines) else b""
        idx = min(idx + 1, len(lines))
        return val

    return readline


async def test_dispatch_spawns_subprocess(dispatcher):
    task = Task(project="spare", description="Test", exit_criteria="done")
    project = Project(name="spare", repo="/tmp/fake-repo")

    mock_result = json.dumps({
        "result": (
            "---DOBBY_RESULT---\n"
            '{"status": "completed", "summary": "Done", "pr_url": null,'
            ' "files_changed": [], "metrics": {}, "error": null}'
            "\n---END_DOBBY_RESULT---"
        ),
        "cost_usd": 0.10,
        "input_tokens": 1000,
        "output_tokens": 500,
    })

    mock_proc = AsyncMock()
    mock_proc.stdout.readline = _make_readline_mock(mock_result.encode())
    mock_proc.returncode = 0
    mock_proc.pid = 12345
    mock_proc.wait = AsyncMock()

    with patch("asyncio.create_subprocess_exec", return_value=mock_proc) as mock_exec:
        result = await dispatcher.dispatch(task, project)

    assert result.status == "completed"
    assert result.summary == "Done"
    mock_exec.assert_called_once()
    call_args = mock_exec.call_args[0]
    assert "claude" in call_args[0]


async def test_dispatch_respects_concurrency_limit(dispatcher):
    """Should not exceed max_concurrent (2) running agents."""
    assert dispatcher.active_count == 0
    task = Task(project="spare", description="Test")
    project = Project(name="spare", repo="/tmp")

    max_observed = 0
    output = (
        b'---DOBBY_RESULT---\n'
        b'{"status": "completed", "summary": "Done", "pr_url": null,'
        b' "files_changed": [], "metrics": {}, "error": null}'
        b'\n---END_DOBBY_RESULT---'
    )
    lines = [line + b"\n" for line in output.split(b"\n") if line] + [b""]

    async def slow_readline():
        nonlocal max_observed
        max_observed = max(max_observed, dispatcher.active_count)
        await asyncio.sleep(0.05)
        val = lines.pop(0) if lines else b""
        return val

    mock_proc = AsyncMock()
    mock_proc.stdout.readline = slow_readline
    mock_proc.returncode = 0
    mock_proc.pid = 1
    mock_proc.wait = AsyncMock()

    with patch("asyncio.create_subprocess_exec", return_value=mock_proc):
        t1 = asyncio.create_task(dispatcher.dispatch(task, project))
        t2 = asyncio.create_task(dispatcher.dispatch(task, project))
        t3 = asyncio.create_task(dispatcher.dispatch(task, project))
        await asyncio.gather(t1, t2, t3)

    assert max_observed <= 2


async def test_dispatch_skips_permissions(dispatcher):
    """Dispatcher should always pass --dangerously-skip-permissions for headless operation."""
    task = Task(project="spare", description="Test")
    project = Project(name="spare", repo="/tmp")

    output = (
        b'---DOBBY_RESULT---\n'
        b'{"status": "completed", "summary": "Done", "pr_url": null,'
        b' "files_changed": [], "metrics": {}, "error": null}'
        b'\n---END_DOBBY_RESULT---'
    )
    mock_proc = AsyncMock()
    mock_proc.stdout.readline = _make_readline_mock(output)
    mock_proc.returncode = 0
    mock_proc.pid = 1
    mock_proc.wait = AsyncMock()

    with patch("asyncio.create_subprocess_exec", return_value=mock_proc) as mock_exec:
        await dispatcher.dispatch(task, project)

    call_args = mock_exec.call_args[0]
    args_str = " ".join(str(a) for a in call_args)
    assert "--dangerously-skip-permissions" in args_str


async def test_dispatch_timeout(dispatcher):
    dispatcher.default_timeout = 1
    task = Task(project="spare", description="Slow task")
    project = Project(name="spare", repo="/tmp")

    async def hang_readline():
        await asyncio.sleep(100)
        return b""

    mock_proc = AsyncMock()
    mock_proc.stdout.readline = hang_readline
    mock_proc.returncode = -15
    mock_proc.pid = 1
    mock_proc.kill = MagicMock()
    mock_proc.wait = AsyncMock()

    with patch("asyncio.create_subprocess_exec", return_value=mock_proc):
        result = await dispatcher.dispatch(task, project, timeout=1)

    assert result.status == "failed"
