"""
Coverage tests for SocketIOService — targeting ~17 previously uncovered
emission failure paths and edge-case branches.

Focuses on:
- emit_socket_event failure when room IS specified
- emit_to_subscribers with enable_logging=False (success path)
- remove_subscriptions_for_research when nothing was stored (None branch)
- __handle_connect direct invocation
- __handle_subscribe with no research_id (falsy)
- __handle_subscribe snapshot with empty log (no emit)
- __handle_subscribe snapshot with log entry (emit path)
- __handle_socket_error and __handle_default_error return values
- run() when werkzeug import fails (ImportError branch)
- __handle_disconnect cleanup_current_thread raises generic Exception
- __log_info / __log_error / __log_exception silenced when logging disabled
- emit_to_subscribers per-subscriber exception with logging disabled
"""

from unittest.mock import patch, MagicMock, Mock

MODULE = "local_deep_research.web.services.socket_service"


# ---------------------------------------------------------------------------
# Helper: create a fresh SocketIOService with MockSocketIO injected
# ---------------------------------------------------------------------------


class _MockSocketIO:
    """Minimal SocketIO stand-in that records emitted events."""

    def __init__(self, app=None, **kwargs):
        self.emitted_events = []
        self._handlers = {}

    def emit(self, event, data, room=None):
        self.emitted_events.append({"event": event, "data": data, "room": room})

    def on(self, event):
        def decorator(f):
            self._handlers[event] = f
            return f

        return decorator

    @property
    def on_error(self):
        def decorator(f):
            self._handlers["error"] = f
            return f

        return decorator

    @property
    def on_error_default(self):
        def decorator(f):
            self._handlers["error_default"] = f
            return f

        return decorator

    def run(self, app, **kwargs):
        pass


def _make_service():
    from local_deep_research.web.services.socket_service import SocketIOService

    SocketIOService._instance = None
    mock_app = Mock()
    mock_app.config = {}
    mock_app.debug = False

    with (
        patch(f"{MODULE}.SocketIO", _MockSocketIO),
        patch(
            "local_deep_research.settings.env_registry.get_env_setting",
            return_value=None,
        ),
    ):
        service = SocketIOService(app=mock_app)
    return service


class TestEmitSocketEventRoomFailure:
    """emit_socket_event returns False when room is specified and emit raises."""

    def setup_method(self):
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        self._orig = SocketIOService._instance

    def teardown_method(self):
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        SocketIOService._instance = self._orig

    def test_emit_with_room_raises_returns_false(self):
        """When room is set and socketio.emit raises, returns False."""
        service = _make_service()
        service._SocketIOService__socketio.emit = Mock(
            side_effect=RuntimeError("room disconnected")
        )

        result = service.emit_socket_event(
            "test_event", {"x": 1}, room="sid_abc"
        )

        assert result is False

    def test_emit_with_room_exception_does_not_propagate(self):
        """Exception from room-targeted emit is swallowed, not re-raised."""
        service = _make_service()
        service._SocketIOService__socketio.emit = Mock(
            side_effect=OSError("network gone")
        )

        try:
            result = service.emit_socket_event("ev", {}, room="r1")
        except Exception:
            assert False, (
                "Exception should not propagate from emit_socket_event"
            )

        assert result is False


class TestEmitToSubscribersLoggingDisabled:
    """emit_to_subscribers with enable_logging=False (success path)."""

    def setup_method(self):
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        self._orig = SocketIOService._instance

    def teardown_method(self):
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        SocketIOService._instance = self._orig

    def test_logging_disabled_then_restored_on_success(self):
        """Logging flag is False during call and True again after."""
        service = _make_service()
        rid = "r_logging"

        with service._SocketIOService__lock:
            service._SocketIOService__socket_subscriptions[rid] = {"s1"}

        logging_during_call = []
        original_emit = service._SocketIOService__socketio.emit

        def capturing_emit(event, data, room=None):
            logging_during_call.append(
                service._SocketIOService__logging_enabled
            )
            original_emit(event, data, room=room)

        service._SocketIOService__socketio.emit = capturing_emit

        result = service.emit_to_subscribers(
            "ev", rid, {}, enable_logging=False
        )

        assert result is True
        # During call logging was disabled
        assert logging_during_call == [False]
        # After call logging is restored
        assert service._SocketIOService__logging_enabled is True

    def test_logging_disabled_broadcast_path_restored(self):
        """Logging is restored even via the broadcast (no-subscriber) path."""
        service = _make_service()

        result = service.emit_to_subscribers(
            "ev", "no_such_research", {}, enable_logging=False
        )

        assert result is True
        assert service._SocketIOService__logging_enabled is True


