"""Choreographer.waive_finding — the auditor's close-without-fix verb.

``mark_waived`` was the long-unwired repo method; this is its only caller.
Severity-scoped: blocker/major must be fixed, never waived. Only open
findings are waivable, and a note is required. No task status changes —
the ledger row ``open -> waived`` plus a ``task.finding_waived`` audit
event is the durable record.
"""

from __future__ import annotations

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

import pytest
from roboco.services.gateway.choreographer import Choreographer, ChoreographerDeps


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


def _finding_row(
    *,
    severity: str = "minor",
    status: str = "open",
    origin: str = "qa",
) -> MagicMock:
    row = MagicMock()
    row.id = uuid4()
    row.task_id = uuid4()
    row.severity = severity
    row.status = status
    row.origin = origin
    return row


def _patch_repo(monkeypatch: pytest.MonkeyPatch, row: Any | None) -> MagicMock:
    """Patch the board-module ReviewFindingsRepository to return ``row`` from
    ``get`` and a recording ``mark_waived``. ``row=None`` simulates not-found."""
    repo_mock = MagicMock()
    repo_mock.get = AsyncMock(return_value=row)
    repo_mock.mark_waived = AsyncMock(return_value=True)
    monkeypatch.setattr(
        "roboco.services.gateway.choreographer.board.ReviewFindingsRepository",
        lambda *_a, **_k: repo_mock,
    )
    return repo_mock


def _choreographer(
    monkeypatch: pytest.MonkeyPatch, row: Any | None
) -> tuple[Choreographer, MagicMock]:
    deps = _make_deps()
    deps.task.session = MagicMock()
    c = Choreographer(deps)
    return c, _patch_repo(monkeypatch, row)


@pytest.mark.asyncio
async def test_waive_minor_open_finding_succeeds(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    row = _finding_row(severity="minor", status="open")
    c, repo_mock = _choreographer(monkeypatch, row)

    env = await c.waive_finding(uuid4(), row.id, "cosmetic, not worth a fix")

    assert env.error is None
    assert env.status == "waived"
    repo_mock.mark_waived.assert_awaited_once_with(row.id, "cosmetic, not worth a fix")
    c.audit.log_task_event.assert_awaited_once()
    assert c.audit.log_task_event.await_args.kwargs["event_type"] == (
        "task.finding_waived"
    )


@pytest.mark.asyncio
async def test_waive_nit_open_finding_succeeds(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    row = _finding_row(severity="nit", status="open")
    c, repo_mock = _choreographer(monkeypatch, row)
    env = await c.waive_finding(uuid4(), row.id, "preference only")
    assert env.error is None
    repo_mock.mark_waived.assert_awaited_once()


@pytest.mark.asyncio
async def test_waive_rejects_blocker(monkeypatch: pytest.MonkeyPatch) -> None:
    row = _finding_row(severity="blocker", status="open")
    c, repo_mock = _choreographer(monkeypatch, row)
    env = await c.waive_finding(uuid4(), row.id, "try to skip the fix")
    assert env.error == "invalid_state"
    repo_mock.mark_waived.assert_not_awaited()


@pytest.mark.asyncio
async def test_waive_rejects_major(monkeypatch: pytest.MonkeyPatch) -> None:
    row = _finding_row(severity="major", status="open")
    c, repo_mock = _choreographer(monkeypatch, row)
    env = await c.waive_finding(uuid4(), row.id, "try to skip the fix")
    assert env.error == "invalid_state"
    repo_mock.mark_waived.assert_not_awaited()


@pytest.mark.asyncio
async def test_waive_rejects_already_addressed(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    row = _finding_row(severity="minor", status="addressed")
    c, repo_mock = _choreographer(monkeypatch, row)
    env = await c.waive_finding(uuid4(), row.id, "already closed")
    assert env.error == "invalid_state"
    repo_mock.mark_waived.assert_not_awaited()


@pytest.mark.asyncio
async def test_waive_rejects_blank_note(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    row = _finding_row(severity="minor", status="open")
    c, repo_mock = _choreographer(monkeypatch, row)
    env = await c.waive_finding(uuid4(), row.id, "   ")
    assert env.error == "invalid_state"
    repo_mock.mark_waived.assert_not_awaited()


@pytest.mark.asyncio
async def test_waive_unknown_finding_returns_not_found(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    c, repo_mock = _choreographer(monkeypatch, None)
    env = await c.waive_finding(uuid4(), uuid4(), "note")
    assert env.error == "not_found"
    repo_mock.mark_waived.assert_not_awaited()


@pytest.mark.asyncio
async def test_waive_succeeds_even_if_audit_log_fails(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """The audit event is best-effort: a log failure must not undo the waive."""
    row = _finding_row(severity="minor", status="open")
    c, repo_mock = _choreographer(monkeypatch, row)
    c.audit.log_task_event = AsyncMock(side_effect=RuntimeError("db gone"))

    env = await c.waive_finding(uuid4(), row.id, "still waive me")
    assert env.error is None
    repo_mock.mark_waived.assert_awaited_once()
