"""
Tests for the Scheduler module.
"""

import pytest
import time
from unittest.mock import Mock

from bgx.scheduler import (
    TaskScheduler, ScheduledHandle, CronParser, CronField,
    ScheduledTask
)


class TestCronField:
    """Test CronField dataclass."""
    
    def test_cron_field_creation(self):
        """Test creating CronField objects."""
        # Wildcard field
        field = CronField(is_wildcard=True)
        assert field.is_wildcard is True
        assert field.values == set()
        
        # Regular field
        field = CronField(values={1, 2, 3})
        assert field.is_wildcard is False
        assert field.values == {1, 2, 3}
        
        # Step field
        field = CronField(values={0, 15, 30, 45}, is_step=True, step=15)
        assert field.is_step is True
        assert field.step == 15


class TestCronParser:
    """Test CronParser functionality."""
    
    def test_simple_expressions(self):
        """Test parsing simple cron expressions."""
        # Every minute
        parser = CronParser("* * * * *")
        assert parser.fields['minute'].is_wildcard
        assert parser.fields['hour'].is_wildcard
        assert parser.fields['day_of_month'].is_wildcard
        assert parser.fields['month'].is_wildcard
        assert parser.fields['day_of_week'].is_wildcard
        
        # Specific minute
        parser = CronParser("5 * * * *")
        assert parser.fields['minute'].values == {5}
        
        # Multiple minutes
        parser = CronParser("5,10,15 * * * *")
        assert parser.fields['minute'].values == {5, 10, 15}
        
        # Range
        parser = CronParser("1-5 * * * *")
        assert parser.fields['minute'].values == {1, 2, 3, 4, 5}
    
    def test_step_expressions(self):
        """Test parsing step expressions."""
        # Every 15 minutes
        parser = CronParser("*/15 * * * *")
        assert parser.fields['minute'].is_step is True
        assert parser.fields['minute'].step == 15
        assert parser.fields['minute'].values == {0, 15, 30, 45}
        
        # Range with step
        parser = CronParser("1-31/3 * * * *")
        assert parser.fields['minute'].is_step is True
        assert parser.fields['minute'].step == 3
        assert parser.fields['minute'].values == {1, 4, 7, 10, 13, 16, 19, 22, 25, 28, 31}
    
    def test_month_names(self):
        """Test month name substitutions."""
        parser = CronParser("* * 1 JAN,FEB,MAR *")
        assert parser.fields['month'].values == {1, 2, 3}
        
        # Case insensitive
        parser = CronParser("* * 1 jan,feb,mar *")
        assert parser.fields['month'].values == {1, 2, 3}
    
    def test_day_names(self):
        """Test day name substitutions."""
        parser = CronParser("* * * * MON,TUE,WED")
        assert parser.fields['day_of_week'].values == {1, 2, 3}  # Monday=1, Tuesday=2, Wednesday=3
        
        # Case insensitive
        parser = CronParser("* * * * mon,tue,wed")
        assert parser.fields['day_of_week'].values == {1, 2, 3}
    
    def test_invalid_expressions(self):
        """Test invalid cron expressions."""
        # Too few fields
        with pytest.raises(ValueError):
            CronParser("* * * *")
        
        # Too many fields
        with pytest.raises(ValueError):
            CronParser("* * * * * *")
        
        # Invalid field values
        with pytest.raises(ValueError):
            CronParser("99 * * * *")  # Invalid minute (99 > 59)
        
        with pytest.raises(ValueError):
            CronParser("* 25 * * *")  # Invalid hour
    
    def test_next_run_time_basic(self):
        """Test basic next run time calculation."""
        # Run every minute at 00:00
        parser = CronParser("0 * * * *")
        
        # Test at 00:30 - should run at 01:00
        import datetime
        current_dt = datetime.datetime(2023, 1, 1, 0, 30)
        next_time = parser.next_run_time(current_dt.timestamp())
        next_dt = datetime.datetime.fromtimestamp(next_time)
        
        assert next_dt.hour == 1
        assert next_dt.minute == 0
    
    def test_next_run_time_hourly(self):
        """Test hourly next run time calculation."""
        # Run at minute 15 every hour
        parser = CronParser("15 * * * *")
        
        import datetime
        current_dt = datetime.datetime(2023, 1, 1, 0, 30)
        next_time = parser.next_run_time(current_dt.timestamp())
        next_dt = datetime.datetime.fromtimestamp(next_time)
        
        # At 00:30, next run should be at 01:15 (same hour, next matching minute)
        assert next_dt.hour == 1
        assert next_dt.minute == 15
    
    def test_next_run_time_daily(self):
        """Test daily next run time calculation."""
        # Run at 9:00 AM every day
        parser = CronParser("0 9 * * *")
        
        import datetime
        current_dt = datetime.datetime(2023, 1, 1, 8, 30)
        next_time = parser.next_run_time(current_dt.timestamp())
        next_dt = datetime.datetime.fromtimestamp(next_time)
        
        assert next_dt.hour == 9
        assert next_dt.minute == 0
        assert next_dt.day == 1
    
    def test_next_run_time_weekly(self):
        """Test weekly next run time calculation."""
        # Run at 9:00 AM every Monday (day 1)
        parser = CronParser("0 9 * * 1")
        
        import datetime
        # Sunday (day 0) - should run Monday
        current_dt = datetime.datetime(2023, 1, 1, 8, 30)  # Sunday
        next_time = parser.next_run_time(current_dt.timestamp())
        next_dt = datetime.datetime.fromtimestamp(next_time)
        
        # Should run on Monday at 9:00 AM
        assert next_dt.hour == 9
        assert next_dt.minute == 0
        assert next_dt.day == 2  # Monday


