"""
Tests for engine.display.backends.websocket module.
"""

from unittest.mock import MagicMock, patch

import pytest

from engine.display.backends.websocket import WebSocketDisplay


class TestWebSocketDisplayImport:
    """Test that websocket module can be imported."""

    def test_import_does_not_error(self):
        """Module imports without error."""
        from engine.display import backends

        assert backends is not None


class TestWebSocketDisplayInit:
    """Tests for WebSocketDisplay initialization."""

    def test_default_init(self):
        """Default initialization sets correct defaults."""
        with patch("engine.display.backends.websocket.websockets", None):
            display = WebSocketDisplay()
            assert display.host == "0.0.0.0"
            assert display.port == 8765
            assert display.http_port == 8766
            assert display.width == 80
            assert display.height == 24

    def test_custom_init(self):
        """Custom initialization uses provided values."""
        with patch("engine.display.backends.websocket.websockets", None):
            display = WebSocketDisplay(host="localhost", port=9000, http_port=9001)
            assert display.host == "localhost"
            assert display.port == 9000
            assert display.http_port == 9001

    def test_is_available_when_websockets_present(self):
        """is_available returns True when websockets is available."""
        pytest.importorskip("websockets")
        display = WebSocketDisplay()
        assert display.is_available() is True

    @pytest.mark.skipif(
        pytest.importorskip("websockets") is not None, reason="websockets is available"
    )
    def test_is_available_when_websockets_missing(self):
        """is_available returns False when websockets is not available."""
        display = WebSocketDisplay()
        assert display.is_available() is False


class TestWebSocketDisplayProtocol:
    """Test that WebSocketDisplay satisfies Display protocol."""

    def test_websocket_display_is_display(self):
        """WebSocketDisplay satisfies Display protocol."""
        with patch("engine.display.backends.websocket.websockets", MagicMock()):
            display = WebSocketDisplay()
            assert hasattr(display, "init")
            assert hasattr(display, "show")
            assert hasattr(display, "clear")
            assert hasattr(display, "cleanup")


class TestWebSocketDisplayMethods:
    """Tests for WebSocketDisplay methods."""

    def test_init_stores_dimensions(self):
        """init stores terminal dimensions."""
        with patch("engine.display.backends.websocket.websockets", MagicMock()):
            display = WebSocketDisplay()
            display.init(100, 40)
            assert display.width == 100
            assert display.height == 40

    @pytest.mark.skip(reason="port binding conflict in CI environment")
    def test_client_count_initially_zero(self):
        """client_count returns 0 when no clients connected."""
        with patch("engine.display.backends.websocket.websockets", MagicMock()):
            display = WebSocketDisplay()
            assert display.client_count() == 0

    def test_get_ws_port(self):
        """get_ws_port returns configured port."""
        with patch("engine.display.backends.websocket.websockets", MagicMock()):
            display = WebSocketDisplay(port=9000)
            assert display.get_ws_port() == 9000

    def test_get_http_port(self):
        """get_http_port returns configured port."""
        with patch("engine.display.backends.websocket.websockets", MagicMock()):
            display = WebSocketDisplay(http_port=9001)
            assert display.get_http_port() == 9001

    def test_frame_delay_defaults_to_zero(self):
        """get_frame_delay returns 0 by default."""
        with patch("engine.display.backends.websocket.websockets", MagicMock()):
            display = WebSocketDisplay()
            assert display.get_frame_delay() == 0.0

    def test_set_frame_delay(self):
        """set_frame_delay stores the value."""
        with patch("engine.display.backends.websocket.websockets", MagicMock()):
            display = WebSocketDisplay()
            display.set_frame_delay(0.05)
            assert display.get_frame_delay() == 0.05


class TestWebSocketDisplayCallbacks:
    """Tests for WebSocketDisplay callback methods."""

    def test_set_client_connected_callback(self):
        """set_client_connected_callback stores callback."""
        with patch("engine.display.backends.websocket.websockets", MagicMock()):
            display = WebSocketDisplay()
            callback = MagicMock()
            display.set_client_connected_callback(callback)
            assert display._client_connected_callback is callback

    def test_set_client_disconnected_callback(self):
        """set_client_disconnected_callback stores callback."""
        with patch("engine.display.backends.websocket.websockets", MagicMock()):
            display = WebSocketDisplay()
            callback = MagicMock()
            display.set_client_disconnected_callback(callback)
            assert display._client_disconnected_callback is callback


