"""Test-Driven Development tests for CSI extractor using London School approach."""

import pytest
import numpy as np
from unittest.mock import Mock, patch, AsyncMock, MagicMock
from typing import Dict, Any, Optional
import asyncio
from datetime import datetime, timezone

from src.hardware.csi_extractor import (
    CSIExtractor,
    CSIExtractionError,
    CSIParseError,
    CSIData,
    ESP32CSIParser,
    RouterCSIParser,
    CSIValidationError
)


@pytest.mark.unit
@pytest.mark.tdd
@pytest.mark.london
class TestCSIExtractor:
    """Test CSI extractor using London School TDD - focus on interactions and behavior."""

    @pytest.fixture
    def mock_logger(self):
        """Mock logger for testing."""
        return Mock()

    @pytest.fixture
    def mock_config(self):
        """Mock configuration for CSI extractor."""
        return {
            'hardware_type': 'esp32',
            'sampling_rate': 100,
            'buffer_size': 1024,
            'timeout': 5.0,
            'validation_enabled': True,
            'retry_attempts': 3
        }

    @pytest.fixture
    def csi_extractor(self, mock_config, mock_logger):
        """Create CSI extractor instance for testing."""
        return CSIExtractor(config=mock_config, logger=mock_logger)

    @pytest.fixture
    def sample_csi_data(self):
        """Sample CSI data for testing."""
        return CSIData(
            timestamp=datetime.now(timezone.utc),
            amplitude=np.random.rand(3, 56),
            phase=np.random.rand(3, 56),
            frequency=2.4e9,
            bandwidth=20e6,
            num_subcarriers=56,
            num_antennas=3,
            snr=15.5,
            metadata={'source': 'esp32', 'channel': 6}
        )

    def test_should_initialize_with_valid_config(self, mock_config, mock_logger):
        """Should initialize CSI extractor with valid configuration."""
        extractor = CSIExtractor(config=mock_config, logger=mock_logger)
        
        assert extractor.config == mock_config
        assert extractor.logger == mock_logger
        assert extractor.is_connected == False
        assert extractor.hardware_type == 'esp32'

    def test_should_raise_error_with_invalid_config(self, mock_logger):
        """Should raise error when initialized with invalid configuration."""
        invalid_config = {'invalid': 'config'}
        
        with pytest.raises(ValueError, match="Missing required configuration"):
            CSIExtractor(config=invalid_config, logger=mock_logger)

    def test_should_create_appropriate_parser(self, mock_config, mock_logger):
        """Should create appropriate parser based on hardware type."""
        extractor = CSIExtractor(config=mock_config, logger=mock_logger)
        
        assert isinstance(extractor.parser, ESP32CSIParser)

    @pytest.mark.asyncio
    async def test_should_establish_connection_successfully(self, csi_extractor):
        """Should establish connection to hardware successfully."""
        with patch.object(csi_extractor, '_establish_hardware_connection', new_callable=AsyncMock) as mock_connect:
            mock_connect.return_value = True
            
            result = await csi_extractor.connect()
            
            assert result == True
            assert csi_extractor.is_connected == True
            mock_connect.assert_called_once()

    @pytest.mark.asyncio
    async def test_should_handle_connection_failure(self, csi_extractor):
        """Should handle connection failure gracefully."""
        with patch.object(csi_extractor, '_establish_hardware_connection', new_callable=AsyncMock) as mock_connect:
            mock_connect.side_effect = ConnectionError("Hardware not found")
            
            result = await csi_extractor.connect()
            
            assert result == False
            assert csi_extractor.is_connected == False
            csi_extractor.logger.error.assert_called()

    @pytest.mark.asyncio
    async def test_should_disconnect_properly(self, csi_extractor):
        """Should disconnect from hardware properly."""
        csi_extractor.is_connected = True
        
        with patch.object(csi_extractor, '_close_hardware_connection', new_callable=AsyncMock) as mock_disconnect:
            await csi_extractor.disconnect()
            
            assert csi_extractor.is_connected == False
            mock_disconnect.assert_called_once()

    @pytest.mark.asyncio
    async def test_should_extract_csi_data_successfully(self, csi_extractor, sample_csi_data):
        """Should extract CSI data successfully from hardware."""
        csi_extractor.is_connected = True
        
        with patch.object(csi_extractor, '_read_raw_data', new_callable=AsyncMock) as mock_read:
            with patch.object(csi_extractor.parser, 'parse', return_value=sample_csi_data) as mock_parse:
                mock_read.return_value = b"raw_csi_data"
                
                result = await csi_extractor.extract_csi()
                
                assert result == sample_csi_data
                mock_read.assert_called_once()
                mock_parse.assert_called_once_with(b"raw_csi_data")

    @pytest.mark.asyncio
    async def test_should_handle_extraction_failure_when_not_connected(self, csi_extractor):
        """Should handle extraction failure when not connected."""
        csi_extractor.is_connected = False
        
        with pytest.raises(CSIParseError, match="Not connected to hardware"):
            await csi_extractor.extract_csi()

    @pytest.mark.asyncio
    async def test_should_retry_on_temporary_failure(self, csi_extractor, sample_csi_data):
        """Should retry extraction on temporary failure."""
        csi_extractor.is_connected = True
        
        with patch.object(csi_extractor, '_read_raw_data', new_callable=AsyncMock) as mock_read:
            with patch.object(csi_extractor.parser, 'parse') as mock_parse:
                # First two calls fail, third succeeds
                mock_read.side_effect = [ConnectionError(), ConnectionError(), b"raw_data"]
                mock_parse.return_value = sample_csi_data
                
                result = await csi_extractor.extract_csi()
                
                assert result == sample_csi_data
                assert mock_read.call_count == 3

    def test_should_validate_csi_data_successfully(self, csi_extractor, sample_csi_data):
        """Should validate CSI data successfully."""
        result = csi_extractor.validate_csi_data(sample_csi_data)
        
        assert result == True

    def test_should_reject_invalid_csi_data(self, csi_extractor):
        """Should reject CSI data with invalid structure."""
        invalid_data = CSIData(
            timestamp=datetime.now(timezone.utc),
            amplitude=np.array([]),  # Empty array
            phase=np.array([]),
            frequency=0,  # Invalid frequency
            bandwidth=0,
            num_subcarriers=0,
            num_antennas=0,
            snr=-100,  # Invalid SNR
            metadata={}
        )
        
        with pytest.raises(CSIValidationError):
            csi_extractor.validate_csi_data(invalid_data)

    @pytest.mark.asyncio
    async def test_should_start_streaming_successfully(self, csi_extractor, sample_csi_data):
        """Should start CSI data streaming successfully."""
        csi_extractor.is_connected = True
        callback = Mock()
        
        with patch.object(csi_extractor, 'extract_csi', new_callable=AsyncMock) as mock_extract:
            mock_extract.return_value = sample_csi_data
            
            # Start streaming with limited iterations to avoid infinite loop
            streaming_task = asyncio.create_task(csi_extractor.start_streaming(callback))
            await asyncio.sleep(0.1)  # Let it run briefly
            csi_extractor.stop_streaming()
            await streaming_task
            
            callback.assert_called()

    @pytest.mark.asyncio
    async def test_should_stop_streaming_gracefully(self, csi_extractor):
        """Should stop streaming gracefully."""
        csi_extractor.is_streaming = True
        
        csi_extractor.stop_streaming()
        
        assert csi_extractor.is_streaming == False


