"""Self-heal fix tasks must WAIT for the CEO's Approve-&-Start. ``_originate``
opens them ``confirmed_by_human=False`` (held); the orchestrator holds them out
of dispatch until ``approve_and_start`` flips it True. The 'never self-deploys'
guarantee (no merge) is unchanged.
"""

from __future__ import annotations

from typing import Any, cast
from unittest.mock import AsyncMock, MagicMock

import pytest
import roboco.services.self_heal_engine as _self_heal_mod
from roboco.foundation import identity as _foundation
from roboco.models.base import TaskStatus, TaskType
from roboco.runtime.orchestrator import AgentOrchestrator
from roboco.services.self_heal_engine import RegressionObservation, SelfHealEngine
from roboco.services.task import (
    SELF_HEAL_SOURCE,
    TaskCreateRequest,
    TaskService,
)
from sqlalchemy.dialects import postgresql

MAIN_PM_UUID = _foundation.AGENTS["main-pm"].uuid
SYSTEM_UUID = _foundation.AGENTS["system"].uuid


def _task(
    tid: str,
    source: str,
    *,
    assigned_to: str | None = None,
    confirmed: bool = True,
) -> dict[str, Any]:
    return {
        "id": tid,
        "source": source,
        "assigned_to": assigned_to,
        "confirmed_by_human": confirmed,
    }


# ---------------------------------------------------------------------------
# Orchestrator: a held self-heal task (not yet CEO-confirmed) is NOT dispatched
# ---------------------------------------------------------------------------


def _pm_stub(tasks: list[dict[str, Any]]) -> MagicMock:
    stub = MagicMock()
    stub._fetch_tasks = AsyncMock(return_value=tasks)
    stub._is_task_handled_this_tick = MagicMock(return_value=False)
    stub._resolve_agent_slug = MagicMock(return_value="main-pm")
    stub._BOARD_AGENTS = frozenset()
    stub._route_unassigned_pm_task = AsyncMock()
    stub._handle_pm_assigned_task = AsyncMock()
    stub._handle_board_assigned_task = AsyncMock()
    return stub


@pytest.mark.asyncio
async def test_held_self_heal_task_is_not_dispatched() -> None:
    """A self-heal fix task the CEO has NOT yet approved (confirmed_by_human=False)
    is held out of the assigned-PM dispatch path — no autonomous dispatch."""
    tasks = [
        _task("A", SELF_HEAL_SOURCE, assigned_to="main-pm", confirmed=False),
    ]
    stub = _pm_stub(tasks)
    client: Any = MagicMock()
    await AgentOrchestrator._dispatch_pm_work(cast("AgentOrchestrator", stub), client)

    stub._handle_pm_assigned_task.assert_not_awaited()
    stub._route_unassigned_pm_task.assert_not_awaited()


@pytest.mark.asyncio
async def test_ceo_approved_self_heal_task_dispatches() -> None:
    """Once the CEO has approved the self-heal task (confirmed_by_human=True via
    approve_and_start), it dispatches through the assigned-PM path normally."""
    tasks = [
        _task("A", SELF_HEAL_SOURCE, assigned_to="main-pm", confirmed=True),
        _task("B", SELF_HEAL_SOURCE, assigned_to="main-pm", confirmed=False),
    ]
    stub = _pm_stub(tasks)
    client: Any = MagicMock()
    await AgentOrchestrator._dispatch_pm_work(cast("AgentOrchestrator", stub), client)

    handled = [c.args[0]["id"] for c in stub._handle_pm_assigned_task.await_args_list]
    assert handled == ["A"]  # only the CEO-approved one


@pytest.mark.asyncio
async def test_non_self_heal_tasks_dispatch_regardless_of_confirmation() -> None:
    """The hold is specific to self-heal — an ordinary confirmed-by-human task
    is NOT gated by this skip (regression guard for the board/intake flow)."""
    tasks = [
        _task("A", "manual", assigned_to="main-pm", confirmed=False),
        _task("B", "manual", assigned_to="main-pm", confirmed=True),
    ]
    stub = _pm_stub(tasks)
    client: Any = MagicMock()
    await AgentOrchestrator._dispatch_pm_work(cast("AgentOrchestrator", stub), client)

    handled = [c.args[0]["id"] for c in stub._handle_pm_assigned_task.await_args_list]
    assert set(handled) == {"A", "B"}


# ---------------------------------------------------------------------------
# _originate: the opened task is held for the CEO (confirmed_by_human=False)
# ---------------------------------------------------------------------------


def _originate_engine(
    captured: dict[str, Any],
) -> tuple[SelfHealEngine, MagicMock, MagicMock, Any]:
    """An engine whose task/project services are mocks that capture the create
    request — so the held-for-CEO invariant is unit-testable without a DB."""
    session = MagicMock()
    session.flush = AsyncMock()

    task_svc = MagicMock()
    task_svc.list_open_self_heal_tasks = AsyncMock(return_value=[])
    task_svc.create = AsyncMock(
        side_effect=lambda req: (captured.setdefault("req", req), MagicMock(id="t1"))[1]
    )

    project_svc = MagicMock()
    project_svc.get_by_slug = AsyncMock(return_value=MagicMock(id="p1"))

    engine = SelfHealEngine.__new__(SelfHealEngine)  # skip __init__ (no source)
    engine.session = session
    engine.log = MagicMock()
    engine._source = MagicMock()
    return engine, task_svc, project_svc, _self_heal_mod


