"""Interpreter-shutdown handling in the conversation loop's outer retry path.

When the process starts tearing down (TUI quit, SIGTERM, one-shot exit) while
a turn — most commonly the post-turn background-review fork's daemon thread —
is still mid-request, every further API attempt raises ``RuntimeError:
cannot schedule new futures after interpreter shutdown``.

Before the fix, the outer loop treated that as a retryable API error: it
printed an un-gated ``❌ Error during OpenAI-compatible API call #N`` line
per attempt (spamming the user's shell AFTER the TUI already exited) and
retried until the iteration budget or interpreter froze the thread.

Now the loop recognizes the shutdown signal via the shared predicate in
``tools.interpreter_shutdown`` (same class as cron delivery #55924/#58720
and concurrent tool submission), logs one warning, and abandons the turn —
no print, no traceback, no retry.
"""

from types import SimpleNamespace

def _text_response(text: str):
    return SimpleNamespace(
        choices=[
            SimpleNamespace(
                message=SimpleNamespace(content=text, reasoning=None, tool_calls=[]),
                finish_reason="stop",
            )
        ],
        usage=None,
    )

class _ShutdownThenTextCompletions:
    """First call raises the CPython shutdown error; later calls would succeed.

    The 'would succeed' part is the sabotage detector: if the loop wrongly
    retries after the shutdown signal, call #2 returns a normal response and
    the assertions on call count / failed flag below fail loudly.
    """

    def __init__(self):
        self.calls = 0

    def create(self, **kwargs):
        self.calls += 1
        if self.calls == 1:
            raise RuntimeError(
                "cannot schedule new futures after interpreter shutdown"
            )
        return _text_response("should never be reached")

class _AlwaysFailingCompletions:
    def __init__(self):
        self.calls = 0

    def create(self, **kwargs):
        self.calls += 1
        raise Exception("API down")

def _make_agent(monkeypatch, completions):
    from run_agent import AIAgent

    client = SimpleNamespace(chat=SimpleNamespace(completions=completions))
    monkeypatch.setattr("agent.process_bootstrap.OpenAI", lambda **kwargs: client)
    monkeypatch.setattr("model_tools.get_tool_definitions", lambda *a, **k: [])

    agent = AIAgent(
        model="test-model",
        api_key="test-key",
        base_url="http://localhost:8080/v1",
        platform="cli",
        max_iterations=4,
        quiet_mode=True,
        skip_memory=True,
    )
    agent._disable_streaming = True
    return agent

def test_shutdown_error_exits_loop_without_retry(monkeypatch, capsys):
    completions = _ShutdownThenTextCompletions()
    agent = _make_agent(monkeypatch, completions)

    result = agent.run_conversation("hello")

    # Precondition: the shutdown error actually fired on call #1.
    assert completions.calls >= 1
    # The core contract: NO retry after the shutdown signal.
    assert completions.calls == 1, (
        "conversation loop retried an API call after interpreter-shutdown "
        f"signal (made {completions.calls} calls)"
    )
    assert result["failed"] is True
    assert "shutting down" in result["final_response"]

    # And no ❌ spam on the user's terminal.
    out = capsys.readouterr()
    assert "❌" not in out.out
    assert "cannot schedule new futures" not in out.out

def test_shutdown_variant_without_interpreter_word_also_exits(monkeypatch):
    """CPython's plain-ThreadPoolExecutor variant omits 'interpreter'."""

    class _Completions:
        def __init__(self):
            self.calls = 0

        def create(self, **kwargs):
            self.calls += 1
            raise RuntimeError("cannot schedule new futures after shutdown")

    completions = _Completions()
    agent = _make_agent(monkeypatch, completions)

    result = agent.run_conversation("hello")

    assert completions.calls == 1
    assert result["failed"] is True

def test_suppress_status_output_gates_error_print_for_ordinary_api_errors(
    monkeypatch, capsys
):
    """suppress_status_output routes ❌ lines to the log, not stdout.

    This is the leak path from the field report: the background-review fork
    runs with suppress_status_output=True (which gates _vprint and the
    buffered retry trace), yet the bare print() in the outer error handler
    bypassed it.
    """
    completions = _AlwaysFailingCompletions()
    agent = _make_agent(monkeypatch, completions)
    agent.suppress_status_output = True

    result = agent.run_conversation("hello")

    # Precondition: the ordinary (non-shutdown) error path actually retried
    # up to the budget — this test exercises the print gating, not early exit.
    assert completions.calls > 1
    assert "API down" in result["final_response"]

    out = capsys.readouterr()
    assert "❌" not in out.out

def test_non_quiet_mode_still_prints_error(monkeypatch, capsys):
    completions = _AlwaysFailingCompletions()
    agent = _make_agent(monkeypatch, completions)
    agent.quiet_mode = False

    agent.run_conversation("hello")

    out = capsys.readouterr()
    assert "❌" in out.out

class TestSharedPredicate:

    def test_ignores_unrelated_errors(self):
        from tools.interpreter_shutdown import interpreter_shutting_down

        assert interpreter_shutting_down(RuntimeError("boom")) is False
        assert interpreter_shutting_down(None) is False
