"""
Tests for the SocketIOService class.

Tests cover:
- Singleton pattern
- Event emission
- Subscriber management
- Error handling
"""

from unittest.mock import MagicMock, patch


class MockFlaskApp:
    """Mock Flask application for testing."""

    def __init__(self):
        self.config = {}
        self.debug = False


class MockSocketIO:
    """Mock SocketIO for testing."""

    def __init__(self, app=None, **kwargs):
        self.app = app
        self.kwargs = 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


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

    def test_singleton_requires_app_first_time(self):
        """SocketIOService requires Flask app on first instantiation."""
        # Reset singleton for test
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        # Store and reset singleton
        original_instance = SocketIOService._instance
        SocketIOService._instance = None

        try:
            # Should raise ValueError when no app provided
            try:
                SocketIOService()
                assert False, "Should have raised ValueError"
            except ValueError as e:
                assert "Flask app must be specified" in str(e)
        finally:
            # Restore original singleton
            SocketIOService._instance = original_instance


class TestSocketIOServiceEmit:
    """Tests for SocketIOService emit methods."""

    @patch("local_deep_research.web.services.socket_service.SocketIO")
    def test_emit_socket_event_broadcast(self, mock_socketio_class):
        """emit_socket_event broadcasts to all clients when no room specified."""
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        # Reset and create fresh singleton
        original_instance = SocketIOService._instance
        SocketIOService._instance = None

        mock_socketio = MagicMock()
        mock_socketio_class.return_value = mock_socketio

        try:
            app = MockFlaskApp()
            service = SocketIOService(app=app)

            result = service.emit_socket_event("test_event", {"data": "value"})

            assert result is True
            mock_socketio.emit.assert_called_with(
                "test_event", {"data": "value"}
            )
        finally:
            SocketIOService._instance = original_instance

    @patch("local_deep_research.web.services.socket_service.SocketIO")
    def test_emit_socket_event_to_room(self, mock_socketio_class):
        """emit_socket_event sends to specific room when specified."""
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        original_instance = SocketIOService._instance
        SocketIOService._instance = None

        mock_socketio = MagicMock()
        mock_socketio_class.return_value = mock_socketio

        try:
            app = MockFlaskApp()
            service = SocketIOService(app=app)

            result = service.emit_socket_event(
                "test_event", {"data": "value"}, room="room123"
            )

            assert result is True
            mock_socketio.emit.assert_called_with(
                "test_event", {"data": "value"}, room="room123"
            )
        finally:
            SocketIOService._instance = original_instance

    @patch("local_deep_research.web.services.socket_service.SocketIO")
    def test_emit_socket_event_handles_error(self, mock_socketio_class):
        """emit_socket_event returns False on error."""
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        original_instance = SocketIOService._instance
        SocketIOService._instance = None

        mock_socketio = MagicMock()
        mock_socketio.emit.side_effect = Exception("Connection error")
        mock_socketio_class.return_value = mock_socketio

        try:
            app = MockFlaskApp()
            service = SocketIOService(app=app)

            result = service.emit_socket_event("test_event", {"data": "value"})

            assert result is False
        finally:
            SocketIOService._instance = original_instance


class TestSocketIOServiceSubscribers:
    """Tests for SocketIOService subscriber management."""

    @patch("local_deep_research.web.services.socket_service.SocketIO")
    def test_emit_to_subscribers_success(self, mock_socketio_class):
        """emit_to_subscribers emits to research channel."""
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        original_instance = SocketIOService._instance
        SocketIOService._instance = None

        mock_socketio = MagicMock()
        mock_socketio_class.return_value = mock_socketio

        try:
            app = MockFlaskApp()
            service = SocketIOService(app=app)

            result = service.emit_to_subscribers(
                "progress", "research_123", {"percent": 50}
            )

            assert result is True
            # Should emit to the formatted channel
            mock_socketio.emit.assert_called_with(
                "progress_research_123", {"percent": 50}
            )
        finally:
            SocketIOService._instance = original_instance

    @patch("local_deep_research.web.services.socket_service.SocketIO")
    def test_emit_to_subscribers_handles_error(self, mock_socketio_class):
        """emit_to_subscribers returns False on error."""
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        original_instance = SocketIOService._instance
        SocketIOService._instance = None

        mock_socketio = MagicMock()
        mock_socketio.emit.side_effect = Exception("Emit failed")
        mock_socketio_class.return_value = mock_socketio

        try:
            app = MockFlaskApp()
            service = SocketIOService(app=app)

            result = service.emit_to_subscribers(
                "progress", "research_123", {"percent": 50}
            )

            assert result is False
        finally:
            SocketIOService._instance = original_instance