class TestWebSocketDisplayUnavailable:
    """Tests when WebSocket support is unavailable."""

    @pytest.mark.skipif(
        pytest.importorskip("websockets") is not None, reason="websockets is available"
    )
    def test_start_server_noop_when_unavailable(self):
        """start_server does nothing when websockets unavailable."""
        display = WebSocketDisplay()
        display.start_server()
        assert display._server_thread is None

    @pytest.mark.skipif(
        pytest.importorskip("websockets") is not None, reason="websockets is available"
    )
    def test_start_http_server_noop_when_unavailable(self):
        """start_http_server does nothing when websockets unavailable."""
        display = WebSocketDisplay()
        display.start_http_server()
        assert display._http_thread is None

    @pytest.mark.skipif(
        pytest.importorskip("websockets") is not None, reason="websockets is available"
    )
    def test_show_noops_when_unavailable(self):
        """show does nothing when websockets unavailable."""
        display = WebSocketDisplay()
        display.show(["line1", "line2"])


class TestWebSocketUIPanelIntegration:
    """Tests for WebSocket-UIPanel integration for remote control."""

    def test_set_controller_stores_controller(self):
        """set_controller stores the controller reference."""
        with patch("engine.display.backends.websocket.websockets", MagicMock()):
            display = WebSocketDisplay()
            mock_controller = MagicMock()
            display.set_controller(mock_controller)
            assert display._controller is mock_controller

    def test_set_command_callback_stores_callback(self):
        """set_command_callback stores the callback."""
        with patch("engine.display.backends.websocket.websockets", MagicMock()):
            display = WebSocketDisplay()
            callback = MagicMock()
            display.set_command_callback(callback)
            assert display._command_callback is callback

    def test_get_state_snapshot_returns_none_without_controller(self):
        """_get_state_snapshot returns None when no controller is set."""
        with patch("engine.display.backends.websocket.websockets", MagicMock()):
            display = WebSocketDisplay()
            assert display._get_state_snapshot() is None

    def test_get_state_snapshot_returns_controller_state(self):
        """_get_state_snapshot returns state from controller."""
        with patch("engine.display.backends.websocket.websockets", MagicMock()):
            display = WebSocketDisplay()

            # Create mock controller with expected attributes
            mock_controller = MagicMock()
            mock_controller.stages = {
                "test_stage": MagicMock(
                    enabled=True, params={"intensity": 0.5}, selected=False
                )
            }
            mock_controller._current_preset = "demo"
            mock_controller._presets = ["demo", "test"]
            mock_controller.selected_stage = "test_stage"

            display.set_controller(mock_controller)
            state = display._get_state_snapshot()

            assert state is not None
            assert "stages" in state
            assert "test_stage" in state["stages"]
            assert state["stages"]["test_stage"]["enabled"] is True
            assert state["stages"]["test_stage"]["params"] == {"intensity": 0.5}
            assert state["preset"] == "demo"
            assert state["presets"] == ["demo", "test"]
            assert state["selected_stage"] == "test_stage"

    def test_get_state_snapshot_handles_missing_attributes(self):
        """_get_state_snapshot handles controller without all attributes."""
        with patch("engine.display.backends.websocket.websockets", MagicMock()):
            display = WebSocketDisplay()

            # Create mock controller without stages attribute using spec
            # This prevents MagicMock from auto-creating the attribute
            mock_controller = MagicMock(spec=[])  # Empty spec means no attributes

            display.set_controller(mock_controller)
            state = display._get_state_snapshot()

            assert state == {}

    def test_broadcast_state_sends_to_clients(self):
        """broadcast_state sends state update to all connected clients."""
        with patch("engine.display.backends.websocket.websockets", MagicMock()):
            display = WebSocketDisplay()

            # Mock client with send method
            mock_client = MagicMock()
            mock_client.send = MagicMock()
            display._clients.add(mock_client)

            test_state = {"test": "state"}
            display.broadcast_state(test_state)

            # Verify send was called with JSON containing state
            mock_client.send.assert_called_once()
            call_args = mock_client.send.call_args[0][0]
            assert '"type": "state"' in call_args
            assert '"test"' in call_args

    def test_broadcast_state_noop_when_no_clients(self):
        """broadcast_state does nothing when no clients connected."""
        with patch("engine.display.backends.websocket.websockets", MagicMock()):
            display = WebSocketDisplay()
            display._clients.clear()

            # Should not raise error
            display.broadcast_state({"test": "state"})


