"""
Enhanced cron syntax compatibility tests for croniter backend.

This test suite mirrors the frontend cron-parser tests to ensure
complete compatibility between frontend and backend cron processing.
"""

import unittest
from datetime import UTC, datetime, timedelta

import pytest
import pytz
from croniter import CroniterBadCronError

from libs.schedule_utils import calculate_next_run_at


class TestCronCompatibility(unittest.TestCase):
    """Test enhanced cron syntax compatibility with frontend."""

    def setUp(self):
        """Set up test environment with fixed time."""
        self.base_time = datetime(2024, 1, 15, 10, 0, 0, tzinfo=UTC)

    def test_enhanced_dayofweek_syntax(self):
        """Test enhanced day-of-week syntax compatibility."""
        test_cases = [
            ("0 9 * * 7", 0),  # Sunday as 7
            ("0 9 * * 0", 0),  # Sunday as 0
            ("0 9 * * MON", 1),  # Monday abbreviation
            ("0 9 * * TUE", 2),  # Tuesday abbreviation
            ("0 9 * * WED", 3),  # Wednesday abbreviation
            ("0 9 * * THU", 4),  # Thursday abbreviation
            ("0 9 * * FRI", 5),  # Friday abbreviation
            ("0 9 * * SAT", 6),  # Saturday abbreviation
            ("0 9 * * SUN", 0),  # Sunday abbreviation
        ]

        for expr, expected_weekday in test_cases:
            with self.subTest(expr=expr):
                next_time = calculate_next_run_at(expr, "UTC", self.base_time)
                assert next_time is not None
                assert (next_time.weekday() + 1 if next_time.weekday() < 6 else 0) == expected_weekday
                assert next_time.hour == 9
                assert next_time.minute == 0

    def test_enhanced_month_syntax(self):
        """Test enhanced month syntax compatibility."""
        test_cases = [
            ("0 9 1 JAN *", 1),  # January abbreviation
            ("0 9 1 FEB *", 2),  # February abbreviation
            ("0 9 1 MAR *", 3),  # March abbreviation
            ("0 9 1 APR *", 4),  # April abbreviation
            ("0 9 1 MAY *", 5),  # May abbreviation
            ("0 9 1 JUN *", 6),  # June abbreviation
            ("0 9 1 JUL *", 7),  # July abbreviation
            ("0 9 1 AUG *", 8),  # August abbreviation
            ("0 9 1 SEP *", 9),  # September abbreviation
            ("0 9 1 OCT *", 10),  # October abbreviation
            ("0 9 1 NOV *", 11),  # November abbreviation
            ("0 9 1 DEC *", 12),  # December abbreviation
        ]

        for expr, expected_month in test_cases:
            with self.subTest(expr=expr):
                next_time = calculate_next_run_at(expr, "UTC", self.base_time)
                assert next_time is not None
                assert next_time.month == expected_month
                assert next_time.day == 1
                assert next_time.hour == 9

    def test_predefined_expressions(self):
        """Test predefined cron expressions compatibility."""
        test_cases = [
            ("@yearly", lambda dt: dt.month == 1 and dt.day == 1 and dt.hour == 0),
            ("@annually", lambda dt: dt.month == 1 and dt.day == 1 and dt.hour == 0),
            ("@monthly", lambda dt: dt.day == 1 and dt.hour == 0),
            ("@weekly", lambda dt: dt.weekday() == 6 and dt.hour == 0),  # Sunday = 6 in weekday()
            ("@daily", lambda dt: dt.hour == 0 and dt.minute == 0),
            ("@midnight", lambda dt: dt.hour == 0 and dt.minute == 0),
            ("@hourly", lambda dt: dt.minute == 0),
        ]

        for expr, validator in test_cases:
            with self.subTest(expr=expr):
                next_time = calculate_next_run_at(expr, "UTC", self.base_time)
                assert next_time is not None
                assert validator(next_time), f"Validator failed for {expr}: {next_time}"

    def test_special_characters(self):
        """Test special characters in cron expressions."""
        test_cases = [
            "0 9 ? * 1",  # ? wildcard
            "0 12 * * 7",  # Sunday as 7
            "0 15 L * *",  # Last day of month
        ]

        for expr in test_cases:
            with self.subTest(expr=expr):
                try:
                    next_time = calculate_next_run_at(expr, "UTC", self.base_time)
                    assert next_time is not None
                    assert next_time > self.base_time
                except Exception as e:
                    self.fail(f"Expression '{expr}' should be valid but raised: {e}")

    def test_range_and_list_syntax(self):
        """Test range and list syntax with abbreviations."""
        test_cases = [
            "0 9 * * MON-FRI",  # Weekday range with abbreviations
            "0 9 * JAN-MAR *",  # Month range with abbreviations
            "0 9 * * SUN,WED,FRI",  # Weekday list with abbreviations
            "0 9 1 JAN,JUN,DEC *",  # Month list with abbreviations
        ]

        for expr in test_cases:
            with self.subTest(expr=expr):
                try:
                    next_time = calculate_next_run_at(expr, "UTC", self.base_time)
                    assert next_time is not None
                    assert next_time > self.base_time
                except Exception as e:
                    self.fail(f"Expression '{expr}' should be valid but raised: {e}")

    def test_invalid_enhanced_syntax(self):
        """Test that invalid enhanced syntax is properly rejected."""
        invalid_expressions = [
            "0 12 * JANUARY *",  # Full month name (not supported)
            "0 12 * * MONDAY",  # Full day name (not supported)
            "0 12 32 JAN *",  # Invalid day with valid month
            "15 10 1 * 8",  # Invalid day of week
            "15 10 1 INVALID *",  # Invalid month abbreviation
            "15 10 1 * INVALID",  # Invalid day abbreviation
            "@invalid",  # Invalid predefined expression
        ]

        for expr in invalid_expressions:
            with self.subTest(expr=expr):
                with pytest.raises((CroniterBadCronError, ValueError)):
                    calculate_next_run_at(expr, "UTC", self.base_time)

    def test_edge_cases_with_enhanced_syntax(self):
        """Test edge cases with enhanced syntax."""
        test_cases = [
            ("0 0 29 FEB *", lambda dt: dt.month == 2 and dt.day == 29),  # Feb 29 with month abbreviation
        ]

        for expr, validator in test_cases:
            with self.subTest(expr=expr):
                try:
                    next_time = calculate_next_run_at(expr, "UTC", self.base_time)
                    if next_time:  # Some combinations might not occur soon
                        assert validator(next_time), f"Validator failed for {expr}: {next_time}"
                except (CroniterBadCronError, ValueError):
                    # Some edge cases might be valid but not have upcoming occurrences
                    pass

        # Test complex expressions that have specific constraints
        complex_expr = "59 23 31 DEC SAT"  # December 31st at 23:59 on Saturday
        try:
            next_time = calculate_next_run_at(complex_expr, "UTC", self.base_time)
            if next_time:
                # The next occurrence might not be exactly Dec 31 if it's not a Saturday
                # Just verify it's a valid result
                assert next_time is not None
                assert next_time.hour == 23
                assert next_time.minute == 59
        except Exception:
            # Complex date constraints might not have near-future occurrences
            pass


