"""Regression coverage for preserving manually named sessions (#3230)."""
import contextlib
import json
import threading
from unittest.mock import MagicMock, patch

import pytest

import api.config as config
import api.models as models
import api.profiles as profiles_api
import api.streaming as streaming
from api.models import Session
from api.session_ops import apply_session_title_rename, mark_session_title_generated


@pytest.fixture(autouse=True)
def _isolate_state(tmp_path, monkeypatch):
    session_dir = tmp_path / "sessions"
    session_dir.mkdir()
    index_file = session_dir / "_index.json"
    monkeypatch.setattr(models, "SESSION_DIR", session_dir)
    monkeypatch.setattr(models, "SESSION_INDEX_FILE", index_file)
    monkeypatch.setattr(config, "SESSION_INDEX_FILE", index_file, raising=False)
    models.SESSIONS.clear()
    config.STREAMS.clear()
    config.CANCEL_FLAGS.clear()
    config.AGENT_INSTANCES.clear()
    config.SESSION_AGENT_LOCKS.clear()
    yield session_dir
    models.SESSIONS.clear()
    config.STREAMS.clear()
    config.CANCEL_FLAGS.clear()
    config.AGENT_INSTANCES.clear()
    config.SESSION_AGENT_LOCKS.clear()


def _user_msg(text):
    return {"role": "user", "content": text}


def _assistant_msg(text):
    return {"role": "assistant", "content": text}


def _exchange_messages(count):
    messages = []
    for idx in range(count):
        messages.append(_user_msg(f"question {idx}"))
        messages.append(_assistant_msg(f"answer {idx}"))
    return messages


def _put_event(events):
    def put(name, data):
        events.append((name, data))

    return put


def test_rename_marks_title_manual_and_persists_flag():
    session = Session(
        session_id="manual-title-session",
        title="Old generated title",
        messages=_exchange_messages(1),
        llm_title_generated=True,
    )

    apply_session_title_rename(session, "Long-lived Work Plan")
    session.save()

    loaded = Session.load("manual-title-session")
    assert loaded.title == "Long-lived Work Plan"
    assert loaded.manual_title is True
    assert loaded.llm_title_generated is False
    assert loaded.compact()["manual_title"] is True

    payload = json.loads(session.path.read_text(encoding="utf-8"))
    assert payload["manual_title"] is True
    assert payload["llm_title_generated"] is False


def test_blank_or_auto_title_clears_manual_lock():
    session = Session(
        session_id="clear-manual-title",
        title="Pinned investigation",
        manual_title=True,
        llm_title_generated=True,
    )

    apply_session_title_rename(session, "  ")
    assert session.title == "Untitled"
    assert session.manual_title is False
    assert session.llm_title_generated is False

    apply_session_title_rename(session, "New Chat")
    assert session.title == "New Chat"
    assert session.manual_title is False
    assert session.llm_title_generated is False


def test_adaptive_refresh_skips_manual_title_even_at_configured_interval():
    session = Session(
        session_id="manual-refresh-skip",
        title="Release Review Notes",
        messages=_exchange_messages(5),
        llm_title_generated=True,
        manual_title=True,
    )

    with patch("api.streaming._get_title_refresh_interval", return_value=5), \
         patch("threading.Thread") as thread_cls:
        streaming._maybe_schedule_title_refresh(session, lambda *_args: None, agent=None)

    assert thread_cls.called is False


def test_adaptive_refresh_still_schedules_for_generated_titles_without_manual_lock():
    session = Session(
        session_id="generated-refresh",
        title="Generated Session Title",
        messages=_exchange_messages(5),
        llm_title_generated=True,
        manual_title=False,
    )

    with patch("api.streaming._get_title_refresh_interval", return_value=5), \
         patch("threading.Thread") as thread_cls:
        thread = MagicMock()
        thread_cls.return_value = thread
        streaming._maybe_schedule_title_refresh(session, lambda *_args: None, agent=None)

    assert thread_cls.called is True
    assert thread.start.called is True


def test_generated_title_clears_manual_lock():
    session = Session(
        session_id="generated-clears-manual",
        title="Manual Before Regenerate",
        manual_title=True,
        llm_title_generated=False,
    )

    mark_session_title_generated(session)

    assert session.manual_title is False
    assert session.llm_title_generated is True


def test_cleared_title_allows_initial_auto_generation(monkeypatch):
    session = Session(
        session_id="cleared-title-auto-generation",
        title="Untitled",
        messages=_exchange_messages(1),
        manual_title=False,
        llm_title_generated=False,
    )
    session.save = MagicMock()
    events = []

    monkeypatch.setattr(streaming, "get_session", lambda _sid: session)
    monkeypatch.setattr(streaming, "SESSIONS", {session.session_id: session})
    monkeypatch.setattr(streaming, "LOCK", threading.Lock())
    monkeypatch.setattr(streaming, "_aux_title_configured", lambda: True)
    monkeypatch.setattr(
        streaming,
        "_generate_llm_session_title_via_aux",
        lambda *_args, **_kwargs: ("Generated Follow-up Title", "llm_ok", "raw"),
    )
    monkeypatch.setattr(
        profiles_api,
        "profile_env_for_background_worker",
        lambda *_args, **_kwargs: contextlib.nullcontext(),
    )
    monkeypatch.setattr(
        "api.state_sync.sync_session_title",
        lambda *_args, **_kwargs: None,
    )

    streaming._run_background_title_update(
        session.session_id,
        "question",
        "answer",
        "Untitled",
        _put_event(events),
        agent=None,
    )

    assert session.title == "Generated Follow-up Title"
    assert session.manual_title is False
    assert session.llm_title_generated is True
    assert any(name == "title" and data["title"] == "Generated Follow-up Title" for name, data in events)


def test_clear_endpoint_resets_manual_title_lock():
    """Clearing a manually-named session must drop the manual_title lock so the
    reused session can auto-name again (#3542 gap: /api/session/clear reset the
    title directly, leaving manual_title=True). The endpoint now routes the
    title reset through apply_session_title_rename — assert that helper clears
    the lock for the auto-label the clear path uses."""
    session = Session(
        session_id="clear-endpoint-manual-title",
        title="My Deliberate Name",
        messages=_exchange_messages(2),
        manual_title=True,
        llm_title_generated=False,
    )

    # This mirrors exactly what /api/session/clear now does for the title.
    apply_session_title_rename(session, "Untitled")

    assert session.title == "Untitled"
    assert session.manual_title is False, (
        "clearing a manually-named session must drop the manual_title lock"
    )
    assert session.llm_title_generated is False


def test_clear_route_uses_rename_helper_not_bare_title_assignment():
    """Static guard: the /api/session/clear handler must reset the title via
    apply_session_title_rename (which clears manual_title), not a bare
    `s.title = "Untitled"` that would strand the manual-title lock (#3542)."""
    import pathlib
    routes_src = (pathlib.Path(__file__).resolve().parent.parent / "api" / "routes.py").read_text(
        encoding="utf-8"
    )
    clear_idx = routes_src.find('if parsed.path == "/api/session/clear"')
    assert clear_idx != -1, "/api/session/clear handler not found"
    # Window from the clear handler to the next route branch.
    next_idx = routes_src.find('if parsed.path == "/api/session/truncate"', clear_idx)
    clear_block = routes_src[clear_idx:next_idx if next_idx != -1 else clear_idx + 2000]
    assert "apply_session_title_rename(s, \"Untitled\")" in clear_block, (
        "clear handler must reset the title via apply_session_title_rename to "
        "clear the manual_title lock"
    )
