"""Unit tests for PM3DeviceManager."""
import pytest
import asyncio
from unittest.mock import Mock, patch, AsyncMock, MagicMock
from datetime import datetime

from app.backend.managers.pm3_device_manager import (
    PM3DeviceManager,
    PM3Device,
    DeviceStatus,
    PM3FirmwareInfo
)


@pytest.fixture
def device_manager():
    """Create a PM3DeviceManager instance."""
    return PM3DeviceManager()


@pytest.fixture
def mock_device():
    """Create a mock PM3Device."""
    return PM3Device(
        device_id="pm3_test123",
        device_path="/dev/ttyACM0",
        serial_number="ABC123",
        friendly_name="Test PM3",
        usb_vid="9ac4",
        usb_pid="4b8f",
        status=DeviceStatus.CONNECTED
    )


class TestPM3DeviceManager:
    """Tests for PM3DeviceManager class."""

    def test_initialization(self, device_manager):
        """Test device manager initialization."""
        assert device_manager is not None
        assert device_manager.auto_discover is True
        assert device_manager.discovery_interval == 30
        assert len(device_manager._devices) == 0

    def test_generate_device_id(self, device_manager):
        """Test device ID generation."""
        device_id = device_manager._generate_device_id("/dev/ttyACM0")
        assert device_id.startswith("pm3_")
        assert len(device_id) == 12  # pm3_ + 8 char hash

        # Same path should generate same ID
        device_id2 = device_manager._generate_device_id("/dev/ttyACM0")
        assert device_id == device_id2

        # Different path should generate different ID
        device_id3 = device_manager._generate_device_id("/dev/ttyACM1")
        assert device_id != device_id3

    def test_generate_device_id_with_serial(self, device_manager):
        """Test device ID generation with serial number."""
        device_id = device_manager._generate_device_id("/dev/ttyACM0", serial="ABC123")
        assert device_id.startswith("pm3_")

        # Same serial should generate same ID even with different path
        device_id2 = device_manager._generate_device_id("/dev/ttyACM1", serial="ABC123")
        assert device_id == device_id2

    @pytest.mark.asyncio
    async def test_get_device(self, device_manager, mock_device):
        """Test getting device by ID."""
        # Add device to manager
        device_manager._devices[mock_device.device_id] = mock_device

        # Get existing device
        device = await device_manager.get_device(mock_device.device_id)
        assert device is not None
        assert device.device_id == mock_device.device_id

        # Get non-existent device
        device = await device_manager.get_device("nonexistent")
        assert device is None

    @pytest.mark.asyncio
    async def test_get_all_devices(self, device_manager, mock_device):
        """Test getting all devices."""
        # Initially empty
        devices = await device_manager.get_all_devices()
        assert len(devices) == 0

        # Add device
        device_manager._devices[mock_device.device_id] = mock_device

        # Get all devices
        devices = await device_manager.get_all_devices()
        assert len(devices) == 1
        assert devices[0].device_id == mock_device.device_id

    @pytest.mark.asyncio
    async def test_get_available_devices(self, device_manager):
        """Test getting available (not in use) devices."""
        # Create devices with different statuses
        device1 = PM3Device(
            device_id="pm3_1",
            device_path="/dev/ttyACM0",
            status=DeviceStatus.CONNECTED
        )
        device2 = PM3Device(
            device_id="pm3_2",
            device_path="/dev/ttyACM1",
            status=DeviceStatus.IN_USE
        )
        device3 = PM3Device(
            device_id="pm3_3",
            device_path="/dev/ttyACM2",
            status=DeviceStatus.DISCONNECTED
        )

        device_manager._devices = {
            device1.device_id: device1,
            device2.device_id: device2,
            device3.device_id: device3,
        }

        # Get available devices (only CONNECTED)
        available = await device_manager.get_available_devices()
        assert len(available) == 1
        assert available[0].device_id == device1.device_id

    @pytest.mark.asyncio
    async def test_get_connected_devices(self, device_manager):
        """Test getting connected devices."""
        # Create devices with different statuses
        device1 = PM3Device(
            device_id="pm3_1",
            device_path="/dev/ttyACM0",
            status=DeviceStatus.CONNECTED
        )
        device2 = PM3Device(
            device_id="pm3_2",
            device_path="/dev/ttyACM1",
            status=DeviceStatus.IN_USE
        )
        device3 = PM3Device(
            device_id="pm3_3",
            device_path="/dev/ttyACM2",
            status=DeviceStatus.DISCONNECTED
        )

        device_manager._devices = {
            device1.device_id: device1,
            device2.device_id: device2,
            device3.device_id: device3,
        }

        # Get connected devices (CONNECTED + IN_USE, not DISCONNECTED)
        connected = await device_manager.get_connected_devices()
        assert len(connected) == 2
        assert device1 in connected
        assert device2 in connected
        assert device3 not in connected

    @pytest.mark.asyncio
    async def test_set_device_status(self, device_manager, mock_device):
        """Test setting device status."""
        device_manager._devices[mock_device.device_id] = mock_device

        # Set status
        result = await device_manager.set_device_status(
            mock_device.device_id,
            DeviceStatus.IN_USE
        )
        assert result is True
        assert mock_device.status == DeviceStatus.IN_USE

        # Set status for non-existent device
        result = await device_manager.set_device_status(
            "nonexistent",
            DeviceStatus.IN_USE
        )
        assert result is False

    def test_parse_firmware_version(self, device_manager):
        """Test parsing firmware version from hw version output."""
        output = """
        Proxmark3 RFID instrument
          bootrom: RRG/Iceman/master/v4.14831
          os: RRG/Iceman/master/v4.14831
          client: RRG/Iceman/master/v4.14831
        """

        info = device_manager._parse_firmware_version(output)

        assert info.bootrom_version == "v4.14831"
        assert info.os_version == "v4.14831"
        assert info.client_version == "v4.14831"
        assert info.compatible is True
        assert info.needs_upgrade is False
        assert info.needs_downgrade is False

    def test_parse_firmware_version_mismatch(self, device_manager):
        """Test parsing mismatched firmware versions."""
        output = """
        Proxmark3 RFID instrument
          bootrom: RRG/Iceman/master/v4.14000
          os: RRG/Iceman/master/v4.14000
          client: RRG/Iceman/master/v4.14831
        """

        info = device_manager._parse_firmware_version(output)

        assert info.bootrom_version == "v4.14000"
        assert info.os_version == "v4.14000"
        assert info.client_version == "v4.14831"
        assert info.compatible is False
        assert info.needs_upgrade is True

    @pytest.mark.asyncio
    async def test_discover_via_dev(self, device_manager):
        """Test device discovery via /dev scanning."""
        with patch('pathlib.Path.glob') as mock_glob:
            # Mock /dev/ttyACM* files
            mock_device1 = Mock()
            mock_device1.is_char_device.return_value = True
            mock_device1.__str__ = lambda x: "/dev/ttyACM0"

            mock_device2 = Mock()
            mock_device2.is_char_device.return_value = True
            mock_device2.__str__ = lambda x: "/dev/ttyACM1"

            mock_glob.return_value = [mock_device1, mock_device2]

            devices = await device_manager._discover_via_dev()

            assert len(devices) == 2
            assert "/dev/ttyACM0" in devices
            assert "/dev/ttyACM1" in devices

    @pytest.mark.asyncio
    async def test_identify_device_not_found(self, device_manager):
        """Test identifying a device that doesn't exist."""
        with pytest.raises(ValueError, match="Device .* not found"):
            await device_manager.identify_device("nonexistent")

    @pytest.mark.asyncio
    async def test_identify_device_no_worker(self, device_manager):
        """Test identifying a device without a worker."""
        device = PM3Device(
            device_id="pm3_test",
            device_path="/dev/ttyACM0",
            status=DeviceStatus.CONNECTED,
            worker=None
        )
        device_manager._devices[device.device_id] = device

        with pytest.raises(RuntimeError, match="has no worker"):
            await device_manager.identify_device(device.device_id)

    def test_device_to_dict(self, mock_device):
        """Test converting PM3Device to dictionary."""
        data = mock_device.to_dict()

        assert data["device_id"] == mock_device.device_id
        assert data["device_path"] == mock_device.device_path
        assert data["serial_number"] == mock_device.serial_number
        assert data["friendly_name"] == mock_device.friendly_name
        assert data["usb_vid"] == mock_device.usb_vid
        assert data["usb_pid"] == mock_device.usb_pid
        assert data["status"] == mock_device.status.value
        assert data["connected"] is True
        assert "firmware_info" in data

    def test_device_status_enum(self):
        """Test DeviceStatus enum values."""
        assert DeviceStatus.CONNECTED.value == "connected"
        assert DeviceStatus.DISCONNECTED.value == "disconnected"
        assert DeviceStatus.IN_USE.value == "in_use"
        assert DeviceStatus.ERROR.value == "error"
        assert DeviceStatus.VERSION_MISMATCH.value == "version_mismatch"
        assert DeviceStatus.FLASHING.value == "flashing"
        assert DeviceStatus.BOOTLOADER_MODE.value == "bootloader_mode"
        assert DeviceStatus.DISABLED.value == "disabled"


