"""
Comprehensive tests for SocketIOService.
Tests singleton pattern, event emission, subscriptions, and error handling.
"""

import pytest
from unittest.mock import Mock, MagicMock, patch


class TestSocketIOServiceSingleton:
    """Tests for SocketIOService singleton pattern.

    Note: Singleton reset is handled by the global reset_all_singletons fixture
    in tests/conftest.py.
    """

    def test_requires_app_on_first_creation(self):
        """Test that Flask app is required on first creation."""
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        with pytest.raises(ValueError) as exc_info:
            SocketIOService()

        assert "Flask app must be specified" in str(exc_info.value)

    def test_creates_instance_with_app(self, mock_flask_app):
        """Test that instance is created with Flask app."""
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        with patch(
            "local_deep_research.web.services.socket_service.SocketIO"
        ) as mock_socketio:
            service = SocketIOService(app=mock_flask_app)

            assert service is not None
            mock_socketio.assert_called_once()

    def test_returns_same_instance_on_second_call(self, mock_flask_app):
        """Test that same instance is returned on subsequent calls."""
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        with patch("local_deep_research.web.services.socket_service.SocketIO"):
            service1 = SocketIOService(app=mock_flask_app)
            service2 = SocketIOService()

            assert service1 is service2

    def test_ignores_app_on_second_call(self, mock_flask_app):
        """Test that app parameter is ignored after singleton creation."""
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        with patch("local_deep_research.web.services.socket_service.SocketIO"):
            service1 = SocketIOService(app=mock_flask_app)
            other_app = Mock()
            service2 = SocketIOService(app=other_app)

            assert service1 is service2


class TestSocketIOServiceInit:
    """Tests for SocketIOService initialization.

    Note: Singleton reset is handled by the global reset_all_singletons fixture
    in tests/conftest.py.
    """

    def test_initializes_socketio_with_correct_params(self, mock_flask_app):
        """Test that SocketIO is initialized with correct parameters."""
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        with patch(
            "local_deep_research.web.services.socket_service.SocketIO"
        ) as mock_socketio:
            SocketIOService(app=mock_flask_app)

            mock_socketio.assert_called_once_with(
                mock_flask_app,
                cors_allowed_origins=None,
                async_mode="threading",
                path="/socket.io",
                logger=False,
                engineio_logger=False,
                ping_timeout=20,
                ping_interval=5,
            )

    def test_registers_connect_handler(self, mock_flask_app):
        """Test that connect handler is registered."""
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        mock_socketio = MagicMock()

        with patch(
            "local_deep_research.web.services.socket_service.SocketIO",
            return_value=mock_socketio,
        ):
            SocketIOService(app=mock_flask_app)

            # Check that on("connect") was called
            mock_socketio.on.assert_any_call("connect")

    def test_registers_disconnect_handler(self, mock_flask_app):
        """Test that disconnect handler is registered."""
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        mock_socketio = MagicMock()

        with patch(
            "local_deep_research.web.services.socket_service.SocketIO",
            return_value=mock_socketio,
        ):
            SocketIOService(app=mock_flask_app)

            mock_socketio.on.assert_any_call("disconnect")

    def test_registers_subscribe_handler(self, mock_flask_app):
        """Test that subscribe_to_research handler is registered."""
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        mock_socketio = MagicMock()

        with patch(
            "local_deep_research.web.services.socket_service.SocketIO",
            return_value=mock_socketio,
        ):
            SocketIOService(app=mock_flask_app)

            mock_socketio.on.assert_any_call("subscribe_to_research")

    def test_registers_join_alias_handler(self, mock_flask_app):
        """Test that the 'join' alias handler is registered.

        The JS client emits 'join' on subscribe; without this alias, the
        catch-up snapshot in __handle_subscribe never fires for fresh page
        loads and per-client targeting falls through to broadcast.
        """
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        mock_socketio = MagicMock()

        with patch(
            "local_deep_research.web.services.socket_service.SocketIO",
            return_value=mock_socketio,
        ):
            SocketIOService(app=mock_flask_app)

            mock_socketio.on.assert_any_call("join")

    def test_registers_leave_alias_handler(self, mock_flask_app):
        """Test that the 'leave' alias handler is registered."""
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        mock_socketio = MagicMock()

        with patch(
            "local_deep_research.web.services.socket_service.SocketIO",
            return_value=mock_socketio,
        ):
            SocketIOService(app=mock_flask_app)

            mock_socketio.on.assert_any_call("leave")

    def test_registers_unsubscribe_handler(self, mock_flask_app):
        """Test that unsubscribe_from_research handler is registered."""
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        mock_socketio = MagicMock()

        with patch(
            "local_deep_research.web.services.socket_service.SocketIO",
            return_value=mock_socketio,
        ):
            SocketIOService(app=mock_flask_app)

            mock_socketio.on.assert_any_call("unsubscribe_from_research")


