"""
Tests for OCR Service Pool

Tests OCRServicePool functionality including acquire, release, and concurrency.
"""

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

# Mock paddle before importing service_pool to avoid import errors
# when paddle is not installed in the test environment
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.service_pool import (
    OCRServicePool,
    PooledService,
    PoolConfig,
    ServiceState,
    get_service_pool,
    shutdown_service_pool,
)


class TestPoolConfig:
    """Tests for PoolConfig class"""

    def test_default_values(self):
        """Test default configuration values"""
        config = PoolConfig()
        assert config.max_services_per_device == 1
        assert config.max_total_services == 2
        assert config.acquire_timeout_seconds == 300.0
        assert config.max_queue_size == 50
        assert config.max_consecutive_errors == 3

    def test_custom_values(self):
        """Test custom configuration values"""
        config = PoolConfig(
            max_services_per_device=2,
            max_total_services=4,
            acquire_timeout_seconds=60.0,
        )
        assert config.max_services_per_device == 2
        assert config.max_total_services == 4
        assert config.acquire_timeout_seconds == 60.0


class TestPooledService:
    """Tests for PooledService class"""

    def test_creation(self):
        """Test PooledService creation"""
        mock_service = Mock()
        pooled = PooledService(
            service=mock_service,
            device="GPU:0",
        )
        assert pooled.service is mock_service
        assert pooled.device == "GPU:0"
        assert pooled.state == ServiceState.AVAILABLE
        assert pooled.use_count == 0
        assert pooled.error_count == 0


