"""Per-dev sequenced queues: a dev works its own code queue one task at a time.

A PM delegates a full per-dev queue of `code` subtasks up front. The dispatch
barrier holds a dev's higher-sequence code leaf until its own lower-sequence
code siblings under the same parent are terminal, so the dev works its queue in
order — while the OTHER dev's lane runs concurrently (two-dev parallelism).
Keyed on the assignee (not the team like the merge barrier) and gates only
`code`. Loop-free (not dispatched, not rejected); best-effort on lookup failure.
"""

from __future__ import annotations

from typing import Any, cast
from unittest.mock import AsyncMock, MagicMock, patch
from uuid import uuid4

import pytest
from roboco.models.base import TaskStatus
from roboco.runtime.orchestrator import AgentOrchestrator


def _new_orchestrator() -> AgentOrchestrator:
    return AgentOrchestrator.__new__(AgentOrchestrator)


def _sibling(
    seq: int,
    owner: str,
    status: TaskStatus,
    *,
    task_type: str = "code",
) -> MagicMock:
    return MagicMock(
        id=uuid4(),
        sequence=seq,
        assigned_to=owner,
        status=status,
        task_type=task_type,
    )


def _patch_siblings(siblings: list[MagicMock]) -> Any:
    """Patch the orchestrator's direct-DB sibling lookup to return ``siblings``."""
    svc = MagicMock()
    svc.get_subtasks = AsyncMock(return_value=siblings)

    class _CM:
        async def __aenter__(self) -> MagicMock:
            return MagicMock()

        async def __aexit__(self, *_a: Any) -> bool:
            return False

    factory = MagicMock(return_value=_CM())
    return (
        patch("roboco.db.base.get_session_factory", return_value=factory),
        patch("roboco.services.task.get_task_service", return_value=svc),
    )


def _task(seq: int, owner: str, *, task_type: str = "code") -> dict[str, Any]:
    return {
        "id": str(uuid4()),
        "parent_task_id": str(uuid4()),
        "sequence": seq,
        "assigned_to": owner,
        "task_type": task_type,
    }


@pytest.mark.asyncio
async def test_blocks_when_same_dev_has_earlier_active_code_sibling() -> None:
    orch = _new_orchestrator()
    task = _task(1, "be-dev-1")
    siblings = [_sibling(0, "be-dev-1", TaskStatus.IN_PROGRESS)]
    p1, p2 = _patch_siblings(siblings)
    with p1, p2:
        assert await orch._blocked_by_earlier_lane_sibling(task) is True


@pytest.mark.asyncio
async def test_not_blocked_when_earlier_same_dev_sibling_terminal() -> None:
    orch = _new_orchestrator()
    task = _task(1, "be-dev-1")
    siblings = [
        _sibling(0, "be-dev-1", TaskStatus.COMPLETED),
        _sibling(0, "be-dev-1", TaskStatus.CANCELLED),
    ]
    p1, p2 = _patch_siblings(siblings)
    with p1, p2:
        assert await orch._blocked_by_earlier_lane_sibling(task) is False


@pytest.mark.asyncio
async def test_other_devs_earlier_sibling_does_not_block() -> None:
    """The whole point of two-dev parallelism: be-dev-2's in-flight wave-0 leaf
    must NOT hold be-dev-1's own wave-0 leaf. Lanes are independent."""
    orch = _new_orchestrator()
    task = _task(0, "be-dev-1")
    siblings = [_sibling(0, "be-dev-2", TaskStatus.IN_PROGRESS)]
    p1, p2 = _patch_siblings(siblings)
    with p1, p2:
        assert await orch._blocked_by_earlier_lane_sibling(task) is False


@pytest.mark.asyncio
async def test_earlier_non_code_sibling_does_not_block() -> None:
    """Only the code queue is gated this way; a planning/doc sibling is irrelevant."""
    orch = _new_orchestrator()
    task = _task(1, "be-dev-1")
    siblings = [_sibling(0, "be-dev-1", TaskStatus.IN_PROGRESS, task_type="planning")]
    p1, p2 = _patch_siblings(siblings)
    with p1, p2:
        assert await orch._blocked_by_earlier_lane_sibling(task) is False


@pytest.mark.asyncio
async def test_higher_sequence_same_dev_sibling_does_not_block() -> None:
    orch = _new_orchestrator()
    task = _task(0, "be-dev-1")
    siblings = [_sibling(1, "be-dev-1", TaskStatus.IN_PROGRESS)]
    p1, p2 = _patch_siblings(siblings)
    with p1, p2:
        assert await orch._blocked_by_earlier_lane_sibling(task) is False


@pytest.mark.asyncio
async def test_non_code_task_is_never_gated_without_db() -> None:
    orch = _new_orchestrator()
    # A planning/doc task short-circuits before any lookup.
    task = _task(1, "fe-pm", task_type="planning")
    assert await orch._blocked_by_earlier_lane_sibling(task) is False


@pytest.mark.asyncio
async def test_no_parent_or_owner_short_circuits_without_db() -> None:
    orch = _new_orchestrator()
    assert (
        await orch._blocked_by_earlier_lane_sibling(
            {"id": str(uuid4()), "sequence": 0, "task_type": "code"}
        )
        is False
    )


@pytest.mark.asyncio
async def test_db_failure_falls_through_to_dispatch() -> None:
    orch = _new_orchestrator()
    task = _task(1, "be-dev-1")
    boom = patch(
        "roboco.db.base.get_session_factory", side_effect=RuntimeError("db down")
    )
    with boom:
        assert await orch._blocked_by_earlier_lane_sibling(task) is False


@pytest.mark.asyncio
async def test_spawn_pending_dev_holds_gated_lane_before_validating(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """_spawn_pending_dev must short-circuit a gated lane before validating or
    spawning — the dev's earlier queue item is still live."""
    orch = _new_orchestrator()
    task = _task(1, "be-dev-1")
    spawn = AsyncMock()
    validate = AsyncMock()
    monkeypatch.setattr(orch, "_is_agent_active", MagicMock(return_value=False))
    monkeypatch.setattr(
        orch, "_blocked_by_earlier_lane_sibling", AsyncMock(return_value=True)
    )
    monkeypatch.setattr(orch, "_validate_task_for_spawn", validate)
    monkeypatch.setattr(orch, "spawn_agent", spawn)

    await orch._spawn_pending_dev(cast("Any", MagicMock()), task, "be-dev-1")

    spawn.assert_not_awaited()
    validate.assert_not_awaited()


@pytest.mark.asyncio
async def test_spawn_pending_dev_proceeds_when_lane_clear(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """When the lane is clear (no earlier sibling), the dev is spawned normally."""
    orch = _new_orchestrator()
    task = _task(0, "be-dev-1")
    spawn = AsyncMock()
    monkeypatch.setattr(orch, "_is_agent_active", MagicMock(return_value=False))
    monkeypatch.setattr(
        orch, "_blocked_by_earlier_lane_sibling", AsyncMock(return_value=False)
    )
    monkeypatch.setattr(orch, "_validate_task_for_spawn", AsyncMock(return_value=None))
    monkeypatch.setattr(orch, "spawn_agent", spawn)
    monkeypatch.setattr(orch, "_get_prompt_for_agent", MagicMock(return_value="prompt"))
    monkeypatch.setattr(orch, "_task_git_context", MagicMock(return_value={}))

    await orch._spawn_pending_dev(cast("Any", MagicMock()), task, "be-dev-1")

    spawn.assert_awaited_once()