def _bind(svc: object, name: str, value: object) -> None:
    """Stub `name` on `svc` without tripping mypy's method-assign check."""
    object.__setattr__(svc, name, value)


@pytest.mark.asyncio
async def test_originate_opens_task_held_for_ceo(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """The opened self-heal fix task is ``confirmed_by_human=False`` — held for
    the CEO's Approve-&-Start, NOT auto-confirmed for autonomous dispatch."""
    captured: dict[str, Any] = {}
    engine, task_svc, project_svc, mod = _originate_engine(captured)
    monkeypatch.setattr(mod, "get_task_service", lambda _s: task_svc)
    monkeypatch.setattr(mod, "get_project_service", lambda _s: project_svc)
    monkeypatch.setattr(mod, "markers", MagicMock())  # set_self_heal_fingerprint no-op

    obs = [
        RegressionObservation(
            fingerprint="fp1",
            signal_name="ci:roboco",
            repo_hint="roboco",
            summary="x",
            detail="d",
            raw_ref="r",
        )
    ]
    # session.flush is set in _originate_engine (MagicMock) — _originate awaits it.

    count = await engine._originate(obs)

    assert count == 1
    req: TaskCreateRequest = captured["req"]
    assert req.confirmed_by_human is False  # held for the CEO — the F059 fix
    assert req.status == TaskStatus.PENDING
    assert req.source == SELF_HEAL_SOURCE
    # A Main-PM-owned coordination root (not code — the Main PM delegates the
    # fix to a cell dev). `code` + `main_pm` is rejected by the
    # main_pm_cannot_own_code guard, so the self-heal root must be `planning`.
    assert req.task_type == TaskType.PLANNING


# ---------------------------------------------------------------------------
# approve_and_start: the CEO's start gate lifts the hold
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_approve_and_start_lifts_the_ceo_hold(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """``approve_and_start`` is the CEO's start gate — it flips
    ``confirmed_by_human`` True so a held self-heal task finally dispatches."""
    session = MagicMock()
    session.flush = AsyncMock()
    svc = TaskService(session)
    task = MagicMock()
    task.status = TaskStatus.PENDING
    task.team = MagicMock()
    task.team.value = "main_pm"
    task.board_review_complete = True
    task.confirmed_by_human = False  # held self-heal task
    task.assigned_to = "someone-else"
    task.task_type = MagicMock()
    task.task_type.value = "code"
    _bind(svc, "get", AsyncMock(return_value=task))
    main_pm = MagicMock(id=MAIN_PM_UUID)
    agent_svc = MagicMock()
    agent_svc.get_by_slug = AsyncMock(return_value=main_pm)
    monkeypatch.setattr(
        "roboco.services.agent.get_agent_service",
        MagicMock(return_value=agent_svc),
    )
    monkeypatch.setattr(svc, "_activate_batch_root_subtasks", AsyncMock())
    monkeypatch.setattr(svc, "_emit_task_event", AsyncMock())
    monkeypatch.setattr(
        "roboco.services.task.main_pm_cannot_own_code", lambda *_args, **_kwargs: False
    )

    await svc.approve_and_start(_foundation.AGENTS["main-pm"].uuid, notes=None)

    assert task.confirmed_by_human is True  # the hold lifts on CEO approval


# ---------------------------------------------------------------------------
# list_pending_for_agent: give_me_work never offers a held task
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_list_pending_for_agent_excludes_held_self_heal() -> None:
    """A held self-heal fix task is never offered via give_me_work — an
    already-alive PM can't grab it before the CEO approves.

    Asserted at the SQL layer: the query scopes the hold to self-heal
    (``source != 'self_heal' OR confirmed_by_human``), so the database drops a
    held self-heal task before the agent ever sees the list.
    """
    session = MagicMock()
    result = MagicMock()
    result.scalars.return_value.all.return_value = []
    session.execute = AsyncMock(return_value=result)
    svc = TaskService(session)
    _bind(svc, "unmet_dependency_ids", AsyncMock(return_value=[]))

    await svc.list_pending_for_agent(MAIN_PM_UUID)

    stmt = session.execute.await_args.args[0]
    compiled = str(
        stmt.compile(
            dialect=postgresql.dialect(), compile_kwargs={"literal_binds": True}
        )
    )
    assert "self_heal" in compiled  # scoped to self-heal, not a universal gate
    assert "confirmed_by_human" in compiled


@pytest.mark.asyncio
async def test_list_pending_for_agent_still_offers_delegated_subtask() -> None:
    """Regression guard: the hold is scoped to self-heal. A delegated
    subtask (source != self_heal, confirmed_by_human=False — the default for
    PM-delegated work, where the delegation IS the authorization to start) must
    STILL be offered via give_me_work. A universal confirmed_by_human filter
    would starve devs of all delegated work."""
    session = MagicMock()

    delegated = MagicMock()
    delegated.source = "manual"  # not self_heal
    delegated.confirmed_by_human = False  # the delegation default
    delegated.dependency_ids = []

    result = MagicMock()
    result.scalars.return_value.all.return_value = [delegated]
    session.execute = AsyncMock(return_value=result)
    svc = TaskService(session)
    _bind(svc, "unmet_dependency_ids", AsyncMock(return_value=[]))

    available = await svc.list_pending_for_agent(MAIN_PM_UUID)

    assert delegated in available


if __name__ == "__main__":
    pytest.main([__file__, "-q"])
