"""
Tests for OCR Service Memory Integration

Tests the integration of MemoryGuard with OCRService patterns,
including pre-operation memory checks and CPU fallback logic.
"""

import pytest
from unittest.mock import Mock, patch, MagicMock
import sys

# Mock paddle before importing memory_manager
paddle_mock = MagicMock()
paddle_mock.is_compiled_with_cuda.return_value = False
paddle_mock.device.cuda.device_count.return_value = 0
paddle_mock.device.cuda.memory_allocated.return_value = 0
paddle_mock.device.cuda.memory_reserved.return_value = 0
paddle_mock.device.cuda.empty_cache = MagicMock()
sys.modules['paddle'] = paddle_mock

from app.services.memory_manager import (
    MemoryGuard,
    MemoryConfig,
    MemoryStats,
)


class TestMemoryGuardIntegration:
    """Tests for MemoryGuard integration patterns used in OCRService"""

    def setup_method(self):
        """Setup for each test"""
        self.config = MemoryConfig(
            warning_threshold=0.80,
            critical_threshold=0.95,
            emergency_threshold=0.98,
            enable_cpu_fallback=True,
        )

    def teardown_method(self):
        """Cleanup after each test"""
        pass

    def test_memory_check_below_threshold_allows_processing(self):
        """Test that memory check returns True when below thresholds"""
        guard = MemoryGuard(self.config)

        # Mock stats below warning threshold
        with patch.object(guard, 'get_memory_stats') as mock_stats:
            mock_stats.return_value = MemoryStats(
                gpu_used_ratio=0.50,
                gpu_free_mb=4000,
                gpu_total_mb=8000,
            )

            is_available, stats = guard.check_memory(required_mb=2000)

            assert is_available is True
            assert stats.gpu_free_mb >= 2000

        guard.shutdown()

    def test_memory_check_above_critical_blocks_processing(self):
        """Test that memory check returns False when above critical threshold"""
        guard = MemoryGuard(self.config)

        # Mock stats above critical threshold
        with patch.object(guard, 'get_memory_stats') as mock_stats:
            mock_stats.return_value = MemoryStats(
                gpu_used_ratio=0.96,
                gpu_free_mb=320,
                gpu_total_mb=8000,
            )

            is_available, stats = guard.check_memory(required_mb=1000)

            assert is_available is False

        guard.shutdown()

    def test_memory_check_insufficient_free_memory(self):
        """Test that memory check returns False when free memory < required"""
        guard = MemoryGuard(self.config)

        # Mock stats with insufficient free memory but below critical ratio
        with patch.object(guard, 'get_memory_stats') as mock_stats:
            mock_stats.return_value = MemoryStats(
                gpu_used_ratio=0.70,
                gpu_free_mb=500,
                gpu_total_mb=8000,
            )

            is_available, stats = guard.check_memory(required_mb=1000)

            # Should return False (not enough free memory)
            assert is_available is False

        guard.shutdown()


class TestCPUFallbackPattern:
    """Tests for CPU fallback pattern as used in OCRService"""

    def test_cpu_fallback_activation_pattern(self):
        """Test the CPU fallback activation pattern"""
        # Simulate the pattern used in OCRService._activate_cpu_fallback

        class MockOCRService:
            def __init__(self):
                self._cpu_fallback_active = False
                self.use_gpu = True
                self.gpu_available = True
                self.gpu_info = {'device_id': 0}
                self._memory_guard = Mock()

            def _activate_cpu_fallback(self):
                if self._cpu_fallback_active:
                    return

                self._cpu_fallback_active = True
                self.use_gpu = False
                self.gpu_info['cpu_fallback'] = True
                self.gpu_info['fallback_reason'] = 'GPU memory insufficient'

                if self._memory_guard:
                    self._memory_guard.clear_gpu_cache()

        service = MockOCRService()

        # Verify initial state
        assert service._cpu_fallback_active is False
        assert service.use_gpu is True

        # Activate fallback
        service._activate_cpu_fallback()

        # Verify fallback state
        assert service._cpu_fallback_active is True
        assert service.use_gpu is False
        assert service.gpu_info.get('cpu_fallback') is True
        service._memory_guard.clear_gpu_cache.assert_called_once()

    def test_cpu_fallback_idempotent(self):
        """Test that CPU fallback activation is idempotent"""
        class MockOCRService:
            def __init__(self):
                self._cpu_fallback_active = False
                self.use_gpu = True
                self._memory_guard = Mock()
                self.gpu_info = {}

            def _activate_cpu_fallback(self):
                if self._cpu_fallback_active:
                    return
                self._cpu_fallback_active = True
                self.use_gpu = False
                if self._memory_guard:
                    self._memory_guard.clear_gpu_cache()

        service = MockOCRService()

        # Activate twice
        service._activate_cpu_fallback()
        service._activate_cpu_fallback()

        # clear_gpu_cache should only be called once
        assert service._memory_guard.clear_gpu_cache.call_count == 1

    def test_gpu_mode_restoration_pattern(self):
        """Test the GPU mode restoration pattern"""
        # Simulate the pattern used in OCRService._restore_gpu_mode

        class MockOCRService:
            def __init__(self):
                self._cpu_fallback_active = True
                self.use_gpu = False
                self.gpu_available = True
                self.gpu_info = {
                    'device_id': 0,
                    'cpu_fallback': True,
                    'fallback_reason': 'test'
                }
                self._memory_guard = Mock()

            def _restore_gpu_mode(self):
                if not self._cpu_fallback_active:
                    return

                if not self.gpu_available:
                    return

                # Check if GPU memory is now available
                if self._memory_guard:
                    is_available, stats = self._memory_guard.check_memory(required_mb=2000)
                    if is_available:
                        self._cpu_fallback_active = False
                        self.use_gpu = True
                        self.gpu_info.pop('cpu_fallback', None)
                        self.gpu_info.pop('fallback_reason', None)

        service = MockOCRService()

        # Mock memory guard to indicate sufficient memory
        mock_stats = Mock()
        mock_stats.gpu_free_mb = 5000
        service._memory_guard.check_memory.return_value = (True, mock_stats)

        # Restore GPU mode
        service._restore_gpu_mode()

        # Verify GPU mode restored
        assert service._cpu_fallback_active is False
        assert service.use_gpu is True
        assert 'cpu_fallback' not in service.gpu_info

    def test_gpu_mode_not_restored_when_memory_still_low(self):
        """Test that GPU mode is not restored when memory is still low"""
        class MockOCRService:
            def __init__(self):
                self._cpu_fallback_active = True
                self.use_gpu = False
                self.gpu_available = True
                self.gpu_info = {'cpu_fallback': True}
                self._memory_guard = Mock()

            def _restore_gpu_mode(self):
                if not self._cpu_fallback_active:
                    return
                if not self.gpu_available:
                    return
                if self._memory_guard:
                    is_available, stats = self._memory_guard.check_memory(required_mb=2000)
                    if is_available:
                        self._cpu_fallback_active = False
                        self.use_gpu = True

        service = MockOCRService()

        # Mock memory guard to indicate insufficient memory
        mock_stats = Mock()
        mock_stats.gpu_free_mb = 500
        service._memory_guard.check_memory.return_value = (False, mock_stats)

        # Try to restore GPU mode
        service._restore_gpu_mode()

        # Verify still in fallback mode
        assert service._cpu_fallback_active is True
        assert service.use_gpu is False


