"""
End-to-end test: start daemon, add task via API, verify dispatch and result collection.
Uses mocked Claude subprocess to avoid real API calls.
"""
import json
from unittest.mock import AsyncMock, patch

import pytest
from aiohttp.test_utils import TestClient, TestServer

from dobby.config import DobbyConfig
from dobby.core.api import create_app
from dobby.core.daemon import DobbyDaemon
from dobby.models import Project


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"")
    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


@pytest.fixture
async def e2e(tmp_path):
    config = DobbyConfig(
        db_path=str(tmp_path / "e2e.db"),
        results_dir=str(tmp_path / "results"),
        max_concurrent_agents=2,
        port=0,
    )
    daemon = DobbyDaemon(config)
    await daemon.initialize()

    await daemon.store.register_project(Project(
        name="test-project",
        repo=str(tmp_path / "fake-repo"),
        context="Test project for e2e",
    ))

    app = create_app(daemon.store, daemon.dispatcher)
    server = TestServer(app)
    client = TestClient(server)
    await client.start_server()

    yield {"daemon": daemon, "client": client, "config": config}

    await client.close()
    await daemon.shutdown()


async def test_full_lifecycle(e2e):
    client = e2e["client"]
    daemon = e2e["daemon"]

    # 1. Create a task via API
    resp = await client.post("/tasks", json={
        "project": "test-project",
        "description": "Write a hello world script",
        "exit_criteria": "File created",
    })
    assert resp.status == 201
    task_data = await resp.json()
    task_id = task_data["id"]

    # 2. Verify task is pending
    resp = await client.get(f"/tasks/{task_id}")
    data = await resp.json()
    assert data["status"] == "pending"

    # 3. Mock claude subprocess and run one daemon tick
    dobby_result = json.dumps({
        "status": "completed",
        "summary": "Created hello.py",
        "pr_url": None,
        "files_changed": ["hello.py"],
        "metrics": {},
        "error": None,
    })
    mock_output = f"Working on task...\n---DOBBY_RESULT---\n{dobby_result}\n---END_DOBBY_RESULT---"

    mock_proc = AsyncMock()
    mock_proc.stdout.readline = _make_readline_mock(mock_output.encode())
    mock_proc.returncode = 0
    mock_proc.pid = 9999
    mock_proc.wait = AsyncMock()

    with patch("asyncio.create_subprocess_exec", return_value=mock_proc):
        await daemon.tick()

    # 4. Verify task is completed
    resp = await client.get(f"/tasks/{task_id}")
    data = await resp.json()
    assert data["status"] == "completed"

    # 5. Verify result exists
    resp = await client.get(f"/results/{task_id}")
    assert resp.status == 200
    result = await resp.json()
    assert result["summary"] == "Created hello.py"

    # 6. Verify stats
    resp = await client.get("/stats")
    data = await resp.json()
    assert data["completed"] >= 1


async def test_task_failure_lifecycle(e2e):
    client = e2e["client"]
    daemon = e2e["daemon"]

    resp = await client.post("/tasks", json={
        "project": "test-project",
        "description": "This will fail",
    })
    task_id = (await resp.json())["id"]

    mock_proc = AsyncMock()
    mock_proc.stdout.readline = _make_readline_mock(b"Error: something went wrong")
    mock_proc.returncode = 1
    mock_proc.pid = 1234
    mock_proc.wait = AsyncMock()

    with patch("asyncio.create_subprocess_exec", return_value=mock_proc):
        await daemon.tick()

    resp = await client.get(f"/tasks/{task_id}")
    data = await resp.json()
    assert data["status"] == "failed"

    result = await (await client.get(f"/results/{task_id}")).json()
    assert result["status"] == "failed"


async def test_cancel_pending_task(e2e):
    client = e2e["client"]
    daemon = e2e["daemon"]

    resp = await client.post("/tasks", json={
        "project": "test-project",
        "description": "Long task",
    })
    task_id = (await resp.json())["id"]

    resp = await client.patch(f"/tasks/{task_id}", json={"status": "cancelled"})
    assert resp.status == 200
    data = await resp.json()
    assert data["status"] == "cancelled"

    await daemon.tick()
    resp = await client.get(f"/tasks/{task_id}")
    data = await resp.json()
    assert data["status"] == "cancelled"