class TestTimezoneCompatibility(unittest.TestCase):
    """Test timezone compatibility between frontend and backend."""

    def setUp(self):
        """Set up test environment."""
        self.base_time = datetime(2024, 1, 15, 10, 0, 0, tzinfo=UTC)

    def test_timezone_consistency(self):
        """Test that calculations are consistent across different timezones."""
        timezones = [
            "UTC",
            "America/New_York",
            "Europe/London",
            "Asia/Tokyo",
            "Asia/Kolkata",
            "Australia/Sydney",
        ]

        expression = "0 12 * * *"  # Daily at noon

        for timezone in timezones:
            with self.subTest(timezone=timezone):
                next_time = calculate_next_run_at(expression, timezone, self.base_time)
                assert next_time is not None

                # Convert back to the target timezone to verify it's noon
                tz = pytz.timezone(timezone)
                local_time = next_time.astimezone(tz)
                assert local_time.hour == 12
                assert local_time.minute == 0

    def test_dst_handling(self):
        """Test DST boundary handling."""
        # Test around DST spring forward (March 2024)
        dst_base = datetime(2024, 3, 8, 10, 0, 0, tzinfo=UTC)
        expression = "0 2 * * *"  # 2 AM daily (problematic during DST)
        timezone = "America/New_York"

        try:
            next_time = calculate_next_run_at(expression, timezone, dst_base)
            assert next_time is not None

            # During DST spring forward, 2 AM becomes 3 AM - both are acceptable
            tz = pytz.timezone(timezone)
            local_time = next_time.astimezone(tz)
            assert local_time.hour in [2, 3]  # Either 2 AM or 3 AM is acceptable
        except Exception as e:
            self.fail(f"DST handling failed: {e}")

    def test_half_hour_timezones(self):
        """Test timezones with half-hour offsets."""
        timezones_with_offsets = [
            ("Asia/Kolkata", 17, 30),  # UTC+5:30 -> 12:00 UTC = 17:30 IST
            ("Australia/Adelaide", 22, 30),  # UTC+10:30 -> 12:00 UTC = 22:30 ACDT (summer time)
        ]

        expression = "0 12 * * *"  # Noon UTC

        for timezone, expected_hour, expected_minute in timezones_with_offsets:
            with self.subTest(timezone=timezone):
                try:
                    next_time = calculate_next_run_at(expression, timezone, self.base_time)
                    assert next_time is not None

                    tz = pytz.timezone(timezone)
                    local_time = next_time.astimezone(tz)
                    assert local_time.hour == expected_hour
                    assert local_time.minute == expected_minute
                except Exception:
                    # Some complex timezone calculations might vary
                    pass

    def test_invalid_timezone_handling(self):
        """Test handling of invalid timezones."""
        expression = "0 12 * * *"
        invalid_timezone = "Invalid/Timezone"

        with pytest.raises((ValueError, Exception)):  # Should raise an exception
            calculate_next_run_at(expression, invalid_timezone, self.base_time)


