"""``hermes chat -Q`` as a dispatcher's re-run of a failed bot delivery (#111721).

A failed delivery turn persists its user row before the provider call; the policy-gated re-run
replays the same session and payload in a fresh process. Told so via HERMES_RESUME_UNANSWERED_TURN,
the re-run resumes that row instead of appending a second copy of the DM.
"""

from __future__ import annotations

import os
from types import SimpleNamespace

import cli
from agent.context_compressor import _DB_PERSISTED_MARKER
from tools.bot_relay import RESUME_UNANSWERED_TURN_ENV


def _quiet_turn(monkeypatch, history, marker):
    monkeypatch.delenv("HERMES_KANBAN_GOAL_MODE", raising=False)
    monkeypatch.delenv("HERMES_KANBAN_TASK", raising=False)
    if marker is None:
        monkeypatch.delenv(RESUME_UNANSWERED_TURN_ENV, raising=False)
    else:
        monkeypatch.setenv(RESUME_UNANSWERED_TURN_ENV, marker)
    seen = {}

    def run_conversation(**kwargs):
        seen["history"] = list(kwargs["conversation_history"])
        seen["env"] = os.environ.get(RESUME_UNANSWERED_TURN_ENV)
        return {"final_response": "ok"}

    agent = SimpleNamespace(run_conversation=run_conversation, session_id="s-1")
    the_cli = SimpleNamespace(agent=agent, conversation_history=history, session_id="s-1")
    try:
        cli._run_quiet_single_query(the_cli, "hello")
    except SystemExit as exc:
        assert exc.code == 0
    return agent, seen


def test_rerun_resumes_the_unanswered_user_row_it_already_persisted(monkeypatch):
    tail = {"role": "user", "content": "hello"}
    agent, seen = _quiet_turn(monkeypatch, [{"role": "assistant", "content": "earlier"}, tail], "1")
    # The persisted row leaves the history and becomes this turn's staged user dict, already
    # durable — so the agent reuses it as the turn's user message and the flush writes no new row.
    assert seen["history"] == [{"role": "assistant", "content": "earlier"}]
    assert agent._pending_cli_user_message is tail
    assert tail[_DB_PERSISTED_MARKER] is True
    assert seen["env"] is None, "the marker is consumed before the turn so tool subprocesses never inherit it"


def test_without_the_marker_or_an_unanswered_tail_nothing_is_adopted(monkeypatch):
    """A person re-sending the same word after a failed turn has sent a second real message; an
    answered tail is not a re-run either. Only the dispatcher's explicit marker plus an identical
    unanswered tail adopts a row."""
    tail = {"role": "user", "content": "hello"}
    agent, seen = _quiet_turn(monkeypatch, [tail], None)
    assert seen["history"] == [tail] and not hasattr(agent, "_pending_cli_user_message")
    assert _DB_PERSISTED_MARKER not in tail

    answered = [{"role": "user", "content": "hello"}, {"role": "assistant", "content": "done"}]
    agent, seen = _quiet_turn(monkeypatch, list(answered), "1")
    assert seen["history"] == answered and not hasattr(agent, "_pending_cli_user_message")


def test_rerun_adopts_the_dm_behind_the_failed_attempts_tool_scaffolding(monkeypatch):
    """A 503 after a tool round persisted user + assistant(tool_calls) + tool before the failure text,
    and the dispatcher retries it. The DM is still unanswered: the re-run adopts it and drops the
    failed attempt's scaffolding from the in-memory turn instead of appending a second copy."""
    tail = {"role": "user", "content": "hello"}
    scaffolding = [
        {"role": "assistant", "content": None, "tool_calls": [{"id": "c1", "type": "function",
                                                                "function": {"name": "t", "arguments": "{}"}}]},
        {"role": "tool", "tool_call_id": "c1", "content": "result"},
    ]
    agent, seen = _quiet_turn(monkeypatch, [{"role": "assistant", "content": "earlier"}, tail, *scaffolding], "1")
    assert seen["history"] == [{"role": "assistant", "content": "earlier"}]
    assert agent._pending_cli_user_message is tail and tail[_DB_PERSISTED_MARKER] is True


