"""Unit tests for the theme engine (pytest-qt, offscreen)."""

from __future__ import annotations

import pytest

pytest.importorskip("PySide6")
pytest.importorskip("pytestqt")

from PySide6.QtGui import QPalette

from mgf.brand_qt import theme as theme_module
from mgf.brand_qt import tokens
from mgf.brand_qt.fonts import default_font, display_font, load_fonts, mono_font
from mgf.brand_qt.theme import ThemeManager, _qcolor, apply_theme, to_qcolor

pytestmark = pytest.mark.gui


@pytest.fixture
def branded_app(qapp):
    """Yield the shared QApplication, restoring its skin afterwards.

    apply_theme mutates app-global state (palette/stylesheet/font, and
    the PAPER-11 per-app manager registration); later GUI tests in the
    same process must not inherit any of it.
    """
    old_font = qapp.font()
    theme_module._ACTIVE = None
    yield qapp
    theme_module._ACTIVE = None
    qapp.setStyleSheet("")
    qapp.setPalette(QPalette())
    qapp.setFont(old_font)


def test_qcolor_parses_hex_and_rgba() -> None:
    assert _qcolor("#1e2a4a").name() == "#1e2a4a"
    rgba = _qcolor("rgba(233,236,242,0.25)")
    assert (rgba.red(), rgba.green(), rgba.blue()) == (233, 236, 242)
    assert rgba.alpha() == round(0.25 * 255)


def test_load_fonts_registers_brand_families(branded_app) -> None:
    families = set(load_fonts())
    # Degradation is legal per-file, but with the bundled TTFs present
    # all three families must register on a healthy Qt.
    assert any("Inter" in f for f in families)
    assert any("Space Grotesk" in f for f in families)
    assert any("JetBrains Mono" in f for f in families)


def test_font_helpers_carry_the_scale() -> None:
    body = default_font()
    assert body.pointSizeF() == tokens.TEXT_PT["base"]
    display = display_font(tokens.TEXT_PT["lg"])
    assert display.family() == tokens.FONT_DISPLAY
    mono = mono_font(tokens.TEXT_PT["sm"])
    assert mono.fixedPitch()


def test_apply_theme_brands_the_application(branded_app) -> None:
    seen: list[str] = []
    manager = apply_theme(branded_app, "light", seen.append)
    assert branded_app.styleSheet()
    window = branded_app.palette().color(QPalette.ColorRole.Window)
    assert window.name() == tokens.LIGHT.bg_canvas
    assert not branded_app.windowIcon().isNull()
    assert manager.resolved() == "light"
    assert seen == []  # initial apply must not re-persist


def test_set_mode_flips_palette_persists_and_signals(branded_app, qtbot) -> None:
    persisted: list[str] = []
    manager = ThemeManager(branded_app, "light", persisted.append)
    with qtbot.waitSignal(manager.theme_changed, timeout=1000) as blocker:
        manager.set_mode("dark")
    assert blocker.args == ["dark"]
    assert persisted == ["dark"]
    window = branded_app.palette().color(QPalette.ColorRole.Window)
    assert window.name() == tokens.DARK.bg_canvas
    # No-op switch neither persists nor re-applies.
    manager.set_mode("dark")
    assert persisted == ["dark"]


def test_system_mode_resolves_to_light_or_dark(branded_app) -> None:
    manager = ThemeManager(branded_app, "system", None)
    assert manager.resolved() in {"light", "dark"}


def test_to_qcolor_is_the_public_parser() -> None:
    """PAPER-05: the token→QColor parse is API, not a private copy."""
    assert to_qcolor("#f2a93b").name() == "#f2a93b"
    assert to_qcolor is not None and _qcolor is to_qcolor


def test_apply_theme_twice_returns_the_same_manager(branded_app) -> None:
    """PAPER-11 pin: the second call must NOT construct a second manager.

    The seeded defect (two constructions → setStyle x 2 + re-polish)
    SIGSEGVs with a live QWizard and cannot be asserted from Python;
    manager identity across calls is the observable contract that
    forecloses the crash path.
    """
    first = apply_theme(branded_app, "light")
    second = apply_theme(branded_app, "light")
    assert second is first
    assert branded_app.styleSheet()  # the repeat call still applied


def test_apply_theme_reuse_updates_mode_and_callback(branded_app) -> None:
    persisted_a: list[str] = []
    persisted_b: list[str] = []
    manager = apply_theme(branded_app, "light", persisted_a.append)
    again = apply_theme(branded_app, "dark", persisted_b.append)
    assert again is manager
    assert manager.resolved() == "dark"
    assert persisted_a == []  # the old callback was replaced, not fired
    assert persisted_b == ["dark"]


def test_extra_qss_survives_theme_flips(branded_app) -> None:
    """PAPER-05: consumer QSS is appended on EVERY re-apply, not dropped."""
    manager = apply_theme(branded_app, "light")
    manager.set_extra_qss("QLabel#helpBadge { border: 1px solid red; }")
    assert "helpBadge" in branded_app.styleSheet()
    manager.set_mode("dark")
    assert "helpBadge" in branded_app.styleSheet()  # survived the flip
    manager.set_extra_qss("")
    assert "helpBadge" not in branded_app.styleSheet()


def test_the_registry_keeps_the_manager_alive(branded_app) -> None:
    """0.1.3: a caller that drops the return value keeps the guarantee.

    0.1.2 held the registry by weakref; figrecovery's suite dropped the
    returned manager, GC collected it, the registry forgot it, and the
    next apply_theme walked the full construction path again — the
    PAPER-11 crash class, back within the hour of release. The registry
    is a strong reference now: the kit owns this lifetime.
    """
    import gc
    import weakref as _weakref

    manager = apply_theme(branded_app, "light")
    probe = _weakref.ref(manager)
    del manager
    gc.collect()
    assert probe() is not None  # survived the caller dropping it
    again = apply_theme(branded_app, "light")
    assert again is probe()
