"""Tests for slot_error_boundary decorator."""
from __future__ import annotations

import logging
from unittest.mock import MagicMock

from pygui.backend.utils import slot_error_boundary


class _FakeSlot:
    """Minimal QObject-like object to test the decorator against."""

    def __init__(self, has_log_message=True):
        self.logMessage = MagicMock() if has_log_message else None
        self.__class__.__module__ = "test_module"


class TestSlotErrorBoundary:

    def test_passes_through_on_success(self):
        @slot_error_boundary
        def good(self) -> str:
            return "ok"

        result = good(_FakeSlot())
        assert result == "ok"

    def test_catches_and_logs_exception(self, caplog):
        @slot_error_boundary
        def bad(self) -> None:
            raise ValueError("boom")

        with caplog.at_level(logging.ERROR):
            bad(_FakeSlot())

        assert "ValueError" in caplog.text
        assert "bad" in caplog.text

    def test_emits_log_message_on_error(self):
        obj = _FakeSlot(has_log_message=True)

        @slot_error_boundary
        def bad(self) -> None:
            raise RuntimeError("fail")

        bad(obj)

        obj.logMessage.emit.assert_called_once()
        emitted_msg = obj.logMessage.emit.call_args[0][0]
        assert "bad" in emitted_msg
        assert "fail" in emitted_msg

    def test_returns_none_on_error(self):
        @slot_error_boundary
        def bad(self) -> None:
            raise KeyError("missing")

        result = bad(_FakeSlot())
        assert result is None

    def test_no_log_message_signal_is_ok(self):
        """Controller without logMessage signal should not crash."""

        class _NoLogMsg:
            __module__ = "test_module"

        @slot_error_boundary
        def bad(self) -> None:
            raise ValueError("boom")

        # Should not raise — hasattr check guards the emit
        result = bad(_NoLogMsg())
        assert result is None

    def test_preserves_function_name(self):
        @slot_error_boundary
        def named_method(self) -> None:
            pass

        assert named_method.__name__ == "named_method"

    def test_passes_args_through(self):
        @slot_error_boundary
        def with_args(self, a: int, b: str) -> tuple[int, str]:
            return (a, b)

        result = with_args(_FakeSlot(), 42, "hello")
        assert result == (42, "hello")

    def test_exception_in_wrapper_does_not_leak(self):
        """The decorator must never re-raise — QML would crash."""

        @slot_error_boundary
        def crashing(self) -> None:
            raise TypeError("uncaught type error")

        # Should return None, not raise
        result = crashing(_FakeSlot())
        assert result is None