class TestOCRServicePool:
    """Tests for OCRServicePool class"""

    def setup_method(self):
        """Reset singleton before each test"""
        shutdown_service_pool()
        OCRServicePool._instance = None
        OCRServicePool._lock = threading.Lock()

    def teardown_method(self):
        """Cleanup after each test"""
        shutdown_service_pool()
        OCRServicePool._instance = None

    def test_singleton_pattern(self):
        """Test that OCRServicePool is a singleton"""
        pool1 = OCRServicePool()
        pool2 = OCRServicePool()
        assert pool1 is pool2
        pool1.shutdown()

    def test_initialize_device(self):
        """Test device initialization"""
        config = PoolConfig()
        pool = OCRServicePool(config)

        # Default device should be initialized
        assert "GPU:0" in pool.services
        assert "GPU:0" in pool.semaphores

        # Test adding new device
        pool._initialize_device("GPU:1")
        assert "GPU:1" in pool.services
        assert "GPU:1" in pool.semaphores

        pool.shutdown()

    def test_acquire_creates_service(self):
        """Test that acquire creates a new service if none available"""
        config = PoolConfig(max_services_per_device=1)
        pool = OCRServicePool(config)

        # Pre-populate with a mock service
        mock_service = Mock()
        mock_service.process = Mock()
        mock_service.get_gpu_status = Mock()
        pooled_service = PooledService(service=mock_service, device="GPU:0")
        pool.services["GPU:0"].append(pooled_service)

        pooled = pool.acquire(device="GPU:0", timeout=5.0)
        assert pooled is not None
        assert pooled.state == ServiceState.IN_USE
        assert pooled.use_count == 1

        pool.shutdown()

    def test_acquire_reuses_available_service(self):
        """Test that acquire reuses available services"""
        config = PoolConfig(max_services_per_device=1)
        pool = OCRServicePool(config)

        # Pre-populate with a mock service
        mock_service = Mock()
        pooled_service = PooledService(service=mock_service, device="GPU:0")
        pool.services["GPU:0"].append(pooled_service)

        # First acquire
        pooled1 = pool.acquire(device="GPU:0")
        service_id = id(pooled1.service)
        pool.release(pooled1)

        # Second acquire should get the same service
        pooled2 = pool.acquire(device="GPU:0")
        assert id(pooled2.service) == service_id
        assert pooled2.use_count == 2

        pool.shutdown()

    def test_release_makes_service_available(self):
        """Test that release makes service available again"""
        config = PoolConfig()
        pool = OCRServicePool(config)

        # Pre-populate with a mock service
        mock_service = Mock()
        pooled_service = PooledService(service=mock_service, device="GPU:0")
        pool.services["GPU:0"].append(pooled_service)

        pooled = pool.acquire(device="GPU:0")
        assert pooled.state == ServiceState.IN_USE

        pool.release(pooled)
        assert pooled.state == ServiceState.AVAILABLE

        pool.shutdown()

    def test_release_with_error(self):
        """Test that release with error increments error count"""
        config = PoolConfig(max_consecutive_errors=3)
        pool = OCRServicePool(config)

        # Pre-populate with a mock service
        mock_service = Mock()
        pooled_service = PooledService(service=mock_service, device="GPU:0")
        pool.services["GPU:0"].append(pooled_service)

        pooled = pool.acquire(device="GPU:0")
        pool.release(pooled, error=Exception("Test error"))

        assert pooled.error_count == 1
        assert pooled.state == ServiceState.AVAILABLE

        pool.shutdown()

    def test_release_marks_unhealthy_after_errors(self):
        """Test that service is marked unhealthy after too many errors"""
        config = PoolConfig(max_consecutive_errors=2)
        pool = OCRServicePool(config)

        # Pre-populate with a mock service
        mock_service = Mock()
        pooled_service = PooledService(service=mock_service, device="GPU:0")
        pool.services["GPU:0"].append(pooled_service)

        pooled = pool.acquire(device="GPU:0")
        pool.release(pooled, error=Exception("Error 1"))

        pooled = pool.acquire(device="GPU:0")
        pool.release(pooled, error=Exception("Error 2"))

        assert pooled.state == ServiceState.UNHEALTHY
        assert pooled.error_count == 2

        pool.shutdown()

    def test_acquire_context_manager(self):
        """Test context manager for acquire/release"""
        config = PoolConfig()
        pool = OCRServicePool(config)

        # Pre-populate with a mock service
        mock_service = Mock()
        pooled_service = PooledService(service=mock_service, device="GPU:0")
        pool.services["GPU:0"].append(pooled_service)

        with pool.acquire_context(device="GPU:0") as pooled:
            assert pooled is not None
            assert pooled.state == ServiceState.IN_USE

        # After context, service should be available
        assert pooled.state == ServiceState.AVAILABLE

        pool.shutdown()

    def test_acquire_context_manager_with_error(self):
        """Test context manager releases on error"""
        config = PoolConfig()
        pool = OCRServicePool(config)

        # Pre-populate with a mock service
        mock_service = Mock()
        pooled_service = PooledService(service=mock_service, device="GPU:0")
        pool.services["GPU:0"].append(pooled_service)

        with pytest.raises(ValueError):
            with pool.acquire_context(device="GPU:0") as pooled:
                raise ValueError("Test error")

        # Service should still be available after error
        assert pooled.error_count == 1

        pool.shutdown()

    def test_acquire_timeout(self):
        """Test that acquire times out when no service available"""
        config = PoolConfig(
            max_services_per_device=1,
            max_total_services=1,
        )
        pool = OCRServicePool(config)

        # Pre-populate with a mock service
        mock_service = Mock()
        pooled_service = PooledService(service=mock_service, device="GPU:0")
        pool.services["GPU:0"].append(pooled_service)

        # Acquire the only service
        pooled1 = pool.acquire(device="GPU:0")
        assert pooled1 is not None

        # Try to acquire another - should timeout
        pooled2 = pool.acquire(device="GPU:0", timeout=0.5)
        assert pooled2 is None

        pool.shutdown()

    def test_get_pool_stats(self):
        """Test pool statistics"""
        config = PoolConfig()
        pool = OCRServicePool(config)

        # Pre-populate with a mock service
        mock_service = Mock()
        pooled_service = PooledService(service=mock_service, device="GPU:0")
        pool.services["GPU:0"].append(pooled_service)

        # Acquire a service
        pooled = pool.acquire(device="GPU:0")

        stats = pool.get_pool_stats()
        assert stats["total_services"] == 1
        assert stats["in_use_services"] == 1
        assert stats["available_services"] == 0
        assert stats["metrics"]["total_acquisitions"] == 1

        pool.release(pooled)

        stats = pool.get_pool_stats()
        assert stats["available_services"] == 1
        assert stats["metrics"]["total_releases"] == 1

        pool.shutdown()

    def test_health_check(self):
        """Test health check functionality"""
        config = PoolConfig()
        pool = OCRServicePool(config)

        # Pre-populate with a mock service
        mock_service = Mock()
        mock_service.process = Mock()
        mock_service.get_gpu_status = Mock()
        pooled_service = PooledService(service=mock_service, device="GPU:0")
        pool.services["GPU:0"].append(pooled_service)

        # Acquire and release to update use_count
        pooled = pool.acquire(device="GPU:0")
        pool.release(pooled)

        health = pool.health_check()
        assert health["healthy"] is True
        assert len(health["services"]) == 1
        assert health["services"][0]["responsive"] is True

        pool.shutdown()

    def test_concurrent_acquire(self):
        """Test concurrent service acquisition"""
        config = PoolConfig(
            max_services_per_device=2,
            max_total_services=2,
        )
        pool = OCRServicePool(config)

        # Pre-populate with 2 mock services
        for i in range(2):
            mock_service = Mock()
            pooled_service = PooledService(service=mock_service, device="GPU:0")
            pool.services["GPU:0"].append(pooled_service)

        results = []

        def worker(worker_id):
            pooled = pool.acquire(device="GPU:0", timeout=5.0, task_id=f"task_{worker_id}")
            if pooled:
                results.append((worker_id, pooled))
                time.sleep(0.1)  # Simulate work
                pool.release(pooled)

        threads = [threading.Thread(target=worker, args=(i,)) for i in range(4)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        # All workers should have acquired a service
        assert len(results) == 4

        pool.shutdown()


class TestGetServicePool:
    """Tests for get_service_pool helper function"""

    def setup_method(self):
        """Reset singleton before each test"""
        shutdown_service_pool()
        OCRServicePool._instance = None

    def teardown_method(self):
        """Cleanup after each test"""
        shutdown_service_pool()
        OCRServicePool._instance = None

    def test_get_service_pool_creates_singleton(self):
        """Test that get_service_pool creates a singleton"""
        pool1 = get_service_pool()
        pool2 = get_service_pool()
        assert pool1 is pool2
        shutdown_service_pool()

    def test_shutdown_service_pool(self):
        """Test shutdown_service_pool cleans up"""
        pool = get_service_pool()
        shutdown_service_pool()

        # Should be able to create new pool
        new_pool = get_service_pool()
        assert new_pool._initialized is True
        shutdown_service_pool()


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