"""Restart drain-window recovery must be able to dedup an interrupted turn.

The Discord missed-message backfill (``_run_missed_message_backfill``) exists
to recover messages the bot never saw while it was down.  A gateway RESTART
produces a harder case: the message WAS received and a turn WAS started, then
the drain window force-interrupted it.  The transcript is the only durable
record of that, and the transcript row for the user turn is written WITHOUT
the platform-side message id — so nothing downstream can ask "did this
Discord message already reach the transcript?" and the recovery pass has no
authority to dedup against.

``SessionDB`` already carries a ``platform_message_id`` column, a partial
unique index over ``(session_id, platform_message_id)``, and a
``has_platform_message_id`` lookup — the storage and the query exist.  What is
missing is the WRITE on the normal agent-persisted turn path: the id is only
attached on the gateway-side transient-failure fallback
(``_handle_message_with_agent``), never on the path the agent itself flushes.
"""

from __future__ import annotations

import types

from hermes_state import SessionDB

def _make_db(tmp_path) -> SessionDB:
    return SessionDB(db_path=tmp_path / "state.db")

class _MinimalAgent:
    """The narrow slice of AIAgent that ``_apply_persist_user_message_override``
    and ``_flush_messages_to_session_db`` read."""

    def __init__(self, db: SessionDB, session_id: str):
        self._session_db = db
        self._session_db_created = True
        self.session_id = session_id
        self._last_flushed_db_idx = 0
        self._flushed_db_message_ids = set()
        self._flushed_db_message_session_id = session_id
        self._persist_user_message_idx = None
        self._persist_user_message_override = None
        self._persist_user_message_timestamp = None
        self._persist_disabled = False

    def _ensure_db_session(self):  # pragma: no cover - already created
        return None

def test_build_turn_context_stamps_the_platform_message_id_on_the_user_turn():
    """The turn prologue must carry the platform id onto the user turn dict.

    This is the row the early crash-resilience persist writes, so it is the
    only place a drain-interrupted turn can pick the id up.
    """
    from agent.turn_context import build_turn_context

    agent = types.SimpleNamespace()
    ctx = _build_turn_context_for_test(
        build_turn_context, agent, persist_user_platform_id="discord-991"
    )

    user_msgs = [m for m in ctx.messages if m.get("role") == "user"]
    assert user_msgs, "no user turn in the built context"
    assert user_msgs[-1].get("platform_message_id") == "discord-991", (
        "the user turn reached persistence without its platform message id — a "
        "drain-interrupted turn is then unrecoverable/undedupable by "
        "has_platform_message_id"
    )

def test_persisted_interrupted_turn_is_findable_by_platform_message_id(tmp_path):
    """E2E: flush a turn the way the agent does, then ask the dedup authority.

    This is the exact question the restart drain-window recovery pass asks
    before re-dispatching a message.  On main the answer is False even though
    the turn IS in the transcript, so recovery would re-run a turn that
    already ran (duplicate work, duplicate spend, duplicate reply).
    """
    from run_agent import AIAgent

    db = _make_db(tmp_path)
    session_id = db.create_session("sess-drain-window", "gateway")

    agent = _MinimalAgent(db, session_id)
    agent._persist_user_message_idx = 0
    agent._persist_user_message_platform_id = "discord-4242"

    messages = [{"role": "user", "content": "please do the thing"}]

    AIAgent._apply_persist_user_message_override(agent, messages)
    AIAgent._flush_messages_to_session_db_unlocked(
        agent, messages, conversation_history=None
    )

    assert db.has_platform_message_id(session_id, "discord-4242"), (
        "the interrupted turn is in the transcript but carries no "
        "platform_message_id, so restart drain-window recovery cannot tell it "
        "already ran and will re-dispatch it"
    )

def test_platform_message_id_survives_a_persist_content_override(tmp_path):
    """The id must not be lost on the override path.

    Group-chat / observed-context turns route through
    ``_persist_user_message_override``; the id has to survive that rewrite or
    the dedup authority is blind for exactly the busy channels that need it.
    """
    from run_agent import AIAgent

    db = _make_db(tmp_path)
    session_id = db.create_session("sess-override", "gateway")

    agent = _MinimalAgent(db, session_id)
    agent._persist_user_message_idx = 0
    agent._persist_user_message_override = "clean transcript text"
    agent._persist_user_message_platform_id = "discord-7777"

    messages = [{"role": "user", "content": "api-facing text with context"}]

    AIAgent._apply_persist_user_message_override(agent, messages)
    AIAgent._flush_messages_to_session_db_unlocked(
        agent, messages, conversation_history=None
    )

    assert db.has_platform_message_id(session_id, "discord-7777")

def _build_turn_context_for_test(build_turn_context, agent, **overrides):
    """Construct a minimal build_turn_context call.

    Mirrors ``tests/agent/test_turn_context.py::_build`` but is kept local so
    this file stays self-contained.
    """
    from tests.agent.test_turn_context import _FakeAgent

    fake = _FakeAgent()
    kwargs = dict(
        agent=fake,
        user_message="hello",
        system_message=None,
        conversation_history=None,
        task_id=None,
        stream_callback=None,
        persist_user_message=None,
        restore_or_build_system_prompt=lambda *a, **k: None,
        install_safe_stdio=lambda: None,
        sanitize_surrogates=lambda s: s,
        summarize_user_message_for_log=lambda s: s,
        set_session_context=lambda _sid: None,
        set_current_write_origin=lambda _o: None,
        ra=lambda: types.SimpleNamespace(_set_interrupt=lambda *a, **k: None),
    )
    kwargs.update(overrides)
    return build_turn_context(**kwargs)