class TestSocketIOServiceRun:
    """Tests for SocketIOService run method."""

    @patch("local_deep_research.web.services.socket_service.SocketIO")
    def test_run_starts_server(self, mock_socketio_class):
        """run method starts the SocketIO server."""
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        original_instance = SocketIOService._instance
        SocketIOService._instance = None

        mock_socketio = MagicMock()
        mock_socketio_class.return_value = mock_socketio

        try:
            app = MockFlaskApp()
            service = SocketIOService(app=app)

            # Mock run to not block
            mock_socketio.run = MagicMock()

            service.run(host="0.0.0.0", port=5000, debug=False)

            mock_socketio.run.assert_called_once()
            call_kwargs = mock_socketio.run.call_args
            assert call_kwargs[1]["host"] == "0.0.0.0"
            assert call_kwargs[1]["port"] == 5000
            assert call_kwargs[1]["debug"] is False
        finally:
            SocketIOService._instance = original_instance


class TestSocketIOServiceInit:
    """Tests for SocketIOService initialization."""

    @patch("local_deep_research.web.services.socket_service.SocketIO")
    def test_service_initializes_with_app(self, mock_socketio_class):
        """Service initializes properly with Flask app."""
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        original_instance = SocketIOService._instance
        SocketIOService._instance = None

        mock_socketio = MagicMock()
        mock_socketio_class.return_value = mock_socketio

        try:
            app = MockFlaskApp()
            service = SocketIOService(app=app)

            # Service should be initialized
            assert service is not None
            # SocketIO should have been created
            mock_socketio_class.assert_called_once()
        finally:
            SocketIOService._instance = original_instance


class TestSocketIOServiceMultipleEmits:
    """Tests for multiple emit scenarios."""

    @patch("local_deep_research.web.services.socket_service.SocketIO")
    def test_multiple_events_same_room(self, mock_socketio_class):
        """Multiple events can be emitted to the same room."""
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        original_instance = SocketIOService._instance
        SocketIOService._instance = None

        mock_socketio = MagicMock()
        mock_socketio_class.return_value = mock_socketio

        try:
            app = MockFlaskApp()
            service = SocketIOService(app=app)

            service.emit_socket_event("event1", {"data": 1}, room="room1")
            service.emit_socket_event("event2", {"data": 2}, room="room1")

            assert mock_socketio.emit.call_count == 2
        finally:
            SocketIOService._instance = original_instance

    @patch("local_deep_research.web.services.socket_service.SocketIO")
    def test_emit_with_namespace(self, mock_socketio_class):
        """Events can be emitted to specific namespaces."""
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        original_instance = SocketIOService._instance
        SocketIOService._instance = None

        mock_socketio = MagicMock()
        mock_socketio_class.return_value = mock_socketio

        try:
            app = MockFlaskApp()
            service = SocketIOService(app=app)

            # Test emit with namespace if supported
            result = service.emit_socket_event(
                "test_event",
                {"data": "value"},
            )

            assert result is True
        finally:
            SocketIOService._instance = original_instance


