"""Unit tests for BLE GATT Server.

Tests the GATT server implementation to ensure:
- Proper delegation to service layer
- Correct data format conversion (BLE ↔ Service)
- Error handling and response formatting
- Notification sending
- ZERO business logic in GATT handlers (all in services!)
"""
import pytest
import json
from unittest.mock import Mock, AsyncMock, patch

from app.backend.ble.gatt_server import DangerousPiGATTServer
from app.backend.ble.characteristics import PM3CharacteristicUUIDs, WiFiCharacteristicUUIDs
from app.backend.services.pm3_service import PM3ServiceResult, PM3ServiceError


class TestGATTServerInitialization:
    """Test GATT server initialization."""

    def test_initialization(self):
        """Test GATT server initializes with all characteristics."""
        # Act
        server = DangerousPiGATTServer()

        # Assert
        assert server.is_running is False
        characteristics = server.get_all_characteristics()
        assert len(characteristics) > 0

        # Verify PM3 characteristics registered
        assert PM3CharacteristicUUIDs.COMMAND_WRITE in characteristics
        assert PM3CharacteristicUUIDs.STATUS in characteristics

    @pytest.mark.asyncio
    async def test_start_stop(self):
        """Test GATT server start and stop."""
        # Arrange
        server = DangerousPiGATTServer()

        # Act
        await server.start()
        assert server.is_running is True

        await server.stop()
        assert server.is_running is False


class TestPM3GATTCharacteristics:
    """Test PM3 GATT characteristic handlers.

    These tests verify that handlers are THIN ADAPTERS that delegate
    to PM3Service with zero business logic.
    """

    @pytest.mark.asyncio
    async def test_command_write_success(self):
        """Test PM3 command execution via BLE delegates to PM3Service."""
        # Arrange
        server = DangerousPiGATTServer()

        # Mock PM3Service response
        mock_service_result = PM3ServiceResult(
            success=True,
            data={
                "output": "Proxmark3 RFID instrument",
                "command": "hw version",
                "session_id": "test-session"
            }
        )

        # Mock notification callback
        notification_called = False
        notification_value = None

        async def mock_notify(value):
            nonlocal notification_called, notification_value
            notification_called = True
            notification_value = value

        server.register_notification_callback(
            PM3CharacteristicUUIDs.COMMAND_RESULT,
            mock_notify
        )

        # Prepare BLE command
        command_data = {"command": "hw version", "session_id": "test-session"}
        command_bytes = json.dumps(command_data).encode('utf-8')

        # Act - patch container and call handler
        with patch('app.backend.ble.gatt_server.container') as mock_container:
            mock_container.pm3_service.execute_command = AsyncMock(
                return_value=mock_service_result
            )

            result = await server._handle_pm3_command_write(command_bytes)

            # Verify service was called with correct params
            mock_container.pm3_service.execute_command.assert_called_once_with(
                command="hw version",
                session_id="test-session"
            )

        # Assert
        assert result["success"] is True
        assert "Proxmark3" in result["output"]
        # Notification should have been sent
        assert notification_called is True

    @pytest.mark.asyncio
    async def test_command_write_session_locked(self):
        """Test PM3 command with session locked error from service."""
        # Arrange
        server = DangerousPiGATTServer()

        mock_service_result = PM3ServiceResult(
            success=False,
            error=PM3ServiceError(
                code="session_locked",
                message="Another session is active"
            )
        )

        command_data = {"command": "hw version", "session_id": "test-session"}
        command_bytes = json.dumps(command_data).encode('utf-8')

        # Act
        with patch('app.backend.ble.gatt_server.container') as mock_container:
            mock_container.pm3_service.execute_command = AsyncMock(
                return_value=mock_service_result
            )

            result = await server._handle_pm3_command_write(command_bytes)

        # Assert
        assert result["success"] is False
        assert result["error"]["code"] == "session_locked"
        assert "Another session is active" in result["error"]["message"]

    @pytest.mark.asyncio
    async def test_status_read_delegates_to_service(self):
        """Test PM3 status read delegates to PM3Service."""
        # Arrange
        server = DangerousPiGATTServer()

        mock_service_result = PM3ServiceResult(
            success=True,
            data={
                "connected": True,
                "device_path": "/dev/ttyACM0",
                "version": "Proxmark3 v4.0",
                "session_active": False
            }
        )

        # Act
        with patch('app.backend.ble.gatt_server.container') as mock_container:
            mock_container.pm3_service.get_status = AsyncMock(
                return_value=mock_service_result
            )

            result_bytes = await server._handle_pm3_status_read()
            result = json.loads(result_bytes.decode('utf-8'))

        # Assert
        assert result["success"] is True
        assert result["connected"] is True
        assert result["device_path"] == "/dev/ttyACM0"
        # Verify service was called
        mock_container.pm3_service.get_status.assert_called_once()

    @pytest.mark.asyncio
    async def test_session_create_delegates_to_service(self):
        """Test session creation delegates to PM3Service."""
        # Arrange
        server = DangerousPiGATTServer()

        mock_service_result = PM3ServiceResult(
            success=True,
            data={"session_id": "new-session-123"}
        )

        request_data = {"force_takeover": False}
        request_bytes = json.dumps(request_data).encode('utf-8')

        # Act
        with patch('app.backend.ble.gatt_server.container') as mock_container:
            mock_container.pm3_service.create_session = Mock(
                return_value=mock_service_result
            )

            result = await server._handle_session_create_write(request_bytes)

        # Assert
        assert result["success"] is True
        assert result["session_id"] == "new-session-123"
        mock_container.pm3_service.create_session.assert_called_once_with(
            force_takeover=False
        )


