"""Tests for duty_teller.api.calendar_ics (_to_date, _event_date_range, _get_events_from_ics, get_calendar_events)."""

from datetime import date, datetime
from unittest.mock import patch

import pytest
from icalendar import Calendar, Event

from duty_teller.api import calendar_ics as mod


class TestToDate:
    """Tests for _to_date."""

    def test_datetime_returns_date(self):
        dt = datetime(2025, 3, 15, 10, 0, 0)
        assert mod._to_date(dt) == date(2025, 3, 15)

    def test_date_returns_same(self):
        d = date(2025, 3, 15)
        assert mod._to_date(d) == date(2025, 3, 15)

    def test_invalid_returns_none(self):
        assert mod._to_date("2025-03-15") is None
        assert mod._to_date(None) is None
        assert mod._to_date(123) is None


class TestEventDateRange:
    """Tests for _event_date_range. DTEND is exclusive in iCalendar."""

    def _vevent(self, dtstart_value, dtend_value=None):
        """Build a minimal VEVENT component using icalendar.Event."""
        cal = Calendar()
        ev = Event()
        ev.add("dtstart", dtstart_value)
        if dtend_value is not None:
            ev.add("dtend", dtend_value)
        cal.add_component(ev)
        for c in cal.walk():
            if c.name == "VEVENT":
                return c
        raise AssertionError("no vevent")

    def test_dtstart_and_dtend_exclusive_end(self):
        start = date(2025, 1, 10)
        end_exclusive = date(2025, 1, 13)  # last day of event = 2025-01-12
        comp = self._vevent(start, end_exclusive)
        s, e = mod._event_date_range(comp)
        assert s == date(2025, 1, 10)
        assert e == date(2025, 1, 12)

    def test_only_dtstart_returns_same_for_start_and_end(self):
        start = date(2025, 2, 1)
        comp = self._vevent(start)
        s, e = mod._event_date_range(comp)
        assert s == date(2025, 2, 1)
        assert e == date(2025, 2, 1)

    def test_no_dtstart_returns_none_none(self):
        cal = Calendar()
        ev = Event()
        ev.add("summary", "No dates")
        cal.add_component(ev)
        for c in cal.walk():
            if c.name == "VEVENT":
                s, e = mod._event_date_range(c)
                assert s is None
                assert e is None
                return
        pytest.fail("no vevent")


class TestGetEventsFromIcs:
    """Tests for _get_events_from_ics."""

    def test_minimal_ics_one_event(self):
        cal = Calendar()
        ev = Event()
        ev.add("dtstart", date(2025, 1, 15))
        ev.add("summary", "Meeting")
        cal.add_component(ev)
        raw = cal.to_ical()
        result = mod._get_events_from_ics(raw, "2025-01-01", "2025-01-31")
        assert len(result) == 1
        assert result[0]["date"] == "2025-01-15"
        assert result[0]["summary"] == "Meeting"

    def test_event_filtered_by_date_range(self):
        cal = Calendar()
        ev = Event()
        ev.add("dtstart", date(2025, 1, 10))
        ev.add("dtend", date(2025, 1, 14))  # 10,11,12,13
        ev.add("summary", "Multi")
        cal.add_component(ev)
        raw = cal.to_ical()
        result = mod._get_events_from_ics(raw, "2025-01-12", "2025-01-13")
        assert len(result) == 2
        assert result[0]["date"] == "2025-01-12"
        assert result[1]["date"] == "2025-01-13"

    def test_empty_ics_returns_empty_list(self):
        cal = Calendar()
        raw = cal.to_ical()
        result = mod._get_events_from_ics(raw, "2025-01-01", "2025-01-31")
        assert result == []

    def test_broken_ics_returns_empty_list(self):
        result = mod._get_events_from_ics(
            b"not ical at all", "2025-01-01", "2025-01-31"
        )
        assert result == []

    def test_recurring_events_skipped(self):
        cal = Calendar()
        ev = Event()
        ev.add("dtstart", date(2025, 1, 15))
        ev.add("rrule", {"freq": "daily", "count": 3})
        ev.add("summary", "Recurring")
        cal.add_component(ev)
        raw = cal.to_ical()
        result = mod._get_events_from_ics(raw, "2025-01-01", "2025-01-31")
        assert result == []


class TestGetCalendarEvents:
    """Tests for get_calendar_events (with mocked _fetch_ics, no real HTTP)."""

    def test_empty_url_returns_empty(self):
        assert mod.get_calendar_events("", "2025-01-01", "2025-01-31") == []

    def test_disallowed_url_scheme_returns_empty(self):
        """get_calendar_events: file:// or ftp:// URL does not call urlopen, returns []."""
        result = mod.get_calendar_events(
            "file:///etc/passwd", "2025-01-01", "2025-01-31"
        )
        assert result == []
        result = mod.get_calendar_events(
            "ftp://example.com/cal.ics", "2025-01-01", "2025-01-31"
        )
        assert result == []

    def test_from_after_to_returns_empty(self):
        assert (
            mod.get_calendar_events(
                "https://example.com/a.ics", "2025-02-01", "2025-01-01"
            )
            == []
        )

    @patch.object(mod, "_fetch_ics", return_value=None)
    def test_fetch_returns_none_returns_empty(self, mock_fetch):
        result = mod.get_calendar_events(
            "https://example.com/a.ics", "2025-01-01", "2025-01-31"
        )
        assert result == []
        mock_fetch.assert_called_once()

    @patch.object(mod, "_fetch_ics")
    def test_success_returns_events(self, mock_fetch):
        cal = Calendar()
        ev = Event()
        ev.add("dtstart", date(2025, 1, 20))
        ev.add("summary", "Test event")
        cal.add_component(ev)
        mock_fetch.return_value = cal.to_ical()
        result = mod.get_calendar_events(
            "https://example.com/cal.ics", "2025-01-01", "2025-01-31"
        )
        assert len(result) == 1
        assert result[0]["date"] == "2025-01-20"
        assert result[0]["summary"] == "Test event"
        mock_fetch.assert_called_once_with("https://example.com/cal.ics")
