"""
Tests for GateWay Cron Scheduler Module.
"""

from datetime import datetime, timedelta
from unittest.mock import AsyncMock

import pytest

from verlex_server.gateway.backend.cron_scheduler import (
    CronExpression,
    CronScheduler,
    ScheduledJob,
    ScheduleExecution,
    ScheduleStatus,
    ScheduleType,
    describe_cron,
    get_next_n_runs,
    validate_cron_expression,
)


class TestCronExpression:
    """Tests for CronExpression parsing and matching."""

    def test_parse_simple_cron(self):
        """Test parsing a simple cron expression."""
        cron = CronExpression.parse("0 * * * *")

        assert 0 in cron.minute
        assert len(cron.minute) == 1
        assert cron.hour == set(range(24))

    def test_parse_every_minute(self):
        """Test parsing every minute cron."""
        cron = CronExpression.parse("* * * * *")

        assert cron.minute == set(range(60))
        assert cron.hour == set(range(24))
        assert cron.day_of_month == set(range(1, 32))

    def test_parse_step_values(self):
        """Test parsing step values."""
        cron = CronExpression.parse("*/15 * * * *")

        assert cron.minute == {0, 15, 30, 45}

    def test_parse_range(self):
        """Test parsing range values."""
        cron = CronExpression.parse("0 9-17 * * *")

        assert cron.hour == set(range(9, 18))

    def test_parse_list(self):
        """Test parsing list values."""
        cron = CronExpression.parse("0 9,12,18 * * *")

        assert cron.hour == {9, 12, 18}

    def test_parse_weekdays(self):
        """Test parsing weekday cron."""
        cron = CronExpression.parse("0 9 * * 1-5")

        assert cron.day_of_week == {1, 2, 3, 4, 5}

    def test_parse_invalid_fields(self):
        """Test parsing with invalid number of fields."""
        with pytest.raises(ValueError):
            CronExpression.parse("* * *")  # Only 3 fields

    def test_parse_invalid_value(self):
        """Test parsing with invalid value."""
        with pytest.raises(ValueError):
            CronExpression.parse("60 * * * *")  # 60 is out of range for minutes

    def test_matches_datetime(self):
        """Test matching datetime."""
        cron = CronExpression.parse("30 9 * * *")

        # Should match 9:30 AM
        match_dt = datetime(2025, 1, 15, 9, 30)
        assert cron.matches(match_dt) is True

        # Should not match 9:31 AM
        no_match_dt = datetime(2025, 1, 15, 9, 31)
        assert cron.matches(no_match_dt) is False

    def test_matches_weekday(self):
        """Test matching weekday."""
        # Monday through Friday at 9:00
        cron = CronExpression.parse("0 9 * * 1-5")

        # Monday Jan 13, 2025
        monday = datetime(2025, 1, 13, 9, 0)
        assert cron.matches(monday) is True

        # Saturday Jan 18, 2025
        saturday = datetime(2025, 1, 18, 9, 0)
        assert cron.matches(saturday) is False

    def test_get_next_run(self):
        """Test getting next run time."""
        cron = CronExpression.parse("0 * * * *")  # Every hour at :00

        after = datetime(2025, 1, 15, 9, 30)
        next_run = cron.get_next_run(after)

        assert next_run.minute == 0
        assert next_run.hour == 10

    def test_get_description(self):
        """Test getting human-readable description."""
        cron = CronExpression.parse("0 9 * * *")

        desc = cron.get_description()

        assert "9:00" in desc or "minute 0" in desc


