"""The PLAYBOOKS RAG index write must not commit independently of — and BEFORE —
the playbook status transaction.

``approve()`` / ``reject()`` flush the status only; the index/unindex is a
separate post-commit step the caller runs after the status commits (skipped if
the commit fails), so the RAG corpus never diverges from the DB row.
"""

from __future__ import annotations

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

import pytest
from roboco.api.routes.playbooks import approve_playbook, reject_playbook
from roboco.config import settings
from roboco.models import AgentRole
from roboco.models.permissions import AgentContext
from roboco.services.gateway.content_actions import ContentActions, ContentActionsDeps
from roboco.services.playbook import PlaybookService

_PID = uuid4()
_APPROVER = uuid4()


def _playbook_mock() -> Any:
    pb = MagicMock(name="playbook")
    pb.id = _PID
    # approve/reject act on a DRAFT (their legitimate starting state — F109
    # guards both to DRAFT-only). The index/unindex ordering assertions below
    # are independent of the starting status.
    pb.status = "draft"
    pb.title = "T"
    pb.problem = "P"
    pb.procedure = "Pr"
    pb.tags = []
    pb.team = "backend"
    pb.scope = "team"
    return pb


# ---------------------------------------------------------------------------
# Service-level: approve/reject flush ONLY; index/unindex are separate steps
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_approve_does_not_index_before_commit(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """``approve()`` flushes the status change but must NOT write to the RAG
    index — that writes through its own auto-committing connection, so it would
    durably land before the caller commits the status (the divergence)."""
    monkeypatch.setattr(settings, "org_memory_enabled", True)
    session = AsyncMock()
    session.flush = AsyncMock()
    svc = PlaybookService(session)
    monkeypatch.setattr(svc, "_get_or_raise", AsyncMock(return_value=_playbook_mock()))

    optimal = MagicMock()
    optimal.index_playbook = AsyncMock()
    monkeypatch.setattr(
        "roboco.services.optimal.get_optimal_service",
        AsyncMock(return_value=optimal),
    )

    await svc.approve(_PID, approver_id=_APPROVER)

    optimal.index_playbook.assert_not_awaited()


@pytest.mark.asyncio
async def test_index_approved_is_a_separate_post_commit_step(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """``index_approved`` is the public post-commit index step the caller runs
    AFTER committing the status — it invokes the optimal index."""
    monkeypatch.setattr(settings, "org_memory_enabled", True)
    svc = PlaybookService(AsyncMock())
    optimal = MagicMock()
    optimal.index_playbook = AsyncMock()
    monkeypatch.setattr(
        "roboco.services.optimal.get_optimal_service",
        AsyncMock(return_value=optimal),
    )

    await svc.index_approved(_playbook_mock())

    optimal.index_playbook.assert_awaited_once()


@pytest.mark.asyncio
async def test_reject_does_not_unindex_before_commit(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """``reject()`` flushes the status change but must NOT de-index from the RAG
    index inline — same ordering gap as approve (the de-index auto-commits before
    the status commit, dropping an approved playbook from briefings while its row
    is still APPROVED on rollback)."""
    monkeypatch.setattr(settings, "org_memory_enabled", True)
    session = AsyncMock()
    session.flush = AsyncMock()
    svc = PlaybookService(session)
    monkeypatch.setattr(svc, "_get_or_raise", AsyncMock(return_value=_playbook_mock()))

    optimal = MagicMock()
    optimal.unindex_playbook = AsyncMock()
    optimal.deindex_playbook = AsyncMock()
    monkeypatch.setattr(
        "roboco.services.optimal.get_optimal_service",
        AsyncMock(return_value=optimal),
    )

    await svc.reject(_PID, approver_id=_APPROVER, reason="nope")

    # No de-index call of any name originated from reject() before commit.
    optimal.unindex_playbook.assert_not_awaited()
    optimal.deindex_playbook.assert_not_awaited()


@pytest.mark.asyncio
async def test_unindex_playbook_is_a_separate_post_commit_step(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """``unindex_playbook`` is the public post-commit de-index step."""
    monkeypatch.setattr(settings, "org_memory_enabled", True)
    svc = PlaybookService(AsyncMock())
    optimal = MagicMock()
    optimal.unindex_playbook = AsyncMock()
    monkeypatch.setattr(
        "roboco.services.optimal.get_optimal_service",
        AsyncMock(return_value=optimal),
    )

    await svc.unindex_playbook(_playbook_mock())

    optimal.unindex_playbook.assert_awaited_once()


# ---------------------------------------------------------------------------
# Route-level (panel): commit-then-index, skip index on commit failure
# ---------------------------------------------------------------------------


def _route_service_mock(
    order: list[str], *, commit_fails: bool = False
) -> tuple[Any, Any]:
    def _approve(*_a: Any, **_k: Any) -> Any:
        order.append("approve")
        return _playbook_mock()

    def _reject(*_a: Any, **_k: Any) -> Any:
        order.append("reject")
        return _playbook_mock()

    svc = MagicMock()
    svc.approve = AsyncMock(side_effect=_approve)
    svc.reject = AsyncMock(side_effect=_reject)
    svc.index_approved = AsyncMock(side_effect=lambda *_a, **_k: order.append("index"))
    svc.unindex_playbook = AsyncMock(
        side_effect=lambda *_a, **_k: order.append("unindex")
    )
    db = AsyncMock()
    if commit_fails:
        db.commit = AsyncMock(side_effect=RuntimeError("commit failed"))
    else:
        db.commit = AsyncMock(side_effect=lambda: order.append("commit"))
    return svc, db


@pytest.mark.asyncio
async def test_approve_route_commits_before_indexing(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    order: list[str] = []
    svc, db = _route_service_mock(order)
    monkeypatch.setattr(
        "roboco.api.routes.playbooks.get_playbook_service", lambda _db: svc
    )
    monkeypatch.setattr(
        "roboco.api.routes.playbooks.Playbook.model_validate", lambda obj: obj
    )
    agent = AgentContext(agent_id=_APPROVER, role=AgentRole.AUDITOR)

    await approve_playbook(_PID, db, agent)

    assert order == ["approve", "commit", "index"]


@pytest.mark.asyncio
async def test_approve_route_skips_index_when_commit_fails(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    order: list[str] = []
    svc, db = _route_service_mock(order, commit_fails=True)
    monkeypatch.setattr(
        "roboco.api.routes.playbooks.get_playbook_service", lambda _db: svc
    )
    agent = AgentContext(agent_id=_APPROVER, role=AgentRole.AUDITOR)

    with pytest.raises(RuntimeError):
        await approve_playbook(_PID, db, agent)

    svc.index_approved.assert_not_awaited()
    assert order == ["approve"]


@pytest.mark.asyncio
async def test_reject_route_commits_before_unindexing(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    order: list[str] = []
    svc, db = _route_service_mock(order)
    monkeypatch.setattr(
        "roboco.api.routes.playbooks.get_playbook_service", lambda _db: svc
    )
    monkeypatch.setattr(
        "roboco.api.routes.playbooks.Playbook.model_validate", lambda obj: obj
    )
    agent = AgentContext(agent_id=_APPROVER, role=AgentRole.AUDITOR)

    await reject_playbook(_PID, MagicMock(reason="nope"), db, agent)

    assert order == ["reject", "commit", "unindex"]


# ---------------------------------------------------------------------------
# Gateway-level (Auditor verb): commit-then-index, skip index on commit failure
# ---------------------------------------------------------------------------


def _gateway_actions(
    order: list[str], *, commit_fails: bool = False
) -> tuple[ContentActions, MagicMock]:
    task = MagicMock()
    task.session = AsyncMock()
    if commit_fails:
        task.session.commit = AsyncMock(side_effect=RuntimeError("commit failed"))
    else:
        task.session.commit = AsyncMock(side_effect=lambda: order.append("commit"))
    task.agent_for = AsyncMock(return_value=MagicMock(role="auditor"))
    deps = ContentActionsDeps(
        task=task,
        git=MagicMock(),
        a2a=MagicMock(),
        journal=MagicMock(),
        workspace=MagicMock(),
        notifications=MagicMock(),
    )
    return ContentActions(deps), task


def _gateway_svc_mock(order: list[str]) -> MagicMock:
    def _approve(*_a: Any, **_k: Any) -> Any:
        order.append("approve")
        return _playbook_mock()

    def _reject(*_a: Any, **_k: Any) -> Any:
        order.append("reject")
        return _playbook_mock()

    svc = MagicMock()
    svc.approve = AsyncMock(side_effect=_approve)
    svc.reject = AsyncMock(side_effect=_reject)
    svc.index_approved = AsyncMock(side_effect=lambda *_a, **_k: order.append("index"))
    svc.unindex_playbook = AsyncMock(
        side_effect=lambda *_a, **_k: order.append("unindex")
    )
    return svc


@pytest.mark.asyncio
async def test_gateway_approve_commits_before_indexing(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    order: list[str] = []
    actions, _task = _gateway_actions(order)
    svc = _gateway_svc_mock(order)
    monkeypatch.setattr(
        "roboco.services.playbook.get_playbook_service",
        lambda _session: svc,
    )

    env = await actions.approve_playbook(agent_id=_APPROVER, playbook_id=_PID)

    assert order == ["approve", "commit", "index"]
    assert env.status == "playbook_approved"


@pytest.mark.asyncio
async def test_gateway_approve_skips_index_when_commit_fails(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    order: list[str] = []
    actions, _task = _gateway_actions(order, commit_fails=True)
    svc = _gateway_svc_mock(order)
    monkeypatch.setattr(
        "roboco.services.playbook.get_playbook_service",
        lambda _session: svc,
    )

    with pytest.raises(RuntimeError):
        await actions.approve_playbook(agent_id=_APPROVER, playbook_id=_PID)

    svc.index_approved.assert_not_awaited()
    assert order == ["approve"]


@pytest.mark.asyncio
async def test_gateway_reject_commits_before_unindexing(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    order: list[str] = []
    actions, _task = _gateway_actions(order)
    svc = _gateway_svc_mock(order)
    monkeypatch.setattr(
        "roboco.services.playbook.get_playbook_service",
        lambda _session: svc,
    )

    env = await actions.reject_playbook(
        agent_id=_APPROVER, playbook_id=_PID, reason="nope"
    )

    assert order == ["reject", "commit", "unindex"]
    assert env.status == "playbook_archived"
