"""Тесты `services/ics.py::build_invite` («.ics», блок C).

Пишутся ДО закрепления поведения в реализации (TDD) — покрывают парсимость
результата, соответствие VTIMEZONE и TZID DTSTART, RRULE всех 4 типов
повторения, рост SEQUENCE и корректность DTSTART повторяющейся серии.
"""

import uuid
from datetime import UTC, datetime
from typing import Any

import pytest
from icalendar import Calendar

from models.conference import Conference
from services.ics import ConferenceHasNoScheduleError, build_invite
from services.recurrence import RecurrenceRule, expand_occurrences


def _prop(component: Any, name: str) -> Any:
    """Снять широкий `Union` типов свойств `icalendar` при статической проверке mypy.

    `Component.__getitem__` типизирован как union из полутора десятков
    возможных типов значений (`vText`/`vRecur`/`vDDDTypes`/...) — конкретный
    тип известен только в рантайме по имени ключа; без этой обёртки пришлось
    бы городить `# type: ignore` на каждой строке доступа к свойству события.
    """
    return component[name]


def _conference(**overrides: object) -> Conference:
    defaults: dict[str, object] = {
        "id": uuid.uuid4(),
        "number": "123456789",
        "slug": "test-slug-1",
        "title": "Синк по проекту",
        "owner_id": None,
        "status": "scheduled",
        "is_pinned": False,
        "is_closed": False,
        "scheduled_at": datetime(2026, 8, 3, 10, 0, tzinfo=UTC),
        "duration_minutes": 45,
        "ics_sequence": 0,
    }
    defaults.update(overrides)
    return Conference(**defaults)


def _recurrence(**overrides: object) -> dict[str, object]:
    defaults: dict[str, object] = {
        "type": "weekly",
        "weekdays": [0, 2],
        "anchor_date": "2026-08-03",
        "time_local": "09:00",
        "timezone": "Europe/Moscow",
        "duration_minutes": 30,
    }
    defaults.update(overrides)
    return defaults


class TestBuildInviteOneOff:
    def test_result_parses_back_with_icalendar(self) -> None:
        conference = _conference()
        raw = build_invite(
            conference,
            organizer_email="owner@example.com",
            join_url="https://vidconf.example/j/test-slug-1",
            display_timezone="Europe/Moscow",
        )
        parsed = Calendar.from_ical(raw)
        assert parsed.get("method") == "REQUEST"

    def test_dtstart_uses_display_timezone(self) -> None:
        conference = _conference()
        raw = build_invite(
            conference,
            organizer_email=None,
            join_url="https://vidconf.example/j/test-slug-1",
            display_timezone="Europe/Moscow",
        )
        parsed = Calendar.from_ical(raw)
        event = parsed.walk("VEVENT")[0]
        dtstart = _prop(event, "dtstart")
        assert str(dtstart.params["TZID"]) == "Europe/Moscow"
        assert dtstart.dt.hour == 13  # 10:00 UTC -> 13:00 MSK (UTC+3)

    def test_vtimezone_matches_dtstart_tzid(self) -> None:
        conference = _conference()
        raw = build_invite(
            conference,
            organizer_email=None,
            join_url="https://vidconf.example/j/test-slug-1",
            display_timezone="Europe/Moscow",
        )
        parsed = Calendar.from_ical(raw)
        event = parsed.walk("VEVENT")[0]
        tzid = str(_prop(event, "dtstart").params["TZID"])
        vtimezones = parsed.walk("VTIMEZONE")
        assert any(str(_prop(vt, "tzid")) == tzid for vt in vtimezones)

    def test_uid_is_stable_and_sequence_matches_conference(self) -> None:
        conference = _conference(ics_sequence=2)
        raw = build_invite(
            conference,
            organizer_email=None,
            join_url="https://vidconf.example/j/test-slug-1",
            display_timezone="Europe/Moscow",
        )
        event = Calendar.from_ical(raw).walk("VEVENT")[0]
        assert str(_prop(event, "uid")) == f"{conference.id}@vidconf"
        assert int(_prop(event, "sequence")) == 2

    def test_sequence_grows_when_ics_sequence_increments(self) -> None:
        conference = _conference(ics_sequence=0)
        raw_before = build_invite(
            conference,
            organizer_email=None,
            join_url="https://vidconf.example/j/test-slug-1",
            display_timezone="Europe/Moscow",
        )
        conference.ics_sequence = 1
        raw_after = build_invite(
            conference,
            organizer_email=None,
            join_url="https://vidconf.example/j/test-slug-1",
            display_timezone="Europe/Moscow",
        )
        seq_before = int(_prop(Calendar.from_ical(raw_before).walk("VEVENT")[0], "sequence"))
        seq_after = int(_prop(Calendar.from_ical(raw_after).walk("VEVENT")[0], "sequence"))
        assert seq_after > seq_before

    def test_conference_without_schedule_raises(self) -> None:
        conference = _conference(scheduled_at=None)
        with pytest.raises(ConferenceHasNoScheduleError):
            build_invite(
                conference,
                organizer_email=None,
                join_url="https://vidconf.example/j/test-slug-1",
                display_timezone="Europe/Moscow",
            )