def test_turn_report_is_written_before_the_exit_linger_and_the_path_is_not_inherited(monkeypatch, tmp_path):
    """A spawner that bounds only the turn (cron Bot Chat lane, #113608) reads the outcome from
    HERMES_QUIET_TURN_REPORT_FILE: written the moment the turn ends — before the one-shot exit
    linger — stamped with this pid, and the variable is popped before the turn spawns anything."""
    from hermes_cli import quiet_single_query as qsq

    monkeypatch.delenv("HERMES_KANBAN_GOAL_MODE", raising=False)
    monkeypatch.delenv("HERMES_KANBAN_TASK", raising=False)
    report = tmp_path / "turn.json"
    monkeypatch.setenv(qsq.TURN_REPORT_FILE_ENV, str(report))
    seen = {}

    def run_conversation(**kwargs):
        seen["env_during_turn"] = os.environ.get(qsq.TURN_REPORT_FILE_ENV)
        seen["report_during_turn"] = report.exists()
        return {"final_response": "ok"}

    def linger(*args, **kwargs):
        seen["report_at_linger"] = qsq.read_turn_report(str(report), os.getpid())
        return {"waited": [], "completed": [], "timed_out": []}

    monkeypatch.setattr("tools.process_registry.process_registry.wait_for_pending_completions", linger)
    agent = SimpleNamespace(run_conversation=run_conversation, session_id="s-1")
    try:
        cli._run_quiet_single_query(SimpleNamespace(agent=agent, conversation_history=[], session_id="s-1"), "hello")
    except SystemExit as exc:
        assert exc.code == 0
    assert seen["env_during_turn"] is None and seen["report_during_turn"] is False
    assert seen["report_at_linger"] == {"pid": os.getpid(), "exit_code": 0, "error": "", "reply": "ok"}
    # Another process's record is not this child's report.
    assert qsq.read_turn_report(str(report), os.getpid() + 1) is None


def test_a_follow_up_turn_rewrites_the_report_with_the_answer_it_displaces(monkeypatch, tmp_path):
    """The report says what this run will print. When a teammate's reply during the linger runs a
    follow-up turn whose answer displaces the first (the quiet run's final-answer contract), the
    report is rewritten with it, so a relay booking the child at its cap relays that answer (#114980)."""
    from hermes_cli import quiet_single_query as qsq

    monkeypatch.delenv("HERMES_KANBAN_GOAL_MODE", raising=False)
    monkeypatch.delenv("HERMES_KANBAN_TASK", raising=False)
    report = tmp_path / "turn.json"
    monkeypatch.setenv(qsq.TURN_REPORT_FILE_ENV, str(report))
    answers = iter(["asking the teammate", "teammate says: done"])
    seen = {}

    def run_conversation(**kwargs):
        return {"final_response": next(answers), "messages": []}

    def linger_then_follow_up(session_id, run_turn, **kwargs):
        seen["report_before_follow_up"] = qsq.read_turn_report(str(report), os.getpid())["reply"]
        return run_turn("teammate: done")

    monkeypatch.setattr(qsq, "continue_quiet_notify_completions", linger_then_follow_up)
    monkeypatch.setattr("tools.process_registry.process_registry.wait_for_pending_completions",
                        lambda *a, **k: {"waited": [], "completed": [], "timed_out": []})
    agent = SimpleNamespace(run_conversation=run_conversation, session_id="s-1")
    printed = []
    monkeypatch.setattr("builtins.print", lambda *a, **k: printed.append(a))
    try:
        cli._run_quiet_single_query(SimpleNamespace(agent=agent, conversation_history=[], session_id="s-1"), "hello")
    except SystemExit as exc:
        assert exc.code == 0
    assert seen["report_before_follow_up"] == "asking the teammate"
    assert qsq.read_turn_report(str(report), os.getpid())["reply"] == "teammate says: done"
    assert ("teammate says: done",) in printed, "the report and stdout name the same answer"