class TestSocketIOServiceEmitSocketEvent:
    """Tests for emit_socket_event method.

    Note: Singleton reset is handled by the global reset_all_singletons fixture
    in tests/conftest.py.
    """

    @pytest.fixture
    def service(self, mock_flask_app):
        """Create SocketIOService instance with mocked SocketIO."""
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        mock_socketio = MagicMock()

        with patch(
            "local_deep_research.web.services.socket_service.SocketIO",
            return_value=mock_socketio,
        ):
            service = SocketIOService(app=mock_flask_app)
            service._SocketIOService__socketio = mock_socketio
            return service, mock_socketio

    def test_emits_to_all_when_no_room(self, service):
        """Test emitting to all clients when no room specified."""
        svc, mock_socketio = service

        result = svc.emit_socket_event("test_event", {"key": "value"})

        assert result is True
        mock_socketio.emit.assert_called_once_with(
            "test_event", {"key": "value"}
        )

    def test_emits_to_specific_room(self, service):
        """Test emitting to specific room."""
        svc, mock_socketio = service

        result = svc.emit_socket_event(
            "test_event", {"key": "value"}, room="room-123"
        )

        assert result is True
        mock_socketio.emit.assert_called_once_with(
            "test_event", {"key": "value"}, room="room-123"
        )

    def test_returns_false_on_exception(self, service):
        """Test that False is returned on emission error."""
        svc, mock_socketio = service
        mock_socketio.emit.side_effect = Exception("Emission failed")

        result = svc.emit_socket_event("test_event", {"key": "value"})

        assert result is False

    def test_logs_exception_on_error(self, service):
        """Test that exception is logged on error."""
        svc, mock_socketio = service
        mock_socketio.emit.side_effect = Exception("Emission failed")

        with patch(
            "local_deep_research.web.services.socket_service.logger"
        ) as mock_logger:
            svc.emit_socket_event("test_event", {"key": "value"})

            mock_logger.exception.assert_called_once()


