"""
Performance tests for FMUS-VID optimization utilities.
"""

import pytest
import numpy as np
import time
import threading
from pathlib import Path
import torch
import cupy
from fmusvid.utils.optimization import (
    BatchProcessor,
    BatchConfig,
    FrameCache,
    LazyFrameLoader,
    GPUAccelerator,
    MemoryManager
)

def test_batch_processor_performance():
    """Test BatchProcessor performance with different configurations."""
    # Membuat data uji
    frame_size = (1080, 1920, 3)
    num_frames = 100
    frames = [np.random.randint(0, 255, frame_size, dtype=np.uint8)
             for _ in range(num_frames)]

    def dummy_operation(frame):
        """Simulate frame processing."""
        return frame * 1.5

    # Pengujian dengan konfigurasi berbeda
    configs = [
        BatchConfig(batch_size=1, num_workers=1),
        BatchConfig(batch_size=10, num_workers=2),
        BatchConfig(batch_size=32, num_workers=4),
        BatchConfig(batch_size=32, num_workers=4, use_multiprocessing=True)
    ]

    results = {}
    for config in configs:
        processor = BatchProcessor(config)
        start_time = time.time()
        processor.process_frames(frames, dummy_operation)
        elapsed = time.time() - start_time
        results[f"batch_{config.batch_size}_workers_{config.num_workers}"] = elapsed

    # Memverifikasi bahwa konfigurasi dengan batch lebih besar lebih cepat
    assert results["batch_32_workers_4"] < results["batch_1_workers_1"]

def test_frame_cache_performance():
    """Test FrameCache performance and memory management."""
    cache = FrameCache(max_size_mb=100)  # 100MB cache
    frame_size = (1080, 1920, 3)
    frame = np.random.randint(0, 255, frame_size, dtype=np.uint8)
    frame_size_mb = frame.nbytes / (1024 * 1024)

    # Menguji kecepatan akses cache
    num_frames = 10
    for i in range(num_frames):
        key = f"frame_{i}"
        cache.put(key, frame.copy())

    # Mengukur waktu akses cache vs tanpa cache
    start_time = time.time()
    for i in range(num_frames):
        _ = cache.get(f"frame_{i}")
    cached_time = time.time() - start_time

    start_time = time.time()
    for i in range(num_frames):
        _ = frame.copy()
    uncached_time = time.time() - start_time

    assert cached_time < uncached_time

    # Memverifikasi manajemen memori
    initial_size = len(cache._cache)
    extra_frames = int(100 / frame_size_mb) + 1  # Melebihi ukuran cache
    for i in range(extra_frames):
        cache.put(f"extra_frame_{i}", frame.copy())

    assert len(cache._cache) < initial_size + extra_frames

@pytest.mark.asyncio
async def test_lazy_frame_loader_performance():
    """Test LazyFrameLoader preloading performance."""
    class TestLoader(LazyFrameLoader):
        def _load_frame(self, frame_number):
            time.sleep(0.01)  # Simulasi loading
            return np.random.randint(0, 255, (1080, 1920, 3), dtype=np.uint8)

    loader = TestLoader(Path("test.mp4"), preload_seconds=1.0, max_preload_frames=30)

    # Mengukur waktu akses dengan dan tanpa preloading
    start_time = time.time()
    first_frame = loader.get_frame(0)
    first_access = time.time() - start_time

    # Menunggu preloading
    time.sleep(0.1)

    start_time = time.time()
    next_frame = loader.get_frame(1)
    preloaded_access = time.time() - start_time

    assert preloaded_access < first_access
    assert isinstance(first_frame, np.ndarray)
    assert isinstance(next_frame, np.ndarray)

@pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA not available")
def test_gpu_accelerator_performance():
    """Test GPUAccelerator performance with CUDA."""
    accelerator = GPUAccelerator()

    # Membuat data uji besar
    data_size = (1000, 1000)
    cpu_data = np.random.random(data_size).astype(np.float32)

    # Mengukur waktu operasi pada CPU
    start_time = time.time()
    cpu_result = np.dot(cpu_data, cpu_data.T)
    cpu_time = time.time() - start_time

    # Mengukur waktu operasi pada GPU
    gpu_data = accelerator.to_gpu(cpu_data)
    start_time = time.time()
    if isinstance(gpu_data, torch.Tensor):
        gpu_result = torch.matmul(gpu_data, gpu_data.T)
    else:  # cupy array
        gpu_result = cupy.dot(gpu_data, gpu_data.T)
    gpu_result = accelerator.to_cpu(gpu_result)
    gpu_time = time.time() - start_time

    assert gpu_time < cpu_time
    np.testing.assert_allclose(cpu_result, gpu_result, rtol=1e-5)

