"""Regression coverage for nesquena/hermes-webui#7543.

Bug: manual "Regenerate title" failed with missing_exchange for sessions
whose transcript opens with consecutive user rows (no assistant text before
the second user turn) — _first_exchange_snippets() aborted at the second
user message, the aux call was skipped, and the deterministic local fallback
was persisted (200 + identical wrong title on every retry).
"""

from unittest.mock import MagicMock

import api.profiles as profiles_api
import api.streaming as streaming


class _ProfileEnv:
    def __enter__(self):
        return None

    def __exit__(self, exc_type, exc, tb):
        return False


def _capturing_llm_result(captured, title="LLM Title", status="llm_aux"):
    # Guard-faithful stand-in for the aux route: mirrors the documented
    # generate_title_raw_via_aux contract — empty assistant_text is rejected
    # with missing_exchange (the provider edge), never the selection logic.
    def _fake(user_text, assistant_text, **kwargs):
        captured["user_text"] = user_text
        captured["assistant_text"] = assistant_text
        if not user_text or not assistant_text:
            return None, "missing_exchange", ""
        return title, status, ""

    return _fake


def _run_generation(monkeypatch, messages, captured, prefer_latest=False):
    monkeypatch.setattr(profiles_api, "profile_env_for_background_worker", lambda *a, **k: _ProfileEnv())
    monkeypatch.setattr(streaming, "_aux_title_generation_enabled", lambda: True)
    monkeypatch.setattr(streaming, "_generate_llm_session_title_via_aux", _capturing_llm_result(captured))
    session = MagicMock()
    session.messages = messages
    session.session_id = "issue7543"
    return streaming.generate_session_title_for_session(session, prefer_latest=prefer_latest)


# --- Fix A: _first_exchange_snippets scans past consecutive user rows (opt-in) ---


def test_first_exchange_snippets_scan_past_consecutive_user_rows():
    # Channel-backed import/projection shape: opening run of user rows,
    # first assistant answer only much later (#7543 repro shape).
    # scan_past_consecutive_users=True is the manual-regen behavior.
    messages = [
        {"role": "user", "content": "Opening question about certificates"},
        {"role": "user", "content": "Follow-up that used to trigger the break"},
        {"role": "user", "content": "Another queued user turn"},
        {"role": "assistant", "content": "## Real first answer with substance"},
    ]
    user_text, asst_text = streaming._first_exchange_snippets(
        messages, scan_past_consecutive_users=True
    )
    assert user_text == "Opening question about certificates"
    assert asst_text == "## Real first answer with substance"


def test_first_exchange_snippets_default_stops_at_second_user_row():
    # The DEFAULT (automatic in-stream background-title path) must keep master's
    # exact behavior: a second populated user row before any assistant text ends
    # the opening exchange with an empty assistant snippet. This is load-bearing —
    # _background_title_generation_inputs treats an empty assistant snippet as
    # "not yet eligible", which keeps the stream teardown on its synchronous
    # stream_end path (regression guard for test_issue3929 emits_done).
    messages = [
        {"role": "user", "content": "Opening question about certificates"},
        {"role": "user", "content": "Follow-up that used to trigger the break"},
        {"role": "user", "content": "Another queued user turn"},
        {"role": "assistant", "content": "## Real first answer with substance"},
    ]
    user_text, asst_text = streaming._first_exchange_snippets(messages)
    assert user_text == "Opening question about certificates"
    assert asst_text == ""


def test_first_exchange_snippets_normal_pair_unchanged():
    # Classic [user, assistant] opening must keep its exact behavior in BOTH modes.
    messages = [
        {"role": "user", "content": "Please fix the stale sidebar title controls"},
        {"role": "assistant", "content": "I will add a regenerate-title action."},
        {"role": "user", "content": "Second question"},
    ]
    for scan in (False, True):
        user_text, asst_text = streaming._first_exchange_snippets(
            messages, scan_past_consecutive_users=scan
        )
        assert user_text == "Please fix the stale sidebar title controls"
        assert asst_text == "I will add a regenerate-title action."


def test_first_exchange_snippets_without_any_assistant_text_still_empty():
    # No assistant text anywhere -> still unusable for the LLM path in BOTH modes;
    # the missing_exchange rejection in generate_title_raw_via_aux stays intact.
    messages = [
        {"role": "user", "content": "Question one"},
        {"role": "user", "content": "Question two"},
    ]
    for scan in (False, True):
        user_text, asst_text = streaming._first_exchange_snippets(
            messages, scan_past_consecutive_users=scan
        )
        assert user_text == "Question one"
        assert asst_text == ""


def test_issue7543_real_transcript_shape_reaches_llm_path(monkeypatch):
    captured = {}
    messages = [
        {"role": "user", "content": "Wie kann ich auf einem windows server 2025 ein zertifikat erstellen"} ,
        {"role": "user", "content": "Wächst das Log so nicht in eine unendliche Schleife"},
        {"role": "assistant", "content": "## Zertifikat für Windows Admin Center mit AD-CS"},
    ]
    title, status, _raw = _run_generation(monkeypatch, messages, captured)
    assert status == "llm_aux"
    assert title == "LLM Title"
    assert captured["user_text"].startswith("Wie kann ich auf einem windows server 2025")
    assert captured["assistant_text"].startswith("## Zertifikat")


def test_regenerate_helper_errors_with_real_walkers_when_no_user_text_exists(monkeypatch):
    """Observable error path through the real walkers: no user text anywhere
    (orphan assistant rows only) -> empty_user_message, aux never called."""
    captured = {}
    messages = [{"role": "assistant", "content": "orphan answer without any user turn"}]
    title, status, _raw = _run_generation(monkeypatch, messages, captured)
    assert title is None
    assert status == "empty_user_message"
    assert "user_text" not in captured  # aux path never reached


def test_regenerate_helper_prefer_latest_path_unchanged(monkeypatch):
    captured = {}
    messages = [
        {"role": "user", "content": "First question"},
        {"role": "assistant", "content": "First answer"},
        {"role": "user", "content": "Latest question"},
        {"role": "assistant", "content": "Latest answer"},
    ]
    title, status, _raw = _run_generation(monkeypatch, messages, captured, prefer_latest=True)
    assert status == "llm_aux"
    assert captured["user_text"] == "Latest question"
    assert captured["assistant_text"] == "Latest answer"


def test_regenerate_helper_prefer_latest_with_empty_last_user_message_errors(monkeypatch):
    captured = {}
    messages = [
        {"role": "user", "content": "First question"},
        {"role": "assistant", "content": "First answer"},
        {"role": "user", "content": "   "},
    ]
    title, status, _raw = _run_generation(monkeypatch, messages, captured, prefer_latest=True)
    assert title is None
    assert status == "empty_user_message"
    assert "user_text" not in captured
