"""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

import pytest

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, _stub_runtime_main

    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)


def test_gateway_run_agent_threads_the_event_message_id_into_the_turn():
    """AST proof that the gateway call site passes the id down.

    The unit tests above prove the persistence layer STORES the id once it is
    given one.  This pins the wiring: without the gateway forwarding
    ``event_message_id`` as ``persist_user_platform_id``, the whole path is
    dead code and every real inbound turn still persists without its id.
    """
    import ast
    import inspect

    import gateway.run_turn_runner as gateway_run

    source = inspect.getsource(gateway_run)
    tree = ast.parse(source)

    forwards = [
        node
        for node in ast.walk(tree)
        if isinstance(node, ast.Subscript)
        and isinstance(node.slice, ast.Constant)
        and node.slice.value == "persist_user_platform_id"
    ]
    assert forwards, (
        "gateway/run.py never forwards persist_user_platform_id — the inbound "
        "platform message id never reaches the persisted user turn, so a "
        "drain-interrupted turn stays undedupable"
    )


def test_run_conversation_accepts_persist_user_platform_id():
    """The public forwarder must expose the kwarg the gateway passes."""
    import inspect

    from agent.conversation_loop import run_conversation
    from run_agent import AIAgent

    assert (
        "persist_user_platform_id"
        in inspect.signature(run_conversation).parameters
    )
    assert (
        "persist_user_platform_id"
        in inspect.signature(AIAgent.run_conversation).parameters
    )