class TestWiFiGATTCharacteristics:
    """Test WiFi GATT characteristic handlers."""

    @pytest.mark.asyncio
    async def test_wifi_status_read_delegates_to_service(self):
        """Test WiFi status read delegates to WiFiService."""
        # Arrange
        server = DangerousPiGATTServer()

        from app.backend.services.pm3_service import PM3ServiceResult

        mock_service_result = PM3ServiceResult(
            success=True,
            data={
                "mode": "ap",
                "interfaces": [],
                "client": None,
                "access_point": {"ssid": "dangerous-pi", "ip": "10.3.141.1"},
                "supports_dual": False
            }
        )

        # Act
        with patch('app.backend.ble.gatt_server.container') as mock_container:
            mock_container.wifi_service.get_status = AsyncMock(
                return_value=mock_service_result
            )

            result_bytes = await server._handle_wifi_status_read()
            result = json.loads(result_bytes.decode('utf-8'))

        # Assert
        assert result["mode"] == "ap"
        assert result["access_point"]["ssid"] == "dangerous-pi"
        mock_container.wifi_service.get_status.assert_called_once()

    @pytest.mark.asyncio
    async def test_wifi_connect_delegates_to_service(self):
        """Test WiFi connect delegates to WiFiService."""
        # Arrange
        server = DangerousPiGATTServer()

        mock_service_result = PM3ServiceResult(
            success=True,
            data={
                "message": "Connected to TestNetwork",
                "ssid": "TestNetwork",
                "ip": "192.168.1.100"
            }
        )

        request_data = {
            "ssid": "TestNetwork",
            "password": "testpass123",
            "hidden": False
        }
        request_bytes = json.dumps(request_data).encode('utf-8')

        # Act
        with patch('app.backend.ble.gatt_server.container') as mock_container:
            mock_container.wifi_service.connect = AsyncMock(
                return_value=mock_service_result
            )

            result = await server._handle_wifi_connect_write(request_bytes)

        # Assert
        assert result["success"] is True
        assert result["ssid"] == "TestNetwork"
        assert result["ip"] == "192.168.1.100"

        # Verify service was called with correct params
        mock_container.wifi_service.connect.assert_called_once_with(
            ssid="TestNetwork",
            password="testpass123",
            hidden=False
        )