class TestFrontendBackendIntegration(unittest.TestCase):
    """Test integration patterns that mirror frontend usage."""

    def setUp(self):
        """Set up test environment."""
        self.base_time = datetime(2024, 1, 15, 10, 0, 0, tzinfo=UTC)

    def test_execution_time_calculator_pattern(self):
        """Test the pattern used by execution-time-calculator.ts."""
        # This mirrors the exact usage from execution-time-calculator.ts:47
        test_data = {
            "cron_expression": "30 14 * * 1-5",  # 2:30 PM weekdays
            "timezone": "America/New_York",
        }

        # Get next 5 execution times (like the frontend does)
        execution_times = []
        current_base = self.base_time

        for _ in range(5):
            next_time = calculate_next_run_at(test_data["cron_expression"], test_data["timezone"], current_base)
            assert next_time is not None
            execution_times.append(next_time)
            current_base = next_time + timedelta(seconds=1)  # Move slightly forward

        assert len(execution_times) == 5

        # Validate each execution time
        for exec_time in execution_times:
            # Convert to local timezone
            tz = pytz.timezone(test_data["timezone"])
            local_time = exec_time.astimezone(tz)

            # Should be weekdays (1-5)
            assert local_time.weekday() in [0, 1, 2, 3, 4]  # Mon-Fri in Python weekday

            # Should be 2:30 PM in local time
            assert local_time.hour == 14
            assert local_time.minute == 30
            assert local_time.second == 0

    def test_schedule_service_integration(self):
        """Test integration with ScheduleService patterns."""
        from dify_graph.nodes.trigger_schedule.entities import VisualConfig
        from services.trigger.schedule_service import ScheduleService

        # Test enhanced syntax through visual config conversion
        visual_configs = [
            # Test with month abbreviations
            {
                "frequency": "monthly",
                "config": VisualConfig(time="9:00 AM", monthly_days=[1]),
                "expected_cron": "0 9 1 * *",
            },
            # Test with weekday abbreviations
            {
                "frequency": "weekly",
                "config": VisualConfig(time="2:30 PM", weekdays=["mon", "wed", "fri"]),
                "expected_cron": "30 14 * * 1,3,5",
            },
        ]

        for test_case in visual_configs:
            with self.subTest(frequency=test_case["frequency"]):
                cron_expr = ScheduleService.visual_to_cron(test_case["frequency"], test_case["config"])
                assert cron_expr == test_case["expected_cron"]

                # Verify the generated cron expression is valid
                next_time = calculate_next_run_at(cron_expr, "UTC", self.base_time)
                assert next_time is not None

    def test_error_handling_consistency(self):
        """Test that error handling matches frontend expectations."""
        invalid_expressions = [
            "60 10 1 * *",  # Invalid minute
            "15 25 1 * *",  # Invalid hour
            "15 10 32 * *",  # Invalid day
            "15 10 1 13 *",  # Invalid month
            "15 10 1",  # Too few fields
            "15 10 1 * * *",  # 6 fields (not supported in frontend)
            "0 15 10 1 * * *",  # 7 fields (not supported in frontend)
            "invalid expression",  # Completely invalid
        ]

        for expr in invalid_expressions:
            with self.subTest(expr=repr(expr)):
                with pytest.raises((CroniterBadCronError, ValueError, Exception)):
                    calculate_next_run_at(expr, "UTC", self.base_time)

        # Note: Empty/whitespace expressions are not tested here as they are
        # not expected in normal usage due to database constraints (nullable=False)

    def test_performance_requirements(self):
        """Test that complex expressions parse within reasonable time."""
        import time

        complex_expressions = [
            "*/5 9-17 * * 1-5",  # Every 5 minutes, weekdays, business hours
            "0 */2 1,15 * *",  # Every 2 hours on 1st and 15th
            "30 14 * * 1,3,5",  # Mon, Wed, Fri at 14:30
            "15,45 8-18 * * 1-5",  # 15 and 45 minutes past hour, weekdays
            "0 9 * JAN-MAR MON-FRI",  # Enhanced syntax: Q1 weekdays at 9 AM
            "0 12 ? * SUN",  # Enhanced syntax: Sundays at noon with ?
        ]

        start_time = time.time()

        for expr in complex_expressions:
            with self.subTest(expr=expr):
                try:
                    next_time = calculate_next_run_at(expr, "UTC", self.base_time)
                    assert next_time is not None
                except CroniterBadCronError:
                    # Some enhanced syntax might not be supported, that's OK
                    pass

        end_time = time.time()
        execution_time = (end_time - start_time) * 1000  # Convert to milliseconds

        # Should complete within reasonable time (less than 150ms like frontend)
        assert execution_time < 150, "Complex expressions should parse quickly"


if __name__ == "__main__":
    # Import timedelta for the test
    from datetime import timedelta

    unittest.main()