class TestScheduledHandle:
    """Test ScheduledHandle functionality."""
    
    def test_handle_creation(self):
        """Test creating scheduled handles."""
        mock_scheduler = Mock()
        handle = ScheduledHandle("task_123", mock_scheduler)
        
        assert handle.task_id == "task_123"
        assert handle.is_cancelled is False
    
    def test_cancel_success(self):
        """Test successful cancellation."""
        mock_scheduler = Mock()
        mock_scheduler.cancel_scheduled_task.return_value = True
        
        handle = ScheduledHandle("task_123", mock_scheduler)
        result = handle.cancel()
        
        assert result is True
        assert handle.is_cancelled is True
        mock_scheduler.cancel_scheduled_task.assert_called_once_with("task_123")
    
    def test_cancel_not_found(self):
        """Test cancellation of non-existent task."""
        mock_scheduler = Mock()
        mock_scheduler.cancel_scheduled_task.return_value = False
        
        handle = ScheduledHandle("task_123", mock_scheduler)
        result = handle.cancel()
        
        assert result is False
        assert handle.is_cancelled is False
    
    def test_cancel_already_cancelled(self):
        """Test cancelling already cancelled task."""
        mock_scheduler = Mock()
        mock_scheduler.cancel_scheduled_task.return_value = True
        
        handle = ScheduledHandle("task_123", mock_scheduler)
        handle.cancel()  # First cancellation
        result = handle.cancel()  # Second cancellation
        
        assert result is False
        assert handle.is_cancelled is True


