from __future__ import annotations from datetime import date import pytest import app.calendar_agent as calendar_module from app.calendar_agent import CalendarAvailabilityAgent class _FakeFreeBusy: def __init__(self, payload: dict[str, object]) -> None: self.payload = payload def query(self, body: dict[str, object]) -> _FakeFreeBusy: return self def execute(self) -> dict[str, object]: return self.payload class _FakeCalendarService: def __init__(self, payload: dict[str, object]) -> None: self._freebusy = _FakeFreeBusy(payload) def freebusy(self) -> _FakeFreeBusy: return self._freebusy def _build_agent(payload: dict[str, object]) -> CalendarAvailabilityAgent: return CalendarAvailabilityAgent(calendar_service=_FakeCalendarService(payload)) def test_weekday_boundaries_and_busy_subtraction() -> None: agent = _build_agent( { "calendars": { "primary": { "busy": [ { "start": "2026-03-09T09:00:00+01:00", "end": "2026-03-09T10:00:00+01:00", } ] }, "work": { "busy": [ { "start": "2026-03-09T09:30:00+01:00", "end": "2026-03-09T11:00:00+01:00", }, { "start": "2026-03-09T12:00:00+01:00", "end": "2026-03-09T12:20:00+01:00", }, ] }, } } ) result = agent.get_available_meeting_intervals( start="2026-03-09T07:00:00+01:00", end="2026-03-09T22:00:00+01:00", calendar_ids=["primary", "work"], ) assert result.timezone == "Europe/Paris" assert result.checked_calendars == ["primary", "work"] assert result.meeting_intervals == [ { "start": "2026-03-09T08:30:00+01:00", "end": "2026-03-09T09:00:00+01:00", }, { "start": "2026-03-09T11:00:00+01:00", "end": "2026-03-09T12:00:00+01:00", }, { "start": "2026-03-09T12:20:00+01:00", "end": "2026-03-09T21:30:00+01:00", }, ] def test_saturday_window_only() -> None: agent = _build_agent({"calendars": {"primary": {"busy": []}}}) result = agent.get_available_meeting_intervals( start="2026-03-14T08:00:00+01:00", end="2026-03-14T13:00:00+01:00", calendar_ids=None, ) assert result.meeting_intervals == [ { "start": "2026-03-14T09:30:00+01:00", "end": "2026-03-14T12:00:00+01:00", } ] def test_sunday_excluded() -> None: agent = _build_agent({"calendars": {"primary": {"busy": []}}}) result = agent.get_available_meeting_intervals( start="2026-03-15T08:00:00+01:00", end="2026-03-15T14:00:00+01:00", calendar_ids=None, ) assert result.meeting_intervals == [] def test_france_holiday_excluded(monkeypatch) -> None: def _fake_country_holidays( country: str, *, years: range, observed: bool, subdiv: str | None = None, ) -> dict[date, str]: assert observed is False if country == "FR": return {date(2026, 7, 14): "Bastille Day"} if country == "CA" and subdiv == "QC": return {} return {} monkeypatch.setattr(calendar_module.holidays, "country_holidays", _fake_country_holidays) agent = _build_agent({"calendars": {"primary": {"busy": []}}}) result = agent.get_available_meeting_intervals( start="2026-07-14T08:00:00+02:00", end="2026-07-14T22:00:00+02:00", calendar_ids=None, ) assert result.meeting_intervals == [] def test_quebec_holiday_excluded(monkeypatch) -> None: def _fake_country_holidays( country: str, *, years: range, observed: bool, subdiv: str | None = None, ) -> dict[date, str]: assert observed is False if country == "FR": return {} if country == "CA" and subdiv == "QC": return {date(2026, 6, 24): "Saint-Jean-Baptiste Day"} return {} monkeypatch.setattr(calendar_module.holidays, "country_holidays", _fake_country_holidays) agent = _build_agent({"calendars": {"primary": {"busy": []}}}) result = agent.get_available_meeting_intervals( start="2026-06-24T08:00:00+02:00", end="2026-06-24T22:00:00+02:00", calendar_ids=None, ) assert result.meeting_intervals == [] def test_intervals_shorter_than_30_minutes_are_filtered() -> None: agent = _build_agent( { "calendars": { "primary": { "busy": [ { "start": "2026-03-09T08:40:00+01:00", "end": "2026-03-09T09:30:00+01:00", } ] } } } ) result = agent.get_available_meeting_intervals( start="2026-03-09T08:30:00+01:00", end="2026-03-09T09:30:00+01:00", calendar_ids=None, ) assert result.meeting_intervals == [] def test_range_greater_than_90_days_rejected() -> None: agent = _build_agent({"calendars": {"primary": {"busy": []}}}) with pytest.raises(ValueError, match="cannot exceed 90 days"): agent.get_available_meeting_intervals( start="2026-01-01T08:00:00+01:00", end="2026-04-02T08:00:01+02:00", calendar_ids=None, ) def test_requires_timezone_offset() -> None: agent = _build_agent({"calendars": {"primary": {"busy": []}}}) with pytest.raises(ValueError, match="timezone offset"): agent.get_available_meeting_intervals( start="2026-03-10T08:00:00", end="2026-03-10T10:00:00+01:00", calendar_ids=None, )