"""
Regression tests for the ImGui IM_ASSERT propagation handling.

The bug: when `immapp.run` raises a `RuntimeError` (e.g. from an ImGui
scope mismatch like `IM_ASSERT((0) && "Missing End()")`), the exception
propagates out of `app.run()` and may cause the controller's `_io_pool`
to shut down. The hook server thread (separate `ThreadingHTTPServer`)
survives, but subsequent test clicks fail with
`RuntimeError: cannot schedule new futures after shutdown`.

The fix (per user feedback 2026-06-08): wrap `immapp.run` in a
`try/except RuntimeError` that:
  1. Does NOT silently swallow the error (user rejected silent failures)
  2. Logs the error at ERROR level
  3. Records the failure on the controller so the `/api/gui_health`
     endpoint can surface it
  4. Does NOT call `self.shutdown()` so the hook server stays alive

These tests verify the controller's state attributes exist and that
the error path correctly records the failure.
"""
import sys
from pathlib import Path
from unittest.mock import patch
from typing import Any

import pytest

ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))


def test_app_run_records_degraded_state_on_imgui_assert(mock_app: Any) -> None:
 """When immapp.run raises RuntimeError, AppController records the
 failure on `gui_degraded_reason` so the health endpoint can surface it.

 The fix is in `src/gui_2.py:app.run` which wraps `immapp.run` in a
 try/except. The exception is caught, the controller's state is updated,
 and the run() method returns normally (so the GUI process keeps
 responding to the hook server).
 """
 app = mock_app
 ctrl = app.controller
 # Precondition: degraded reason is None
 assert getattr(ctrl, "_gui_degraded_reason", None) is None
 # Simulate the immapp.run raising IM_ASSERT
 from imgui_bundle import immapp
 with patch.object(immapp, "run", side_effect=RuntimeError("IM_ASSERT((0) && \"Missing End()\")")):
  # Call app.run() — should catch the exception, not propagate
  try:
   app.run()
  except RuntimeError as e:
   pytest.fail(f"app.run() should have caught IM_ASSERT, but raised: {e}")
 # Postcondition: degraded reason is set
 assert ctrl._gui_degraded_reason is not None
 assert "IM_ASSERT" in ctrl._gui_degraded_reason
 # And the last assert contains the full message
 assert ctrl._last_imgui_assert is not None
 assert "Missing End" in ctrl._last_imgui_assert