class TestTaskScheduler:
    """Test TaskScheduler functionality."""
    
    def test_scheduler_creation(self):
        """Test scheduler creation."""
        callback = Mock()
        scheduler = TaskScheduler(submit_callback=callback)
        
        assert scheduler._submit_callback == callback
        assert scheduler.get_scheduled_count() == 0
    
    def test_schedule_delay(self):
        """Test scheduling a delayed task."""
        callback = Mock()
        scheduler = TaskScheduler(submit_callback=callback)
        
        def test_func():
            return "executed"
        
        # Schedule task to run in 0.1 seconds
        handle = scheduler.schedule(test_func, 0.1, "arg1", kwarg1="value1")
        
        assert isinstance(handle, ScheduledHandle)
        assert handle.is_cancelled is False
        assert scheduler.get_scheduled_count() == 1
        
        # Wait for execution
        time.sleep(0.2)
        
        # Task should have been submitted
        callback.assert_called_once_with(test_func, "arg1", kwarg1="value1")
    
    def test_every_interval(self):
        """Test scheduling recurring task with interval."""
        callback = Mock()
        scheduler = TaskScheduler(submit_callback=callback)
        
        def test_func():
            return "executed"
        
        # Schedule task to run every 0.1 seconds
        handle = scheduler.every(test_func, 0.1, "arg1")
        
        assert isinstance(handle, ScheduledHandle)
        assert scheduler.get_scheduled_count() == 1
        
        # Wait for multiple executions
        time.sleep(0.35)
        
        # Should have been called multiple times
        assert callback.call_count >= 2
    
    def test_cron_scheduling(self):
        """Test scheduling with cron expression."""
        callback = Mock()
        scheduler = TaskScheduler(submit_callback=callback)
        
        def test_func():
            return "executed"
        
        # Schedule task to run every minute
        handle = scheduler.cron(test_func, "* * * * *")
        
        assert isinstance(handle, ScheduledHandle)
        assert scheduler.get_scheduled_count() == 1
        
        # Wait for next minute execution
        # Note: This test might be flaky due to timing
        time.sleep(1.1)
        
        # Should have been called at least once
        assert callback.call_count >= 1
    
    def test_cancel_scheduled_task(self):
        """Test cancelling scheduled tasks."""
        callback = Mock()
        scheduler = TaskScheduler(submit_callback=callback)
        
        def test_func():
            return "executed"
        
        # Schedule multiple tasks
        handle1 = scheduler.schedule(test_func, 10.0)
        handle2 = scheduler.every(test_func, 5.0)
        handle3 = scheduler.cron(test_func, "0 * * * *")
        
        assert scheduler.get_scheduled_count() == 3
        
        # Cancel one task
        result = scheduler.cancel_scheduled_task(handle1.task_id)
        assert result is True
        assert scheduler.get_scheduled_count() == 2
        
        # Try to cancel non-existent task
        result = scheduler.cancel_scheduled_task("non_existent")
        assert result is False
        assert scheduler.get_scheduled_count() == 2
    
    def test_handle_cancel_integration(self):
        """Test handle cancellation integration."""
        callback = Mock()
        scheduler = TaskScheduler(submit_callback=callback)
        
        def test_func():
            return "executed"
        
        handle = scheduler.schedule(test_func, 10.0)
        assert scheduler.get_scheduled_count() == 1
        
        # Cancel via handle
        result = handle.cancel()
        assert result is True
        assert handle.is_cancelled is True
        assert scheduler.get_scheduled_count() == 0
    
    def test_get_scheduled_info(self):
        """Test getting scheduled task information."""
        callback = Mock()
        scheduler = TaskScheduler(submit_callback=callback)
        
        def test_func():
            return "executed"
        
        # Schedule different types of tasks
        delay_handle = scheduler.schedule(test_func, 10.0)
        interval_handle = scheduler.every(test_func, 5.0)
        cron_handle = scheduler.cron(test_func, "0 * * * *")
        
        info = scheduler.get_scheduled_info()
        assert len(info) == 3
        
        # Check delay task info
        delay_info = next(i for i in info if i['id'] == delay_handle.task_id)
        assert delay_info['type'] == 'delay'
        assert delay_info['time_until'] > 9.0  # Should be around 10 seconds
        
        # Check interval task info
        interval_info = next(i for i in info if i['id'] == interval_handle.task_id)
        assert interval_info['type'] == 'interval'
        assert interval_info['interval'] == 5.0
        
        # Check cron task info
        cron_info = next(i for i in info if i['id'] == cron_handle.task_id)
        assert cron_info['type'] == 'cron'
        assert cron_info['expression'] == "0 * * * *"
    
    def test_shutdown(self):
        """Test scheduler shutdown."""
        callback = Mock()
        scheduler = TaskScheduler(submit_callback=callback)
        
        def test_func():
            return "executed"
        
        # Schedule some tasks
        scheduler.schedule(test_func, 10.0)
        scheduler.every(test_func, 5.0)
        
        assert scheduler.get_scheduled_count() == 2
        
        # Shutdown scheduler
        scheduler.shutdown()
        
        # Should clear scheduled tasks
        assert scheduler.get_scheduled_count() == 0