class TestScheduledJob:
    """Tests for ScheduledJob dataclass."""

    def test_create_cron_job(self):
        """Test creating a cron scheduled job."""
        job = ScheduledJob(
            id="sched_12345",
            user_id="user_123",
            name="Test Job",
            cron_expression="0 9 * * *",
            function_payload={"function": "test"},
            resources={"cpu": 1},
        )

        assert job.id == "sched_12345"
        assert job.schedule_type == ScheduleType.CRON
        assert job.status == ScheduleStatus.ACTIVE

    def test_create_once_job(self):
        """Test creating a one-time scheduled job."""
        run_at = datetime.utcnow() + timedelta(hours=1)

        job = ScheduledJob(
            id="sched_12345",
            user_id="user_123",
            name="One-time Job",
            schedule_type=ScheduleType.ONCE,
            run_at=run_at,
            function_payload={"function": "test"},
            resources={},
        )

        assert job.schedule_type == ScheduleType.ONCE

    def test_create_interval_job(self):
        """Test creating an interval scheduled job."""
        job = ScheduledJob(
            id="sched_12345",
            user_id="user_123",
            name="Interval Job",
            schedule_type=ScheduleType.INTERVAL,
            interval_seconds=3600,
            function_payload={"function": "test"},
            resources={},
        )

        assert job.schedule_type == ScheduleType.INTERVAL
        assert job.interval_seconds == 3600

    def test_get_next_run_time_cron(self):
        """Test getting next run time for cron job."""
        job = ScheduledJob(
            id="sched_12345",
            user_id="user_123",
            name="Test Job",
            cron_expression="0 * * * *",  # Every hour
            function_payload={},
            resources={},
        )

        next_run = job.get_next_run_time()

        assert next_run is not None
        assert next_run.minute == 0

    def test_get_next_run_time_once_not_run(self):
        """Test next run for one-time job not yet run."""
        run_at = datetime.utcnow() + timedelta(hours=1)

        job = ScheduledJob(
            id="sched_12345",
            user_id="user_123",
            name="One-time Job",
            schedule_type=ScheduleType.ONCE,
            run_at=run_at,
            function_payload={},
            resources={},
        )

        next_run = job.get_next_run_time()

        assert next_run == run_at

    def test_get_next_run_time_once_already_run(self):
        """Test next run for one-time job already run."""
        run_at = datetime.utcnow() + timedelta(hours=1)

        job = ScheduledJob(
            id="sched_12345",
            user_id="user_123",
            name="One-time Job",
            schedule_type=ScheduleType.ONCE,
            run_at=run_at,
            function_payload={},
            resources={},
            total_runs=1,  # Already run
        )

        next_run = job.get_next_run_time()

        assert next_run is None

    def test_get_next_run_time_max_runs_reached(self):
        """Test next run when max runs reached."""
        job = ScheduledJob(
            id="sched_12345",
            user_id="user_123",
            name="Test Job",
            cron_expression="0 * * * *",
            function_payload={},
            resources={},
            max_runs=5,
            total_runs=5,
        )

        next_run = job.get_next_run_time()

        assert next_run is None

    def test_get_next_run_time_expired(self):
        """Test next run when schedule expired."""
        job = ScheduledJob(
            id="sched_12345",
            user_id="user_123",
            name="Test Job",
            cron_expression="0 * * * *",
            function_payload={},
            resources={},
            expires_at=datetime.utcnow() - timedelta(hours=1),  # Expired
        )

        next_run = job.get_next_run_time()

        assert next_run is None

    def test_get_next_run_time_paused(self):
        """Test next run for paused job."""
        job = ScheduledJob(
            id="sched_12345",
            user_id="user_123",
            name="Test Job",
            cron_expression="0 * * * *",
            function_payload={},
            resources={},
            status=ScheduleStatus.PAUSED,
        )

        next_run = job.get_next_run_time()

        assert next_run is None

    def test_to_dict(self):
        """Test serialization."""
        job = ScheduledJob(
            id="sched_12345",
            user_id="user_123",
            name="Test Job",
            cron_expression="0 9 * * *",
            function_payload={"test": True},
            resources={},
        )

        data = job.to_dict()

        assert data["id"] == "sched_12345"
        assert data["cron_expression"] == "0 9 * * *"
        assert data["schedule_type"] == "cron"

    def test_from_dict(self):
        """Test deserialization."""
        data = {
            "id": "sched_12345",
            "user_id": "user_123",
            "name": "Test Job",
            "schedule_type": "cron",
            "cron_expression": "0 9 * * *",
            "function_payload": {"test": True},
            "resources": {},
            "status": "active",
        }

        job = ScheduledJob.from_dict(data)

        assert job.id == "sched_12345"
        assert job.cron_expression == "0 9 * * *"


