# SPDX-License-Identifier: AGPL-3.0-only
# Copyright (c) 2026 sol pbc

from __future__ import annotations

import json

import pytest
from flask import Flask

from solstone.apps.chat.copy import (
    CHAT_DEFERRED_NOT_ANALYZED,
    CHAT_THINKING_ENGINE_NOT_CHOSEN,
)
from solstone.convey.chat import chat_bp, compose_honest_degradation
from solstone.convey.chat_stream import read_chat_events
from solstone.think.pipeline_health import SegmentBacklog, SegmentCompletion
from solstone.think.processing import (
    format_awaiting_analysis,
    parse_processing_settings,
)


def _setup_journal(tmp_path, monkeypatch):
    journal = tmp_path / "journal"
    journal.mkdir()
    monkeypatch.setenv("SOLSTONE_JOURNAL", str(journal))
    return journal


def _reset_chat_state(chat_module) -> None:
    chat_module.stop_all_chat_runtime()
    with chat_module._state_lock:
        chat_module._current_chat_use_id = None
        chat_module._current_chat_state = None
        chat_module._queued_triggers.clear()
        chat_module._active_talents.clear()
        chat_module._reserved_use_ids.clear()
        chat_module._thinking_buffers.clear()
        chat_module._thinking_providers.clear()
        for timer in chat_module._watchdog_timers.values():
            timer.cancel()
        chat_module._watchdog_timers.clear()
        chat_module._last_use_id = 0


@pytest.fixture(autouse=True)
def _default_thinking_engine_selected(monkeypatch):
    monkeypatch.setattr(
        "solstone.convey.chat._no_thinking_engine_chosen", lambda: False
    )


def _set_current_chat(chat_module, logical_use_id: str, raw_use_id: str | None) -> None:
    with chat_module._state_lock:
        chat_module._current_chat_use_id = logical_use_id
        chat_module._current_chat_state = {
            "raw_use_id": raw_use_id,
            "raw_use_ids_seen": {raw_use_id} if raw_use_id else set(),
            "trigger": {"type": "owner_message", "message": "help"},
            "location": {"app": "sol", "path": "/app/sol", "facet": "work"},
            "retry_count": 0,
        }


@pytest.fixture
def chat_client(tmp_path, monkeypatch):
    import solstone.convey.chat as chat

    _setup_journal(tmp_path, monkeypatch)
    _reset_chat_state(chat)

    app = Flask(__name__)
    app.config["TESTING"] = True
    app.register_blueprint(chat_bp)
    return app.test_client()


def _settings(mode: str):
    return parse_processing_settings({"mode": mode}, strict=False)


def _backlog(
    day: str,
    *,
    not_sensed: int,
    not_thought: int,
    errors: tuple[str, ...] = (),
) -> SegmentBacklog:
    total = not_sensed + not_thought
    completion = SegmentCompletion(
        blockers=[],
        not_sensed=not_sensed,
        not_thought=not_thought,
        total=total,
        capped=0,
        exhausted=(),
    )
    return SegmentBacklog(
        days=(day,),
        not_thought=not_thought,
        not_sensed=not_sensed,
        total=total,
        per_day={day: completion},
        errors=errors,
    )


def _empty_backlog(day: str) -> SegmentBacklog:
    return SegmentBacklog(
        days=(day,),
        not_thought=0,
        not_sensed=0,
        total=0,
        per_day={},
        errors=(),
    )


def _patch_backlog_reads(
    monkeypatch,
    *,
    mode: str,
    backlog: SegmentBacklog,
) -> None:
    monkeypatch.setattr(
        "solstone.convey.chat.load_processing_settings",
        lambda: _settings(mode),
    )
    monkeypatch.setattr(
        "solstone.convey.chat.read_segment_backlog",
        lambda: backlog,
    )


def _patch_finish_side_effects(monkeypatch):
    emitted: dict[str, list] = {"finish": [], "error": [], "cortex": []}
    monkeypatch.setattr(
        "solstone.convey.chat._emit_cortex_event",
        lambda *args, **kwargs: emitted["cortex"].append((args, kwargs)),
    )
    monkeypatch.setattr(
        "solstone.convey.chat._emit_finish",
        lambda *args: emitted["finish"].append(args),
    )
    monkeypatch.setattr(
        "solstone.convey.chat._emit_error",
        lambda *args, **kwargs: emitted["error"].append((args, kwargs)),
    )
    monkeypatch.setattr("solstone.convey.chat._run_next_action", lambda _action: None)
    return emitted


def _finish_chat(chat_module, message_text: str) -> None:
    _set_current_chat(chat_module, "logical-chat", "raw-chat")
    chat_module._on_cortex_finish(
        {
            "use_id": "raw-chat",
            "result": json.dumps(
                {
                    "message": message_text,
                    "notes": "ok",
                    "talent_request": None,
                }
            ),
        }
    )


def _events_of_kind(day: str, kind: str) -> list[dict]:
    return [event for event in read_chat_events(day) if event["kind"] == kind]


def test_thinking_engine_not_chosen_copy_is_pinned_byte_for_byte():
    assert (
        CHAT_THINKING_ENGINE_NOT_CHOSEN
        == "no thinking engine is chosen yet. choose one in thinking so i can "
        "answer from your journal."
    )


def test_compose_honest_degradation_fires_for_deferred_today_pending():
    import solstone.convey.chat as chat

    today = chat._today_day()

    result = compose_honest_degradation(
        _settings("deferred"),
        _backlog(today, not_sensed=2, not_thought=1),
    )

    assert result is not None
    assert CHAT_DEFERRED_NOT_ANALYZED in result
    assert format_awaiting_analysis(3) in result


