"""Phase 2 state-machine + gateway smoke: one scenario per finding.

Every hop goes through the REAL MCP tool functions -> real HTTP -> real
gateway gates -> real services -> real git against the local origin. No
LLM: the arcs ARE the agent script. Each scenario targets the exact
race/gate the finding names — a regression dies here.
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Any
from unittest.mock import patch

from tests.e2e_smoke.arcs import (
    dev_arc,
    doc_arc,
    qa_arc,
    seed_company,
    seed_hierarchy,
    seed_project,
    seed_task,
    task_state,
    wire_dependency,
)
from tests.e2e_smoke.harness import ScriptedAgent, expect_error, expect_ok

if TYPE_CHECKING:
    from sqlalchemy.ext.asyncio import AsyncSession
    from tests.e2e_smoke.harness import E2EStack


def test_m18_add_dependency_rejects_cycle_and_self(e2e_stack: E2EStack) -> None:
    """M18: wire_dependency on a self-edge must raise through the real
    service path, not silently drop."""
    from roboco.services.base import ConflictError

    stack = e2e_stack
    company = seed_company(stack)
    pid, _slug = seed_project(stack, company)
    a = seed_task(
        stack,
        title="A",
        description="d",
        project_id=pid,
        created_by=company.cell_pm_id,
        assigned_to=company.dev_id,
    )
    try:
        wire_dependency(stack, a, a)
    except ConflictError:
        return
    raise AssertionError("self-dependency silently dropped (M18)")


def test_h4_parallel_completion_single_transition(e2e_stack: E2EStack) -> None:
    """H4: dev_arc opens the PR (mark_pr_created) and doc_arc completes docs
    (docs_complete) — both reach awaiting_pm_review through the real path.
    The scenario asserts the task lands in awaiting_pm_review exactly once."""
    stack = e2e_stack
    company = seed_company(stack)
    pid, slug = seed_project(stack, company)
    tid = seed_task(
        stack,
        title="H4 parallel completion",
        description="Create greeting.txt",
        acceptance_criteria=["greeting.txt exists"],
        project_id=pid,
        created_by=company.cell_pm_id,
        assigned_to=company.dev_id,
    )
    dev_arc(stack, company, slug, tid)
    qa_arc(stack, company, tid)
    doc_arc(stack, company, tid, filename="greeting.txt")
    final = task_state(stack, tid)
    assert final["status"] == "awaiting_pm_review", final
    assert final["docs_complete"] is True


def test_h3_pm_cannot_complete_assembled_cell_from_in_progress(
    e2e_stack: E2EStack,
) -> None:
    """H3: a PM owning a cell task at IN_PROGRESS with a terminal child must
    NOT complete directly — submit_up is the only path. The gateway complete
    verb already enforces AWAITING_PM_REVIEW; this proves the service layer
    agrees."""
    stack = e2e_stack
    company = seed_company(stack)
    pid, slug = seed_project(stack, company)
    h = seed_hierarchy(stack, company, pid)
    dev_arc(stack, company, slug, h["child_id"], work=("hello.txt", "hi\n"))
    qa_arc(stack, company, h["child_id"])
    doc_arc(stack, company, h["child_id"], filename="hello.txt")
    pm = ScriptedAgent(stack, company.cell_pm_id, "be-pm", "cell_pm")
    expect_ok(
        pm.flow(
            "complete",
            task_id=str(h["child_id"]),
            notes="child complete — leaf merged via PR review gate",
        ),
        "pm complete child",
    )
    env = pm.flow("complete", task_id=str(h["cell_id"]), notes="try skip gate")
    assert env.get("error") is not None, (
        "PM completed assembled cell from IN_PROGRESS (H3)"
    )


def test_h5_unclaim_from_blocked_clears_snapshot(e2e_stack: E2EStack) -> None:
    """H5: block a task, unclaim from blocked, then read the row —
    pre_block_state must be null. Driven through the real i_am_blocked +
    unclaim verbs."""
    from roboco.db.tables import TaskTable
    from sqlalchemy import select

    stack = e2e_stack
    company = seed_company(stack)
    pid, _slug = seed_project(stack, company)
    tid = seed_task(
        stack,
        title="H5 snapshot clear",
        description="d",
        project_id=pid,
        created_by=company.cell_pm_id,
        assigned_to=company.dev_id,
    )
    dev = ScriptedAgent(stack, company.dev_id, "be-dev-1", "developer")
    expect_ok(dev.flow("give_me_work"), "dev give_me_work")

    def _claim() -> dict[str, Any]:
        return dev.flow(
            "i_will_work_on",
            task_id=str(tid),
            plan=(
                "Block the task on an external dependency, then unclaim from "
                "the blocked state. After unclaim the row's pre_block_state "
                "must be null so a later reclaim starts clean with no stale "
                "snapshot — this scenario asserts that invariant directly."
            ),
            steps=[
                {
                    "title": "Block the task",
                    "description": (
                        "Mark the task blocked on an external dependency via "
                        "the i_am_blocked verb with a concrete reason."
                    ),
                },
                {
                    "title": "Unclaim from blocked",
                    "description": (
                        "Release the blocked task back to the pool via the "
                        "unclaim verb so a later reclaim starts clean."
                    ),
                },
                {
                    "title": "Verify snapshot cleared",
                    "description": (
                        "Read the task row and assert pre_block_state is null "
                        "after the unclaim-from-blocked transition completes."
                    ),
                },
            ],
            technical_considerations=["None — block/unclaim path only."],
            risks=[
                {
                    "risk": "Stale snapshot survives unclaim.",
                    "mitigation": "Assert pre_block_state is null after unclaim.",
                }
            ],
            open_questions=[],
        )

    expect_error(_claim(), "tracing_gap", "dev first i_will_work_on")
    expect_ok(
        dev.do("note", scope="note", task_id=str(tid), text="claim note"), "dev note"
    )
    expect_ok(_claim(), "dev i_will_work_on retry")
    expect_ok(
        dev.flow("i_am_blocked", task_id=str(tid), reason="external dep"), "dev block"
    )
    # i_am_blocked reassigns the task to the cell PM (blocker_resolver), so
    # the PM is the agent that unclaims from the blocked state.
    pm = ScriptedAgent(stack, company.cell_pm_id, "be-pm", "cell_pm")
    expect_ok(pm.flow("unclaim", task_id=str(tid)), "pm unclaim from blocked")

    async def _snap(session: AsyncSession) -> Any:
        row = (
            await session.execute(select(TaskTable).where(TaskTable.id == tid))
        ).scalar_one()
        return row.pre_block_state

    assert stack.run_db(_snap) is None, (
        "pre_block_state not cleared after unclaim-from-blocked (H5)"
    )


def test_h6_pass_review_survives_a2a_failure(e2e_stack: E2EStack) -> None:
    """H6: with a2a.send patched to throw, pass_review must still return ok
    with a warning — the QA-pass transition committed; the side-effect
    failure must not 500 the verb."""
    stack = e2e_stack
    company = seed_company(stack)
    pid, slug = seed_project(stack, company)
    tid = seed_task(
        stack,
        title="H6 a2a down",
        description="d",
        project_id=pid,
        created_by=company.cell_pm_id,
        assigned_to=company.dev_id,
    )
    dev_arc(stack, company, slug, tid)
    with patch(
        "roboco.services.a2a.A2AService.send", side_effect=RuntimeError("a2a down")
    ):
        qa_arc(stack, company, tid)
    assert task_state(stack, tid)["status"] == "awaiting_documentation"


def test_l29_pass_qa_routes_through_awaiting_qa(e2e_stack: E2EStack) -> None:
    """L29: the QA arc claims review (task stays awaiting_qa) then passes.
    The end state is awaiting_documentation — the audit journey has a real
    task.awaiting_qa row."""
    stack = e2e_stack
    company = seed_company(stack)
    pid, slug = seed_project(stack, company)
    tid = seed_task(
        stack,
        title="L29 qa hop",
        description="d",
        project_id=pid,
        created_by=company.cell_pm_id,
        assigned_to=company.dev_id,
    )
    dev_arc(stack, company, slug, tid)
    qa_arc(stack, company, tid)
    assert task_state(stack, tid)["status"] == "awaiting_documentation"