class TestScheduleExecution:
    """Tests for ScheduleExecution dataclass."""

    def test_create_execution(self):
        """Test creating an execution record."""
        exec_record = ScheduleExecution(
            id="exec_12345",
            schedule_id="sched_12345",
            job_id="job_12345",
            scheduled_at=datetime.utcnow(),
        )

        assert exec_record.id == "exec_12345"
        assert exec_record.status == "pending"

    def test_execution_to_dict(self):
        """Test execution serialization."""
        exec_record = ScheduleExecution(
            id="exec_12345",
            schedule_id="sched_12345",
            job_id="job_12345",
            scheduled_at=datetime.utcnow(),
            status="completed",
            duration_seconds=120.5,
        )

        data = exec_record.to_dict()

        assert data["status"] == "completed"
        assert data["duration_seconds"] == 120.5


class TestCronScheduler:
    """Tests for CronScheduler class."""

    @pytest.fixture
    def scheduler(self):
        """Create a fresh scheduler."""
        return CronScheduler()

    @pytest.mark.asyncio
    async def test_create_schedule_cron(self, scheduler):
        """Test creating a cron schedule."""
        schedule = await scheduler.create_schedule(
            user_id="user_123",
            name="Test Schedule",
            function_payload={"function": "test"},
            resources={"cpu": 1},
            cron_expression="0 9 * * *",
        )

        assert schedule.id.startswith("sched_")
        assert schedule.schedule_type == ScheduleType.CRON
        assert schedule.next_run_at is not None

    @pytest.mark.asyncio
    async def test_create_schedule_once(self, scheduler):
        """Test creating a one-time schedule."""
        run_at = datetime.utcnow() + timedelta(hours=1)

        schedule = await scheduler.create_schedule(
            user_id="user_123",
            name="One-time Schedule",
            function_payload={"function": "test"},
            resources={},
            run_at=run_at,
        )

        assert schedule.schedule_type == ScheduleType.ONCE

    @pytest.mark.asyncio
    async def test_create_schedule_interval(self, scheduler):
        """Test creating an interval schedule."""
        schedule = await scheduler.create_schedule(
            user_id="user_123",
            name="Interval Schedule",
            function_payload={"function": "test"},
            resources={},
            interval_seconds=3600,
        )

        assert schedule.schedule_type == ScheduleType.INTERVAL

    @pytest.mark.asyncio
    async def test_create_schedule_no_type(self, scheduler):
        """Test creating schedule without type fails."""
        with pytest.raises(ValueError):
            await scheduler.create_schedule(
                user_id="user_123",
                name="Invalid Schedule",
                function_payload={},
                resources={},
                # No cron_expression, run_at, or interval_seconds
            )

    @pytest.mark.asyncio
    async def test_create_schedule_invalid_cron(self, scheduler):
        """Test creating schedule with invalid cron."""
        with pytest.raises(ValueError):
            await scheduler.create_schedule(
                user_id="user_123",
                name="Invalid Cron",
                function_payload={},
                resources={},
                cron_expression="invalid cron",
            )

    @pytest.mark.asyncio
    async def test_list_user_schedules(self, scheduler):
        """Test listing user schedules."""
        await scheduler.create_schedule(
            user_id="user_123",
            name="Schedule 1",
            function_payload={},
            resources={},
            cron_expression="0 9 * * *",
        )
        await scheduler.create_schedule(
            user_id="user_123",
            name="Schedule 2",
            function_payload={},
            resources={},
            cron_expression="0 18 * * *",
        )

        schedules = await scheduler.list_user_schedules("user_123")

        assert len(schedules) == 2

    @pytest.mark.asyncio
    async def test_get_schedule(self, scheduler):
        """Test getting a specific schedule."""
        created = await scheduler.create_schedule(
            user_id="user_123",
            name="Test Schedule",
            function_payload={},
            resources={},
            cron_expression="0 9 * * *",
        )

        schedule = await scheduler.get_schedule(created.id, "user_123")

        assert schedule is not None
        assert schedule.id == created.id

    @pytest.mark.asyncio
    async def test_get_schedule_wrong_user(self, scheduler):
        """Test getting schedule with wrong user."""
        created = await scheduler.create_schedule(
            user_id="user_123",
            name="Test Schedule",
            function_payload={},
            resources={},
            cron_expression="0 9 * * *",
        )

        schedule = await scheduler.get_schedule(created.id, "user_wrong")

        assert schedule is None

    @pytest.mark.asyncio
    async def test_update_schedule(self, scheduler):
        """Test updating a schedule."""
        created = await scheduler.create_schedule(
            user_id="user_123",
            name="Test Schedule",
            function_payload={},
            resources={},
            cron_expression="0 9 * * *",
        )

        updated = await scheduler.update_schedule(
            schedule_id=created.id,
            user_id="user_123",
            name="Updated Schedule",
            cron_expression="0 18 * * *",
        )

        assert updated.name == "Updated Schedule"
        assert updated.cron_expression == "0 18 * * *"

    @pytest.mark.asyncio
    async def test_pause_schedule(self, scheduler):
        """Test pausing a schedule."""
        created = await scheduler.create_schedule(
            user_id="user_123",
            name="Test Schedule",
            function_payload={},
            resources={},
            cron_expression="0 9 * * *",
        )

        paused = await scheduler.pause_schedule(created.id, "user_123")

        assert paused is True

        schedule = await scheduler.get_schedule(created.id, "user_123")
        assert schedule.status == ScheduleStatus.PAUSED
        assert schedule.next_run_at is None

    @pytest.mark.asyncio
    async def test_resume_schedule(self, scheduler):
        """Test resuming a schedule."""
        created = await scheduler.create_schedule(
            user_id="user_123",
            name="Test Schedule",
            function_payload={},
            resources={},
            cron_expression="0 9 * * *",
        )

        await scheduler.pause_schedule(created.id, "user_123")
        resumed = await scheduler.resume_schedule(created.id, "user_123")

        assert resumed is True

        schedule = await scheduler.get_schedule(created.id, "user_123")
        assert schedule.status == ScheduleStatus.ACTIVE
        assert schedule.next_run_at is not None

    @pytest.mark.asyncio
    async def test_delete_schedule(self, scheduler):
        """Test deleting a schedule."""
        created = await scheduler.create_schedule(
            user_id="user_123",
            name="Test Schedule",
            function_payload={},
            resources={},
            cron_expression="0 9 * * *",
        )

        deleted = await scheduler.delete_schedule(created.id, "user_123")

        assert deleted is True

        schedule = await scheduler.get_schedule(created.id, "user_123")
        assert schedule is None

    @pytest.mark.asyncio
    async def test_list_schedules_with_filter(self, scheduler):
        """Test listing schedules with status filter."""
        await scheduler.create_schedule(
            user_id="user_123",
            name="Active Schedule",
            function_payload={},
            resources={},
            cron_expression="0 9 * * *",
        )

        paused = await scheduler.create_schedule(
            user_id="user_123",
            name="Paused Schedule",
            function_payload={},
            resources={},
            cron_expression="0 18 * * *",
        )
        await scheduler.pause_schedule(paused.id, "user_123")

        active = await scheduler.list_user_schedules("user_123", status=ScheduleStatus.ACTIVE)
        paused_list = await scheduler.list_user_schedules("user_123", status=ScheduleStatus.PAUSED)

        assert len(active) == 1
        assert len(paused_list) == 1

    @pytest.mark.asyncio
    async def test_list_schedules_with_tags(self, scheduler):
        """Test listing schedules with tag filter."""
        await scheduler.create_schedule(
            user_id="user_123",
            name="ML Schedule",
            function_payload={},
            resources={},
            cron_expression="0 9 * * *",
            tags=["ml", "training"],
        )
        await scheduler.create_schedule(
            user_id="user_123",
            name="ETL Schedule",
            function_payload={},
            resources={},
            cron_expression="0 18 * * *",
            tags=["etl", "data"],
        )

        ml_schedules = await scheduler.list_user_schedules("user_123", tags=["ml"])

        assert len(ml_schedules) == 1
        assert "ml" in ml_schedules[0].tags

    @pytest.mark.asyncio
    async def test_trigger_schedule_manually(self, scheduler):
        """Test manual schedule trigger."""
        job_submitter = AsyncMock(return_value="job_12345")
        scheduler.set_job_submitter(job_submitter)

        created = await scheduler.create_schedule(
            user_id="user_123",
            name="Test Schedule",
            function_payload={"function": "test"},
            resources={},
            cron_expression="0 9 * * *",
        )

        job_id = await scheduler.trigger_schedule(created.id, "user_123")

        assert job_id is not None

        schedule = await scheduler.get_schedule(created.id, "user_123")
        assert schedule.total_runs == 1

    @pytest.mark.asyncio
    async def test_trigger_schedule_max_concurrent(self, scheduler):
        """Test trigger respects max concurrent limit."""
        scheduler._active_executions["sched_test"] = 1

        created = await scheduler.create_schedule(
            user_id="user_123",
            name="Test Schedule",
            function_payload={},
            resources={},
            cron_expression="0 9 * * *",
            max_concurrent=1,
        )

        # Simulate max concurrent reached
        scheduler._active_executions[created.id] = 1

        job_id = await scheduler.trigger_schedule(created.id, "user_123")

        # Should not trigger because max concurrent reached
        assert job_id is None

    @pytest.mark.asyncio
    async def test_get_metrics(self, scheduler):
        """Test getting scheduler metrics."""
        await scheduler.create_schedule(
            user_id="user_123",
            name="Schedule 1",
            function_payload={},
            resources={},
            cron_expression="0 9 * * *",
        )

        metrics = scheduler.get_metrics()

        assert "total_schedules" in metrics
        assert metrics["total_schedules"] == 1
        assert metrics["active_schedules"] == 1