def test_compose_honest_degradation_ignores_untracked_queried_day():
    import solstone.convey.chat as chat

    today = chat._today_day()

    result = compose_honest_degradation(
        _settings("deferred"),
        _backlog(today, not_sensed=2, not_thought=1),
        queried_day="20260101",
    )

    assert result is None


def test_compose_honest_degradation_ignores_empty_backlog():
    import solstone.convey.chat as chat

    today = chat._today_day()

    result = compose_honest_degradation(
        _settings("deferred"),
        _backlog(today, not_sensed=0, not_thought=0),
    )

    assert result is None


def test_compose_honest_degradation_ignores_realtime_mode():
    import solstone.convey.chat as chat

    today = chat._today_day()

    result = compose_honest_degradation(
        _settings("realtime"),
        _backlog(today, not_sensed=2, not_thought=1),
    )

    assert result is None


def test_compose_honest_degradation_ignores_indeterminate_backlog():
    import solstone.convey.chat as chat

    today = chat._today_day()

    result = compose_honest_degradation(
        _settings("deferred"),
        _backlog(today, not_sensed=2, not_thought=1, errors=(today,)),
    )

    assert result is None


def test_compose_honest_degradation_fires_for_no_engine_realtime(monkeypatch):
    import solstone.convey.chat as chat

    monkeypatch.setattr(chat, "_no_thinking_engine_chosen", lambda: True)
    today = chat._today_day()

    result = compose_honest_degradation(
        _settings("realtime"),
        _backlog(today, not_sensed=2, not_thought=1),
    )

    assert result is not None
    assert CHAT_THINKING_ENGINE_NOT_CHOSEN in result
    assert format_awaiting_analysis(3) in result


def test_empty_chat_finish_substitutes_honest_degradation(chat_client, monkeypatch):
    import solstone.convey.chat as chat

    today = chat._today_day()
    _patch_backlog_reads(
        monkeypatch,
        mode="deferred",
        backlog=_backlog(today, not_sensed=2, not_thought=1),
    )
    emitted = _patch_finish_side_effects(monkeypatch)

    _finish_chat(chat, "")

    sol_message = _events_of_kind(today, "sol_message")[0]
    assert CHAT_DEFERRED_NOT_ANALYZED in sol_message["text"]
    assert format_awaiting_analysis(3) in sol_message["text"]
    assert [
        event
        for event in read_chat_events(today)
        if event["kind"] == "chat_error"
        and event["reason"] == "provider_response_invalid"
    ] == []
    assert emitted["finish"] == [("logical-chat", sol_message["text"])]
    assert emitted["error"] == []


def test_empty_chat_finish_remains_invalid_in_realtime(chat_client, monkeypatch):
    import solstone.convey.chat as chat

    today = chat._today_day()
    _patch_backlog_reads(
        monkeypatch,
        mode="realtime",
        backlog=_backlog(today, not_sensed=2, not_thought=1),
    )
    emitted = _patch_finish_side_effects(monkeypatch)

    _finish_chat(chat, "")

    sol_message = _events_of_kind(today, "sol_message")[0]
    assert sol_message["text"] == ""
    chat_errors = _events_of_kind(today, "chat_error")
    assert chat_errors[0]["reason"] == "provider_response_invalid"
    assert emitted["finish"] == []
    assert emitted["error"] == [(("logical-chat", "provider_response_invalid"), {})]


def test_empty_chat_finish_remains_invalid_with_empty_backlog(chat_client, monkeypatch):
    import solstone.convey.chat as chat

    today = chat._today_day()
    _patch_backlog_reads(
        monkeypatch,
        mode="deferred",
        backlog=_empty_backlog(today),
    )
    emitted = _patch_finish_side_effects(monkeypatch)

    _finish_chat(chat, "")

    sol_message = _events_of_kind(today, "sol_message")[0]
    assert sol_message["text"] == ""
    assert _events_of_kind(today, "chat_error")[0]["reason"] == (
        "provider_response_invalid"
    )
    assert emitted["finish"] == []
    assert emitted["error"] == [(("logical-chat", "provider_response_invalid"), {})]


def test_empty_chat_finish_remains_invalid_with_backlog_errors(
    chat_client, monkeypatch
):
    import solstone.convey.chat as chat

    today = chat._today_day()
    _patch_backlog_reads(
        monkeypatch,
        mode="deferred",
        backlog=_backlog(today, not_sensed=2, not_thought=1, errors=(today,)),
    )
    emitted = _patch_finish_side_effects(monkeypatch)

    _finish_chat(chat, "")

    sol_message = _events_of_kind(today, "sol_message")[0]
    assert sol_message["text"] == ""
    assert _events_of_kind(today, "chat_error")[0]["reason"] == (
        "provider_response_invalid"
    )
    assert emitted["finish"] == []
    assert emitted["error"] == [(("logical-chat", "provider_response_invalid"), {})]


def test_non_empty_chat_finish_is_not_replaced(chat_client, monkeypatch):
    import solstone.convey.chat as chat

    today = chat._today_day()
    _patch_backlog_reads(
        monkeypatch,
        mode="deferred",
        backlog=_backlog(today, not_sensed=2, not_thought=1),
    )
    emitted = _patch_finish_side_effects(monkeypatch)

    _finish_chat(chat, "Here is your answer.")

    sol_message = _events_of_kind(today, "sol_message")[0]
    assert sol_message["text"] == "Here is your answer."
    assert emitted["finish"] == [("logical-chat", "Here is your answer.")]
    assert emitted["error"] == []