class TestSocketIOServiceEmitToSubscribers:
    """Tests for emit_to_subscribers method.

    Note: Singleton reset is handled by the global reset_all_singletons fixture
    in tests/conftest.py.
    """

    @pytest.fixture
    def service(self, mock_flask_app):
        """Create SocketIOService instance with mocked SocketIO."""
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        mock_socketio = MagicMock()

        with patch(
            "local_deep_research.web.services.socket_service.SocketIO",
            return_value=mock_socketio,
        ):
            service = SocketIOService(app=mock_flask_app)
            service._SocketIOService__socketio = mock_socketio
            return service, mock_socketio

    def test_emits_formatted_event(self, service, sample_research_id):
        """Test that event is emitted with formatted name to subscribers."""
        svc, mock_socketio = service
        # Targeted delivery is the only path emit_to_subscribers takes;
        # without a registered subscriber the event is dropped (the
        # no-subscribers test pins that contract separately).
        svc._SocketIOService__socket_subscriptions = {
            sample_research_id: {"sid-1"}
        }

        svc.emit_to_subscribers(
            "research_update", sample_research_id, {"data": "test"}
        )

        # Should emit with formatted event name, scoped to the room.
        mock_socketio.emit.assert_any_call(
            f"research_update_{sample_research_id}",
            {"data": "test"},
            room="sid-1",
        )

    def test_emits_to_individual_subscribers(self, service, sample_research_id):
        """Test that event is emitted to each subscriber."""
        svc, mock_socketio = service

        # Add subscribers
        svc._SocketIOService__socket_subscriptions = {
            sample_research_id: {"sid-1", "sid-2"}
        }

        svc.emit_to_subscribers(
            "research_update", sample_research_id, {"data": "test"}
        )

        # Should emit to each subscriber
        calls = mock_socketio.emit.call_args_list
        room_calls = [c for c in calls if "room" in c.kwargs]
        assert len(room_calls) == 2

    def test_returns_true_on_success(self, service, sample_research_id):
        """Test that True is returned on success."""
        svc, mock_socketio = service

        result = svc.emit_to_subscribers(
            "research_update", sample_research_id, {"data": "test"}
        )

        assert result is True

    def test_returns_false_on_exception(self, service, sample_research_id):
        """Test that False is returned on error.

        Per-subscriber emit failures are now caught inside the inner
        loop (so other subscribers still receive the event), so the
        method returns False only when something outside the loop
        raises — simulated here by raising during ``__lock`` acquisition.
        """
        svc, mock_socketio = service
        # Make the lock context manager raise to force the outer
        # except path. The lock is a private attribute on the service.
        broken_lock = MagicMock()
        broken_lock.__enter__ = MagicMock(side_effect=Exception("Failed"))
        broken_lock.__exit__ = MagicMock(return_value=False)
        svc._SocketIOService__lock = broken_lock

        result = svc.emit_to_subscribers(
            "research_update", sample_research_id, {"data": "test"}
        )

        assert result is False

    def test_disables_logging_when_requested(self, service, sample_research_id):
        """Test that logging can be disabled."""
        svc, mock_socketio = service

        svc.emit_to_subscribers(
            "research_update",
            sample_research_id,
            {"data": "test"},
            enable_logging=False,
        )

        # After call, logging should be re-enabled
        assert svc._SocketIOService__logging_enabled is True

    def test_re_enables_logging_after_exception(
        self, service, sample_research_id
    ):
        """Test that logging is re-enabled even after exception."""
        svc, mock_socketio = service
        mock_socketio.emit.side_effect = Exception("Failed")

        svc.emit_to_subscribers(
            "research_update",
            sample_research_id,
            {"data": "test"},
            enable_logging=False,
        )

        # Logging should be re-enabled after exception
        assert svc._SocketIOService__logging_enabled is True

    def test_handles_no_subscribers(self, service, sample_research_id):
        """No-subscribers: drop the event rather than broadcasting.

        A prior implementation broadcast room-less to all clients in
        this case, which leaked research payloads across users. Early
        events are recovered by the catch-up snapshot in
        ``__handle_subscribe``; no cross-user broadcast is needed.
        """
        svc, mock_socketio = service

        result = svc.emit_to_subscribers(
            "research_update", sample_research_id, {"data": "test"}
        )

        assert result is True
        # MUST NOT emit when there are no targeted subscribers — the
        # absence of a room= argument would broadcast to every client.
        mock_socketio.emit.assert_not_called()

    def test_handles_subscriber_emit_error(self, service, sample_research_id):
        """Test that individual subscriber errors don't stop other emissions."""
        svc, mock_socketio = service

        # Add subscribers
        svc._SocketIOService__socket_subscriptions = {
            sample_research_id: {"sid-1", "sid-2"}
        }

        # First call succeeds, second fails
        call_count = [0]

        def emit_side_effect(*args, **kwargs):
            call_count[0] += 1
            if call_count[0] == 2:  # Fail on second subscriber
                raise Exception("Subscriber error")

        mock_socketio.emit.side_effect = emit_side_effect

        result = svc.emit_to_subscribers(
            "research_update", sample_research_id, {"data": "test"}
        )

        # Should still return True (overall success)
        assert result is True


