"""PM recovery of a rejected coordination task from needs_revision.

The in-path PR-review gate (and qa_fail / ceo_reject) can land a PM-owned
coordination/assembled task in ``needs_revision``. That state used to be
developer-claim-only, so the task had no actor and no exit but ``cancel`` — the
cell PM escalated in a loop. Now a PM re-claims its rejected task
(``i_will_plan``), revises the plan, and re-delegates the fixes.

Scope: ``pr_fail`` / ``qa_fail`` reassign the task to its owning PM, and
``give_me_work`` only ever offers an agent its OWN assigned tasks — so a PM is
never handed a developer's leaf, exactly as a developer is never handed a PM's
coordination root. (The scope lives in routing, not a gateway-only ownership
gate, which would break the spec=gateway parity invariant.)
"""

from __future__ import annotations

from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock
from uuid import uuid4

import pytest
from roboco.foundation.policy import lifecycle as spec
from roboco.services.gateway.choreographer import Choreographer, ChoreographerDeps

# --------------------------------------------------------------------------- #
# Lifecycle authority: PMs may now claim needs_revision
# --------------------------------------------------------------------------- #


def test_pm_can_claim_needs_revision() -> None:
    task = SimpleNamespace(
        status="needs_revision", task_type="planning", assigned_to=None
    )
    for role in (spec.Role.CELL_PM, spec.Role.MAIN_PM):
        assert spec.can_invoke_action(role, "claim", task).allowed
    # Developers still own leaf revisions; QA / documenter still cannot claim it.
    assert spec.can_invoke_action(spec.Role.DEVELOPER, "claim", task).allowed
    assert not spec.can_invoke_action(spec.Role.QA, "claim", task).allowed
    assert not spec.can_invoke_action(spec.Role.DOCUMENTER, "claim", task).allowed


def test_pm_claim_needs_revision_works_for_code_typed_root() -> None:
    # Main-PM coordination roots can be code-typed, so the claim must NOT be
    # task_type-gated — authority is status-based, scoped by routing.
    task = SimpleNamespace(status="needs_revision", task_type="code", assigned_to=None)
    assert spec.can_invoke_action(spec.Role.MAIN_PM, "claim", task).allowed


# --------------------------------------------------------------------------- #
# Routing scope: give_me_work offers a PM its OWN rejected coordination task
# --------------------------------------------------------------------------- #


def _make_deps(task_svc: AsyncMock) -> ChoreographerDeps:
    repo = AsyncMock()
    for m in (
        "list_unread_a2a",
        "list_unread_mentions",
        "list_pending_notifications",
        "task_metadata_gaps",
        "recent_team_activity",
        "blockers_in_lane",
        "journal_highlights_for_task",
        "company_goals",
    ):
        getattr(repo, m).return_value = []
    return ChoreographerDeps(
        task=task_svc,
        work_session=AsyncMock(),
        git=AsyncMock(),
        a2a=AsyncMock(),
        journal=AsyncMock(),
        audit=AsyncMock(),
        evidence_repo=repo,
    )


@pytest.mark.asyncio
async def test_give_me_work_offers_pm_its_needs_revision_task() -> None:
    pm_id, task_id = uuid4(), uuid4()
    task = MagicMock(
        id=task_id, status="needs_revision", team="backend", dependency_ids=[]
    )
    task_svc = AsyncMock()
    task_svc.list_pending_for_agent.return_value = []
    task_svc.list_assigned_for_agent.return_value = [task]
    task_svc.agent_for.return_value = MagicMock(role="cell_pm", team="backend")
    c = Choreographer(_make_deps(task_svc))

    env = await c.give_me_work(pm_id)
    body = env.as_dict()

    assert body["task_id"] == str(task_id)
    # The PM is told to re-plan (revise) the rejected task, not a dev verb.
    assert "i_will_plan" in body["next"]