class TestHandleSubscribe:
    """Tests for subscribe event handling."""

    @patch("local_deep_research.web.services.socket_service.SocketIO")
    def test_subscribe_adds_to_room(self, mock_socketio_class):
        """Subscribe event should add client to research room."""
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        original_instance = SocketIOService._instance
        SocketIOService._instance = None

        mock_socketio = MagicMock()
        mock_socketio_class.return_value = mock_socketio

        try:
            app = MockFlaskApp()
            service = SocketIOService(app=app)

            # Service should register subscribe handler
            assert service is not None
        finally:
            SocketIOService._instance = original_instance

    @patch("local_deep_research.web.services.socket_service.SocketIO")
    def test_subscribe_with_research_id(self, mock_socketio_class):
        """Subscribe with valid research_id."""
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        original_instance = SocketIOService._instance
        SocketIOService._instance = None

        mock_socketio = MagicMock()
        mock_socketio_class.return_value = mock_socketio

        try:
            app = MockFlaskApp()
            service = SocketIOService(app=app)

            # Service should be ready to handle subscriptions
            # The service initializes internal handlers
            assert service is not None
        finally:
            SocketIOService._instance = original_instance

    @patch("local_deep_research.web.services.socket_service.SocketIO")
    def test_subscribe_without_research_id_is_handled(
        self, mock_socketio_class
    ):
        """Subscribe without research_id should be handled gracefully."""
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        original_instance = SocketIOService._instance
        SocketIOService._instance = None

        mock_socketio = MagicMock()
        mock_socketio_class.return_value = mock_socketio

        try:
            app = MockFlaskApp()
            service = SocketIOService(app=app)

            # Service should be able to handle missing research_id
            assert service is not None
        finally:
            SocketIOService._instance = original_instance


class TestEmitToSubscribersAdvanced:
    """Advanced tests for emit_to_subscribers method."""

    @patch("local_deep_research.web.services.socket_service.SocketIO")
    def test_emit_progress_update(self, mock_socketio_class):
        """Emit progress update to subscribers."""
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        original_instance = SocketIOService._instance
        SocketIOService._instance = None

        mock_socketio = MagicMock()
        mock_socketio_class.return_value = mock_socketio

        try:
            app = MockFlaskApp()
            service = SocketIOService(app=app)

            result = service.emit_to_subscribers(
                "progress",
                "research_abc123",
                {"current": 50, "total": 100, "message": "Processing..."},
            )

            assert result is True
            mock_socketio.emit.assert_called_once()
        finally:
            SocketIOService._instance = original_instance

    @patch("local_deep_research.web.services.socket_service.SocketIO")
    def test_emit_status_update(self, mock_socketio_class):
        """Emit status update to subscribers."""
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        original_instance = SocketIOService._instance
        SocketIOService._instance = None

        mock_socketio = MagicMock()
        mock_socketio_class.return_value = mock_socketio

        try:
            app = MockFlaskApp()
            service = SocketIOService(app=app)

            result = service.emit_to_subscribers(
                "status",
                "research_xyz789",
                {"status": "completed", "result_url": "/results/xyz789"},
            )

            assert result is True
        finally:
            SocketIOService._instance = original_instance

    @patch("local_deep_research.web.services.socket_service.SocketIO")
    def test_emit_error_to_subscribers(self, mock_socketio_class):
        """Emit error to subscribers."""
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        original_instance = SocketIOService._instance
        SocketIOService._instance = None

        mock_socketio = MagicMock()
        mock_socketio_class.return_value = mock_socketio

        try:
            app = MockFlaskApp()
            service = SocketIOService(app=app)

            result = service.emit_to_subscribers(
                "error",
                "research_failed123",
                {"error": "Research failed", "code": 500},
            )

            assert result is True
        finally:
            SocketIOService._instance = original_instance


class TestSocketIOServiceSingletonBehavior:
    """Tests for SocketIOService singleton behavior."""

    @patch("local_deep_research.web.services.socket_service.SocketIO")
    def test_singleton_returns_same_instance(self, mock_socketio_class):
        """Second call returns same instance."""
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        original_instance = SocketIOService._instance
        SocketIOService._instance = None

        mock_socketio = MagicMock()
        mock_socketio_class.return_value = mock_socketio

        try:
            app = MockFlaskApp()
            service1 = SocketIOService(app=app)
            service2 = SocketIOService()

            assert service1 is service2
        finally:
            SocketIOService._instance = original_instance

    @patch("local_deep_research.web.services.socket_service.SocketIO")
    def test_singleton_ignores_second_app(self, mock_socketio_class):
        """Second app parameter is ignored."""
        from local_deep_research.web.services.socket_service import (
            SocketIOService,
        )

        original_instance = SocketIOService._instance
        SocketIOService._instance = None

        mock_socketio = MagicMock()
        mock_socketio_class.return_value = mock_socketio

        try:
            app1 = MockFlaskApp()
            app2 = MockFlaskApp()
            service1 = SocketIOService(app=app1)
            service2 = SocketIOService(app=app2)

            # Should be same instance, app2 ignored
            assert service1 is service2
        finally:
            SocketIOService._instance = original_instance