class TestSocketIOServiceSubscriptionManagement:
    """Tests for subscription management.

    Note: Singleton reset is handled by the global reset_all_singletons fixture
    in tests/conftest.py.
    """

    @pytest.fixture
    def service_with_mocks(self, mock_flask_app):
        """Create service with accessible internal state."""
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        mock_socketio = MagicMock()

        with patch(
            "local_deep_research.web.services.socket_service.SocketIO",
            return_value=mock_socketio,
        ):
            service = SocketIOService(app=mock_flask_app)
            service._SocketIOService__socketio = mock_socketio
            return service, mock_socketio

    def test_subscribe_adds_client(
        self, service_with_mocks, mock_request, sample_research_id
    ):
        """Test that subscribing adds client to subscription set."""
        svc, _ = service_with_mocks

        data = {"research_id": sample_research_id}

        svc._SocketIOService__handle_subscribe(data, mock_request)

        subscriptions = svc._SocketIOService__socket_subscriptions
        assert sample_research_id in subscriptions
        assert mock_request.sid in subscriptions[sample_research_id]

    def test_subscribe_creates_set_for_new_research(
        self, service_with_mocks, mock_request
    ):
        """Test that subscribing creates new set for new research."""
        svc, _ = service_with_mocks

        data = {"research_id": "new-research-id"}

        svc._SocketIOService__handle_subscribe(data, mock_request)

        subscriptions = svc._SocketIOService__socket_subscriptions
        assert "new-research-id" in subscriptions
        assert isinstance(subscriptions["new-research-id"], set)

    def test_subscribe_ignores_empty_research_id(
        self, service_with_mocks, mock_request
    ):
        """Test that empty research_id is ignored."""
        svc, _ = service_with_mocks

        data = {"research_id": ""}

        svc._SocketIOService__handle_subscribe(data, mock_request)

        subscriptions = svc._SocketIOService__socket_subscriptions
        assert "" not in subscriptions

    def test_subscribe_ignores_missing_research_id(
        self, service_with_mocks, mock_request
    ):
        """Test that missing research_id is ignored."""
        svc, _ = service_with_mocks

        data = {}

        svc._SocketIOService__handle_subscribe(data, mock_request)

        subscriptions = svc._SocketIOService__socket_subscriptions
        assert len(subscriptions) == 0

    def test_disconnect_removes_subscription(
        self, service_with_mocks, mock_request
    ):
        """Test that disconnecting removes client from all research subscriptions.

        Schema: __socket_subscriptions is research_id → set of sids.
        Disconnect must discard the sid from every research_id's set.
        """
        svc, _ = service_with_mocks

        # Set up correct schema: research_id → {sids}
        svc._SocketIOService__socket_subscriptions = {
            "research-1": {mock_request.sid, "other-sid"},
            "research-2": {mock_request.sid},
        }

        svc._SocketIOService__handle_disconnect(
            mock_request, "client disconnect"
        )

        subscriptions = svc._SocketIOService__socket_subscriptions
        # sid should be removed from research-1, leaving only other-sid
        assert mock_request.sid not in subscriptions.get("research-1", set())
        assert "other-sid" in subscriptions["research-1"]
        # research-2 had only this sid, so the key should be cleaned up
        assert "research-2" not in subscriptions

    def test_disconnect_handles_no_subscription(
        self, service_with_mocks, mock_request
    ):
        """Test that disconnect handles non-subscribed client."""
        svc, _ = service_with_mocks

        # No subscriptions exist
        svc._SocketIOService__socket_subscriptions = {}

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

    def test_disconnect_leaves_other_clients_intact(
        self, service_with_mocks, mock_request
    ):
        """Test that disconnect only removes the disconnecting client."""
        svc, _ = service_with_mocks

        svc._SocketIOService__socket_subscriptions = {
            "research-1": {mock_request.sid, "sid-A", "sid-B"},
        }

        svc._SocketIOService__handle_disconnect(
            mock_request, "client disconnect"
        )

        subscriptions = svc._SocketIOService__socket_subscriptions
        assert subscriptions["research-1"] == {"sid-A", "sid-B"}

    def test_disconnect_noop_when_sid_not_subscribed(
        self, service_with_mocks, mock_request
    ):
        """Test that disconnect is a no-op when sid has no subscriptions."""
        svc, _ = service_with_mocks

        svc._SocketIOService__socket_subscriptions = {
            "research-1": {"other-sid-1", "other-sid-2"},
        }

        svc._SocketIOService__handle_disconnect(
            mock_request, "client disconnect"
        )

        # Nothing should change
        subscriptions = svc._SocketIOService__socket_subscriptions
        assert subscriptions["research-1"] == {"other-sid-1", "other-sid-2"}

    def test_subscribe_then_disconnect_round_trip(
        self, service_with_mocks, mock_request
    ):
        """Test full subscribe → disconnect cycle uses consistent schema."""
        svc, _ = service_with_mocks

        # Subscribe to two research IDs
        svc._SocketIOService__handle_subscribe(
            {"research_id": "r1"}, mock_request
        )
        svc._SocketIOService__handle_subscribe(
            {"research_id": "r2"}, mock_request
        )

        subscriptions = svc._SocketIOService__socket_subscriptions
        assert mock_request.sid in subscriptions["r1"]
        assert mock_request.sid in subscriptions["r2"]

        # Disconnect should clean up both
        svc._SocketIOService__handle_disconnect(
            mock_request, "client disconnect"
        )

        assert mock_request.sid not in subscriptions.get("r1", set())
        assert mock_request.sid not in subscriptions.get("r2", set())

    def test_unsubscribe_discards_sid(
        self, service_with_mocks, mock_request, sample_research_id
    ):
        """Unsubscribe handler removes the sid from the subscription set."""
        svc, _ = service_with_mocks
        svc._SocketIOService__socket_subscriptions = {
            sample_research_id: {mock_request.sid, "other-sid"},
        }

        svc._SocketIOService__handle_unsubscribe(
            {"research_id": sample_research_id}, mock_request
        )

        subscriptions = svc._SocketIOService__socket_subscriptions
        assert mock_request.sid not in subscriptions[sample_research_id]
        # Other clients are untouched
        assert "other-sid" in subscriptions[sample_research_id]

    def test_unsubscribe_ignores_missing_research_id(
        self, service_with_mocks, mock_request
    ):
        """Unsubscribe with missing research_id is a no-op."""
        svc, _ = service_with_mocks
        svc._SocketIOService__socket_subscriptions = {
            "research-1": {"sid-A"},
        }

        # Should not raise
        svc._SocketIOService__handle_unsubscribe({}, mock_request)
        svc._SocketIOService__handle_unsubscribe(None, mock_request)

        # Existing subscriptions are unchanged
        subscriptions = svc._SocketIOService__socket_subscriptions
        assert subscriptions["research-1"] == {"sid-A"}

    def test_unsubscribe_handles_unknown_research_id(
        self, service_with_mocks, mock_request
    ):
        """Unsubscribe for a research_id with no subscribers is a no-op."""
        svc, _ = service_with_mocks
        svc._SocketIOService__socket_subscriptions = {}

        # Should not raise
        svc._SocketIOService__handle_unsubscribe(
            {"research_id": "never-subscribed"}, mock_request
        )

    def test_unsubscribe_prunes_empty_subscription_set(
        self, service_with_mocks, mock_request, sample_research_id
    ):
        """Removing the last sid for a research_id deletes the dict entry.

        Otherwise __socket_subscriptions accumulates stale keys for every
        research_id seen over a long-running server, even after all clients
        have left.
        """
        svc, _ = service_with_mocks
        svc._SocketIOService__socket_subscriptions = {
            sample_research_id: {mock_request.sid},
        }

        svc._SocketIOService__handle_unsubscribe(
            {"research_id": sample_research_id}, mock_request
        )

        assert (
            sample_research_id not in svc._SocketIOService__socket_subscriptions
        )

    def test_unsubscribe_keeps_set_when_other_clients_remain(
        self, service_with_mocks, mock_request, sample_research_id
    ):
        """Unsubscribe must not delete the set while other sids are present."""
        svc, _ = service_with_mocks
        svc._SocketIOService__socket_subscriptions = {
            sample_research_id: {mock_request.sid, "other-sid"},
        }

        svc._SocketIOService__handle_unsubscribe(
            {"research_id": sample_research_id}, mock_request
        )

        assert svc._SocketIOService__socket_subscriptions[
            sample_research_id
        ] == {"other-sid"}