class TestWebSocketHTTPServerPath:
    """Tests for WebSocket HTTP server client directory path calculation."""

    def test_client_dir_path_calculation(self):
        """Client directory path is correctly calculated from websocket.py location."""
        import os

        # Use the actual websocket.py file location, not the test file
        websocket_module = __import__(
            "engine.display.backends.websocket", fromlist=["WebSocketDisplay"]
        )
        websocket_file = websocket_module.__file__
        parts = websocket_file.split(os.sep)

        if "engine" in parts:
            engine_idx = parts.index("engine")
            project_root = os.sep.join(parts[:engine_idx])
            client_dir = os.path.join(project_root, "client")
        else:
            # Fallback calculation (shouldn't happen in normal test runs)
            client_dir = os.path.join(
                os.path.dirname(
                    os.path.dirname(os.path.dirname(os.path.dirname(websocket_file)))
                ),
                "client",
            )

        # Verify the client directory exists and contains expected files
        assert os.path.exists(client_dir), f"Client directory not found: {client_dir}"
        assert "index.html" in os.listdir(client_dir), (
            "index.html not found in client directory"
        )
        assert "editor.html" in os.listdir(client_dir), (
            "editor.html not found in client directory"
        )

        # Verify the path is correct (should be .../Mainline/client)
        assert client_dir.endswith("client"), (
            f"Client dir should end with 'client': {client_dir}"
        )
        assert "Mainline" in client_dir, (
            f"Client dir should contain 'Mainline': {client_dir}"
        )

    def test_http_server_directory_serves_client_files(self):
        """HTTP server directory correctly serves client files."""
        import os

        # Use the actual websocket.py file location, not the test file
        websocket_module = __import__(
            "engine.display.backends.websocket", fromlist=["WebSocketDisplay"]
        )
        websocket_file = websocket_module.__file__
        parts = websocket_file.split(os.sep)

        if "engine" in parts:
            engine_idx = parts.index("engine")
            project_root = os.sep.join(parts[:engine_idx])
            client_dir = os.path.join(project_root, "client")
        else:
            client_dir = os.path.join(
                os.path.dirname(
                    os.path.dirname(os.path.dirname(os.path.dirname(websocket_file)))
                ),
                "client",
            )

        # Verify the handler would be able to serve files from this directory
        # We can't actually instantiate the handler without a valid request,
        # but we can verify the directory is accessible
        assert os.access(client_dir, os.R_OK), (
            f"Client directory not readable: {client_dir}"
        )

        # Verify key files exist
        index_path = os.path.join(client_dir, "index.html")
        editor_path = os.path.join(client_dir, "editor.html")

        assert os.path.exists(index_path), f"index.html not found at: {index_path}"
        assert os.path.exists(editor_path), f"editor.html not found at: {editor_path}"

        # Verify files are readable
        assert os.access(index_path, os.R_OK), "index.html not readable"
        assert os.access(editor_path, os.R_OK), "editor.html not readable"

    def test_old_buggy_path_does_not_find_client_directory(self):
        """The old buggy path (3 dirname calls) should NOT find the client directory.

        This test verifies that the old buggy behavior would have failed.
        The old code used:
            client_dir = os.path.join(
                os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "client"
            )

        This would resolve to: .../engine/client (which doesn't exist)
        Instead of: .../Mainline/client (which does exist)
        """
        import os

        # Use the actual websocket.py file location
        websocket_module = __import__(
            "engine.display.backends.websocket", fromlist=["WebSocketDisplay"]
        )
        websocket_file = websocket_module.__file__

        # OLD BUGGY CODE: 3 dirname calls
        old_buggy_client_dir = os.path.join(
            os.path.dirname(os.path.dirname(os.path.dirname(websocket_file))), "client"
        )

        # This path should NOT exist (it's the buggy path)
        assert not os.path.exists(old_buggy_client_dir), (
            f"Old buggy path should not exist: {old_buggy_client_dir}\n"
            f"If this assertion fails, the bug may have been fixed elsewhere or "
            f"the test needs updating."
        )

        # The buggy path should be .../engine/client, not .../Mainline/client
        assert old_buggy_client_dir.endswith("engine/client"), (
            f"Old buggy path should end with 'engine/client': {old_buggy_client_dir}"
        )

        # Verify that going up one more level (4 dirname calls) finds the correct path
        correct_client_dir = os.path.join(
            os.path.dirname(
                os.path.dirname(os.path.dirname(os.path.dirname(websocket_file)))
            ),
            "client",
        )
        assert os.path.exists(correct_client_dir), (
            f"Correct path should exist: {correct_client_dir}"
        )
        assert "index.html" in os.listdir(correct_client_dir), (
            f"index.html should exist in correct path: {correct_client_dir}"
        )
