"""Unit tests for recurrence engine - daily/weekly/monthly/yearly/custom + exceptions."""

from datetime import UTC, date, datetime

from app.plugins.builtins.calendar.recurrence import (
    _days_in_month,
    _extract_bydays,
    _extract_int,
    _extract_setpos,
    _nth_weekday_of_month,
    _parse_date,
    _parse_datetime,
    generate_occurrences,
)


class TestParseDate:
    def test_parse_date_from_date(self):
        d = date(2026, 6, 15)
        assert _parse_date(d) == d

    def test_parse_date_from_datetime(self):
        dt = datetime(2026, 6, 15, 10, 30, tzinfo=UTC)
        assert _parse_date(dt) == date(2026, 6, 15)

    def test_parse_date_from_iso_string(self):
        assert _parse_date("2026-06-15") == date(2026, 6, 15)
        assert _parse_date("2026-06-15T10:30:00") == date(2026, 6, 15)

    def test_parse_date_from_invalid_string(self):
        assert _parse_date("not-a-date") is None

    def test_parse_date_none(self):
        assert _parse_date(None) is None


class TestParseDatetime:
    def test_parse_datetime_from_datetime(self):
        dt = datetime(2026, 6, 15, 10, 30, tzinfo=UTC)
        assert _parse_datetime(dt) == dt

    def test_parse_datetime_from_date(self):
        d = date(2026, 6, 15)
        result = _parse_datetime(d)
        assert result == datetime(2026, 6, 15)

    def test_parse_datetime_from_iso_string(self):
        result = _parse_datetime("2026-06-15T10:30:00")
        assert result == datetime(2026, 6, 15, 10, 30)

    def test_parse_datetime_from_invalid_string(self):
        assert _parse_datetime("not-a-datetime") is None

    def test_parse_datetime_none(self):
        assert _parse_datetime(None) is None


class TestGenerateOccurrencesDaily:
    def test_daily_basic(self):
        base = datetime(2026, 6, 1, 9, 0, tzinfo=UTC)
        rec = {"pattern": "daily"}
        occs = generate_occurrences(rec, base, date(2026, 6, 1), date(2026, 6, 5))
        assert len(occs) == 5
        assert occs[0].date() == date(2026, 6, 1)
        assert occs[-1].date() == date(2026, 6, 5)

    def test_daily_with_interval(self):
        base = datetime(2026, 6, 1, 9, 0, tzinfo=UTC)
        rec = {"pattern": "daily", "custom_rule": "INTERVAL=2"}
        occs = generate_occurrences(rec, base, date(2026, 6, 1), date(2026, 6, 7))
        assert len(occs) == 4
        assert occs[0].date() == date(2026, 6, 1)
        assert occs[1].date() == date(2026, 6, 3)

    def test_daily_with_exception(self):
        base = datetime(2026, 6, 1, 9, 0, tzinfo=UTC)
        rec = {"pattern": "daily", "exceptions": ["2026-06-02", "2026-06-04"]}
        occs = generate_occurrences(rec, base, date(2026, 6, 1), date(2026, 6, 5))
        assert len(occs) == 3
        dates = [o.date() for o in occs]
        assert date(2026, 6, 2) not in dates
        assert date(2026, 6, 4) not in dates

    def test_daily_with_end_date(self):
        base = datetime(2026, 6, 1, 9, 0, tzinfo=UTC)
        rec = {"pattern": "daily", "end_date": "2026-06-03"}
        occs = generate_occurrences(rec, base, date(2026, 6, 1), date(2026, 6, 10))
        assert len(occs) == 3

    def test_no_recurrence(self):
        base = datetime(2026, 6, 1, 9, 0, tzinfo=UTC)
        occs = generate_occurrences(None, base, date(2026, 6, 1), date(2026, 6, 5))
        assert len(occs) == 1
        assert occs[0].date() == date(2026, 6, 1)

    def test_no_base_start(self):
        occs = generate_occurrences({"pattern": "daily"}, None, date(2026, 6, 1), date(2026, 6, 5))
        assert occs == []


class TestGenerateOccurrencesWeekly:
    def test_weekly_basic(self):
        base = datetime(2026, 6, 2, 9, 0, tzinfo=UTC)
        rec = {"pattern": "weekly"}
        occs = generate_occurrences(rec, base, date(2026, 6, 1), date(2026, 6, 22))
        assert len(occs) == 3
        assert all(o.weekday() == 1 for o in occs)

    def test_weekly_with_bydays(self):
        base = datetime(2026, 6, 1, 9, 0, tzinfo=UTC)
        rec = {"pattern": "weekly", "custom_rule": "BYDAY=MO,WE,FR"}
        occs = generate_occurrences(rec, base, date(2026, 6, 1), date(2026, 6, 7))
        assert len(occs) == 3

    def test_weekly_with_interval(self):
        base = datetime(2026, 6, 2, 9, 0, tzinfo=UTC)
        rec = {"pattern": "weekly", "custom_rule": "INTERVAL=2"}
        occs = generate_occurrences(rec, base, date(2026, 6, 1), date(2026, 6, 30))
        assert len(occs) == 3


