"""Unit tests for auditor-targeted rework alert producers.

Covers the reactive audit dispatch path: ``NotificationDeliveryService``
creates ``ALERT`` notifications addressed to the auditor, and ``TaskService``
invokes that producer at the QA-fail / rework chokepoints
(``fail_qa``, ``pr_fail``, ``request_changes``).
"""

from __future__ import annotations

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

import pytest
from roboco.models.base import (
    NotificationPriority,
    NotificationType,
    TaskStatus,
)
from roboco.services.notification_delivery import NotificationDeliveryService
from roboco.services.task import TaskService
from roboco.utils.converters import require_uuid


def _mock_task(
    task_id: UUID | None = None, status: Any = TaskStatus.AWAITING_QA
) -> MagicMock:
    task = MagicMock()
    task.id = task_id or uuid4()
    task.title = "Test task"
    task.status = status
    task.assigned_to = uuid4()
    task.claimed_by = None
    task.active_claimant_id = None
    task.team = MagicMock()
    task.team.value = "backend"
    task.qa_verified = True
    task.qa_notes = None
    task.dev_notes = None
    task.orchestration_markers = {}
    task.notes_structured = None
    task.revision_count = 0
    return task


def _mock_agent(*, role: str = "auditor", slug: str = "auditor") -> MagicMock:
    agent = MagicMock()
    agent.id = uuid4()
    agent.role = role
    agent.slug = slug
    return agent


def _session_with_agent(agent: MagicMock | None) -> MagicMock:
    """A session whose execute().scalars().first() returns ``agent``.

    ``session.add`` assigns a fresh UUID to the notification so that
    ``_persist_and_deliver`` can call ``require_uuid(notification.id)``.
    """
    session = MagicMock()

    def _assign_id(obj: Any) -> None:
        if getattr(obj, "id", None) is None:
            obj.id = uuid4()

    session.add = MagicMock(side_effect=_assign_id)
    session.flush = AsyncMock()
    result = MagicMock()
    result.scalars.return_value.first.return_value = agent
    result.scalar_one_or_none.return_value = agent
    session.execute = AsyncMock(return_value=result)
    return session