class TestHelperFunctions:
    """Tests for helper functions."""

    def test_validate_cron_expression_valid(self):
        """Test validating a valid cron expression."""
        valid, error = validate_cron_expression("0 9 * * *")

        assert valid is True
        assert error is None

    def test_validate_cron_expression_invalid(self):
        """Test validating an invalid cron expression."""
        valid, error = validate_cron_expression("invalid")

        assert valid is False
        assert error is not None

    def test_get_next_n_runs(self):
        """Test getting next N run times."""
        runs = get_next_n_runs("0 * * * *", n=5)

        assert len(runs) == 5
        for run in runs:
            assert run.minute == 0

    def test_describe_cron(self):
        """Test getting cron description."""
        desc = describe_cron("0 9 * * 1-5")

        assert desc is not None
        assert isinstance(desc, str)

    def test_describe_cron_invalid(self):
        """Test describing invalid cron."""
        desc = describe_cron("invalid")

        assert "Invalid" in desc


class TestCronSchedulerIntegration:
    """Integration tests for cron scheduler."""

    @pytest.fixture
    def scheduler(self):
        """Create a fresh scheduler."""
        return CronScheduler()

    @pytest.mark.asyncio
    async def test_full_schedule_lifecycle(self, scheduler):
        """Test complete schedule lifecycle."""
        # Create schedule
        schedule = await scheduler.create_schedule(
            user_id="user_123",
            name="Test Schedule",
            description="A test scheduled job",
            function_payload={"function": "test"},
            resources={"cpu": 2},
            cron_expression="*/5 * * * *",  # Every 5 minutes
            tags=["test", "integration"],
        )

        assert schedule.id is not None
        assert schedule.next_run_at is not None

        # List schedules
        schedules = await scheduler.list_user_schedules("user_123")
        assert len(schedules) == 1

        # Update schedule
        updated = await scheduler.update_schedule(
            schedule_id=schedule.id,
            user_id="user_123",
            description="Updated description",
        )
        assert updated.description == "Updated description"

        # Pause schedule
        await scheduler.pause_schedule(schedule.id, "user_123")
        paused = await scheduler.get_schedule(schedule.id, "user_123")
        assert paused.status == ScheduleStatus.PAUSED

        # Resume schedule
        await scheduler.resume_schedule(schedule.id, "user_123")
        resumed = await scheduler.get_schedule(schedule.id, "user_123")
        assert resumed.status == ScheduleStatus.ACTIVE

        # Delete schedule
        deleted = await scheduler.delete_schedule(schedule.id, "user_123")
        assert deleted is True

        # Verify deletion
        schedules = await scheduler.list_user_schedules("user_123")
        assert len(schedules) == 0
