"""Unit tests for the cron pipeline schedule config."""

from __future__ import annotations

import pytest
from sqlalchemy.ext.asyncio import AsyncSession

from app.exceptions import ValidationError
from app.scheduler import SCHEDULE_DEFAULTS, validate_cron
from app.services.admin_service import get_schedule_config, update_schedule_config


@pytest.fixture
async def session() -> AsyncSession:
    from tests.conftest import _test_session_factory

    async with _test_session_factory() as session:
        yield session


class TestValidateCron:
    def test_accepts_valid(self):
        validate_cron("0 7 * * *", "Europe/Berlin")
        validate_cron("0 14-22 * * 1-5", "UTC")

    def test_rejects_bad_cron(self):
        with pytest.raises(Exception):
            validate_cron("not a cron", "UTC")

    def test_rejects_bad_timezone(self):
        with pytest.raises(Exception):
            validate_cron("0 7 * * *", "Mars/Phobos")


class TestTradingDayCrons:
    """APScheduler's from_crontab() uses 0=Monday, so numeric "1-5" means
    Tue–Sat: it skips every Monday and fires on Saturdays. Weekday schedules
    must therefore be spelled with day *names*.
    """

    _WEEKDAY_KEYS = (
        "schedule_near_close_pipeline_cron",
        "schedule_after_close_pipeline_cron",
        "schedule_intraday_pipeline_cron",
    )

    @pytest.mark.parametrize("key", _WEEKDAY_KEYS)
    def test_fires_monday_and_never_saturday(self, key: str):
        from datetime import datetime, timedelta

        from apscheduler.triggers.cron import CronTrigger

        trigger = CronTrigger.from_crontab(
            SCHEDULE_DEFAULTS[key], timezone=SCHEDULE_DEFAULTS["schedule_timezone"]
        )
        # Walk a full week of fire times from a known Sunday.
        cursor = datetime(2026, 7, 19, tzinfo=trigger.timezone)
        weekdays = set()
        previous = None
        for _ in range(12):
            fire = trigger.get_next_fire_time(previous, cursor)
            weekdays.add(fire.strftime("%a"))
            previous = fire
            cursor = fire + timedelta(seconds=1)

        assert "Mon" in weekdays, f"{key} skips Mondays — numeric day-of-week?"
        assert {"Sat", "Sun"}.isdisjoint(weekdays), f"{key} fires on a weekend"

    def test_fundamentals_runs_on_monday(self):
        from datetime import datetime

        from apscheduler.triggers.cron import CronTrigger

        trigger = CronTrigger.from_crontab(
            SCHEDULE_DEFAULTS["schedule_fundamentals_cron"],
            timezone=SCHEDULE_DEFAULTS["schedule_timezone"],
        )
        fire = trigger.get_next_fire_time(
            None, datetime(2026, 7, 19, tzinfo=trigger.timezone)
        )
        assert fire.strftime("%a") == "Mon"


class TestScheduleConfig:
    async def test_defaults_when_unset(self, session: AsyncSession):
        config = await get_schedule_config(session)
        assert config == SCHEDULE_DEFAULTS

    async def test_update_and_read_back(self, session: AsyncSession):
        updated = await update_schedule_config(
            session, {"schedule_daily_pipeline_cron": "30 6 * * *"}
        )
        assert updated["schedule_daily_pipeline_cron"] == "30 6 * * *"
        # untouched keys keep their defaults
        assert updated["schedule_intraday_pipeline_cron"] == SCHEDULE_DEFAULTS["schedule_intraday_pipeline_cron"]

        config = await get_schedule_config(session)
        assert config["schedule_daily_pipeline_cron"] == "30 6 * * *"

    async def test_rejects_bad_cron(self, session: AsyncSession):
        with pytest.raises(ValidationError):
            await update_schedule_config(session, {"schedule_fundamentals_cron": "every monday"})

    async def test_rejects_bad_timezone(self, session: AsyncSession):
        with pytest.raises(ValidationError):
            await update_schedule_config(session, {"schedule_timezone": "Nowhere/Void"})

    async def test_rejects_unknown_key(self, session: AsyncSession):
        with pytest.raises(ValidationError):
            await update_schedule_config(session, {"schedule_bogus": "0 0 * * *"})