class TestPM3FirmwareInfo:
    """Tests for PM3FirmwareInfo dataclass."""

    def test_default_values(self):
        """Test default firmware info values."""
        info = PM3FirmwareInfo()

        assert info.bootrom_version == "unknown"
        assert info.os_version == "unknown"
        assert info.client_version == "unknown"
        assert info.compatible is False
        assert info.needs_upgrade is False
        assert info.needs_downgrade is False
        assert info.bootloader_outdated is False

    def test_compatibility_check(self):
        """Test firmware compatibility logic."""
        # Compatible versions
        info1 = PM3FirmwareInfo(
            bootrom_version="v4.14831",
            os_version="v4.14831",
            client_version="v4.14831"
        )
        info1.compatible = (
            info1.os_version == info1.client_version and
            info1.bootrom_version == info1.client_version
        )
        assert info1.compatible is True

        # Incompatible versions
        info2 = PM3FirmwareInfo(
            bootrom_version="v4.14000",
            os_version="v4.14000",
            client_version="v4.14831"
        )
        info2.compatible = (
            info2.os_version == info2.client_version and
            info2.bootrom_version == info2.client_version
        )
        assert info2.compatible is False


@pytest.mark.asyncio
async def test_device_manager_lifecycle(device_manager):
    """Test device manager start/stop lifecycle."""
    # Start manager
    await device_manager.start()
    assert device_manager._monitor_task is not None

    # Stop manager
    await device_manager.stop()
    # Monitor task should be cancelled


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
