"""Integration tests for multi-device discovery."""
import pytest
import asyncio
from unittest.mock import Mock, patch

from app.backend.managers.pm3_device_manager import (
    PM3DeviceManager,
    DeviceStatus
)
from app.backend.managers.session_manager import SessionManager


@pytest.fixture
async def device_manager():
    """Create and start device manager."""
    manager = PM3DeviceManager()
    # Don't start monitoring for tests
    yield manager
    await manager.stop()


@pytest.fixture
def session_manager():
    """Create session manager."""
    return SessionManager()


class TestMultiDeviceDiscovery:
    """Integration tests for device discovery."""

    @pytest.mark.asyncio
    async def test_discover_no_devices(self, device_manager):
        """Test discovery when no devices are connected."""
        with patch('app.backend.managers.pm3_device_manager.SERIAL_AVAILABLE', False):
            with patch('pathlib.Path.glob', return_value=[]):
                devices = await device_manager.discover_devices()
                assert len(devices) == 0

    @pytest.mark.asyncio
    async def test_discover_single_device(self, device_manager):
        """Test discovering a single device."""
        with patch('pathlib.Path.glob') as mock_glob:
            # Mock single device
            mock_device = Mock()
            mock_device.is_char_device.return_value = True
            mock_device.__str__ = lambda x: "/dev/ttyACM0"
            mock_glob.return_value = [mock_device]

            devices = await device_manager.discover_devices()

            assert len(devices) == 1
            assert devices[0].device_path == "/dev/ttyACM0"
            assert devices[0].status == DeviceStatus.CONNECTED

    @pytest.mark.asyncio
    async def test_discover_multiple_devices(self, device_manager):
        """Test discovering multiple devices."""
        with patch('pathlib.Path.glob') as mock_glob:
            # Mock multiple devices
            mock_devices = []
            for i in range(3):
                mock_device = Mock()
                mock_device.is_char_device.return_value = True
                mock_device.__str__ = lambda x, i=i: f"/dev/ttyACM{i}"
                mock_devices.append(mock_device)

            mock_glob.return_value = mock_devices

            devices = await device_manager.discover_devices()

            assert len(devices) == 3
            paths = [d.device_path for d in devices]
            assert "/dev/ttyACM0" in paths
            assert "/dev/ttyACM1" in paths
            assert "/dev/ttyACM2" in paths

    @pytest.mark.asyncio
    async def test_device_hotplug_simulation(self, device_manager):
        """Test device hotplug by simulating device connect/disconnect."""
        with patch('pathlib.Path.glob') as mock_glob:
            # First discovery: 1 device
            mock_device1 = Mock()
            mock_device1.is_char_device.return_value = True
            mock_device1.__str__ = lambda x: "/dev/ttyACM0"
            mock_glob.return_value = [mock_device1]

            devices = await device_manager.discover_devices()
            assert len(devices) == 1
            device1_id = devices[0].device_id

            # Second discovery: 2 devices (device plugged in)
            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_devices()
            assert len(devices) == 2

            # Third discovery: 1 device (device unplugged)
            mock_glob.return_value = [mock_device1]

            devices = await device_manager.discover_devices()
            # Still 2 devices tracked, but one is DISCONNECTED
            assert len(devices) == 2

            connected = [d for d in devices if d.status == DeviceStatus.CONNECTED]
            disconnected = [d for d in devices if d.status == DeviceStatus.DISCONNECTED]

            assert len(connected) == 1
            assert len(disconnected) == 1

    @pytest.mark.asyncio
    async def test_device_persistence_across_discovery(self, device_manager):
        """Test that device objects persist across discovery runs."""
        with patch('pathlib.Path.glob') as mock_glob:
            mock_device = Mock()
            mock_device.is_char_device.return_value = True
            mock_device.__str__ = lambda x: "/dev/ttyACM0"
            mock_glob.return_value = [mock_device]

            # First discovery
            devices1 = await device_manager.discover_devices()
            device_id = devices1[0].device_id

            # Second discovery
            devices2 = await device_manager.discover_devices()

            # Device ID should be the same
            assert len(devices2) == 1
            assert devices2[0].device_id == device_id


class TestMultiDeviceSessionIsolation:
    """Integration tests for session isolation across devices."""

    @pytest.mark.asyncio
    async def test_single_session_per_device(self, device_manager, session_manager):
        """Test that each device can have its own session."""
        # Note: This will be fully implemented in Sprint 2
        # For now, just verify session manager exists
        assert session_manager is not None

        # TODO (Sprint 2): Test multiple devices with independent sessions


class TestDeviceFiltering:
    """Integration tests for device filtering."""

    @pytest.mark.asyncio
    async def test_filter_by_status(self, device_manager):
        """Test filtering devices by status."""
        with patch('pathlib.Path.glob') as mock_glob:
            mock_devices = []
            for i in range(3):
                mock_device = Mock()
                mock_device.is_char_device.return_value = True
                mock_device.__str__ = lambda x, i=i: f"/dev/ttyACM{i}"
                mock_devices.append(mock_device)

            mock_glob.return_value = mock_devices

            # Discover devices
            await device_manager.discover_devices()

            # Set different statuses
            devices = await device_manager.get_all_devices()
            await device_manager.set_device_status(devices[0].device_id, DeviceStatus.CONNECTED)
            await device_manager.set_device_status(devices[1].device_id, DeviceStatus.IN_USE)
            await device_manager.set_device_status(devices[2].device_id, DeviceStatus.DISCONNECTED)

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

            # Get connected devices (CONNECTED + IN_USE)
            connected = await device_manager.get_connected_devices()
            assert len(connected) == 2


@pytest.mark.hardware
@pytest.mark.asyncio
async def test_real_hardware_discovery():
    """Test discovery with real hardware.

    This test requires actual Proxmark3 hardware connected.
    Run with: pytest -m hardware
    """
    manager = PM3DeviceManager()

    try:
        devices = await manager.discover_devices()

        # If hardware is connected, verify it was found
        if devices:
            print(f"\nFound {len(devices)} PM3 device(s):")
            for device in devices:
                print(f"  - {device.device_path} (ID: {device.device_id})")
                print(f"    Status: {device.status.value}")
                print(f"    VID:PID: {device.usb_vid}:{device.usb_pid}")
        else:
            print("\nNo PM3 devices found (this is OK for CI/CD)")

    finally:
        await manager.stop()


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