class TestRemoveSubscriptionsNoneBranch:
    """remove_subscriptions_for_research when nothing exists (removed=None)."""

    def setup_method(self):
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        self._orig = SocketIOService._instance

    def teardown_method(self):
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        SocketIOService._instance = self._orig

    def test_remove_nonexistent_does_not_log(self):
        """No log_info call when removed is None (pop returns None)."""
        service = _make_service()

        with patch.object(service, "_SocketIOService__log_info") as mock_log:
            service.remove_subscriptions_for_research("totally_unknown")

        # __log_info should NOT be called since removed is None
        mock_log.assert_not_called()

    def test_remove_existing_does_log(self):
        """__log_info IS called when subscriptions were present."""
        service = _make_service()
        rid = "r_exists"

        with service._SocketIOService__lock:
            service._SocketIOService__socket_subscriptions[rid] = {
                "sid_a",
                "sid_b",
            }

        with patch.object(service, "_SocketIOService__log_info") as mock_log:
            service.remove_subscriptions_for_research(rid)

        mock_log.assert_called_once()
        assert rid not in service._SocketIOService__socket_subscriptions


class TestHandleConnect:
    """Direct invocation of __handle_connect."""

    def setup_method(self):
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        self._orig = SocketIOService._instance

    def teardown_method(self):
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        SocketIOService._instance = self._orig

    def test_handle_connect_rejects_unauthenticated(self):
        """__handle_connect returns False when no session username."""
        service = _make_service()
        mock_request = MagicMock()
        mock_request.sid = "connect_client_999"

        with patch(f"{MODULE}.session", {}):
            result = service._SocketIOService__handle_connect(mock_request)

        assert result is False

    def test_handle_connect_rejects_when_no_db_session_and_no_password(self):
        """No active DB session AND no stored password → reject."""
        service = _make_service()
        mock_request = MagicMock()
        mock_request.sid = "connect_client_999"

        with (
            patch(
                f"{MODULE}.session",
                {"username": "alice", "session_id": "sess-1"},
            ),
            patch(f"{MODULE}.db_manager") as mock_db,
            patch(f"{MODULE}.session_password_store") as mock_store,
        ):
            mock_db.is_user_connected.return_value = False
            mock_store.get_session_password.return_value = None
            result = service._SocketIOService__handle_connect(mock_request)

        assert result is False
        mock_db.open_user_database.assert_not_called()

    def test_handle_connect_lazy_opens_db_when_password_available(self):
        """No active DB session but password stored → lazy-open and accept."""
        service = _make_service()
        mock_request = MagicMock()
        mock_request.sid = "connect_client_999"

        with (
            patch(
                f"{MODULE}.session",
                {"username": "alice", "session_id": "sess-1"},
            ),
            patch(f"{MODULE}.db_manager") as mock_db,
            patch(f"{MODULE}.session_password_store") as mock_store,
            patch(f"{MODULE}.join_room"),
        ):
            mock_db.is_user_connected.return_value = False
            mock_store.get_session_password.return_value = "pw"
            result = service._SocketIOService__handle_connect(mock_request)

        assert result is True
        mock_db.open_user_database.assert_called_once_with("alice", "pw")

    def test_handle_connect_rejects_when_lazy_open_raises(self):
        """Lazy open raising (e.g., wrong password) → reject."""
        service = _make_service()
        mock_request = MagicMock()
        mock_request.sid = "connect_client_999"

        with (
            patch(
                f"{MODULE}.session",
                {"username": "alice", "session_id": "sess-1"},
            ),
            patch(f"{MODULE}.db_manager") as mock_db,
            patch(f"{MODULE}.session_password_store") as mock_store,
            patch.object(service, "_SocketIOService__log_error"),
        ):
            mock_db.is_user_connected.return_value = False
            mock_store.get_session_password.return_value = "pw"
            mock_db.open_user_database.side_effect = ValueError("bad key")
            result = service._SocketIOService__handle_connect(mock_request)

        assert result is False

    def test_handle_connect_accepts_authenticated(self):
        """__handle_connect returns True for authenticated users with DB session."""
        service = _make_service()
        mock_request = MagicMock()
        mock_request.sid = "connect_client_999"

        with (
            patch(f"{MODULE}.session", {"username": "alice"}),
            patch(f"{MODULE}.db_manager") as mock_db,
            patch(f"{MODULE}.join_room"),
            patch.object(service, "_SocketIOService__log_info") as mock_log,
        ):
            mock_db.is_user_connected.return_value = True
            result = service._SocketIOService__handle_connect(mock_request)

        assert result is True
        mock_log.assert_called_once()
        assert "connect_client_999" in str(mock_log.call_args)

    def test_handle_connect_joins_per_user_room(self):
        """Each connected socket joins its owner's per-user room, so user-scoped
        events (settings_changed, which carries raw setting values) reach only
        that user's tabs and are never broadcast to other connected clients."""
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        service = _make_service()
        mock_request = MagicMock()
        mock_request.sid = "connect_client_999"

        with (
            patch(f"{MODULE}.session", {"username": "alice"}),
            patch(f"{MODULE}.db_manager") as mock_db,
            patch(f"{MODULE}.join_room") as mock_join,
            patch.object(service, "_SocketIOService__log_info"),
        ):
            mock_db.is_user_connected.return_value = True
            service._SocketIOService__handle_connect(mock_request)

        mock_join.assert_called_once_with(SocketIOService.user_room("alice"))
        assert SocketIOService.user_room("alice") == "user:alice"


