"""
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="*",
                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")


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."""
        svc, mock_socketio = service

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

        # Should emit with formatted event name
        mock_socketio.emit.assert_any_call(
            f"research_update_{sample_research_id}", {"data": "test"}
        )

    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."""
        svc, mock_socketio = service
        mock_socketio.emit.side_effect = Exception("Failed")

        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):
        """Test handling when no subscribers exist."""
        svc, mock_socketio = service

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

        assert result is True
        # Should still emit to general channel
        mock_socketio.emit.assert_called_once()

    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())


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_logs_sid(self, service, mock_request):
        """Test that connect handler logs client SID."""
        with patch(
            "local_deep_research.web.services.socket_service.logger"
        ) as mock_logger:
            service._SocketIOService__handle_connect(mock_request)

            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()
