"""
Performance and stability testing for rateloader.

Tests long-running stability, memory usage, and performance characteristics.
Validates that the application can run continuously without issues.
"""

import os
import sys
import time
import tempfile
import shutil
import unittest
import threading
import psutil
import gc
from unittest.mock import Mock, patch
from datetime import datetime, timedelta

# Add current directory to path for imports
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

from core.config import get_config, reload_config
from core.logger import setup_logging
from core.task_queue import get_task_queue, start_task_queue, stop_task_queue
from core.scheduler import get_scheduler, start_scheduler, stop_scheduler
from core.http_server import get_http_server, start_server, stop_server
from core.tasks import ExportMailerTask, PriceExportTask


class TestPerformanceStability(unittest.TestCase):
    """Performance and stability tests for rateloader."""

    @classmethod
    def setUpClass(cls):
        """Set up test environment."""
        # Create temporary directory for test files
        cls.test_dir = tempfile.mkdtemp()
        cls.original_dir = os.getcwd()
        os.chdir(cls.test_dir)
        
        # Create log directory
        os.makedirs("log", exist_ok=True)
        
        # Initialize test configuration
        cls._setup_test_config()
        
        # Initialize logging
        cls.logger_manager = setup_logging()
        
        print(f"Performance test environment set up in: {cls.test_dir}")

    @classmethod
    def tearDownClass(cls):
        """Clean up test environment."""
        # Restore original directory
        os.chdir(cls.original_dir)
        
        # Clean up test directory
        shutil.rmtree(cls.test_dir, ignore_errors=True)
        
        print(f"Performance test environment cleaned up: {cls.test_dir}")

    def setUp(self):
        """Set up for each test."""
        # Reload configuration
        reload_config()
        self.config = get_config()
        
        # Get process for memory monitoring
        self.process = psutil.Process()

    def tearDown(self):
        """Clean up after each test."""
        # Stop all components
        try:
            stop_server()
            stop_scheduler()
            stop_task_queue()
        except:
            pass
        
        # Wait for threads to stop
        time.sleep(0.5)
        
        # Force garbage collection
        gc.collect()

    @classmethod
    def _setup_test_config(cls):
        """Set up test configuration."""
        config = get_config()
        
        # Test configuration values
        test_config = {
            "oracleconnectstring": "test/test@test",
            "nls_lang": "AMERICAN_AMERICA.CL8MSWIN1251",
            "file_encoding": "utf-8",
            "url": "http://test.com/select.sql",
            "block-url": "http://test.com/selectBlocks.sql",
            "sendfrom": "test@example.com",
            "sendto": "recipient@example.com",
            "mailserver": "smtp.test.com",
            "port": "587",
            "pwd": "testpass",
            "ssl": "1",
            "api_url": "http://test.com/api/prices",
            "token": "test-token",
            "months": "1",
            "server_port": "8001",
            "schedule_hours": "9,15,21"
        }
        
        for key, value in test_config.items():
            config.set(key, value)

    def _get_memory_usage(self):
        """Get current memory usage in MB."""
        return self.process.memory_info().rss / 1024 / 1024

    def _get_cpu_usage(self):
        """Get current CPU usage percentage."""
        return self.process.cpu_percent()

    def test_memory_usage_baseline(self):
        """Test baseline memory usage."""
        # Measure memory usage at startup
        initial_memory = self._get_memory_usage()
        
        # Should be reasonable (less than 100MB)
        self.assertLess(initial_memory, 100, 
                       f"Initial memory usage too high: {initial_memory:.2f}MB")

    def test_memory_usage_task_queue_operations(self):
        """Test memory usage during task queue operations."""
        # Start task queue
        self.assertTrue(start_task_queue())
        task_queue = get_task_queue()
        
        # Measure initial memory
        initial_memory = self._get_memory_usage()
        
        # Create and enqueue many tasks
        tasks = []
        for i in range(100):
            task = ExportMailerTask()
            task.name = f"TestTask_{i}"
            tasks.append(task)
            task_queue.enqueue(task)
        
        # Wait for tasks to be processed
        time.sleep(2)
        
        # Measure memory after operations
        final_memory = self._get_memory_usage()
        memory_growth = final_memory - initial_memory
        
        # Memory growth should be reasonable (less than 50MB)
        self.assertLess(memory_growth, 50,
                       f"Memory growth excessive: {memory_growth:.2f}MB")
        
        # Verify tasks were processed
        status = task_queue.get_status()
        self.assertEqual(status["statistics"]["total_enqueued"], 100)

    def test_memory_usage_scheduler_long_running(self):
        """Test memory usage during long-running scheduler."""
        # Start scheduler
        self.assertTrue(start_scheduler())
        scheduler = get_scheduler()
        
        # Measure initial memory
        initial_memory = self._get_memory_usage()
        
        # Let scheduler run for a while
        time.sleep(10)
        
        # Measure memory after running
        final_memory = self._get_memory_usage()
        memory_growth = final_memory - initial_memory
        
        # Memory growth should be minimal (less than 10MB)
        self.assertLess(memory_growth, 10,
                       f"Scheduler memory growth excessive: {memory_growth:.2f}MB")

    def test_memory_usage_http_server_requests(self):
        """Test memory usage during HTTP server operations."""
        # Start HTTP server
        self.assertTrue(start_server())
        time.sleep(0.5)
        
        # Measure initial memory
        initial_memory = self._get_memory_usage()
        
        try:
            import requests
            
            # Make many requests
            for i in range(50):
                try:
                    response = requests.get("http://localhost:8001/ping", timeout=1)
                    self.assertEqual(response.status_code, 200)
                except requests.exceptions.ConnectionError:
                    self.skipTest("HTTP server not available")
                except requests.exceptions.Timeout:
                    pass  # Continue on timeout
            
            # Measure memory after requests
            final_memory = self._get_memory_usage()
            memory_growth = final_memory - initial_memory
            
            # Memory growth should be reasonable (less than 20MB)
            self.assertLess(memory_growth, 20,
                           f"HTTP server memory growth excessive: {memory_growth:.2f}MB")
            
        except ImportError:
            self.skipTest("requests module not available")

    def test_cpu_usage_baseline(self):
        """Test baseline CPU usage."""
        # Measure CPU usage over time
        cpu_samples = []
        for _ in range(10):
            cpu_samples.append(self._get_cpu_usage())
            time.sleep(0.5)
        
        # Average CPU usage should be low (less than 5% when idle)
        avg_cpu = sum(cpu_samples) / len(cpu_samples)
        self.assertLess(avg_cpu, 5.0,
                       f"Idle CPU usage too high: {avg_cpu:.2f}%")

    def test_cpu_usage_under_load(self):
        """Test CPU usage under load."""
        # Start task queue
        self.assertTrue(start_task_queue())
        task_queue = get_task_queue()
        
        # Create CPU-intensive tasks (mocked to avoid actual work)
        class CPUIntensiveTask:
            def __init__(self, name):
                self.name = name
                self.error_message = None
            
            def execute(self):
                # Simulate CPU work
                end_time = time.time() + 0.1
                while time.time() < end_time:
                    pass
                return True
        
        # Enqueue many tasks
        for i in range(20):
            task = CPUIntensiveTask(f"CPUTask_{i}")
            # Note: This would need to be adapted to work with actual task system
        
        # Measure CPU usage during load
        cpu_samples = []
        for _ in range(10):
            cpu_samples.append(self._get_cpu_usage())
            time.sleep(0.5)
        
        # CPU usage should be reasonable (less than 80% under load)
        max_cpu = max(cpu_samples)
        self.assertLess(max_cpu, 80.0,
                       f"CPU usage under load too high: {max_cpu:.2f}%")

    def test_long_running_stability(self):
        """Test long-running stability."""
        # Start all components
        self.assertTrue(start_task_queue())
        self.assertTrue(start_scheduler())
        self.assertTrue(start_server())
        
        # Monitor for stability issues
        start_time = time.time()
        test_duration = 30  # 30 seconds for testing (would be longer in production)
        
        errors_detected = []
        memory_samples = []
        
        while time.time() - start_time < test_duration:
            try:
                # Check component status
                task_queue = get_task_queue()
                scheduler = get_scheduler()
                http_server = get_http_server()
                
                if not task_queue.is_running():
                    errors_detected.append("Task queue stopped running")
                
                if not scheduler.is_running():
                    errors_detected.append("Scheduler stopped running")
                
                if not http_server.is_running:
                    errors_detected.append("HTTP server stopped running")
                
                # Sample memory usage
                memory_samples.append(self._get_memory_usage())
                
                time.sleep(1)
                
            except Exception as e:
                errors_detected.append(f"Exception during stability test: {str(e)}")
        
        # Verify no critical errors
        self.assertEqual(len(errors_detected), 0,
                       f"Stability issues detected: {errors_detected}")
        
        # Verify memory usage is stable
        if len(memory_samples) > 1:
            memory_growth = memory_samples[-1] - memory_samples[0]
            self.assertLess(memory_growth, 20,
                           f"Memory growth during stability test: {memory_growth:.2f}MB")

    def test_concurrent_operations_stability(self):
        """Test stability during concurrent operations."""
        # Start all components
        self.assertTrue(start_task_queue())
        self.assertTrue(start_scheduler())
        self.assertTrue(start_server())
        
        errors = []
        
        def task_queue_worker():
            """Worker that enqueues tasks."""
            try:
                task_queue = get_task_queue()
                for i in range(20):
                    task = ExportMailerTask()
                    task.name = f"ConcurrentTask_{i}"
                    task_queue.enqueue(task)
                    time.sleep(0.1)
            except Exception as e:
                errors.append(f"Task queue worker error: {str(e)}")
        
        def http_request_worker():
            """Worker that makes HTTP requests."""
            try:
                import requests
                for i in range(20):
                    try:
                        response = requests.get("http://localhost:8001/ping", timeout=2)
                        if response.status_code != 200:
                            errors.append(f"HTTP request failed: {response.status_code}")
                    except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):
                        pass  # Expected in test environment
                    time.sleep(0.1)
            except ImportError:
                pass  # requests not available
            except Exception as e:
                errors.append(f"HTTP request worker error: {str(e)}")
        
        def status_check_worker():
            """Worker that checks system status."""
            try:
                for i in range(20):
                    task_queue = get_task_queue()
                    scheduler = get_scheduler()
                    http_server = get_http_server()
                    
                    if not task_queue.is_running():
                        errors.append("Task queue not running during concurrent test")
                    
                    if not scheduler.is_running():
                        errors.append("Scheduler not running during concurrent test")
                    
                    if not http_server.is_running:
                        errors.append("HTTP server not running during concurrent test")
                    
                    time.sleep(0.1)
            except Exception as e:
                errors.append(f"Status check worker error: {str(e)}")
        
        # Start concurrent workers
        workers = [
            threading.Thread(target=task_queue_worker),
            threading.Thread(target=http_request_worker),
            threading.Thread(target=status_check_worker)
        ]
        
        for worker in workers:
            worker.start()
        
        # Wait for all workers to complete
        for worker in workers:
            worker.join(timeout=10)
        
        # Verify no errors occurred
        self.assertEqual(len(errors), 0,
                       f"Concurrent operation errors: {errors}")

    def test_resource_cleanup(self):
        """Test that resources are properly cleaned up."""
        # Start and stop components multiple times
        for i in range(5):
            # Start components
            self.assertTrue(start_task_queue())
            self.assertTrue(start_scheduler())
            self.assertTrue(start_server())
            
            # Let them run briefly
            time.sleep(0.5)
            
            # Stop components
            self.assertTrue(stop_task_queue())
            self.assertTrue(stop_scheduler())
            self.assertTrue(stop_server())
            
            # Wait for cleanup
            time.sleep(0.5)
        
        # Verify no resource leaks (thread count should be reasonable)
        thread_count = threading.active_count()
        self.assertLess(thread_count, 20,
                       f"Too many active threads after cleanup: {thread_count}")

    def test_error_recovery_performance(self):
        """Test performance during error conditions."""
        # Start task queue
        self.assertTrue(start_task_queue())
        task_queue = get_task_queue()
        
        # Create tasks that will fail
        class FailingTask:
            def __init__(self, name):
                self.name = name
                self.error_message = None
            
            def execute(self):
                raise Exception(f"Task {self.name} failed")
        
        # Measure performance during error conditions
        start_time = time.time()
        
        # Enqueue many failing tasks
        for i in range(50):
            task = FailingTask(f"FailingTask_{i}")
            # Note: This would need to be adapted to work with actual task system
        
        # Wait for processing
        time.sleep(2)
        
        end_time = time.time()
        processing_time = end_time - start_time
        
        # Processing should complete in reasonable time (less than 10 seconds)
        self.assertLess(processing_time, 10.0,
                       f"Error processing too slow: {processing_time:.2f}s")

    def test_log_file_rotation_performance(self):
        """Test performance during log file rotation."""
        # Generate many log entries to trigger rotation
        logger = self.logger_manager.get_rateloader_logger()
        
        start_time = time.time()
        
        # Generate log entries
        for i in range(1000):
            logger.info(f"Performance test log entry {i}")
        
        end_time = time.time()
        logging_time = end_time - start_time
        
        # Logging should be fast (less than 5 seconds for 1000 entries)
        self.assertLess(logging_time, 5.0,
                       f"Logging performance too slow: {logging_time:.2f}s")

    def test_configuration_reload_performance(self):
        """Test performance of configuration reloading."""
        # Measure configuration reload time
        start_time = time.time()
        
        for i in range(10):
            reload_config()
            config = get_config()
            config.get("oracleconnectstring")  # Access config
        
        end_time = time.time()
        reload_time = end_time - start_time
        
        # Configuration reload should be fast (less than 1 second for 10 reloads)
        self.assertLess(reload_time, 1.0,
                       f"Configuration reload too slow: {reload_time:.2f}s")

    def test_memory_leak_detection(self):
        """Detect potential memory leaks."""
        # Baseline memory measurement
        initial_memory = self._get_memory_usage()
        
        # Perform operations that might leak memory
        for cycle in range(5):
            # Start components
            start_task_queue()
            start_scheduler()
            start_server()
            
            # Create and destroy objects
            objects = []
            for i in range(100):
                objects.append({"data": "x" * 1000, "index": i})
            
            # Force cleanup
            del objects
            gc.collect()
            
            # Stop components
            stop_task_queue()
            stop_scheduler()
            stop_server()
            
            time.sleep(0.5)
        
        # Final memory measurement
        final_memory = self._get_memory_usage()
        memory_growth = final_memory - initial_memory
        
        # Memory growth should be minimal (less than 30MB)
        self.assertLess(memory_growth, 30,
                       f"Potential memory leak detected: {memory_growth:.2f}MB growth")

    def test_stress_test_high_load(self):
        """Stress test with high load conditions."""
        # Start all components
        self.assertTrue(start_task_queue())
        self.assertTrue(start_scheduler())
        self.assertTrue(start_server())
        
        # Monitor system under stress
        start_time = time.time()
        test_duration = 10  # 10 seconds of stress
        errors = []
        
        def stress_worker(worker_id):
            """Worker that creates stress."""
            try:
                for i in range(50):
                    # Create configuration objects
                    config = get_config()
                    config.get_all_settings()
                    
                    # Create logger objects
                    logger = self.logger_manager.get_rateloader_logger()
                    logger.debug(f"Stress test {worker_id}-{i}")
                    
                    time.sleep(0.01)
            except Exception as e:
                errors.append(f"Stress worker {worker_id} error: {str(e)}")
        
        # Start multiple stress workers
        workers = []
        for i in range(5):
            worker = threading.Thread(target=stress_worker, args=(i,))
            workers.append(worker)
            worker.start()
        
        # Monitor during stress test
        memory_samples = []
        cpu_samples = []
        
        while time.time() - start_time < test_duration:
            memory_samples.append(self._get_memory_usage())
            cpu_samples.append(self._get_cpu_usage())
            time.sleep(0.5)
        
        # Wait for workers to complete
        for worker in workers:
            worker.join(timeout=5)
        
        # Verify system handled stress
        self.assertEqual(len(errors), 0, f"Errors during stress test: {errors}")
        
        # Verify resource usage stayed reasonable
        if memory_samples:
            max_memory = max(memory_samples)
            self.assertLess(max_memory, 200,
                           f"Memory usage too high under stress: {max_memory:.2f}MB")
        
        if cpu_samples:
            avg_cpu = sum(cpu_samples) / len(cpu_samples)
            self.assertLess(avg_cpu, 90,
                           f"CPU usage too high under stress: {avg_cpu:.2f}%")


def run_performance_stability_tests():
    """Run all performance and stability tests."""
    print("Running performance and stability tests for rateloader...")
    print("=" * 60)
    
    # Create test suite
    suite = unittest.TestLoader().loadTestsFromTestCase(TestPerformanceStability)
    
    # Run tests
    runner = unittest.TextTestRunner(verbosity=2)
    result = runner.run(suite)
    
    # Print summary
    print("=" * 60)
    print(f"Tests run: {result.testsRun}")
    print(f"Failures: {len(result.failures)}")
    print(f"Errors: {len(result.errors)}")
    print(f"Skipped: {len(result.skipped)}")
    
    if result.failures:
        print("\nFailures:")
        for test, traceback in result.failures:
            print(f"  - {test}: {traceback}")
    
    if result.errors:
        print("\nErrors:")
        for test, traceback in result.errors:
            print(f"  - {test}: {traceback}")
    
    success = len(result.failures) == 0 and len(result.errors) == 0
    print(f"\nOverall result: {'PASS' if success else 'FAIL'}")
    
    return success


if __name__ == "__main__":
    success = run_performance_stability_tests()
    sys.exit(0 if success else 1)