class TestHandleSubscribeEdgeCases:
    """Edge cases in __handle_subscribe."""

    def setup_method(self):
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        self._orig = SocketIOService._instance

    def teardown_method(self):
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        SocketIOService._instance = self._orig

    def test_subscribe_with_none_research_id_no_op(self):
        """No subscription added when research_id is absent/None."""
        service = _make_service()
        mock_request = MagicMock()
        mock_request.sid = "client_no_id"

        service._SocketIOService__handle_subscribe({}, mock_request)

        with service._SocketIOService__lock:
            assert len(service._SocketIOService__socket_subscriptions) == 0

    @patch(f"{MODULE}.get_active_research_snapshot")
    def test_subscribe_snapshot_empty_log_no_emit(self, mock_snapshot):
        """When snapshot exists but log is empty, no socket event is emitted."""
        service = _make_service()
        mock_snapshot.return_value = {"progress": 42, "log": []}

        mock_request = MagicMock()
        mock_request.sid = "client_empty_log"

        service._SocketIOService__handle_subscribe(
            {"research_id": "r_empty_log"}, mock_request
        )

        # No emit should happen since latest_log is None/falsy
        assert len(service._SocketIOService__socketio.emitted_events) == 0

    @patch(f"{MODULE}.get_active_research_snapshot")
    def test_subscribe_snapshot_with_log_emits_event(self, mock_snapshot):
        """When snapshot has a log entry, emit_socket_event is called."""
        service = _make_service()
        mock_snapshot.return_value = {
            "progress": 75,
            "log": [{"message": "Searching...", "type": "info"}],
        }

        mock_request = MagicMock()
        mock_request.sid = "client_with_log"

        with patch.object(
            service, "emit_socket_event", return_value=True
        ) as mock_emit:
            service._SocketIOService__handle_subscribe(
                {"research_id": "r_with_log"}, mock_request
            )

        mock_emit.assert_called_once()
        call_args = mock_emit.call_args
        assert (
            "r_with_log" in call_args[0][0]
        )  # event name contains research_id
        assert call_args[1]["room"] == "client_with_log"


