"""TDD tests for phase sanitizer following London School approach."""

import pytest
import numpy as np
import sys
import os
from unittest.mock import Mock, patch, AsyncMock
from datetime import datetime, timezone
import importlib.util

# Resolve paths relative to v1/ (this file lives at v1/tests/unit/)
_TESTS_DIR = os.path.dirname(os.path.abspath(__file__))
_V1_DIR = os.path.abspath(os.path.join(_TESTS_DIR, '..', '..'))
if _V1_DIR not in sys.path:
    sys.path.insert(0, _V1_DIR)

# Import the phase sanitizer module directly
spec = importlib.util.spec_from_file_location(
    'phase_sanitizer',
    os.path.join(_V1_DIR, 'src', 'core', 'phase_sanitizer.py')
)
phase_sanitizer_module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(phase_sanitizer_module)

# Get classes from the module
PhaseSanitizer = phase_sanitizer_module.PhaseSanitizer
PhaseSanitizationError = phase_sanitizer_module.PhaseSanitizationError


@pytest.mark.unit
@pytest.mark.tdd
@pytest.mark.london
class TestPhaseSanitizer:
    """Test phase sanitizer using London School TDD."""

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

    @pytest.fixture
    def sanitizer_config(self):
        """Phase sanitizer configuration for testing."""
        return {
            'unwrapping_method': 'numpy',
            'outlier_threshold': 3.0,
            'smoothing_window': 5,
            'enable_outlier_removal': True,
            'enable_smoothing': True,
            'enable_noise_filtering': True,
            'noise_threshold': 0.1,
            'phase_range': (-np.pi, np.pi)
        }

    @pytest.fixture
    def phase_sanitizer(self, sanitizer_config, mock_logger):
        """Create phase sanitizer for testing."""
        return PhaseSanitizer(config=sanitizer_config, logger=mock_logger)

    @pytest.fixture
    def sample_wrapped_phase(self):
        """Sample wrapped phase data with discontinuities."""
        # Create phase data with wrapping
        phase = np.linspace(0, 4*np.pi, 100)
        wrapped_phase = np.angle(np.exp(1j * phase))  # Wrap to [-π, π]
        return wrapped_phase.reshape(1, -1)  # Shape: (1, 100)

    @pytest.fixture
    def sample_noisy_phase(self):
        """Sample phase data with noise and outliers."""
        clean_phase = np.linspace(-np.pi, np.pi, 50)
        noise = np.random.normal(0, 0.05, 50)
        # Add some outliers
        outliers = np.random.choice(50, 5, replace=False)
        noisy_phase = clean_phase + noise
        noisy_phase[outliers] += np.random.uniform(-2, 2, 5)  # Add outliers
        return noisy_phase.reshape(1, -1)

    # Initialization tests
    def test_should_initialize_with_valid_config(self, sanitizer_config, mock_logger):
        """Should initialize phase sanitizer with valid configuration."""
        sanitizer = PhaseSanitizer(config=sanitizer_config, logger=mock_logger)
        
        assert sanitizer.config == sanitizer_config
        assert sanitizer.logger == mock_logger
        assert sanitizer.unwrapping_method == 'numpy'
        assert sanitizer.outlier_threshold == 3.0
        assert sanitizer.smoothing_window == 5
        assert sanitizer.enable_outlier_removal == True
        assert sanitizer.enable_smoothing == True
        assert sanitizer.enable_noise_filtering == True
        assert sanitizer.noise_threshold == 0.1
        assert sanitizer.phase_range == (-np.pi, np.pi)

    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"):
            PhaseSanitizer(config=invalid_config, logger=mock_logger)

    def test_should_validate_required_fields(self, mock_logger):
        """Should validate required configuration fields."""
        required_fields = ['unwrapping_method', 'outlier_threshold', 'smoothing_window']
        base_config = {
            'unwrapping_method': 'numpy',
            'outlier_threshold': 3.0,
            'smoothing_window': 5
        }
        
        for field in required_fields:
            config = base_config.copy()
            del config[field]
            
            with pytest.raises(ValueError, match="Missing required configuration"):
                PhaseSanitizer(config=config, logger=mock_logger)

    def test_should_use_default_values(self, mock_logger):
        """Should use default values for optional parameters."""
        minimal_config = {
            'unwrapping_method': 'numpy',
            'outlier_threshold': 3.0,
            'smoothing_window': 5
        }
        
        sanitizer = PhaseSanitizer(config=minimal_config, logger=mock_logger)
        
        assert sanitizer.enable_outlier_removal == True  # default
        assert sanitizer.enable_smoothing == True  # default
        assert sanitizer.enable_noise_filtering == False  # default
        assert sanitizer.noise_threshold == 0.05  # default
        assert sanitizer.phase_range == (-np.pi, np.pi)  # default

    def test_should_initialize_without_logger(self, sanitizer_config):
        """Should initialize without logger provided."""
        sanitizer = PhaseSanitizer(config=sanitizer_config)
        
        assert sanitizer.logger is not None  # Should create default logger

    # Phase unwrapping tests
    def test_should_unwrap_phase_successfully(self, phase_sanitizer, sample_wrapped_phase):
        """Should unwrap phase data successfully."""
        result = phase_sanitizer.unwrap_phase(sample_wrapped_phase)
        
        # Check that result has same shape
        assert result.shape == sample_wrapped_phase.shape
        
        # Check that unwrapping removed discontinuities
        phase_diff = np.diff(result.flatten())
        large_jumps = np.abs(phase_diff) > np.pi
        assert np.sum(large_jumps) < np.sum(np.abs(np.diff(sample_wrapped_phase.flatten())) > np.pi)

    def test_should_handle_different_unwrapping_methods(self, sanitizer_config, mock_logger):
        """Should handle different unwrapping methods."""
        for method in ['numpy', 'scipy', 'custom']:
            sanitizer_config['unwrapping_method'] = method
            sanitizer = PhaseSanitizer(config=sanitizer_config, logger=mock_logger)
            
            phase_data = np.random.uniform(-np.pi, np.pi, (2, 50))
            
            with patch.object(sanitizer, f'_unwrap_{method}', return_value=phase_data) as mock_unwrap:
                result = sanitizer.unwrap_phase(phase_data)
                
                assert result.shape == phase_data.shape
                mock_unwrap.assert_called_once()

    def test_should_handle_unwrapping_error(self, phase_sanitizer):
        """Should handle phase unwrapping errors gracefully."""
        invalid_phase = np.array([[]])  # Empty array
        
        with pytest.raises(PhaseSanitizationError, match="Failed to unwrap phase"):
            phase_sanitizer.unwrap_phase(invalid_phase)

    # Outlier removal tests
    def test_should_remove_outliers_successfully(self, phase_sanitizer, sample_noisy_phase):
        """Should remove outliers from phase data successfully."""
        with patch.object(phase_sanitizer, '_detect_outliers') as mock_detect:
            with patch.object(phase_sanitizer, '_interpolate_outliers') as mock_interpolate:
                outlier_mask = np.zeros(sample_noisy_phase.shape, dtype=bool)
                outlier_mask[0, [10, 20, 30]] = True  # Mark some outliers
                clean_phase = sample_noisy_phase.copy()
                
                mock_detect.return_value = outlier_mask
                mock_interpolate.return_value = clean_phase
                
                result = phase_sanitizer.remove_outliers(sample_noisy_phase)
                
                assert result.shape == sample_noisy_phase.shape
                mock_detect.assert_called_once_with(sample_noisy_phase)
                mock_interpolate.assert_called_once()

    def test_should_skip_outlier_removal_when_disabled(self, sanitizer_config, mock_logger, sample_noisy_phase):
        """Should skip outlier removal when disabled."""
        sanitizer_config['enable_outlier_removal'] = False
        sanitizer = PhaseSanitizer(config=sanitizer_config, logger=mock_logger)
        
        result = sanitizer.remove_outliers(sample_noisy_phase)
        
        assert np.array_equal(result, sample_noisy_phase)

    def test_should_handle_outlier_removal_error(self, phase_sanitizer):
        """Should handle outlier removal errors gracefully."""
        with patch.object(phase_sanitizer, '_detect_outliers') as mock_detect:
            mock_detect.side_effect = Exception("Detection error")
            
            phase_data = np.random.uniform(-np.pi, np.pi, (2, 50))
            
            with pytest.raises(PhaseSanitizationError, match="Failed to remove outliers"):
                phase_sanitizer.remove_outliers(phase_data)

    # Smoothing tests
    def test_should_smooth_phase_successfully(self, phase_sanitizer, sample_noisy_phase):
        """Should smooth phase data successfully."""
        with patch.object(phase_sanitizer, '_apply_moving_average') as mock_smooth:
            smoothed_phase = sample_noisy_phase * 0.9  # Simulate smoothing
            mock_smooth.return_value = smoothed_phase
            
            result = phase_sanitizer.smooth_phase(sample_noisy_phase)
            
            assert result.shape == sample_noisy_phase.shape
            mock_smooth.assert_called_once_with(sample_noisy_phase, phase_sanitizer.smoothing_window)

    def test_should_skip_smoothing_when_disabled(self, sanitizer_config, mock_logger, sample_noisy_phase):
        """Should skip smoothing when disabled."""
        sanitizer_config['enable_smoothing'] = False
        sanitizer = PhaseSanitizer(config=sanitizer_config, logger=mock_logger)
        
        result = sanitizer.smooth_phase(sample_noisy_phase)
        
        assert np.array_equal(result, sample_noisy_phase)

    def test_should_handle_smoothing_error(self, phase_sanitizer):
        """Should handle smoothing errors gracefully."""
        with patch.object(phase_sanitizer, '_apply_moving_average') as mock_smooth:
            mock_smooth.side_effect = Exception("Smoothing error")
            
            phase_data = np.random.uniform(-np.pi, np.pi, (2, 50))
            
            with pytest.raises(PhaseSanitizationError, match="Failed to smooth phase"):
                phase_sanitizer.smooth_phase(phase_data)

    # Noise filtering tests
    def test_should_filter_noise_successfully(self, phase_sanitizer, sample_noisy_phase):
        """Should filter noise from phase data successfully."""
        with patch.object(phase_sanitizer, '_apply_low_pass_filter') as mock_filter:
            filtered_phase = sample_noisy_phase * 0.95  # Simulate filtering
            mock_filter.return_value = filtered_phase
            
            result = phase_sanitizer.filter_noise(sample_noisy_phase)
            
            assert result.shape == sample_noisy_phase.shape
            mock_filter.assert_called_once_with(sample_noisy_phase, phase_sanitizer.noise_threshold)

    def test_should_skip_noise_filtering_when_disabled(self, sanitizer_config, mock_logger, sample_noisy_phase):
        """Should skip noise filtering when disabled."""
        sanitizer_config['enable_noise_filtering'] = False
        sanitizer = PhaseSanitizer(config=sanitizer_config, logger=mock_logger)
        
        result = sanitizer.filter_noise(sample_noisy_phase)
        
        assert np.array_equal(result, sample_noisy_phase)

    def test_should_handle_noise_filtering_error(self, phase_sanitizer):
        """Should handle noise filtering errors gracefully."""
        with patch.object(phase_sanitizer, '_apply_low_pass_filter') as mock_filter:
            mock_filter.side_effect = Exception("Filtering error")
            
            phase_data = np.random.uniform(-np.pi, np.pi, (2, 50))
            
            with pytest.raises(PhaseSanitizationError, match="Failed to filter noise"):
                phase_sanitizer.filter_noise(phase_data)

    # Complete sanitization pipeline tests
    def test_should_sanitize_phase_pipeline_successfully(self, phase_sanitizer, sample_wrapped_phase):
        """Should sanitize phase through complete pipeline successfully."""
        with patch.object(phase_sanitizer, 'unwrap_phase', return_value=sample_wrapped_phase) as mock_unwrap:
            with patch.object(phase_sanitizer, 'remove_outliers', return_value=sample_wrapped_phase) as mock_outliers:
                with patch.object(phase_sanitizer, 'smooth_phase', return_value=sample_wrapped_phase) as mock_smooth:
                    with patch.object(phase_sanitizer, 'filter_noise', return_value=sample_wrapped_phase) as mock_filter:
                        
                        result = phase_sanitizer.sanitize_phase(sample_wrapped_phase)
                        
                        assert result.shape == sample_wrapped_phase.shape
                        mock_unwrap.assert_called_once_with(sample_wrapped_phase)
                        mock_outliers.assert_called_once()
                        mock_smooth.assert_called_once()
                        mock_filter.assert_called_once()

    def test_should_handle_sanitization_pipeline_error(self, phase_sanitizer, sample_wrapped_phase):
        """Should handle sanitization pipeline errors gracefully."""
        with patch.object(phase_sanitizer, 'unwrap_phase') as mock_unwrap:
            mock_unwrap.side_effect = PhaseSanitizationError("Unwrapping failed")
            
            with pytest.raises(PhaseSanitizationError):
                phase_sanitizer.sanitize_phase(sample_wrapped_phase)

    # Phase validation tests
    def test_should_validate_phase_data_successfully(self, phase_sanitizer):
        """Should validate phase data successfully."""
        valid_phase = np.random.uniform(-np.pi, np.pi, (3, 56))
        
        result = phase_sanitizer.validate_phase_data(valid_phase)
        
        assert result == True

    def test_should_reject_invalid_phase_shape(self, phase_sanitizer):
        """Should reject phase data with invalid shape."""
        invalid_phase = np.array([1, 2, 3])  # 1D array
        
        with pytest.raises(PhaseSanitizationError, match="Phase data must be 2D"):
            phase_sanitizer.validate_phase_data(invalid_phase)

    def test_should_reject_empty_phase_data(self, phase_sanitizer):
        """Should reject empty phase data."""
        empty_phase = np.array([]).reshape(0, 0)
        
        with pytest.raises(PhaseSanitizationError, match="Phase data cannot be empty"):
            phase_sanitizer.validate_phase_data(empty_phase)

    def test_should_reject_phase_out_of_range(self, phase_sanitizer):
        """Should reject phase data outside valid range."""
        invalid_phase = np.array([[10.0, -10.0, 5.0, -5.0]])  # Outside [-π, π]
        
        with pytest.raises(PhaseSanitizationError, match="Phase values outside valid range"):
            phase_sanitizer.validate_phase_data(invalid_phase)

    # Statistics and monitoring tests
    def test_should_get_sanitization_statistics(self, phase_sanitizer):
        """Should get sanitization statistics."""
        # Simulate some processing
        phase_sanitizer._total_processed = 50
        phase_sanitizer._outliers_removed = 5
        phase_sanitizer._sanitization_errors = 2
        
        stats = phase_sanitizer.get_sanitization_statistics()
        
        assert isinstance(stats, dict)
        assert stats['total_processed'] == 50
        assert stats['outliers_removed'] == 5
        assert stats['sanitization_errors'] == 2
        assert stats['outlier_rate'] == 0.1
        assert stats['error_rate'] == 0.04

    def test_should_reset_statistics(self, phase_sanitizer):
        """Should reset sanitization statistics."""
        phase_sanitizer._total_processed = 50
        phase_sanitizer._outliers_removed = 5
        phase_sanitizer._sanitization_errors = 2
        
        phase_sanitizer.reset_statistics()
        
        assert phase_sanitizer._total_processed == 0
        assert phase_sanitizer._outliers_removed == 0
        assert phase_sanitizer._sanitization_errors == 0

    # Configuration validation tests
    def test_should_validate_unwrapping_method(self, mock_logger):
        """Should validate unwrapping method."""
        invalid_config = {
            'unwrapping_method': 'invalid_method',
            'outlier_threshold': 3.0,
            'smoothing_window': 5
        }
        
        with pytest.raises(ValueError, match="Invalid unwrapping method"):
            PhaseSanitizer(config=invalid_config, logger=mock_logger)

    def test_should_validate_outlier_threshold(self, mock_logger):
        """Should validate outlier threshold."""
        invalid_config = {
            'unwrapping_method': 'numpy',
            'outlier_threshold': -1.0,  # Negative threshold
            'smoothing_window': 5
        }
        
        with pytest.raises(ValueError, match="outlier_threshold must be positive"):
            PhaseSanitizer(config=invalid_config, logger=mock_logger)

    def test_should_validate_smoothing_window(self, mock_logger):
        """Should validate smoothing window."""
        invalid_config = {
            'unwrapping_method': 'numpy',
            'outlier_threshold': 3.0,
            'smoothing_window': 0  # Invalid window size
        }
        
        with pytest.raises(ValueError, match="smoothing_window must be positive"):
            PhaseSanitizer(config=invalid_config, logger=mock_logger)

    # Edge case tests
    def test_should_handle_single_antenna_data(self, phase_sanitizer):
        """Should handle single antenna phase data."""
        single_antenna_phase = np.random.uniform(-np.pi, np.pi, (1, 56))
        
        result = phase_sanitizer.sanitize_phase(single_antenna_phase)
        
        assert result.shape == single_antenna_phase.shape

    def test_should_handle_small_phase_arrays(self, phase_sanitizer):
        """Should handle small phase arrays."""
        small_phase = np.random.uniform(-np.pi, np.pi, (2, 5))
        
        result = phase_sanitizer.sanitize_phase(small_phase)
        
        assert result.shape == small_phase.shape

    def test_should_handle_constant_phase_data(self, phase_sanitizer):
        """Should handle constant phase data."""
        constant_phase = np.full((3, 20), 0.5)
        
        result = phase_sanitizer.sanitize_phase(constant_phase)
        
        assert result.shape == constant_phase.shape