"""Extra coverage tests for socket_service.py — private event handlers and logging."""

from unittest.mock import MagicMock, patch


MODULE = "local_deep_research.web.services.socket_service"


def _get_service_with_patched_init():
    """Get a SocketIOService instance bypassing singleton + Flask requirement."""
    from local_deep_research.web.services.socket_service import SocketIOService

    with patch.object(
        SocketIOService, "__new__", lambda cls, *a, **kw: object.__new__(cls)
    ):
        svc = object.__new__(SocketIOService)

    svc._SocketIOService__socket_subscriptions = {}
    svc._SocketIOService__lock = __import__("threading").Lock()
    svc._SocketIOService__socketio = MagicMock()
    svc._SocketIOService__logging_enabled = True
    return svc


# ===========================================================================
# __handle_connect
# ===========================================================================


class TestHandleConnect:
    def test_rejects_unauthenticated(self):
        svc = _get_service_with_patched_init()
        mock_request = MagicMock()
        mock_request.sid = "client-123"

        with patch(f"{MODULE}.session", {}):
            assert svc._SocketIOService__handle_connect(mock_request) is False

    def test_rejects_when_no_db_session(self):
        svc = _get_service_with_patched_init()
        mock_request = MagicMock()
        mock_request.sid = "client-123"

        with (
            patch(f"{MODULE}.session", {"username": "alice"}),
            patch(f"{MODULE}.db_manager") as mock_db,
        ):
            mock_db.is_user_connected.return_value = False
            assert svc._SocketIOService__handle_connect(mock_request) is False

    def test_accepts_authenticated(self):
        svc = _get_service_with_patched_init()
        mock_request = MagicMock()
        mock_request.sid = "client-123"

        with (
            patch(f"{MODULE}.session", {"username": "alice"}),
            patch(f"{MODULE}.db_manager") as mock_db,
            patch(f"{MODULE}.join_room"),
        ):
            mock_db.is_user_connected.return_value = True
            assert svc._SocketIOService__handle_connect(mock_request) is True


# ===========================================================================
# __handle_disconnect
# ===========================================================================


class TestHandleDisconnect:
    def test_removes_subscription(self):
        svc = _get_service_with_patched_init()
        svc._SocketIOService__socket_subscriptions = {
            "res-1": {"client-1", "client-2"},
            "res-2": {"client-1"},
        }

        mock_request = MagicMock()
        mock_request.sid = "client-1"

        with patch(f"{MODULE}.cleanup_current_thread", create=True):
            svc._SocketIOService__handle_disconnect(
                mock_request, "transport close"
            )

        assert "client-1" not in svc._SocketIOService__socket_subscriptions.get(
            "res-1", set()
        )
        # res-2 should be cleaned up entirely since no clients left
        assert "res-2" not in svc._SocketIOService__socket_subscriptions

    def test_empty_subscriptions_handled(self):
        svc = _get_service_with_patched_init()
        mock_request = MagicMock()
        mock_request.sid = "unknown-client"

        with patch(f"{MODULE}.cleanup_current_thread", create=True):
            svc._SocketIOService__handle_disconnect(mock_request, "timeout")

    def test_cleanup_import_error_swallowed(self):
        svc = _get_service_with_patched_init()
        mock_request = MagicMock()
        mock_request.sid = "client-1"

        with patch.dict(
            "sys.modules",
            {"local_deep_research.database.thread_local_session": None},
        ):
            svc._SocketIOService__handle_disconnect(mock_request, "close")

    def test_cleanup_exception_swallowed(self):
        svc = _get_service_with_patched_init()
        mock_request = MagicMock()
        mock_request.sid = "client-1"

        mock_mod = MagicMock()
        mock_mod.cleanup_current_thread.side_effect = RuntimeError(
            "cleanup fail"
        )

        with patch.dict(
            "sys.modules",
            {"local_deep_research.database.thread_local_session": mock_mod},
        ):
            svc._SocketIOService__handle_disconnect(mock_request, "close")

    def test_outer_exception_swallowed(self):
        svc = _get_service_with_patched_init()
        # Make the lock acquisition fail
        svc._SocketIOService__lock = MagicMock()
        svc._SocketIOService__lock.__enter__ = MagicMock(
            side_effect=RuntimeError("lock fail")
        )
        svc._SocketIOService__lock.__exit__ = MagicMock(return_value=False)

        mock_request = MagicMock()
        mock_request.sid = "client-1"

        # Should not raise
        svc._SocketIOService__handle_disconnect(mock_request, "error")