class TestSocketIOServiceErrorHandling:
    """Tests for error handling methods.

    Note: Singleton reset is handled by the global reset_all_singletons fixture
    in tests/conftest.py.
    """

    @pytest.fixture
    def service(self, mock_flask_app):
        """Create SocketIOService instance."""
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        mock_socketio = MagicMock()

        with patch(
            "local_deep_research.web.services.socket_service.SocketIO",
            return_value=mock_socketio,
        ):
            service = SocketIOService(app=mock_flask_app)
            return service

    def test_socket_error_returns_false(self, service):
        """Test that socket error handler returns False."""
        result = service._SocketIOService__handle_socket_error(
            Exception("Test error")
        )

        assert result is False

    def test_default_error_returns_false(self, service):
        """Test that default error handler returns False."""
        result = service._SocketIOService__handle_default_error(
            Exception("Test error")
        )

        assert result is False

    def test_connect_handler_rejects_unauthenticated(
        self, service, mock_request
    ):
        """Connect handler rejects when no session username."""
        with patch(
            "local_deep_research.web.services.socket_service.session", {}
        ):
            assert (
                service._SocketIOService__handle_connect(mock_request) is False
            )

    def test_connect_handler_rejects_no_db_session_and_no_password(
        self, service, mock_request
    ):
        """Connect handler rejects when user has no active DB session and no stored password."""
        with (
            patch(
                "local_deep_research.web.services.socket_service.session",
                {"username": "alice", "session_id": "sess-1"},
            ),
            patch(
                "local_deep_research.web.services.socket_service.db_manager"
            ) as mock_db,
            patch(
                "local_deep_research.web.services.socket_service.session_password_store"
            ) as mock_store,
        ):
            mock_db.is_user_connected.return_value = False
            mock_store.get_session_password.return_value = None
            assert (
                service._SocketIOService__handle_connect(mock_request) is False
            )
            mock_db.open_user_database.assert_not_called()

    def test_connect_handler_lazy_opens_when_password_available(
        self, service, mock_request
    ):
        """Connect handler lazy-opens the DB when the engine isn't open but a password is stored."""
        with (
            patch(
                "local_deep_research.web.services.socket_service.session",
                {"username": "alice", "session_id": "sess-1"},
            ),
            patch(
                "local_deep_research.web.services.socket_service.db_manager"
            ) as mock_db,
            patch(
                "local_deep_research.web.services.socket_service.session_password_store"
            ) as mock_store,
            patch("local_deep_research.web.services.socket_service.join_room"),
        ):
            mock_db.is_user_connected.return_value = False
            mock_store.get_session_password.return_value = "pw"
            assert (
                service._SocketIOService__handle_connect(mock_request) is True
            )
            mock_db.open_user_database.assert_called_once_with("alice", "pw")

    def test_connect_handler_accepts_authenticated(self, service, mock_request):
        """Connect handler accepts authenticated users with active DB session."""
        with (
            patch(
                "local_deep_research.web.services.socket_service.session",
                {"username": "alice"},
            ),
            patch(
                "local_deep_research.web.services.socket_service.db_manager"
            ) as mock_db,
            patch(
                "local_deep_research.web.services.socket_service.logger"
            ) as mock_logger,
            patch("local_deep_research.web.services.socket_service.join_room"),
        ):
            mock_db.is_user_connected.return_value = True
            assert (
                service._SocketIOService__handle_connect(mock_request) is True
            )
            mock_logger.info.assert_called()
            call_args = mock_logger.info.call_args[0][0]
            assert mock_request.sid in call_args