class TestPreOperationMemoryCheckPattern:
    """Tests for pre-operation memory check pattern as used in OCRService"""

    def test_pre_operation_check_with_fallback(self):
        """Test the pre-operation memory check pattern with fallback"""
        guard = MemoryGuard(MemoryConfig(
            warning_threshold=0.80,
            critical_threshold=0.95,
            enable_cpu_fallback=True,
        ))

        # Simulate the pattern:
        # 1. Check if in CPU fallback mode
        # 2. Try to restore GPU mode if memory available
        # 3. Perform memory check for operation

        class MockService:
            def __init__(self):
                self._cpu_fallback_active = False
                self.use_gpu = True
                self.gpu_available = True
                self._memory_guard = guard

            def _restore_gpu_mode(self):
                pass  # Simplified

            def pre_operation_check(self, required_mb: int) -> bool:
                # Try restore first
                if self._cpu_fallback_active:
                    self._restore_gpu_mode()

                # Perform memory check
                if not self.use_gpu:
                    return True  # CPU mode, no GPU check needed

                is_available, stats = self._memory_guard.check_memory(required_mb=required_mb)
                return is_available

        service = MockService()

        # Mock sufficient memory
        with patch.object(guard, 'get_memory_stats') as mock_stats:
            mock_stats.return_value = MemoryStats(
                gpu_used_ratio=0.50,
                gpu_free_mb=4000,
                gpu_total_mb=8000,
            )

            result = service.pre_operation_check(required_mb=2000)
            assert result is True

        guard.shutdown()

    def test_pre_operation_check_returns_true_in_cpu_mode(self):
        """Test that pre-operation check returns True when in CPU mode"""
        class MockService:
            def __init__(self):
                self._cpu_fallback_active = True
                self.use_gpu = False
                self._memory_guard = Mock()

            def pre_operation_check(self, required_mb: int) -> bool:
                if not self.use_gpu:
                    return True  # CPU mode, no GPU check needed
                return False

        service = MockService()
        result = service.pre_operation_check(required_mb=5000)

        # Should return True because we're in CPU mode
        assert result is True
        # Memory guard should not be called
        service._memory_guard.check_memory.assert_not_called()


class TestMemoryCheckWithCleanup:
    """Tests for memory check with cleanup pattern"""

    def test_memory_check_triggers_cleanup_on_failure(self):
        """Test that memory check triggers cleanup when insufficient"""
        guard = MemoryGuard(MemoryConfig(
            warning_threshold=0.80,
            critical_threshold=0.95,
        ))

        # Track cleanup calls
        cleanup_called = False

        def mock_cleanup():
            nonlocal cleanup_called
            cleanup_called = True

        class MockService:
            def __init__(self):
                self._memory_guard = guard
                self.cleanup_func = mock_cleanup

            def check_gpu_memory(self, required_mb: int) -> bool:
                # First check
                with patch.object(self._memory_guard, 'get_memory_stats') as mock_stats:
                    # First call - low memory
                    mock_stats.return_value = MemoryStats(
                        gpu_used_ratio=0.96,
                        gpu_free_mb=300,
                        gpu_total_mb=8000,
                    )

                    is_available, stats = self._memory_guard.check_memory(required_mb=required_mb)

                    if not is_available:
                        # Trigger cleanup
                        self.cleanup_func()
                        self._memory_guard.clear_gpu_cache()
                        return False

                return True

        service = MockService()
        result = service.check_gpu_memory(required_mb=1000)

        # Cleanup should have been triggered
        assert cleanup_called is True
        assert result is False

        guard.shutdown()


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