def test_memory_manager_performance():
    """Test MemoryManager memory usage optimization."""
    manager = MemoryManager(target_usage_mb=100)

    # Membuat beberapa objek besar
    large_arrays = []
    for i in range(5):
        arr = np.random.random((1000, 1000))
        large_arrays.append(arr)
        manager.register_object(f"array_{i}", arr)

    # Mengukur penggunaan memori sebelum dan sesudah clear_cache
    initial_memory = get_memory_usage()
    manager.clear_cache()
    final_memory = get_memory_usage()

    assert final_memory < initial_memory

def test_batch_processor_error_handling():
    """Test BatchProcessor error handling."""
    processor = BatchProcessor()
    frames = [np.random.randint(0, 255, (100, 100, 3)) for _ in range(10)]

    def failing_operation(_):
        raise ValueError("Simulated error")

    with pytest.raises(ValueError):
        processor.process_frames(frames, failing_operation)

def test_frame_cache_thread_safety():
    """Test FrameCache thread safety."""
    cache = FrameCache(max_size_mb=100)
    frame = np.random.randint(0, 255, (100, 100, 3))

    def cache_operation():
        for i in range(100):
            cache.put(f"thread_{threading.get_ident()}_{i}", frame.copy())
            _ = cache.get(f"thread_{threading.get_ident()}_{i}")

    threads = [threading.Thread(target=cache_operation) for _ in range(4)]
    for thread in threads:
        thread.start()
    for thread in threads:
        thread.join()

    # Tidak ada exception yang dilempar menunjukkan thread safety

def test_optimization_integration():
    """Test integration of multiple optimization components."""
    # Inisialisasi komponen
    processor = BatchProcessor()
    cache = FrameCache()
    accelerator = GPUAccelerator()
    manager = MemoryManager()

    # Membuat data uji
    frames = [np.random.randint(0, 255, (100, 100, 3)) for _ in range(10)]

    # Operasi terintegrasi
    def process_frame(frame):
        # Menggunakan GPU jika tersedia
        gpu_frame = accelerator.to_gpu(frame)
        # Simulasi pemrosesan
        if isinstance(gpu_frame, torch.Tensor):
            result = gpu_frame * 2
        elif isinstance(gpu_frame, cupy.ndarray):
            result = gpu_frame * 2
        else:
            result = frame * 2
        return accelerator.to_cpu(result)

    # Memproses frame dengan batch
    results = processor.process_frames(frames, process_frame)

    # Menyimpan hasil di cache
    for i, result in enumerate(results):
        cache.put(f"result_{i}", result)

    # Membersihkan memori
    manager.clear_cache()
    cache.clear()

    assert len(results) == len(frames)
    for result in results:
        assert isinstance(result, np.ndarray)

def get_memory_usage():
    """Helper function to get current memory usage."""
    import psutil
    process = psutil.Process()
    return process.memory_info().rss

def test_performance_metrics():
    """Test and collect performance metrics."""
    metrics = {}

    # BatchProcessor metrics
    start_time = time.time()
    test_batch_processor_performance()
    metrics['batch_processor'] = time.time() - start_time

    # FrameCache metrics
    start_time = time.time()
    test_frame_cache_performance()
    metrics['frame_cache'] = time.time() - start_time

    # GPU Accelerator metrics (if available)
    if torch.cuda.is_available():
        start_time = time.time()
        test_gpu_accelerator_performance()
        metrics['gpu_accelerator'] = time.time() - start_time

    # Memory Manager metrics
    start_time = time.time()
    test_memory_manager_performance()
    metrics['memory_manager'] = time.time() - start_time

    # Log metrics
    for name, duration in metrics.items():
        print(f"{name} test duration: {duration:.3f} seconds")