class TestSystemGATTCharacteristics:
    """Test System GATT characteristic handlers."""

    @pytest.mark.asyncio
    async def test_system_info_read_delegates_to_service(self):
        """Test system info read delegates to SystemService."""
        # Arrange
        server = DangerousPiGATTServer()

        mock_service_result = PM3ServiceResult(
            success=True,
            data={
                "hostname": "dangerous-pi",
                "platform": "Linux",
                "cpu": {"count": 4, "percent": 25.0, "temperature": 45.5},
                "memory": {"total": 4294967296, "used": 2147483648, "percent": 50.0},
                "disk": {"total": 32212254720, "used": 10737418240, "percent": 33.3},
                "uptime": 86400.0
            }
        )

        # Act
        with patch('app.backend.ble.gatt_server.container') as mock_container:
            mock_container.system_service.get_info = AsyncMock(
                return_value=mock_service_result
            )

            result_bytes = await server._handle_system_info_read()
            result = json.loads(result_bytes.decode('utf-8'))

        # Assert
        assert result["hostname"] == "dangerous-pi"
        assert result["cpu"]["count"] == 4
        mock_container.system_service.get_info.assert_called_once()

    @pytest.mark.asyncio
    async def test_shutdown_write_delegates_to_service(self):
        """Test shutdown request delegates to SystemService."""
        # Arrange
        server = DangerousPiGATTServer()

        mock_service_result = PM3ServiceResult(
            success=True,
            data={"message": "Shutdown initiated"}
        )

        request_data = {"delay": 30}
        request_bytes = json.dumps(request_data).encode('utf-8')

        # Act
        with patch('app.backend.ble.gatt_server.container') as mock_container:
            mock_container.system_service.shutdown = AsyncMock(
                return_value=mock_service_result
            )

            result = await server._handle_system_shutdown_write(request_bytes)

        # Assert
        assert result["success"] is True
        assert "Shutdown initiated" in result["message"]
        mock_container.system_service.shutdown.assert_called_once_with(delay=30)


class TestGATTServerNotifications:
    """Test notification system."""

    @pytest.mark.asyncio
    async def test_notification_callback_registration(self):
        """Test registering notification callbacks."""
        # Arrange
        server = DangerousPiGATTServer()
        callback_called = False
        callback_value = None

        async def test_callback(value):
            nonlocal callback_called, callback_value
            callback_called = True
            callback_value = value

        # Act
        server.register_notification_callback(
            PM3CharacteristicUUIDs.COMMAND_RESULT,
            test_callback
        )

        await server._notify_characteristic(
            PM3CharacteristicUUIDs.COMMAND_RESULT,
            b"test notification"
        )

        # Assert
        assert callback_called is True
        assert callback_value == b"test notification"

    @pytest.mark.asyncio
    async def test_notification_without_callback(self):
        """Test notification when no callback registered (should not error)."""
        # Arrange
        server = DangerousPiGATTServer()

        # Act & Assert (should not raise)
        await server._notify_characteristic(
            "nonexistent-uuid",
            b"test"
        )


class TestErrorHandling:
    """Test error handling in GATT handlers."""

    @pytest.mark.asyncio
    async def test_invalid_json_in_command(self):
        """Test handling of invalid JSON in command."""
        # Arrange
        server = DangerousPiGATTServer()
        invalid_json = b"not valid json{"

        # Act
        result = await server._handle_pm3_command_write(invalid_json)

        # Assert
        assert result["success"] is False
        assert result["error"]["code"] == "invalid_json"

    @pytest.mark.asyncio
    async def test_missing_required_field(self):
        """Test handling of missing required field."""
        # Arrange
        server = DangerousPiGATTServer()
        request_data = {}  # Missing "command" field
        request_bytes = json.dumps(request_data).encode('utf-8')

        # Act
        result = await server._handle_pm3_command_write(request_bytes)

        # Assert
        assert result["success"] is False
        assert result["error"]["code"] == "invalid_request"
