"""
Scheduler integration testing for rateloader.

Tests scheduler timing, task ordering, and integration with task queue.
Validates PRD requirements for scheduled execution at specific times.
"""

import os
import sys
import time
import tempfile
import shutil
import unittest
from unittest.mock import Mock, patch, MagicMock
from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta
import threading

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

from core.scheduler import Scheduler, get_scheduler, start_scheduler, stop_scheduler, get_scheduler_status
from core.task_queue import TaskQueue, get_task_queue, start_task_queue, stop_task_queue
from core.tasks import ExportMailerTask, PriceExportTask
from core.config import get_config, reload_config


class TestSchedulerIntegration(unittest.TestCase):
    """Integration tests for scheduler functionality."""

    @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)
        
        # Backup original settings.ini if exists
        cls.settings_backup = None
        if os.path.exists("settings.ini"):
            cls.settings_backup = "settings.ini.backup"
            shutil.copy("settings.ini", cls.settings_backup)
        
        # Initialize test configuration
        cls._setup_test_config()
        
        print(f"Scheduler 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"Scheduler test environment cleaned up: {cls.test_dir}")

    def setUp(self):
        """Set up for each test."""
        # Reload configuration
        reload_config()
        self.config = get_config()
        
        # Create fresh instances
        self.scheduler = Scheduler()
        self.task_queue = TaskQueue()

    def tearDown(self):
        """Clean up after each test."""
        # Stop scheduler and task queue
        try:
            self.scheduler.stop()
            self.task_queue.stop()
        except:
            pass
        
        # Wait for threads to stop
        time.sleep(0.5)

    @classmethod
    def _setup_test_config(cls):
        """Set up test configuration."""
        config = get_config()
        
        # Test configuration with custom schedule hours for testing
        test_config = {
            "schedule_hours": "9,15,21",  # Test hours
            "server_port": "8001"
        }
        
        for key, value in test_config.items():
            config.set(key, value)

    def test_scheduler_initialization(self):
        """Test scheduler initializes correctly."""
        # Verify schedule hours are loaded
        expected_hours = [9, 15, 21]  # From test config setup
        self.assertEqual(self.scheduler.schedule_hours, expected_hours)
        
        # Verify initial state
        self.assertFalse(self.scheduler.is_running())
        self.assertIsNotNone(self.scheduler._stop_event)
        self.assertIsNone(self.scheduler._scheduler_thread)

    def test_scheduler_start_stop(self):
        """Test scheduler start and stop functionality."""
        # Test starting scheduler
        self.assertTrue(self.scheduler.start())
        self.assertTrue(self.scheduler.is_running())
        self.assertIsNotNone(self.scheduler._scheduler_thread)
        self.assertTrue(self.scheduler._scheduler_thread.is_alive())
        
        # Test stopping scheduler
        self.assertTrue(self.scheduler.stop())
        self.assertFalse(self.scheduler.is_running())
        
        # Wait for thread to stop
        if self.scheduler._scheduler_thread:
            self.scheduler._scheduler_thread.join(timeout=2)
            self.assertFalse(self.scheduler._scheduler_thread.is_alive())

    def test_scheduler_start_already_running(self):
        """Test scheduler start when already running."""
        # Start scheduler
        self.assertTrue(self.scheduler.start())
        
        # Try to start again
        self.assertTrue(self.scheduler.start())  # Should return True but not start new thread
        
        # Clean up
        self.scheduler.stop()

    def test_scheduler_stop_not_running(self):
        """Test scheduler stop when not running."""
        # Stop scheduler when not running
        self.assertTrue(self.scheduler.stop())  # Should return True

    def test_schedule_hours_from_config(self):
        """Test schedule hours are loaded from configuration."""
        # Test different schedule configurations
        test_cases = [
            ("8,12,15,18,20", [8, 12, 15, 18, 20]),
            ("0,6,12,18", [0, 6, 12, 18]),
            ("9", [9]),
            ("invalid", [8, 12, 15, 18, 20])  # Should fall back to default
        ]
        
        for schedule_str, expected_hours in test_cases:
            with self.subTest(schedule_str=schedule_str):
                # Update configuration
                self.config.set("schedule_hours", schedule_str)
                
                # Create new scheduler to test
                scheduler = Scheduler()
                self.assertEqual(scheduler.schedule_hours, expected_hours)

    def test_next_scheduled_time_calculation(self):
        """Test next scheduled time calculation."""
        # Test with current time
        now = datetime.now()
        current_hour = now.hour
        
        # Find next scheduled hour
        next_time = self.scheduler.get_next_scheduled_time()
        self.assertIsNotNone(next_time)
        
        # Verify it's in the future or exactly at a scheduled hour
        if next_time.hour == current_hour and next_time.date() == now.date():
            # Should be exactly at the hour
            self.assertEqual(next_time.minute, 0)
            self.assertEqual(next_time.second, 0)
        else:
            # Should be in the future
            self.assertGreaterEqual(next_time, now)
        
        # Verify it's at a scheduled hour
        self.assertIn(next_time.hour, self.scheduler.schedule_hours)
        self.assertEqual(next_time.minute, 0)
        self.assertEqual(next_time.second, 0)

    def test_next_scheduled_time_tomorrow(self):
        """Test next scheduled time calculation for tomorrow."""
        # Create scheduler with hours that have already passed today
        past_hours = [1, 2, 3]  # Early morning hours
        self.scheduler.schedule_hours = past_hours
        
        now = datetime.now()
        if now.hour > max(past_hours):
            # All scheduled hours have passed today
            next_time = self.scheduler.get_next_scheduled_time()
            
            # Should be tomorrow
            self.assertEqual(next_time.date(), now.date() + timedelta(days=1))
            self.assertIn(next_time.hour, past_hours)

    def test_scheduler_status(self):
        """Test scheduler status reporting."""
        # Test status when not running
        status = self.scheduler.get_status()
        expected_keys = ["running", "schedule_hours", "next_scheduled_time", "thread_alive"]
        for key in expected_keys:
            self.assertIn(key, status)
        
        self.assertFalse(status["running"])
        self.assertEqual(status["schedule_hours"], self.scheduler.schedule_hours)
        self.assertFalse(status["thread_alive"])
        
        # Test status when running
        self.scheduler.start()
        time.sleep(0.1)  # Give thread time to start
        
        status = self.scheduler.get_status()
        self.assertTrue(status["running"])
        self.assertTrue(status["thread_alive"])
        
        self.scheduler.stop()

    def test_task_enqueuing_at_scheduled_time(self):
        """Test that tasks are enqueued at scheduled times."""
        # Mock task queue
        mock_task_queue = Mock()
        mock_task_queue.enqueue.return_value = True
        
        # Create scheduler with mocked task queue
        self.scheduler.task_queue = mock_task_queue
        
        # Manually trigger task enqueuing
        self.scheduler._enqueue_scheduled_tasks()
        
        # Verify tasks were enqueued in correct order
        self.assertEqual(mock_task_queue.enqueue.call_count, 2)
        
        # Check first call (ExportMailer)
        first_call = mock_task_queue.enqueue.call_args_list[0]
        self.assertIsInstance(first_call[0][0], ExportMailerTask)
        
        # Check second call (PriceExport)
        second_call = mock_task_queue.enqueue.call_args_list[1]
        self.assertIsInstance(second_call[0][0], PriceExportTask)

    def test_scheduler_loop_timing(self):
        """Test scheduler loop timing and wake interval."""
        # Mock the task enqueuing to track calls
        with patch.object(self.scheduler, '_enqueue_scheduled_tasks') as mock_enqueue:
            # Start scheduler
            self.scheduler.start()
            
            # Wait a bit for the loop to run
            time.sleep(1.5)  # Should allow at least one loop iteration
            
            # Stop scheduler
            self.scheduler.stop()
            
            # Verify the loop was running (enqueue should have been checked)
            # Note: This test verifies the loop is running, not that tasks were enqueued
            # since we're not at an exact scheduled hour
            self.assertTrue(self.scheduler._stop_event.is_set())

    def test_scheduler_error_handling(self):
        """Test scheduler handles errors gracefully."""
        # Mock task queue to raise exception
        mock_task_queue = Mock()
        mock_task_queue.enqueue.side_effect = Exception("Test error")
        self.scheduler.task_queue = mock_task_queue
        
        # This should not crash the scheduler
        try:
            self.scheduler._enqueue_scheduled_tasks()
        except Exception:
            self.fail("Scheduler should handle exceptions gracefully")

    def test_schedule_hours_update(self):
        """Test updating schedule hours."""
        # Test valid hours
        new_hours = [6, 12, 18, 24]  # 24 is invalid
        self.scheduler.update_schedule_hours(new_hours)
        
        # Should only include valid hours (0-23)
        expected_hours = [6, 12, 18]
        self.assertEqual(self.scheduler.schedule_hours, expected_hours)
        
        # Test empty hours list
        self.scheduler.update_schedule_hours([])
        # Should not update (no valid hours)
        self.assertEqual(self.scheduler.schedule_hours, expected_hours)

    def test_scheduler_integration_with_task_queue(self):
        """Test scheduler integration with task queue."""
        # Start task queue
        self.assertTrue(self.task_queue.start())
        
        # Create scheduler with real task queue
        self.scheduler.task_queue = self.task_queue
        
        # Mock task execution to avoid actual work
        with patch.object(ExportMailerTask, 'execute', return_value=True):
            with patch.object(PriceExportTask, 'execute', return_value=True):
                # Enqueue tasks manually (simulating scheduler trigger)
                self.scheduler._enqueue_scheduled_tasks()
                
                # Wait for tasks to be processed
                time.sleep(1)
                
                # Verify tasks were processed
                status = self.task_queue.get_status()
                self.assertEqual(status["statistics"]["total_enqueued"], 2)
                self.assertEqual(status["statistics"]["total_completed"], 2)

    def test_scheduler_task_ordering(self):
        """Test that tasks are enqueued in the correct order."""
        # Mock task queue to capture order
        enqueued_tasks = []
        
        def mock_enqueue(task):
            enqueued_tasks.append(type(task).__name__)
            return True
        
        mock_task_queue = Mock()
        mock_task_queue.enqueue.side_effect = mock_enqueue
        self.scheduler.task_queue = mock_task_queue
        
        # Enqueue tasks
        self.scheduler._enqueue_scheduled_tasks()
        
        # Verify order: ExportMailer first, then PriceExport
        self.assertEqual(len(enqueued_tasks), 2)
        self.assertEqual(enqueued_tasks[0], "ExportMailerTask")
        self.assertEqual(enqueued_tasks[1], "PriceExportTask")

    def test_scheduler_configuration_integration(self):
        """Test scheduler integrates with configuration system."""
        # Update configuration
        self.config.set("schedule_hours", "10,14,18,22")
        
        # Create new scheduler
        new_scheduler = Scheduler()
        
        # Verify it reads updated configuration
        self.assertEqual(new_scheduler.schedule_hours, [10, 14, 18, 22])

    def test_scheduler_persistence_across_restarts(self):
        """Test scheduler settings persist across restarts."""
        # Set custom schedule
        custom_hours = [11, 17, 23]
        self.scheduler.update_schedule_hours(custom_hours)
        
        # Create new scheduler instance
        new_scheduler = Scheduler()
        
        # Verify schedule hours are preserved (from config)
        self.assertEqual(new_scheduler.schedule_hours, custom_hours)

    def test_scheduler_time_zone_handling(self):
        """Test scheduler handles local time correctly."""
        # Get current local time
        local_now = datetime.now()
        
        # Calculate next scheduled time
        next_time = self.scheduler.get_next_scheduled_time()
        
        # Verify it's based on local time
        self.assertIsNotNone(next_time)
        
        # Verify it's at a scheduled hour in local time
        self.assertIn(next_time.hour, self.scheduler.schedule_hours)
        self.assertEqual(next_time.minute, 0)
        self.assertEqual(next_time.second, 0)

    def test_scheduler_midnight_handling(self):
        """Test scheduler handles midnight correctly."""
        # Create scheduler with midnight schedule
        midnight_scheduler = Scheduler()
        midnight_scheduler.schedule_hours = [0, 12]
        
        # Calculate next scheduled time
        next_time = midnight_scheduler.get_next_scheduled_time()
        
        self.assertIsNotNone(next_time)
        self.assertIn(next_time.hour, [0, 12])

    def test_scheduler_single_instance(self):
        """Test global scheduler instance management."""
        # Test get_scheduler function
        scheduler1 = get_scheduler()
        scheduler2 = get_scheduler()
        
        # Should return same instance
        self.assertIs(scheduler1, scheduler2)

    def test_scheduler_global_functions(self):
        """Test global scheduler functions."""
        # Test start_scheduler function
        self.assertTrue(start_scheduler())
        
        # Verify scheduler is running
        scheduler = get_scheduler()
        self.assertTrue(scheduler.is_running())
        
        # Test get_scheduler_status function
        status = get_scheduler_status()
        self.assertIn("running", status)
        self.assertTrue(status["running"])
        
        # Test stop_scheduler function
        self.assertTrue(stop_scheduler())
        
        # Verify scheduler is stopped
        self.assertFalse(scheduler.is_running())

    def test_scheduler_concurrent_access(self):
        """Test scheduler handles concurrent access safely."""
        # Start scheduler
        self.scheduler.start()
        
        # Create multiple threads to access scheduler
        results = []
        
        def access_scheduler():
            try:
                status = self.scheduler.get_status()
                results.append(status["running"])
            except Exception as e:
                results.append(f"Error: {e}")
        
        # Start multiple threads
        threads = []
        for i in range(5):
            thread = threading.Thread(target=access_scheduler)
            threads.append(thread)
            thread.start()
        
        # Wait for all threads to complete
        for thread in threads:
            thread.join()
        
        # Verify all threads completed successfully
        self.assertEqual(len(results), 5)
        for result in results:
            self.assertEqual(result, True)  # All should show scheduler as running
        
        # Stop scheduler
        self.scheduler.stop()

    def test_scheduler_long_running_stability(self):
        """Test scheduler stability over longer periods."""
        # Start scheduler
        self.scheduler.start()
        
        # Let it run for a short period
        start_time = time.time()
        while time.time() - start_time < 2:  # Run for 2 seconds
            # Check status periodically
            status = self.scheduler.get_status()
            self.assertTrue(status["running"])
            self.assertTrue(status["thread_alive"])
            time.sleep(0.1)
        
        # Stop scheduler
        self.assertTrue(self.scheduler.stop())
        
        # Verify it stopped cleanly
        status = self.scheduler.get_status()
        self.assertFalse(status["running"])

    def test_scheduler_memory_usage(self):
        """Test scheduler doesn't leak memory."""
        import psutil
        import gc
        
        # Get initial memory usage
        process = psutil.Process()
        initial_memory = process.memory_info().rss
        
        # Start and stop scheduler multiple times
        for i in range(10):
            scheduler = Scheduler()
            scheduler.start()
            time.sleep(0.1)
            scheduler.stop()
            del scheduler
            gc.collect()
        
        # Check final memory usage
        final_memory = process.memory_info().rss
        memory_growth = final_memory - initial_memory
        
        # Memory growth should be minimal
        self.assertLess(memory_growth, 10 * 1024 * 1024,  # Less than 10MB
                       f"Memory grew by {memory_growth / 1024 / 1024:.2f}MB")


def run_scheduler_integration_tests():
    """Run all scheduler integration tests."""
    print("Running scheduler integration tests for rateloader...")
    print("=" * 60)
    
    # Create test suite
    suite = unittest.TestLoader().loadTestsFromTestCase(TestSchedulerIntegration)
    
    # 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_scheduler_integration_tests()
    sys.exit(0 if success else 1)