class TestSocketIOServiceRun:
    """Tests for run method.

    Note: Singleton reset is handled by the global reset_all_singletons fixture
    in tests/conftest.py.
    """

    @pytest.fixture
    def service(self, mock_flask_app):
        """Create SocketIOService instance."""
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        mock_socketio = MagicMock()

        with patch(
            "local_deep_research.web.services.socket_service.SocketIO",
            return_value=mock_socketio,
        ):
            service = SocketIOService(app=mock_flask_app)
            service._SocketIOService__socketio = mock_socketio
            return service, mock_socketio, mock_flask_app

    def test_run_calls_socketio_run(self, service):
        """Test that run method calls socketio.run."""
        svc, mock_socketio, mock_app = service

        svc.run("0.0.0.0", 5000)

        mock_socketio.run.assert_called_once()

    def test_run_passes_correct_params(self, service):
        """Test that run passes correct parameters."""
        svc, mock_socketio, mock_app = service

        svc.run("localhost", 8080, debug=True)

        mock_socketio.run.assert_called_once_with(
            mock_app,
            debug=True,
            host="localhost",
            port=8080,
            allow_unsafe_werkzeug=True,
            use_reloader=False,
        )

    def test_run_defaults_debug_to_false(self, service):
        """Test that debug defaults to False."""
        svc, mock_socketio, mock_app = service

        svc.run("0.0.0.0", 5000)

        call_kwargs = mock_socketio.run.call_args[1]
        assert call_kwargs["debug"] is False