# ===========================================================================
# __handle_subscribe
# ===========================================================================


class TestHandleSubscribe:
    def test_adds_subscriber(self):
        svc = _get_service_with_patched_init()
        mock_request = MagicMock()
        mock_request.sid = "client-1"

        with patch(f"{MODULE}.get_active_research_snapshot", return_value=None):
            svc._SocketIOService__handle_subscribe(
                {"research_id": "res-1"}, mock_request
            )

        assert "client-1" in svc._SocketIOService__socket_subscriptions["res-1"]

    def test_sends_current_status_when_available(self):
        svc = _get_service_with_patched_init()
        mock_request = MagicMock()
        mock_request.sid = "client-1"

        snapshot = {
            "progress": 50,
            "log": [{"message": "Searching...", "phase": "search"}],
        }

        with patch(
            f"{MODULE}.get_active_research_snapshot", return_value=snapshot
        ):
            svc._SocketIOService__handle_subscribe(
                {"research_id": "res-1"}, mock_request
            )

        svc._SocketIOService__socketio.emit.assert_called()

    def test_no_status_when_snapshot_none(self):
        svc = _get_service_with_patched_init()
        mock_request = MagicMock()
        mock_request.sid = "client-1"

        with patch(f"{MODULE}.get_active_research_snapshot", return_value=None):
            svc._SocketIOService__handle_subscribe(
                {"research_id": "res-1"}, mock_request
            )

        svc._SocketIOService__socketio.emit.assert_not_called()

    def test_empty_log_no_emit(self):
        svc = _get_service_with_patched_init()
        mock_request = MagicMock()
        mock_request.sid = "client-1"

        snapshot = {"progress": 0, "log": []}

        with patch(
            f"{MODULE}.get_active_research_snapshot", return_value=snapshot
        ):
            svc._SocketIOService__handle_subscribe(
                {"research_id": "res-1"}, mock_request
            )

        svc._SocketIOService__socketio.emit.assert_not_called()

    def test_no_research_id_ignored(self):
        svc = _get_service_with_patched_init()
        mock_request = MagicMock()

        svc._SocketIOService__handle_subscribe({}, mock_request)

        assert len(svc._SocketIOService__socket_subscriptions) == 0


# ===========================================================================
# __handle_socket_error / __handle_default_error
# ===========================================================================


class TestErrorHandlers:
    def test_socket_error_returns_false(self):
        svc = _get_service_with_patched_init()
        result = svc._SocketIOService__handle_socket_error(RuntimeError("test"))
        assert result is False

    def test_default_error_returns_false(self):
        svc = _get_service_with_patched_init()
        result = svc._SocketIOService__handle_default_error(ValueError("test"))
        assert result is False


# ===========================================================================
# __log_info / __log_error / __log_exception — logging control
# ===========================================================================


class TestLoggingControl:
    def test_log_info_when_enabled(self):
        svc = _get_service_with_patched_init()
        svc._SocketIOService__logging_enabled = True
        svc._SocketIOService__log_info("test message")

    def test_log_info_when_disabled(self):
        svc = _get_service_with_patched_init()
        svc._SocketIOService__logging_enabled = False
        svc._SocketIOService__log_info("should not log")

    def test_log_error_when_enabled(self):
        svc = _get_service_with_patched_init()
        svc._SocketIOService__logging_enabled = True
        svc._SocketIOService__log_error("error message")

    def test_log_exception_when_enabled(self):
        svc = _get_service_with_patched_init()
        svc._SocketIOService__logging_enabled = True
        svc._SocketIOService__log_exception("exception message")

    def test_log_exception_when_disabled(self):
        svc = _get_service_with_patched_init()
        svc._SocketIOService__logging_enabled = False
        svc._SocketIOService__log_exception("should not log")