class TestBuildInviteRecurring:
    def test_weekly_rrule_byday(self) -> None:
        conference = _conference(
            scheduled_at=None, recurrence=_recurrence(type="weekly", weekdays=[0, 2])
        )
        raw = build_invite(
            conference,
            organizer_email=None,
            join_url="https://vidconf.example/j/test-slug-1",
            display_timezone="Europe/Moscow",
        )
        event = Calendar.from_ical(raw).walk("VEVENT")[0]
        rrule = _prop(event, "rrule")
        assert rrule["FREQ"] == ["WEEKLY"]
        assert rrule["BYDAY"] == ["MO", "WE"]

    def test_biweekly_rrule_interval_two(self) -> None:
        conference = _conference(
            scheduled_at=None,
            recurrence=_recurrence(type="biweekly", weekdays=[1], anchor_date="2026-08-04"),
        )
        raw = build_invite(
            conference,
            organizer_email=None,
            join_url="https://vidconf.example/j/test-slug-1",
            display_timezone="Europe/Moscow",
        )
        event = Calendar.from_ical(raw).walk("VEVENT")[0]
        rrule = _prop(event, "rrule")
        assert rrule["FREQ"] == ["WEEKLY"]
        assert rrule["INTERVAL"] == [2]
        assert rrule["BYDAY"] == ["TU"]
        assert rrule["WKST"] == ["MO"]

    def test_monthly_rrule_bymonthday(self) -> None:
        conference = _conference(
            scheduled_at=None,
            recurrence=_recurrence(
                type="monthly", weekdays=[], day_of_month=15, anchor_date="2026-08-15"
            ),
        )
        raw = build_invite(
            conference,
            organizer_email=None,
            join_url="https://vidconf.example/j/test-slug-1",
            display_timezone="Europe/Moscow",
        )
        event = Calendar.from_ical(raw).walk("VEVENT")[0]
        rrule = _prop(event, "rrule")
        assert rrule["FREQ"] == ["MONTHLY"]
        assert rrule["BYMONTHDAY"] == [15]

    def test_monthly_rrule_31_maps_to_last_day(self) -> None:
        conference = _conference(
            scheduled_at=None,
            recurrence=_recurrence(
                type="monthly", weekdays=[], day_of_month=31, anchor_date="2026-08-31"
            ),
        )
        raw = build_invite(
            conference,
            organizer_email=None,
            join_url="https://vidconf.example/j/test-slug-1",
            display_timezone="Europe/Moscow",
        )
        event = Calendar.from_ical(raw).walk("VEVENT")[0]
        assert _prop(event, "rrule")["BYMONTHDAY"] == [-1]

    def test_every_n_days_rrule_daily_interval(self) -> None:
        conference = _conference(
            scheduled_at=None,
            recurrence=_recurrence(
                type="every_n_days", weekdays=[], interval_days=3, anchor_date="2026-08-03"
            ),
        )
        raw = build_invite(
            conference,
            organizer_email=None,
            join_url="https://vidconf.example/j/test-slug-1",
            display_timezone="Europe/Moscow",
        )
        event = Calendar.from_ical(raw).walk("VEVENT")[0]
        rrule = _prop(event, "rrule")
        assert rrule["FREQ"] == ["DAILY"]
        assert rrule["INTERVAL"] == [3]

    def test_dtstart_is_first_occurrence_from_expand_occurrences(self) -> None:
        recurrence = _recurrence(type="weekly", weekdays=[0, 2], anchor_date="2026-08-03")
        conference = _conference(scheduled_at=None, recurrence=recurrence)
        raw = build_invite(
            conference,
            organizer_email=None,
            join_url="https://vidconf.example/j/test-slug-1",
            display_timezone="Europe/Moscow",
        )
        event = Calendar.from_ical(raw).walk("VEVENT")[0]
        dtstart = _prop(event, "dtstart").dt

        rule = RecurrenceRule.model_validate(recurrence)
        t_from = datetime(2026, 8, 3, tzinfo=UTC)
        t_to = datetime(2026, 9, 1, tzinfo=UTC)
        expected_first = expand_occurrences(rule, t_from, t_to)[0]
        expected_local = expected_first.astimezone(dtstart.tzinfo)

        assert dtstart == expected_local
        assert str(_prop(event, "dtstart").params["TZID"]) == "Europe/Moscow"

    def test_recurring_vtimezone_matches_rule_timezone(self) -> None:
        conference = _conference(
            scheduled_at=None,
            recurrence=_recurrence(type="weekly", weekdays=[0], timezone="America/New_York"),
        )
        raw = build_invite(
            conference,
            organizer_email=None,
            join_url="https://vidconf.example/j/test-slug-1",
            display_timezone="Europe/Moscow",
        )
        parsed = Calendar.from_ical(raw)
        event = parsed.walk("VEVENT")[0]
        tzid = str(_prop(event, "dtstart").params["TZID"])
        assert tzid == "America/New_York"
        vtimezones = parsed.walk("VTIMEZONE")
        assert any(str(_prop(vt, "tzid")) == tzid for vt in vtimezones)
