"""Anti-soup guard on the flow verbs (reason / notes / issues / title / desc).

Two layers are tested:

- the pure helpers ``_free_text_soup`` (skip empty/None, reject filler, walk
  list items) and ``_soup_or_decision_env`` (soup first, then the spec
  decision, else None);
- one end-to-end wiring test per emit style — ``i_am_blocked`` (folds soup into
  the spec-gate return) — proving a soupy ``reason`` is rejected before any
  state transition and a real reason passes through.
"""

from __future__ import annotations

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

import pytest
from roboco.services.gateway.choreographer import Choreographer, ChoreographerDeps
from roboco.services.gateway.choreographer._impl import Choreographer as _Impl
from roboco.services.gateway.envelope import Envelope

# --------------------------------------------------------------------------- #
# _free_text_soup
# --------------------------------------------------------------------------- #


@pytest.mark.parametrize("clean", ["a real substantive reason", "rate_limited"])
def test_free_text_soup_passes_substantive(clean: str) -> None:
    assert _Impl._free_text_soup((("reason", clean, 8),)) is None


@pytest.mark.parametrize("skip", [None, "", "   "])
def test_free_text_soup_skips_empty_and_none(skip: str | None) -> None:
    # Empty / None means "not supplied" — presence is gated elsewhere.
    assert _Impl._free_text_soup((("notes", skip, 8),)) is None


@pytest.mark.parametrize("soup", ["wip", "asdf", "tbd", "...", "x", "wip wip"])
def test_free_text_soup_rejects_filler(soup: str) -> None:
    env = _Impl._free_text_soup((("reason", soup, 3),))
    assert env is not None
    assert env.error == "invalid_state"


def test_free_text_soup_walks_list_items() -> None:
    # The second issue is filler — the list form must catch it.
    env = _Impl._free_text_soup(
        (("issues", ["a genuine actionable issue", "asdf"], 8),)
    )
    assert env is not None
    assert env.error == "invalid_state"
    assert "issues[1]" in (env.message or "")


def test_free_text_soup_clean_list_passes() -> None:
    env = _Impl._free_text_soup(
        (("issues", ["first real issue", "second real issue"], 8),)
    )
    assert env is None


# --------------------------------------------------------------------------- #
# _soup_or_decision_env
# --------------------------------------------------------------------------- #


def _allow() -> MagicMock:
    return MagicMock(allowed=True)


def _deny() -> MagicMock:
    return MagicMock(
        allowed=False,
        rejection_kind="invalid_state",
        message="bad state",
        remediate="do X",
    )


def test_soup_or_decision_prefers_soup() -> None:
    soup = Envelope.invalid_state(message="soup", remediate="fix", context_briefing={})
    out = _Impl._soup_or_decision_env(soup, _deny(), {})
    assert out is soup  # soup wins even when the decision also rejects


def test_soup_or_decision_falls_back_to_decision() -> None:
    out = _Impl._soup_or_decision_env(None, _deny(), {})
    assert out is not None
    assert out.error == "invalid_state"
    assert out.message == "bad state"


def test_soup_or_decision_none_when_all_clean() -> None:
    assert _Impl._soup_or_decision_env(None, _allow(), {}) is None


# --------------------------------------------------------------------------- #
# Wiring: i_am_blocked rejects a soupy reason before any transition
# --------------------------------------------------------------------------- #


def _make_deps(agent_id: object, task_id: object) -> ChoreographerDeps:
    t = MagicMock(
        id=task_id,
        status="in_progress",
        assigned_to=agent_id,
        task_type="code",
        team="backend",
        dependency_ids=[],
        acceptance_criteria=[],
    )
    task_svc = AsyncMock()
    task_svc.get.return_value = t
    task_svc.agent_for.return_value = MagicMock(
        id=agent_id, role="developer", team="backend", slug="be-dev-1"
    )
    evidence_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",
    ):
        getattr(evidence_repo, m).return_value = []
    return ChoreographerDeps(
        task=task_svc,
        work_session=AsyncMock(),
        git=AsyncMock(),
        a2a=AsyncMock(),
        journal=AsyncMock(),
        audit=AsyncMock(),
        evidence_repo=evidence_repo,
    )


async def test_i_am_blocked_rejects_soup_reason() -> None:
    agent_id, task_id = uuid4(), uuid4()
    deps = _make_deps(agent_id, task_id)
    c = Choreographer(deps)

    env = await c.i_am_blocked(agent_id, task_id, "wip")

    assert env.error == "invalid_state"
    # The block never happened — no struggle journal, no escalate.
    deps.journal.write_struggle.assert_not_awaited()
    deps.task.escalate.assert_not_awaited()


async def test_i_am_blocked_accepts_real_reason() -> None:
    agent_id, task_id = uuid4(), uuid4()
    deps = _make_deps(agent_id, task_id)
    c = Choreographer(deps)

    env = await c.i_am_blocked(
        agent_id, task_id, "Waiting on the upstream auth schema migration."
    )

    # A substantive reason clears the soup guard (then proceeds to the spec
    # gate / block path — which writes the struggle journal).
    assert env.error != "invalid_state" or "placeholder" not in (env.message or "")
    deps.journal.write_struggle.assert_awaited_once()