@pytest.mark.unit
@pytest.mark.tdd
@pytest.mark.london
class TestESP32CSIParser:
    """Test ESP32 CSI parser using London School TDD."""

    @pytest.fixture
    def parser(self):
        """Create ESP32 CSI parser for testing."""
        return ESP32CSIParser()

    @pytest.fixture
    def raw_esp32_data(self):
        """Sample raw ESP32 CSI data with correct 3×56 amplitude and phase values."""
        n_ant, n_sub = 3, 56
        amp = ",".join(["1.0"] * (n_ant * n_sub))
        pha = ",".join(["0.5"] * (n_ant * n_sub))
        return f"CSI_DATA:1234567890,{n_ant},{n_sub},2400,20,15.5,{amp},{pha}".encode()

    def test_should_parse_valid_esp32_data(self, parser, raw_esp32_data):
        """Should parse valid ESP32 CSI data successfully."""
        result = parser.parse(raw_esp32_data)
        
        assert isinstance(result, CSIData)
        assert result.num_antennas == 3
        assert result.num_subcarriers == 56
        assert result.frequency == 2400000000  # 2.4 GHz
        assert result.bandwidth == 20000000    # 20 MHz
        assert result.snr == 15.5

    def test_should_handle_malformed_data(self, parser):
        """Should handle malformed ESP32 data gracefully."""
        malformed_data = b"INVALID_DATA"
        
        with pytest.raises(CSIParseError, match="Invalid ESP32 CSI data format"):
            parser.parse(malformed_data)

    def test_should_handle_empty_data(self, parser):
        """Should handle empty data gracefully."""
        with pytest.raises(CSIParseError, match="Empty data received"):
            parser.parse(b"")


@pytest.mark.unit
@pytest.mark.tdd
@pytest.mark.london
class TestRouterCSIParser:
    """Test Router CSI parser using London School TDD."""

    @pytest.fixture
    def parser(self):
        """Create Router CSI parser for testing."""
        return RouterCSIParser()

    def test_should_parse_atheros_format(self, parser):
        """Should parse Atheros CSI format successfully."""
        raw_data = b"ATHEROS_CSI:mock_data"
        
        with patch.object(parser, '_parse_atheros_format', return_value=Mock(spec=CSIData)) as mock_parse:
            result = parser.parse(raw_data)
            
            mock_parse.assert_called_once()
            assert result is not None

    def test_should_handle_unknown_format(self, parser):
        """Should handle unknown router format gracefully."""
        unknown_data = b"UNKNOWN_FORMAT:data"
        
        with pytest.raises(CSIParseError, match="Unknown router CSI format"):
            parser.parse(unknown_data)