# ---------------------------------------------------------------------------
# NotificationDeliveryService.notify_auditor_of_rework
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_notify_auditor_of_rework_creates_alert_to_auditor(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """The delivery service builds an ALERT notification targeted at the auditor."""
    auditor = _mock_agent(role="auditor", slug="auditor")
    actor = _mock_agent(role="qa", slug="be-qa")
    session = _session_with_agent(auditor)
    svc = NotificationDeliveryService(session)
    monkeypatch.setattr(svc, "_get_auditor_agent", AsyncMock(return_value=auditor))
    monkeypatch.setattr(svc, "_get_agent_by_id", AsyncMock(return_value=actor))
    monkeypatch.setattr(svc, "deliver", AsyncMock(return_value=True))

    task = _mock_task()
    with patch(
        "roboco.services.notification_delivery.all_recipients_recently_notified",
        AsyncMock(return_value=False),
    ):
        await svc.notify_auditor_of_rework(
            task=task,
            task_id=require_uuid(task.id),
            reason="QA review failed",
            actor_agent_id=actor.id,
            actor_role="qa",
        )

    added = [c for c in session.add.call_args_list if c.args]
    assert len(added) == 1
    notification = added[0].args[0]
    assert notification.type == NotificationType.ALERT
    assert notification.priority == NotificationPriority.HIGH
    assert notification.to_agents == [auditor.id]
    assert notification.from_agent == actor.id
    assert "QA review failed" in notification.body
    assert "Actor role: qa" in notification.body
    assert notification.requires_ack is True


@pytest.mark.asyncio
async def test_notify_auditor_of_rework_skips_when_no_auditor(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """If the auditor agent is absent, the producer is a silent no-op."""
    session = _session_with_agent(None)
    svc = NotificationDeliveryService(session)
    monkeypatch.setattr(svc, "_get_auditor_agent", AsyncMock(return_value=None))
    deliver_spy = AsyncMock()
    monkeypatch.setattr(svc, "deliver", deliver_spy)

    task = _mock_task()
    await svc.notify_auditor_of_rework(
        task=task,
        task_id=require_uuid(task.id),
        reason="QA review failed",
    )

    assert not session.add.called
    assert not deliver_spy.called


# ---------------------------------------------------------------------------
# TaskService._alert_auditor_of_rework
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_alert_auditor_of_rework_calls_delivery_service() -> None:
    """TaskService delegates to the notification delivery service."""
    session = MagicMock()
    session.flush = AsyncMock()
    task = _mock_task()
    svc = TaskService(session)

    fake_delivery = AsyncMock()
    fake_delivery.notify_auditor_of_rework = AsyncMock()

    with patch(
        "roboco.services.notification_delivery.get_notification_delivery_service",
        lambda _s: fake_delivery,
    ):
        await svc._alert_auditor_of_rework(
            task,
            reason="QA review failed",
            actor_agent_id=task.assigned_to,
            actor_role="qa",
        )

    fake_delivery.notify_auditor_of_rework.assert_awaited_once()
    call = fake_delivery.notify_auditor_of_rework.await_args
    assert call.kwargs["task"] is task
    assert call.kwargs["reason"] == "QA review failed"
    assert call.kwargs["actor_role"] == "qa"
    assert call.kwargs["actor_agent_id"] == task.assigned_to


@pytest.mark.asyncio
async def test_alert_auditor_of_rework_is_best_effort() -> None:
    """A delivery failure must not raise out of the producer."""
    session = MagicMock()
    task = _mock_task()
    svc = TaskService(session)

    with patch(
        "roboco.services.notification_delivery.get_notification_delivery_service",
        side_effect=RuntimeError("redis down"),
    ):
        await svc._alert_auditor_of_rework(
            task, reason="QA review failed", actor_role="qa"
        )


# ---------------------------------------------------------------------------
# Chokepoint wiring: fail_qa, pr_fail, request_changes
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_fail_qa_emits_auditor_alert(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """``fail_qa`` calls the auditor rework producer with QA attribution."""
    session = MagicMock()
    session.flush = AsyncMock()
    task = _mock_task(status=TaskStatus.AWAITING_QA)
    task.orchestration_markers = {"original_developer": str(uuid4())}

    svc = TaskService(session)
    monkeypatch.setattr(svc, "get", AsyncMock(return_value=task))
    monkeypatch.setattr(svc, "_validate_and_set_status", MagicMock())
    alert_spy = AsyncMock()
    monkeypatch.setattr(svc, "_alert_auditor_of_rework", alert_spy)

    with (
        patch(
            "roboco.services.task.extract_original_developer",
            return_value=str(uuid4()),
        ),
        patch("roboco.services.task.asyncio.create_task", MagicMock()),
    ):
        out = await svc.fail_qa(task.id, notes="missing tests")

    assert out is task
    alert_spy.assert_awaited_once()
    call = alert_spy.await_args
    assert call is not None
    assert call.args[0] is task
    assert call.kwargs["reason"] == "missing tests"
    assert call.kwargs["actor_role"] == "qa"


@pytest.mark.asyncio
async def test_pr_fail_emits_auditor_alert(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """``pr_fail`` calls the auditor rework producer with reviewer attribution."""
    session = MagicMock()
    session.flush = AsyncMock()
    reviewer_id = uuid4()
    pm_id = uuid4()
    task = _mock_task(status=TaskStatus.AWAITING_PR_REVIEW)
    task.claimed_by = reviewer_id

    pm = MagicMock()
    pm.id = pm_id

    svc = TaskService(session)
    monkeypatch.setattr(svc, "get", AsyncMock(return_value=task))
    monkeypatch.setattr(svc, "_validate_and_set_status", MagicMock())
    monkeypatch.setattr(svc, "_revision_pm_for_task", AsyncMock(return_value=pm))
    alert_spy = AsyncMock()
    monkeypatch.setattr(svc, "_alert_auditor_of_rework", alert_spy)

    out = await svc.pr_fail(
        reviewer_id, task.id, notes="convention violation", issues=["mv model"]
    )

    assert out is task
    alert_spy.assert_awaited_once()
    call = alert_spy.await_args
    assert call is not None
    assert call.args[0] is task
    assert call.kwargs["reason"] == "convention violation"
    assert call.kwargs["actor_role"] == "pr_reviewer"
    assert call.kwargs["actor_agent_id"] == reviewer_id


@pytest.mark.asyncio
async def test_request_changes_emits_auditor_alert(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """``request_changes`` calls the auditor rework producer with PM attribution."""
    session = MagicMock()
    session.flush = AsyncMock()
    pm_id = uuid4()
    pm = MagicMock()
    pm.id = pm_id
    task = _mock_task(status=TaskStatus.AWAITING_PM_REVIEW)
    task.claimed_by = pm_id

    svc = TaskService(session)
    monkeypatch.setattr(svc, "get", AsyncMock(return_value=task))
    monkeypatch.setattr(svc, "_validate_and_set_status", MagicMock())
    monkeypatch.setattr(svc, "_revision_pm_for_task", AsyncMock(return_value=pm))
    alert_spy = AsyncMock()
    monkeypatch.setattr(svc, "_alert_auditor_of_rework", alert_spy)

    with patch("roboco.services.task.extract_original_developer", return_value=None):
        out = await svc.request_changes(
            pm_id,
            task.id,
            notes="AC missing",
            issues=["add test"],
            agent_role="cell_pm",
        )

    assert out is task
    alert_spy.assert_awaited_once()
    call = alert_spy.await_args
    assert call is not None
    assert call.args[0] is task
    assert call.kwargs["reason"] == "AC missing"
    assert call.kwargs["actor_role"] == "cell_pm"
    assert call.kwargs["actor_agent_id"] == pm_id
