import pytest
import pandas as pd
import numpy as np
import logging
from unittest.mock import MagicMock
from DataCleaningPipeline import DataCleaningPipeline, PipelineConfig
from DataCleaningPipeline.config import ConfigurationError

class TestAuditPhase2:
    
    def test_config_validation(self):
        """Test that invalid configurations raise errors"""
        # Invalid log level
        with pytest.raises(ConfigurationError, match="Invalid log_level"):
            PipelineConfig(log_level="INVALID_LEVEL")
            
        # Invalid Quality Score
        with pytest.raises(ConfigurationError, match="min_quality_score"):
            PipelineConfig(min_quality_score=150.0)
            
        with pytest.raises(ConfigurationError, match="min_quality_score"):
            PipelineConfig(min_quality_score=-1.0)
            
        # Invalid Memory Limit
        with pytest.raises(ConfigurationError, match="max_memory_mb"):
            PipelineConfig(max_memory_mb=-100)
            
        # Invalid max_checkpoints
        with pytest.raises(ConfigurationError, match="max_checkpoints"):
            PipelineConfig(max_checkpoints=-5)

    def test_memory_management(self):
        """Test that checkpoints are dropped when memory limit is exceeded"""
        # Create a config with very low memory limit (2MB) - increased to be safe
        config = PipelineConfig(max_memory_mb=2, max_checkpoints=10)
        df = pd.DataFrame(np.random.randn(5000, 10)) # Approx 0.4MB payload + overhead
        
        mem_usage = df.memory_usage(deep=True).sum() / (1024 * 1024)
        print(f"\nDataFrame Memory: {mem_usage:.4f} MB")
        
        pipeline = DataCleaningPipeline(df, config)
        
        # Initial state is already checkpointed
        print(f"History after init: {len(pipeline._history)}")
        assert len(pipeline._history) == 1
        
        # 1. 0.8MB < 2MB (Init + 1)
        pipeline._checkpoint_data()
        print(f"History after 1: {len(pipeline._history)}")
        assert len(pipeline._history) == 2
        
        # 2. 1.2MB < 2MB
        pipeline._checkpoint_data()
        print(f"History after 2: {len(pipeline._history)}")
        assert len(pipeline._history) == 3

        # 3. 1.6MB < 2MB
        pipeline._checkpoint_data()
        print(f"History after 3: {len(pipeline._history)}")
        assert len(pipeline._history) == 4
        
        # 4. 2.0MB <= 2MB
        pipeline._checkpoint_data()
        print(f"History after 4: {len(pipeline._history)}")
        # Might be 5 if exactly under limit
        assert len(pipeline._history) == 5
        
        # 5. 2.4MB > 2MB. Should trigger drop.
        pipeline._checkpoint_data()
        print(f"History after 5: {len(pipeline._history)}")
        # Should drop the oldest to make space.
        # 2.4 - 0.4 = 2.0. So should stay at 5.
        assert len(pipeline._history) == 5
        
        # Add more to stress it
        for _ in range(5):
             pipeline._checkpoint_data()
             
        # History should remain stable around 5 (2.0 MB)
        print(f"Final History: {len(pipeline._history)}")
        assert len(pipeline._history) == 5

    def test_custom_function_security_logging(self):
        """Test that custom function execution is logged"""
        config = PipelineConfig(allow_custom_functions=True)
        df = pd.DataFrame({'val': [1, 2, 3]})
        
        # Mock logger
        mock_logger = MagicMock()
        pipeline = DataCleaningPipeline(df, config, logger=mock_logger)
        
        def double(x):
            return x * 2
            
        pipeline.apply_custom_function('val', double)
        
        # Check if warning was logged
        # Expected calls to warning:
        # 1. "SECURITY: Executing custom function..."
        
        assert mock_logger.warning.call_count >= 1
        call_args = mock_logger.warning.call_args[0][0]
        assert "SECURITY" in call_args
        assert "double" in call_args