class TestHandleSocketErrors:
    """__handle_socket_error and __handle_default_error return False."""

    def setup_method(self):
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        self._orig = SocketIOService._instance

    def teardown_method(self):
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        SocketIOService._instance = self._orig

    def test_handle_socket_error_returns_false(self):
        """__handle_socket_error returns False to avoid crashing the server."""
        service = _make_service()
        result = service._SocketIOService__handle_socket_error(
            Exception("socket error")
        )
        assert result is False

    def test_handle_default_error_returns_false(self):
        """__handle_default_error returns False to avoid crashing the server."""
        service = _make_service()
        result = service._SocketIOService__handle_default_error(
            Exception("unhandled error")
        )
        assert result is False


class TestRunWerkzeugImportError:
    """run() when werkzeug cannot be imported."""

    def setup_method(self):
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        self._orig = SocketIOService._instance

    def teardown_method(self):
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        SocketIOService._instance = self._orig

    def test_run_handles_werkzeug_import_error(self):
        """run() logs a warning but still starts when werkzeug is missing."""
        service = _make_service()
        service._SocketIOService__socketio.run = Mock()

        import builtins

        real_import = builtins.__import__

        def mock_import(name, *args, **kwargs):
            if name == "werkzeug.serving":
                raise ImportError("werkzeug not installed")
            return real_import(name, *args, **kwargs)

        with patch("builtins.__import__", side_effect=mock_import):
            service.run(host="127.0.0.1", port=5000, debug=False)

        service._SocketIOService__socketio.run.assert_called_once()


class TestHandleDisconnectCleanupException:
    """__handle_disconnect when cleanup_current_thread raises a generic Exception."""

    def setup_method(self):
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        self._orig = SocketIOService._instance

    def teardown_method(self):
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        SocketIOService._instance = self._orig

    def test_cleanup_exception_is_caught_and_logged(self):
        """Generic Exception from cleanup_current_thread is caught, not re-raised."""
        service = _make_service()
        mock_request = MagicMock()
        mock_request.sid = "client_cleanup_exc"

        def boom_cleanup():
            raise RuntimeError("session cleanup failed")

        mock_module = MagicMock()
        mock_module.cleanup_current_thread = boom_cleanup

        with patch.dict(
            "sys.modules",
            {
                "local_deep_research.database.thread_local_session": mock_module,
            },
        ):
            with patch.object(
                service, "_SocketIOService__log_exception"
            ) as mock_exc:
                # Should NOT raise
                service._SocketIOService__handle_disconnect(
                    mock_request, "cleanup exc test"
                )

        # __log_exception should have been called for the cleanup failure
        assert mock_exc.called


class TestLoggingMethods:
    """__log_info, __log_error, __log_exception are no-ops when logging disabled."""

    def setup_method(self):
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        self._orig = SocketIOService._instance

    def teardown_method(self):
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        SocketIOService._instance = self._orig

    def test_log_info_silent_when_disabled(self):
        """__log_info does not call logger.info when __logging_enabled is False."""
        service = _make_service()
        service._SocketIOService__logging_enabled = False

        with patch(f"{MODULE}.logger") as mock_logger:
            service._SocketIOService__log_info("should be silent")

        mock_logger.info.assert_not_called()

    def test_log_error_silent_when_disabled(self):
        """__log_error does not call logger.error when __logging_enabled is False."""
        service = _make_service()
        service._SocketIOService__logging_enabled = False

        with patch(f"{MODULE}.logger") as mock_logger:
            service._SocketIOService__log_error("should be silent")

        mock_logger.error.assert_not_called()

    def test_log_exception_silent_when_disabled(self):
        """__log_exception does not call logger.exception when __logging_enabled is False."""
        service = _make_service()
        service._SocketIOService__logging_enabled = False

        with patch(f"{MODULE}.logger") as mock_logger:
            service._SocketIOService__log_exception("should be silent")

        mock_logger.exception.assert_not_called()

    def test_log_info_active_when_enabled(self):
        """__log_info calls logger.info when __logging_enabled is True."""
        service = _make_service()
        service._SocketIOService__logging_enabled = True

        with patch(f"{MODULE}.logger") as mock_logger:
            service._SocketIOService__log_info("active message")

        mock_logger.info.assert_called_once_with("active message")