class TestSocketIOServiceLogging:
    """Tests for logging behavior.

    Note: Singleton reset is handled by the global reset_all_singletons fixture
    in tests/conftest.py.
    """

    @pytest.fixture
    def service(self, mock_flask_app):
        """Create SocketIOService instance."""
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        mock_socketio = MagicMock()

        with patch(
            "local_deep_research.web.services.socket_service.SocketIO",
            return_value=mock_socketio,
        ):
            service = SocketIOService(app=mock_flask_app)
            return service

    def test_logging_enabled_by_default(self, service):
        """Test that logging is enabled by default."""
        assert service._SocketIOService__logging_enabled is True

    def test_log_info_logs_when_enabled(self, service):
        """Test that __log_info logs when enabled."""
        with patch(
            "local_deep_research.web.services.socket_service.logger"
        ) as mock_logger:
            service._SocketIOService__log_info("Test message")

            mock_logger.info.assert_called_once_with("Test message")

    def test_log_info_skips_when_disabled(self, service):
        """Test that __log_info skips when disabled."""
        service._SocketIOService__logging_enabled = False

        with patch(
            "local_deep_research.web.services.socket_service.logger"
        ) as mock_logger:
            service._SocketIOService__log_info("Test message")

            mock_logger.info.assert_not_called()

    def test_log_error_logs_when_enabled(self, service):
        """Test that __log_error logs when enabled."""
        with patch(
            "local_deep_research.web.services.socket_service.logger"
        ) as mock_logger:
            service._SocketIOService__log_error("Error message")

            mock_logger.error.assert_called_once_with("Error message")

    def test_log_error_skips_when_disabled(self, service):
        """Test that __log_error skips when disabled."""
        service._SocketIOService__logging_enabled = False

        with patch(
            "local_deep_research.web.services.socket_service.logger"
        ) as mock_logger:
            service._SocketIOService__log_error("Error message")

            mock_logger.error.assert_not_called()

    def test_log_exception_logs_when_enabled(self, service):
        """Test that __log_exception logs when enabled."""
        with patch(
            "local_deep_research.web.services.socket_service.logger"
        ) as mock_logger:
            service._SocketIOService__log_exception("Exception message")

            mock_logger.exception.assert_called_once_with("Exception message")

    def test_log_exception_skips_when_disabled(self, service):
        """Test that __log_exception skips when disabled."""
        service._SocketIOService__logging_enabled = False

        with patch(
            "local_deep_research.web.services.socket_service.logger"
        ) as mock_logger:
            service._SocketIOService__log_exception("Exception message")

            mock_logger.exception.assert_not_called()
