"""The loop exports ``{turn_id, current_turn_user_idx}`` beside the exact ``messages`` it
addresses, and only when that row is this turn's user message verbatim.

Hosts that settle a transcript by index (hermes-webui) must not guess the current-turn
row after the loop rewrote history (alternation repair, compaction, post-turn
micro-compaction): with a repeated prompt a guessed index or a text match relabels the
historical copy and claims its old answer. The producer therefore proves the coordinate on
the final list; when it cannot, the keys are omitted and hosts fail closed.
"""

from __future__ import annotations

from types import SimpleNamespace
from unittest.mock import MagicMock, patch

import pytest

from agent.turn_context import export_current_turn_boundary


class _Agent:
    def __init__(self, turn_id="session:task:abcd1234"):
        self._current_turn_id = turn_id
        self._persist_user_message_idx = None


@pytest.mark.parametrize("user_message, messages, expected_idx", [
    # a repeated prompt resolves to the LAST verbatim row, never the historical copy
    ("same question", [
        {"role": "user", "content": "same question"}, {"role": "assistant", "content": "old answer"},
        {"role": "user", "content": "same question"}, {"role": "assistant", "content": "new answer"},
    ], 2),
    # multimodal content matches by structural equality
    ([{"type": "text", "text": "look"}, {"type": "image_url", "image_url": {"url": "data:x"}}],
     [{"role": "user", "content": [{"type": "text", "text": "look"}, {"type": "image_url", "image_url": {"url": "data:x"}}]},
      {"role": "assistant", "content": "ok"}], 0),
    # a row the repair rewrote (merge-into-tail) is not a proven boundary: export nothing
    ("same question", [{"role": "user", "content": "summary\n\nsame question"}, {"role": "assistant", "content": "answer"}], None),
    # no current row at all: export nothing, persist override untouched
    ("another question", [{"role": "user", "content": "same question"}, {"role": "assistant", "content": "old answer"}], None),
])
def test_boundary_is_exported_only_for_the_verbatim_current_row(user_message, messages, expected_idx):
    agent = _Agent()
    result = export_current_turn_boundary(agent, {"messages": messages}, user_message)
    if expected_idx is None:
        assert "current_turn_user_idx" not in result and "turn_id" not in result
    else:
        assert result["current_turn_user_idx"] == expected_idx
        assert result["turn_id"] == agent._current_turn_id
    # the persist funnel has already run by the time the envelope is stamped: never re-anchor it here
    assert agent._persist_user_message_idx is None


def test_preflight_timeout_envelope_exports_nothing():
    """The preflight-timeout result carries the prior history without this turn's row (#7100);
    a repeated prompt must not be resolved to its historical copy."""
    agent = _Agent()
    result = {"messages": [{"role": "user", "content": "continue"}, {"role": "assistant", "content": "old"}],
              "turn_exit_reason": "context_compression_timeout"}
    out = export_current_turn_boundary(agent, result, "continue")
    assert "current_turn_user_idx" not in out and "turn_id" not in out


@pytest.fixture()
def loop_agent():
    from run_agent import AIAgent

    with (
        patch("model_tools.get_tool_definitions", return_value=[]),
        patch("model_tools.check_toolset_requirements", return_value={}),
        patch("agent.process_bootstrap.OpenAI"),
    ):
        a = AIAgent(
            api_key="test-key-1234567890",
            base_url="https://openrouter.ai/api/v1",
            quiet_mode=True,
            skip_context_files=True,
            skip_memory=True,
        )
        a.client = MagicMock()
        a._cached_system_prompt = "You are helpful."
        a._use_prompt_caching = False
        a.compression_enabled = False
        a.save_trajectories = False
        return a


def _stub(content, finish_reason="stop"):
    from tests.agent.test_run_agent import _mock_assistant_msg

    return SimpleNamespace(
        id="chatcmpl-test",
        model="test/model",
        choices=[SimpleNamespace(index=0, message=_mock_assistant_msg(content=content), finish_reason=finish_reason)],
        usage=None,
    )


def test_run_conversation_exports_the_pair_on_a_success_envelope(loop_agent):
    loop_agent.client.chat.completions.create.side_effect = [_stub("new answer")]
    history = [
        {"role": "user", "content": "same question"},
        {"role": "assistant", "content": "old answer"},
    ]
    with (
        patch.object(loop_agent, "_persist_session"),
        patch.object(loop_agent, "_save_trajectory"),
        patch.object(loop_agent, "_cleanup_task_resources"),
    ):
        result = loop_agent.run_conversation("same question", conversation_history=history)

    assert result["completed"] is True
    idx = result["current_turn_user_idx"]
    assert result["messages"][idx]["role"] == "user"
    assert result["messages"][idx]["content"] == "same question"
    assert idx > 0  # the historical identical prompt at index 0 is never the export
    assert result["turn_id"] == loop_agent._current_turn_id
    assert result["messages"][-1]["content"] == "new answer"