class TestGenerateOccurrencesMonthly:
    def test_monthly_basic(self):
        base = datetime(2026, 6, 15, 10, 0, tzinfo=UTC)
        rec = {"pattern": "monthly"}
        occs = generate_occurrences(rec, base, date(2026, 6, 1), date(2026, 9, 30))
        assert len(occs) == 4
        assert all(o.day == 15 for o in occs)

    def test_monthly_with_interval(self):
        base = datetime(2026, 6, 15, 10, 0, tzinfo=UTC)
        rec = {"pattern": "monthly", "custom_rule": "INTERVAL=2"}
        occs = generate_occurrences(rec, base, date(2026, 6, 1), date(2026, 12, 31))
        assert len(occs) == 4

    def test_monthly_with_bysetpos(self):
        base = datetime(2026, 6, 1, 10, 0, tzinfo=UTC)
        rec = {"pattern": "monthly", "custom_rule": "BYSETPOS=2"}
        occs = generate_occurrences(rec, base, date(2026, 6, 1), date(2026, 7, 31))
        assert len(occs) == 2


class TestGenerateOccurrencesYearly:
    def test_yearly_basic(self):
        base = datetime(2026, 6, 15, 10, 0, tzinfo=UTC)
        rec = {"pattern": "yearly"}
        occs = generate_occurrences(rec, base, date(2026, 1, 1), date(2028, 12, 31))
        assert len(occs) == 3
        assert all(o.month == 6 for o in occs)
        assert all(o.day == 15 for o in occs)

    def test_yearly_with_interval(self):
        base = datetime(2026, 6, 15, 10, 0, tzinfo=UTC)
        rec = {"pattern": "yearly", "custom_rule": "INTERVAL=2"}
        occs = generate_occurrences(rec, base, date(2026, 1, 1), date(2030, 12, 31))
        assert len(occs) == 2  # 2026, 2028 (2030 beyond 2-year cap)


class TestGenerateOccurrencesCustom:
    def test_custom_with_bydays(self):
        base = datetime(2026, 6, 1, 9, 0, tzinfo=UTC)
        rec = {"pattern": "custom", "custom_rule": "INTERVAL=1;BYDAY=TU,TH"}
        occs = generate_occurrences(rec, base, date(2026, 6, 1), date(2026, 6, 7))
        assert len(occs) == 2


class TestExtractHelpers:
    def test_extract_int(self):
        assert _extract_int("INTERVAL=2;BYDAY=MO", "INTERVAL") == 2
        assert _extract_int("BYDAY=MO", "INTERVAL") == 1

    def test_extract_bydays(self):
        assert _extract_bydays("BYDAY=MO,WE,FR") == ["MO", "WE", "FR"]
        assert _extract_bydays("INTERVAL=2") is None

    def test_extract_setpos(self):
        assert _extract_setpos("BYSETPOS=2") == [2]
        assert _extract_setpos("BYSETPOS=1,3") == [1, 3]
        assert _extract_setpos("INTERVAL=2") is None


class TestDateHelpers:
    def test_days_in_month(self):
        assert _days_in_month(2026, 2) == 28
        assert _days_in_month(2024, 2) == 29
        assert _days_in_month(2026, 12) == 31
        assert _days_in_month(2026, 4) == 30

    def test_nth_weekday_of_month_positive(self):
        first_mon = _nth_weekday_of_month(2026, 6, 1, 0)
        assert first_mon == date(2026, 6, 1)
        second_mon = _nth_weekday_of_month(2026, 6, 2, 0)
        assert second_mon == date(2026, 6, 8)

    def test_nth_weekday_of_month_negative(self):
        last_mon = _nth_weekday_of_month(2026, 6, -1, 0)
        assert last_mon == date(2026, 6, 29)

    def test_nth_weekday_of_month_invalid(self):
        result = _nth_weekday_of_month(2026, 2, 5, 0)
        assert result is None


class TestEdgeCases:
    def test_range_before_base_start(self):
        base = datetime(2026, 6, 15, 9, 0, tzinfo=UTC)
        rec = {"pattern": "daily"}
        occs = generate_occurrences(rec, base, date(2026, 6, 1), date(2026, 6, 10))
        assert occs == []

    def test_range_after_end_date(self):
        base = datetime(2026, 6, 1, 9, 0, tzinfo=UTC)
        rec = {"pattern": "daily", "end_date": "2026-06-05"}
        occs = generate_occurrences(rec, base, date(2026, 6, 10), date(2026, 6, 20))
        assert occs == []

    def test_unknown_pattern(self):
        base = datetime(2026, 6, 1, 9, 0, tzinfo=UTC)
        rec = {"pattern": "unknown"}
        occs = generate_occurrences(rec, base, date(2026, 6, 1), date(2026, 6, 5))
        assert occs == []

    def test_max_2_years_forward(self):
        base = datetime(2026, 6, 1, 9, 0, tzinfo=UTC)
        rec = {"pattern": "daily"}
        occs = generate_occurrences(rec, base, date(2026, 6, 1), date(2030, 6, 1))
        assert len(occs) <= 731
