"""The tests for the webdav calendar component."""

from collections.abc import Awaitable, Callable
import datetime
from http import HTTPStatus
import logging
from typing import Any
from unittest.mock import MagicMock, Mock, patch
import zoneinfo

from caldav.objects import Event
from freezegun.api import FrozenDateTimeFactory
import pytest

from homeassistant.components.caldav.api import async_get_calendars
from homeassistant.components.calendar import CalendarEntityFeature
from homeassistant.const import STATE_OFF, STATE_ON, Platform
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util

from tests.common import MockConfigEntry
from tests.typing import ClientSessionGenerator

EVENTS = [
    """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//E-Corp.//CalDAV Client//EN
BEGIN:VEVENT
UID:1
DTSTAMP:20171125T000000Z
DTSTART:20171127T170000Z
DTEND:20171127T180000Z
SUMMARY:This is a normal event
LOCATION:Hamburg
DESCRIPTION:Surprisingly rainy
END:VEVENT
END:VCALENDAR
""",
    """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Global Dynamics.//CalDAV Client//EN
BEGIN:VEVENT
UID:2
DTSTAMP:20171125T000000Z
DTSTART:20171127T100000Z
DTEND:20171127T110000Z
SUMMARY:This is an offset event !!-02:00
LOCATION:Hamburg
DESCRIPTION:Surprisingly shiny
END:VEVENT
END:VCALENDAR
""",
    """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Global Corp.//CalDAV Client//EN
BEGIN:VEVENT
UID:3
DTSTAMP:20171125T000000Z
DTSTART:20171127
DTEND:20171128
SUMMARY:This is an all day event
LOCATION:Hamburg
DESCRIPTION:What a beautiful day
END:VEVENT
END:VCALENDAR
""",
    """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Global Corp.//CalDAV Client//EN
BEGIN:VEVENT
UID:4
DTSTAMP:20171125T000000Z
DTSTART:20171127
SUMMARY:This is an event without dtend or duration
LOCATION:Hamburg
DESCRIPTION:What an endless day
END:VEVENT
END:VCALENDAR
""",
    """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Global Corp.//CalDAV Client//EN
BEGIN:VEVENT
UID:5
DTSTAMP:20171125T000000Z
DTSTART:20171127
DURATION:PT1H
SUMMARY:This is an event with duration
LOCATION:Hamburg
DESCRIPTION:What a day
END:VEVENT
END:VCALENDAR
""",
    """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Global Corp.//CalDAV Client//EN
BEGIN:VEVENT
UID:6
DTSTAMP:20171125T000000Z
DTSTART:20171127T100000Z
DURATION:PT1H
SUMMARY:This is an event with duration
LOCATION:Hamburg
DESCRIPTION:What a day
END:VEVENT
END:VCALENDAR
""",
    """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Global Corp.//CalDAV Client//EN
BEGIN:VEVENT
UID:7
DTSTART;TZID=America/Los_Angeles:20171127T083000
DTSTAMP:20180301T020053Z
DTEND;TZID=America/Los_Angeles:20171127T093000
SUMMARY:Enjoy the sun
LOCATION:San Francisco
DESCRIPTION:Sunny day
END:VEVENT
END:VCALENDAR
""",
    """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Global Corp.//CalDAV Client//EN
BEGIN:VEVENT
UID:8
DTSTART:20171127T190000
DTEND:20171127T200000
SUMMARY:This is a floating Event
LOCATION:Hamburg
DESCRIPTION:What a day
END:VEVENT
END:VCALENDAR
""",
    """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//E-Corp.//CalDAV Client//EN
BEGIN:VEVENT
UID:9
DTSTAMP:20171125T000000Z
DTSTART:20171027T220000Z
DTEND:20171027T223000Z
SUMMARY:This is a recurring event
LOCATION:Hamburg
DESCRIPTION:Every day for a while
RRULE:FREQ=DAILY;UNTIL=20171227T215959
END:VEVENT
END:VCALENDAR
""",
    """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//E-Corp.//CalDAV Client//EN
BEGIN:VEVENT
UID:10
DTSTAMP:20171125T000000Z
DTSTART:20171027T230000Z
DURATION:PT30M
SUMMARY:This is a recurring event with a duration
LOCATION:Hamburg
DESCRIPTION:Every day for a while as well
RRULE:FREQ=DAILY;UNTIL=20171227T215959
END:VEVENT
END:VCALENDAR
""",
    """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//E-Corp.//CalDAV Client//EN
BEGIN:VEVENT
UID:11
DTSTAMP:20171125T000000Z
DTSTART:20171027T233000Z
DTEND:20171027T235959Z
SUMMARY:This is a recurring event that has ended
LOCATION:Hamburg
DESCRIPTION:Every day for a while
RRULE:FREQ=DAILY;UNTIL=20171127T225959
END:VEVENT
END:VCALENDAR
""",
    """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//E-Corp.//CalDAV Client//EN
BEGIN:VEVENT
UID:12
DTSTAMP:20171125T000000Z
DTSTART:20171027T234500Z
DTEND:20171027T235959Z
SUMMARY:This is a recurring event that never ends
LOCATION:Hamburg
DESCRIPTION:Every day forever
RRULE:FREQ=DAILY
END:VEVENT
END:VCALENDAR
""",
    """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Global Corp.//CalDAV Client//EN
BEGIN:VEVENT
UID:13
DTSTAMP:20161125T000000Z
DTSTART:20161127
DTEND:20161128
SUMMARY:This is a recurring all day event
LOCATION:Hamburg
DESCRIPTION:Groundhog Day
RRULE:FREQ=DAILY;COUNT=100
END:VEVENT
END:VCALENDAR
""",
    """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Global Corp.//CalDAV Client//EN
BEGIN:VEVENT
UID:14
DTSTAMP:20151125T000000Z
DTSTART:20151127T000000Z
DTEND:20151127T003000Z
SUMMARY:This is an hourly recurring event
LOCATION:Hamburg
DESCRIPTION:The bell tolls for thee
RRULE:FREQ=HOURLY;INTERVAL=1;COUNT=12
END:VEVENT
END:VCALENDAR
""",
    """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Global Corp.//CalDAV Client//EN
BEGIN:VEVENT
UID:14
DTSTAMP:20151125T000000Z
DTSTART:20151127T000000Z
DTEND:20151127T003000Z
RRULE:FREQ=HOURLY;INTERVAL=1;COUNT=12
END:VEVENT
END:VCALENDAR
""",
    """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Global Corp.//CalDAV Client//EN
BEGIN:VTIMEZONE
TZID:Europe/London
BEGIN:STANDARD
DTSTART:19961027T020000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
TZNAME:GMT
TZOFFSETFROM:+0100
TZOFFSETTO:+0000
END:STANDARD
BEGIN:DAYLIGHT
DTSTART:19810329T010000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
TZNAME:BST
TZOFFSETFROM:+0000
TZOFFSETTO:+0100
END:DAYLIGHT
END:VTIMEZONE
BEGIN:VEVENT
UID:15
DTSTAMP:20221125T000000Z
DTSTART;TZID=Europe/London:20221127T000000
DTEND;TZID=Europe/London:20221127T003000
SUMMARY:Event with a provided Timezone
END:VEVENT
END:VCALENDAR
""",
    """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Global Corp.//CalDAV Client//EN
BEGIN:VEVENT
UID:16
DTSTAMP:20171125T000000Z
DTSTART:20171127
DTEND:20171128
SUMMARY:All day event with same start and end
LOCATION:Hamburg
END:VEVENT
END:VCALENDAR
""",
    """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Global Corp.//CalDAV Client//EN
BEGIN:VEVENT
UID:17
DTSTAMP:20171125T000000Z
DTSTART:20171127T010000
DTEND:20171127T010000
SUMMARY:Event with no duration
LOCATION:Hamburg
END:VEVENT
END:VCALENDAR
""",
]

CALDAV_CONFIG = {
    "platform": "caldav",
    "url": "http://test.local",
    "custom_calendars": [],
}
UTC = "UTC"
AMERICA_NEW_YORK = "America/New_York"
ASIA_BAGHDAD = "Asia/Baghdad"

TEST_ENTITY = "calendar.example"
CALENDAR_NAME = "Example"


@pytest.fixture
def platforms() -> list[Platform]:
    """Fixture to set up config entry platforms."""
    return [Platform.CALENDAR]


@pytest.fixture(name="tz")
def mock_tz() -> str | None:
    """Fixture to specify the Home Assistant timezone to use during the test."""
    return None


@pytest.fixture(autouse=True)
async def set_tz(hass: HomeAssistant, tz: str | None) -> None:
    """Fixture to set the default TZ to the one requested."""
    if tz is not None:
        await hass.config.async_set_time_zone(tz)


@pytest.fixture(autouse=True)
def mock_http(hass: HomeAssistant) -> None:
    """Mock the http component."""
    hass.http = Mock()


@pytest.fixture(name="calendar_names")
def mock_calendar_names() -> list[str]:
    """Fixture to provide calendars returned by CalDAV client."""
    return ["Example"]


@pytest.fixture(name="calendars")
def mock_calendars(calendar_names: list[str]) -> list[Mock]:
    """Fixture to provide calendars returned by CalDAV client."""
    return [_mock_calendar(name) for name in calendar_names]


@pytest.fixture
def get_api_events(
    hass_client: ClientSessionGenerator,
) -> Callable[[str], Awaitable[dict[str, Any]]]:
    """Fixture to return events for a specific calendar using the API."""

    async def api_call(entity_id: str) -> dict[str, Any]:
        client = await hass_client()
        response = await client.get(
            # The start/end times are arbitrary since they are
            # ignored by `_mock_calendar` which just returns all
            # events for the calendar.
            f"/api/calendars/{entity_id}?start=2022-01-01&end=2022-01-01"
        )
        assert response.status == HTTPStatus.OK
        return await response.json()

    return api_call


def _local_datetime(hours: int, minutes: int) -> datetime.datetime:
    """Build a datetime object for testing in the correct timezone."""
    return dt_util.as_local(datetime.datetime(2017, 11, 27, hours, minutes, 0))


def _mock_calendar(name: str, supported_components: list[str] | None = None) -> Mock:
    calendar = Mock()
    events = []
    for idx, event in enumerate(EVENTS):
        events.append(Event(None, f"{idx}.ics", event, calendar, str(idx)))
    if supported_components is None:
        supported_components = ["VEVENT"]
    calendar.search = MagicMock(return_value=events)
    calendar.name = name
    calendar.get_supported_components = MagicMock(return_value=supported_components)
    return calendar


@pytest.fixture(name="config")
def mock_config() -> dict[str, Any]:
    """Fixture to provide calendar configuration.yaml."""
    return {}


@pytest.fixture(name="setup_platform_cb")
async def mock_setup_platform_cb(
    hass: HomeAssistant, config: dict[str, Any]
) -> Callable[[], Awaitable[None]]:
    """Fixture that returns a function to setup the calendar platform."""

    async def _run() -> None:
        assert await async_setup_component(
            hass, "calendar", {"calendar": {**CALDAV_CONFIG, **config}}
        )
        await hass.async_block_till_done()

    return _run


@pytest.mark.parametrize(
    ("calendar_names", "config", "expected_entities"),
    [
        (["First", "Second"], {}, ["calendar.first", "calendar.second"]),
        (
            ["First", "Second"],
            {"calendars": ["none"]},
            [],
        ),
        (["First", "Second"], {"calendars": ["Second"]}, ["calendar.second"]),
        (
            ["First", "Second"],
            {
                "custom_calendars": {
                    "name": "HomeOffice",
                    "calendar": "Second",
                    "search": "HomeOffice",
                },
            },
            ["calendar.second_homeoffice"],
        ),
    ],
    ids=("config", "no_match", "match", "custom"),
)
async def test_setup_component_config(
    hass: HomeAssistant,
    config: dict[str, Any],
    expected_entities: list[str],
    setup_platform_cb: Callable[[], Awaitable[None]],
) -> None:
    """Test setup component with wrong calendar."""
    await setup_platform_cb()

    all_calendar_entities = hass.states.async_entity_ids("calendar")
    assert all_calendar_entities == expected_entities


@pytest.mark.parametrize("tz", [UTC])
@pytest.mark.freeze_time(_local_datetime(17, 45))
async def test_ongoing_event(
    hass: HomeAssistant, setup_platform_cb: Callable[[], Awaitable[None]]
) -> None:
    """Test that the ongoing event is returned."""
    await setup_platform_cb()

    state = hass.states.get(TEST_ENTITY)
    assert state
    assert state.name == CALENDAR_NAME
    assert state.state == STATE_ON
    assert dict(state.attributes) == {
        "friendly_name": CALENDAR_NAME,
        "message": "This is a normal event",
        "all_day": False,
        "offset_reached": False,
        "start_time": "2017-11-27 17:00:00",
        "end_time": "2017-11-27 18:00:00",
        "location": "Hamburg",
        "description": "Surprisingly rainy",
        "supported_features": CalendarEntityFeature.CREATE_EVENT,
    }


@pytest.mark.parametrize("tz", [UTC])
@pytest.mark.freeze_time(_local_datetime(17, 30))
async def test_just_ended_event(
    hass: HomeAssistant, setup_platform_cb: Callable[[], Awaitable[None]]
) -> None:
    """Test that the next ongoing event is returned."""
    await setup_platform_cb()

    state = hass.states.get(TEST_ENTITY)
    assert state
    assert state.name == CALENDAR_NAME
    assert state.state == STATE_ON
    assert dict(state.attributes) == {
        "friendly_name": CALENDAR_NAME,
        "message": "This is a normal event",
        "all_day": False,
        "offset_reached": False,
        "start_time": "2017-11-27 17:00:00",
        "end_time": "2017-11-27 18:00:00",
        "location": "Hamburg",
        "description": "Surprisingly rainy",
        "supported_features": CalendarEntityFeature.CREATE_EVENT,
    }


@pytest.mark.parametrize("tz", [UTC])
@pytest.mark.freeze_time(_local_datetime(17, 00))
async def test_ongoing_event_different_tz(
    hass: HomeAssistant, setup_platform_cb: Callable[[], Awaitable[None]]
) -> None:
    """Test that the ongoing event with another timezone is returned."""
    await setup_platform_cb()

    state = hass.states.get(TEST_ENTITY)
    assert state
    assert state.name == CALENDAR_NAME
    assert state.state == STATE_ON
    assert dict(state.attributes) == {
        "friendly_name": CALENDAR_NAME,
        "message": "Enjoy the sun",
        "all_day": False,
        "offset_reached": False,
        "start_time": "2017-11-27 16:30:00",
        "description": "Sunny day",
        "end_time": "2017-11-27 17:30:00",
        "location": "San Francisco",
        "supported_features": CalendarEntityFeature.CREATE_EVENT,
    }


@pytest.mark.parametrize("tz", [UTC])
@pytest.mark.freeze_time(_local_datetime(19, 10))
async def test_ongoing_floating_event_returned(
    hass: HomeAssistant, setup_platform_cb: Callable[[], Awaitable[None]]
) -> None:
    """Test that floating events without timezones work."""
    await setup_platform_cb()

    state = hass.states.get(TEST_ENTITY)
    assert state
    assert state.name == CALENDAR_NAME
    assert state.state == STATE_ON
    assert dict(state.attributes) == {
        "friendly_name": CALENDAR_NAME,
        "message": "This is a floating Event",
        "all_day": False,
        "offset_reached": False,
        "start_time": "2017-11-27 19:00:00",
        "end_time": "2017-11-27 20:00:00",
        "location": "Hamburg",
        "description": "What a day",
        "supported_features": CalendarEntityFeature.CREATE_EVENT,
    }


@pytest.mark.parametrize("tz", [UTC])
@pytest.mark.freeze_time(_local_datetime(8, 30))
async def test_ongoing_event_with_offset(
    hass: HomeAssistant, setup_platform_cb: Callable[[], Awaitable[None]]
) -> None:
    """Test that the offset is taken into account."""
    await setup_platform_cb()

    state = hass.states.get(TEST_ENTITY)
    assert state
    assert state.name == CALENDAR_NAME
    assert state.state == STATE_OFF
    assert dict(state.attributes) == {
        "friendly_name": CALENDAR_NAME,
        "message": "This is an offset event",
        "all_day": False,
        "offset_reached": True,
        "start_time": "2017-11-27 10:00:00",
        "end_time": "2017-11-27 11:00:00",
        "location": "Hamburg",
        "description": "Surprisingly shiny",
        "supported_features": CalendarEntityFeature.CREATE_EVENT,
    }


@pytest.mark.parametrize(
    ("tz", "config"),
    [
        (
            UTC,
            {
                "custom_calendars": [
                    {
                        "name": CALENDAR_NAME,
                        "calendar": CALENDAR_NAME,
                        "search": "This is a normal event",
                    }
                ]
            },
        )
    ],
)
@pytest.mark.freeze_time(_local_datetime(12, 00))
async def test_matching_filter(
    hass: HomeAssistant, setup_platform_cb: Callable[[], Awaitable[None]]
) -> None:
    """Test that the matching event is returned."""
    await setup_platform_cb()

    state = hass.states.get("calendar.example_example")
    assert state
    assert state.name == CALENDAR_NAME
    assert state.state == STATE_OFF
    assert dict(state.attributes) == {
        "friendly_name": CALENDAR_NAME,
        "message": "This is a normal event",
        "all_day": False,
        "offset_reached": False,
        "start_time": "2017-11-27 17:00:00",
        "end_time": "2017-11-27 18:00:00",
        "location": "Hamburg",
        "description": "Surprisingly rainy",
        "supported_features": CalendarEntityFeature.CREATE_EVENT,
    }


@pytest.mark.parametrize(
    ("tz", "config"),
    [
        (
            UTC,
            {
                "custom_calendars": [
                    {
                        "name": CALENDAR_NAME,
                        "calendar": CALENDAR_NAME,
                        "search": r".*rainy",
                    }
                ]
            },
        )
    ],
)
@pytest.mark.freeze_time(_local_datetime(12, 00))
async def test_matching_filter_real_regexp(
    hass: HomeAssistant, setup_platform_cb: Callable[[], Awaitable[None]]
) -> None:
    """Test that the event matching the regexp is returned."""

    await setup_platform_cb()

    state = hass.states.get("calendar.example_example")
    assert state
    assert state.name == CALENDAR_NAME
    assert state.state == STATE_OFF
    assert dict(state.attributes) == {
        "friendly_name": CALENDAR_NAME,
        "message": "This is a normal event",
        "all_day": False,
        "offset_reached": False,
        "start_time": "2017-11-27 17:00:00",
        "end_time": "2017-11-27 18:00:00",
        "location": "Hamburg",
        "description": "Surprisingly rainy",
        "supported_features": CalendarEntityFeature.CREATE_EVENT,
    }


@pytest.mark.parametrize(
    "config",
    [
        {
            "custom_calendars": [
                {
                    "name": CALENDAR_NAME,
                    "calendar": CALENDAR_NAME,
                    "search": "This is a normal event",
                }
            ]
        }
    ],
)
@pytest.mark.freeze_time(_local_datetime(20, 00))
async def test_filter_matching_past_event(
    hass: HomeAssistant, setup_platform_cb: Callable[[], Awaitable[None]]
) -> None:
    """Test that the matching past event is not returned."""

    await setup_platform_cb()

    state = hass.states.get("calendar.example_example")
    assert state
    assert state.name == CALENDAR_NAME
    assert state.state == "off"
    assert dict(state.attributes) == {
        "friendly_name": CALENDAR_NAME,
        "offset_reached": False,
        "supported_features": CalendarEntityFeature.CREATE_EVENT,
    }


@pytest.mark.parametrize(
    "config",
    [
        {
            "custom_calendars": [
                {
                    "name": CALENDAR_NAME,
                    "calendar": CALENDAR_NAME,
                    "search": "This is a non-existing event",
                }
            ]
        }
    ],
)
@pytest.mark.freeze_time(_local_datetime(12, 00))
async def test_no_result_with_filtering(
    hass: HomeAssistant, setup_platform_cb: Callable[[], Awaitable[None]]
) -> None:
    """Test that nothing is returned since nothing matches."""
    await setup_platform_cb()

    state = hass.states.get("calendar.example_example")
    assert state
    assert state.name == CALENDAR_NAME
    assert state.state == "off"
    assert dict(state.attributes) == {
        "friendly_name": CALENDAR_NAME,
        "offset_reached": False,
        "supported_features": CalendarEntityFeature.CREATE_EVENT,
    }


@pytest.mark.parametrize(
    ("tz", "target_datetime"),
    [
        # Early
        (UTC, datetime.datetime(2017, 11, 27, 0, 30)),
        (AMERICA_NEW_YORK, datetime.datetime(2017, 11, 27, 0, 30)),
        (ASIA_BAGHDAD, datetime.datetime(2017, 11, 27, 0, 30)),
        # Mid
        (UTC, datetime.datetime(2017, 11, 27, 12, 30)),
        (AMERICA_NEW_YORK, datetime.datetime(2017, 11, 27, 12, 30)),
        (ASIA_BAGHDAD, datetime.datetime(2017, 11, 27, 12, 30)),
        # Late
        (UTC, datetime.datetime(2017, 11, 27, 23, 30)),
        (AMERICA_NEW_YORK, datetime.datetime(2017, 11, 27, 23, 30)),
        (ASIA_BAGHDAD, datetime.datetime(2017, 11, 27, 23, 30)),
    ],
)
async def test_all_day_event(
    hass: HomeAssistant,
    freezer: FrozenDateTimeFactory,
    target_datetime: datetime.datetime,
) -> None:
    """Test that the event lasting the whole day is returned.

    Specifically when it's early in the local day.
    """
    freezer.move_to(target_datetime.replace(tzinfo=dt_util.get_default_time_zone()))
    assert await async_setup_component(
        hass,
        "calendar",
        {
            "calendar": {
                **CALDAV_CONFIG,
                "custom_calendars": [
                    {"name": CALENDAR_NAME, "calendar": CALENDAR_NAME, "search": ".*"}
                ],
            }
        },
    )
    await hass.async_block_till_done()

    state = hass.states.get("calendar.example_example")
    assert state
    assert state.name == CALENDAR_NAME
    assert state.state == STATE_ON
    assert dict(state.attributes) == {
        "friendly_name": CALENDAR_NAME,
        "message": "This is an all day event",
        "all_day": True,
        "offset_reached": False,
        "start_time": "2017-11-27 00:00:00",
        "end_time": "2017-11-28 00:00:00",
        "location": "Hamburg",
        "description": "What a beautiful day",
        "supported_features": CalendarEntityFeature.CREATE_EVENT,
    }


@pytest.mark.parametrize("tz", [UTC])
@pytest.mark.freeze_time(_local_datetime(21, 45))
async def test_event_rrule(
    hass: HomeAssistant, setup_platform_cb: Callable[[], Awaitable[None]]
) -> None:
    """Test that the future recurring event is returned."""
    await setup_platform_cb()

    state = hass.states.get(TEST_ENTITY)
    assert state
    assert state.name == CALENDAR_NAME
    assert state.state == STATE_OFF
    assert dict(state.attributes) == {
        "friendly_name": CALENDAR_NAME,
        "message": "This is a recurring event",
        "all_day": False,
        "offset_reached": False,
        "start_time": "2017-11-27 22:00:00",
        "end_time": "2017-11-27 22:30:00",
        "location": "Hamburg",
        "description": "Every day for a while",
        "supported_features": CalendarEntityFeature.CREATE_EVENT,
    }


@pytest.mark.parametrize("tz", [UTC])
@pytest.mark.freeze_time(_local_datetime(22, 15))
async def test_event_rrule_ongoing(
    hass: HomeAssistant, setup_platform_cb: Callable[[], Awaitable[None]]
) -> None:
    """Test that the current recurring event is returned."""
    await setup_platform_cb()

    state = hass.states.get(TEST_ENTITY)
    assert state
    assert state.name == CALENDAR_NAME
    assert state.state == STATE_ON
    assert dict(state.attributes) == {
        "friendly_name": CALENDAR_NAME,
        "message": "This is a recurring event",
        "all_day": False,
        "offset_reached": False,
        "start_time": "2017-11-27 22:00:00",
        "end_time": "2017-11-27 22:30:00",
        "location": "Hamburg",
        "description": "Every day for a while",
        "supported_features": CalendarEntityFeature.CREATE_EVENT,
    }


@pytest.mark.parametrize("tz", [UTC])
@pytest.mark.freeze_time(_local_datetime(22, 45))
async def test_event_rrule_duration(
    hass: HomeAssistant, setup_platform_cb: Callable[[], Awaitable[None]]
) -> None:
    """Test that the future recurring event is returned."""
    await setup_platform_cb()

    state = hass.states.get(TEST_ENTITY)
    assert state
    assert state.name == CALENDAR_NAME
    assert state.state == STATE_OFF
    assert dict(state.attributes) == {
        "friendly_name": CALENDAR_NAME,
        "message": "This is a recurring event with a duration",
        "all_day": False,
        "offset_reached": False,
        "start_time": "2017-11-27 23:00:00",
        "end_time": "2017-11-27 23:30:00",
        "location": "Hamburg",
        "description": "Every day for a while as well",
        "supported_features": CalendarEntityFeature.CREATE_EVENT,
    }


@pytest.mark.parametrize("tz", [UTC])
@pytest.mark.freeze_time(_local_datetime(23, 15))
async def test_event_rrule_duration_ongoing(
    hass: HomeAssistant, setup_platform_cb: Callable[[], Awaitable[None]]
) -> None:
    """Test that the ongoing recurring event is returned."""
    await setup_platform_cb()

    state = hass.states.get(TEST_ENTITY)
    assert state
    assert state.name == CALENDAR_NAME
    assert state.state == STATE_ON
    assert dict(state.attributes) == {
        "friendly_name": CALENDAR_NAME,
        "message": "This is a recurring event with a duration",
        "all_day": False,
        "offset_reached": False,
        "start_time": "2017-11-27 23:00:00",
        "end_time": "2017-11-27 23:30:00",
        "location": "Hamburg",
        "description": "Every day for a while as well",
        "supported_features": CalendarEntityFeature.CREATE_EVENT,
    }


@pytest.mark.parametrize("tz", [UTC])
@pytest.mark.freeze_time(_local_datetime(23, 37))
async def test_event_rrule_endless(
    hass: HomeAssistant, setup_platform_cb: Callable[[], Awaitable[None]]
) -> None:
    """Test that the endless recurring event is returned."""
    await setup_platform_cb()

    state = hass.states.get(TEST_ENTITY)
    assert state
    assert state.name == CALENDAR_NAME
    assert state.state == STATE_OFF
    assert dict(state.attributes) == {
        "friendly_name": CALENDAR_NAME,
        "message": "This is a recurring event that never ends",
        "all_day": False,
        "offset_reached": False,
        "start_time": "2017-11-27 23:45:00",
        "end_time": "2017-11-27 23:59:59",
        "location": "Hamburg",
        "description": "Every day forever",
        "supported_features": CalendarEntityFeature.CREATE_EVENT,
    }


@pytest.mark.parametrize(
    ("tz", "target_datetime"),
    [
        # Early
        (UTC, datetime.datetime(2016, 12, 1, 0, 30)),
        (AMERICA_NEW_YORK, datetime.datetime(2016, 12, 1, 0, 30)),
        (ASIA_BAGHDAD, datetime.datetime(2016, 12, 1, 0, 30)),
        # Mid
        (UTC, datetime.datetime(2016, 12, 1, 17, 30)),
        (AMERICA_NEW_YORK, datetime.datetime(2016, 12, 1, 17, 30)),
        (ASIA_BAGHDAD, datetime.datetime(2016, 12, 1, 17, 30)),
        # Late
        (UTC, datetime.datetime(2016, 12, 1, 23, 30)),
        (AMERICA_NEW_YORK, datetime.datetime(2016, 12, 1, 23, 30)),
        (ASIA_BAGHDAD, datetime.datetime(2016, 12, 1, 23, 30)),
    ],
)
async def test_event_rrule_all_day_early(
    hass: HomeAssistant,
    freezer: FrozenDateTimeFactory,
    target_datetime: datetime.datetime,
) -> None:
    """Test recurring all day event is returned early in the day.

    Verifies it's not returned on the first occurrence.
    """
    freezer.move_to(target_datetime.replace(tzinfo=dt_util.get_default_time_zone()))
    assert await async_setup_component(
        hass,
        "calendar",
        {
            "calendar": {
                **CALDAV_CONFIG,
                "custom_calendars": {
                    "name": CALENDAR_NAME,
                    "calendar": CALENDAR_NAME,
                    "search": ".*",
                },
            },
        },
    )
    await hass.async_block_till_done()

    state = hass.states.get("calendar.example_example")
    assert state
    assert state.name == CALENDAR_NAME
    assert state.state == STATE_ON
    assert dict(state.attributes) == {
        "friendly_name": CALENDAR_NAME,
        "message": "This is a recurring all day event",
        "all_day": True,
        "offset_reached": False,
        "start_time": "2016-12-01 00:00:00",
        "end_time": "2016-12-02 00:00:00",
        "location": "Hamburg",
        "description": "Groundhog Day",
        "supported_features": CalendarEntityFeature.CREATE_EVENT,
    }


@pytest.mark.parametrize("tz", [UTC])
@pytest.mark.freeze_time(dt_util.as_local(datetime.datetime(2015, 11, 27, 0, 15)))
async def test_event_rrule_hourly_on_first(
    hass: HomeAssistant, setup_platform_cb: Callable[[], Awaitable[None]]
) -> None:
    """Test that the endless recurring event is returned."""
    await setup_platform_cb()

    state = hass.states.get(TEST_ENTITY)
    assert state
    assert state.name == CALENDAR_NAME
    assert state.state == STATE_ON
    assert dict(state.attributes) == {
        "friendly_name": CALENDAR_NAME,
        "message": "This is an hourly recurring event",
        "all_day": False,
        "offset_reached": False,
        "start_time": "2015-11-27 00:00:00",
        "end_time": "2015-11-27 00:30:00",
        "location": "Hamburg",
        "description": "The bell tolls for thee",
        "supported_features": CalendarEntityFeature.CREATE_EVENT,
    }


@pytest.mark.parametrize("tz", ["UTC"])
@pytest.mark.freeze_time(dt_util.as_local(datetime.datetime(2015, 11, 27, 11, 15)))
async def test_event_rrule_hourly_on_last(
    hass: HomeAssistant, setup_platform_cb: Callable[[], Awaitable[None]]
) -> None:
    """Test that the endless recurring event is returned."""
    await setup_platform_cb()

    state = hass.states.get(TEST_ENTITY)
    assert state
    assert state.name == CALENDAR_NAME
    assert state.state == STATE_ON
    assert dict(state.attributes) == {
        "friendly_name": CALENDAR_NAME,
        "message": "This is an hourly recurring event",
        "all_day": False,
        "offset_reached": False,
        "start_time": "2015-11-27 11:00:00",
        "end_time": "2015-11-27 11:30:00",
        "location": "Hamburg",
        "description": "The bell tolls for thee",
        "supported_features": CalendarEntityFeature.CREATE_EVENT,
    }


@pytest.mark.parametrize(
    ("target_datetime"),
    [
        datetime.datetime(2015, 11, 27, 0, 45),
        datetime.datetime(2015, 11, 27, 11, 45),
        datetime.datetime(2015, 11, 27, 12, 15),
    ],
)
async def test_event_rrule_hourly(
    hass: HomeAssistant,
    setup_platform_cb: Callable[[], Awaitable[None]],
    freezer: FrozenDateTimeFactory,
    target_datetime: datetime.datetime,
) -> None:
    """Test that the endless recurring event is returned."""
    freezer.move_to(dt_util.as_local(target_datetime))
    await setup_platform_cb()

    state = hass.states.get(TEST_ENTITY)
    assert state
    assert state.name == CALENDAR_NAME
    assert state.state == STATE_OFF


async def test_get_events(
    hass: HomeAssistant,
    get_api_events: Callable[[str], Awaitable[dict[str, Any]]],
    setup_platform_cb: Callable[[], Awaitable[None]],
    calendars: list[Mock],
) -> None:
    """Test that all events are returned on API."""
    await setup_platform_cb()

    events = await get_api_events(TEST_ENTITY)
    assert len(events) == 18
    assert calendars[0].call


@pytest.mark.parametrize(
    "config",
    [
        {
            "custom_calendars": [
                {
                    "name": CALENDAR_NAME,
                    "calendar": CALENDAR_NAME,
                    "search": "This is a normal event",
                }
            ]
        }
    ],
)
async def test_get_events_custom_calendars(
    hass: HomeAssistant,
    get_api_events: Callable[[str], Awaitable[dict[str, Any]]],
    setup_platform_cb: Callable[[], Awaitable[None]],
) -> None:
    """Test that only searched events are returned on API."""
    await setup_platform_cb()

    events = await get_api_events("calendar.example_example")
    assert events == [
        {
            "end": {"dateTime": "2017-11-27T10:00:00-08:00"},
            "start": {"dateTime": "2017-11-27T09:00:00-08:00"},
            "summary": "This is a normal event",
            "location": "Hamburg",
            "description": "Surprisingly rainy",
            "uid": "0",
            "recurrence_id": None,
            "rrule": None,
        }
    ]


async def test_get_events_with_recurrence_id(
    hass: HomeAssistant,
    hass_client: ClientSessionGenerator,
) -> None:
    """Test that uid and recurrence_id are populated from VEVENT data."""
    vevent_with_recurrence_id = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//E-Corp.//CalDAV Client//EN
BEGIN:VEVENT
UID:original-event-uid
RECURRENCE-ID:20171127T170000Z
DTSTAMP:20171125T000000Z
DTSTART:20171127T180000Z
DTEND:20171127T190000Z
SUMMARY:Modified occurrence
LOCATION:Hamburg
DESCRIPTION:This occurrence was moved
END:VEVENT
END:VCALENDAR"""
    calendar = Mock()
    calendar.name = "Example"
    calendar.get_supported_components = MagicMock(return_value=["VEVENT"])
    calendar.search = MagicMock(
        return_value=[
            Event(
                None, "0.ics", vevent_with_recurrence_id, calendar, "original-event-uid"
            )
        ]
    )

    with patch(
        "homeassistant.components.caldav.calendar.caldav.DAVClient"
    ) as mock_client:
        mock_client.return_value.principal.return_value.calendars.return_value = [
            calendar
        ]
        assert await async_setup_component(
            hass, "calendar", {"calendar": CALDAV_CONFIG}
        )
        await hass.async_block_till_done()

    client = await hass_client()
    response = await client.get(
        f"/api/calendars/{TEST_ENTITY}?start=2017-11-27&end=2017-11-28"
    )
    assert response.status == HTTPStatus.OK
    events = await response.json()

    assert len(events) == 1
    assert events[0]["uid"] == "original-event-uid"
    assert events[0]["recurrence_id"] == "2017-11-27 17:00:00+00:00"
    assert events[0]["summary"] == "Modified occurrence"


@pytest.mark.parametrize(
    ("calendars"),
    [
        [
            _mock_calendar("Calendar 1", supported_components=["VEVENT"]),
            _mock_calendar("Calendar 2", supported_components=["VEVENT", "VJOURNAL"]),
            _mock_calendar("Calendar 3", supported_components=["VTODO"]),
            _mock_calendar("Calendar 4", supported_components=[]),
        ]
    ],
)
async def test_calendar_components(hass: HomeAssistant) -> None:
    """Test that only calendars that support events are created."""

    assert await async_setup_component(hass, "calendar", {"calendar": CALDAV_CONFIG})
    await hass.async_block_till_done()

    state = hass.states.get("calendar.calendar_1")
    assert state

    state = hass.states.get("calendar.calendar_2")
    assert state

    # No entity created for To-do only component
    state = hass.states.get("calendar.calendar_3")
    assert not state

    # No entity created when no components exist
    state = hass.states.get("calendar.calendar_4")
    assert not state


@pytest.mark.parametrize("tz", [UTC])
@pytest.mark.freeze_time(_local_datetime(17, 30))
async def test_setup_config_entry(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
) -> None:
    """Test a calendar entity from a config entry."""
    config_entry.add_to_hass(hass)
    await hass.config_entries.async_setup(config_entry.entry_id)

    state = hass.states.get(TEST_ENTITY)
    assert state
    assert state.name == CALENDAR_NAME
    assert state.state == STATE_ON
    assert dict(state.attributes) == {
        "friendly_name": CALENDAR_NAME,
        "message": "This is an all day event",
        "all_day": True,
        "start_time": "2017-11-27 00:00:00",
        "end_time": "2017-11-28 00:00:00",
        "location": "Hamburg",
        "description": "What a beautiful day",
        "supported_features": CalendarEntityFeature.CREATE_EVENT,
    }


@pytest.mark.parametrize(
    ("calendars"),
    [
        [
            _mock_calendar("Calendar 1", supported_components=["VEVENT"]),
            _mock_calendar("Calendar 2", supported_components=["VEVENT", "VJOURNAL"]),
            _mock_calendar("Calendar 3", supported_components=["VTODO"]),
            _mock_calendar("Calendar 4", supported_components=[]),
        ]
    ],
)
async def test_config_entry_supported_components(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
) -> None:
    """Test calendars are only created for VEVENT types.

    This applies when using a config entry.
    """
    config_entry.add_to_hass(hass)
    await hass.config_entries.async_setup(config_entry.entry_id)

    state = hass.states.get("calendar.calendar_1")
    assert state

    state = hass.states.get("calendar.calendar_2")
    assert state

    # No entity created for To-do only component
    state = hass.states.get("calendar.calendar_3")
    assert not state

    # No entity created when no components exist
    state = hass.states.get("calendar.calendar_4")
    assert not state


@pytest.mark.parametrize("tz", [UTC])
@pytest.mark.parametrize(
    ("service_data", "expected_ics_fields"),
    [
        # Basic event with all fields
        (
            {
                "summary": "Test Event",
                "start_date_time": "2025-08-06T10:00:00+00:00",
                "end_date_time": "2025-08-06T11:00:00+00:00",
                "description": "Test Description",
                "location": "Test Location",
            },
            {
                "description": "Test Description",
                "dtend": datetime.datetime(
                    2025, 8, 6, 11, 0, tzinfo=zoneinfo.ZoneInfo(key="UTC")
                ),
                "dtstart": datetime.datetime(
                    2025, 8, 6, 10, 0, tzinfo=zoneinfo.ZoneInfo(key="UTC")
                ),
                "location": "Test Location",
                "summary": "Test Event",
            },
        ),
        # Event with only required fields
        (
            {
                "summary": "Required Only",
                "start_date_time": "2025-08-07T09:00:00+00:00",
                "end_date_time": "2025-08-07T10:00:00+00:00",
            },
            {
                "summary": "Required Only",
                "dtstart": datetime.datetime(
                    2025, 8, 7, 9, 0, tzinfo=zoneinfo.ZoneInfo(key="UTC")
                ),
                "dtend": datetime.datetime(
                    2025, 8, 7, 10, 0, tzinfo=zoneinfo.ZoneInfo(key="UTC")
                ),
            },
        ),
        # All-day event (date only)
        (
            {
                "summary": "All Day Event",
                "start_date": "2025-08-08",
                "end_date": "2025-08-09",
            },
            {
                "summary": "All Day Event",
                "dtstart": datetime.date(2025, 8, 8),
                "dtend": datetime.date(2025, 8, 9),
            },
        ),
        # Event with different timezone
        (
            {
                "summary": "Different TZ",
                "start_date_time": "2025-08-07T09:00:00+02:00",
                "end_date_time": "2025-08-07T10:00:00+02:00",
            },
            {
                "summary": "Different TZ",
                "dtstart": datetime.datetime(
                    2025, 8, 7, 7, 0, tzinfo=zoneinfo.ZoneInfo(key="UTC")
                ),
                "dtend": datetime.datetime(
                    2025, 8, 7, 8, 0, tzinfo=zoneinfo.ZoneInfo(key="UTC")
                ),
            },
        ),
        # Rrule is not supported in API (async_call) calls.
    ],
)
async def test_add_vevent(
    hass: HomeAssistant,
    setup_platform_cb: Callable[[], Awaitable[None]],
    calendars: list[Mock],
    service_data: dict,
    expected_ics_fields: dict,
) -> None:
    """Test adding a VEVENT to the calendar."""
    await setup_platform_cb()

    calendars[0].add_event = MagicMock(return_value=[])
    await hass.services.async_call(
        "calendar",
        "create_event",
        service_data,
        target={"entity_id": TEST_ENTITY},
        blocking=True,
    )
    await hass.async_block_till_done()

    calendars[0].add_event.assert_called_once()
    assert calendars[0].add_event.call_args
    assert calendars[0].add_event.call_args[1] == expected_ics_fields


async def test_missing_supported_components(
    hass: HomeAssistant,
    calendars: list[Mock],
    setup_platform_cb: Callable[[], Awaitable[None]],
    caplog: pytest.LogCaptureFixture,
) -> None:
    """Test setup works when calendar raises KeyError on get_supported_components."""
    caplog.set_level(logging.WARNING, logger="homeassistant.components.caldav.api")
    calendars[0].get_supported_components.side_effect = KeyError()
    await setup_platform_cb()

    assert hass.states.get(TEST_ENTITY)

    warning_msg = (
        "CalDAV server does not report supported components for calendar Example, "
        "assuming it supports the requested component 'VEVENT'"
    )
    assert warning_msg in caplog.text

    # Clear caplog and call async_get_calendars again to verify warning is not logged again
    caplog.clear()
    client = MagicMock()
    client.principal().calendars.return_value = calendars

    await async_get_calendars(hass, client, "VEVENT")
    assert warning_msg not in caplog.text

    # Verify that querying a *different* component for the same calendar DOES log the warning again
    # because de-duplication is keyed by (url, component).
    vjournal_warning = (
        "CalDAV server does not report supported components for calendar Example. "
        "Not assuming support for requested component 'VJOURNAL'"
    )
    await async_get_calendars(hass, client, "VJOURNAL")
    assert vjournal_warning in caplog.text


async def test_missing_supported_components_not_assumed(
    hass: HomeAssistant,
    calendars: list[Mock],
    caplog: pytest.LogCaptureFixture,
) -> None:
    """Test get_calendars excludes calendars when KeyError is raised for non-assumed components."""
    caplog.set_level(logging.WARNING, logger="homeassistant.components.caldav.api")
    calendars[0].get_supported_components.side_effect = KeyError()
    client = MagicMock()
    client.principal().calendars.return_value = calendars

    returned_calendars = await async_get_calendars(hass, client, "VJOURNAL")

    assert len(returned_calendars) == 0
    warning_msg = (
        "CalDAV server does not report supported components for calendar Example. "
        "Not assuming support for requested component 'VJOURNAL'"
    )
    assert warning_msg in caplog.text

    # Clear caplog and call async_get_calendars again to verify warning is not logged again
    caplog.clear()
    await async_get_calendars(hass, client, "VJOURNAL")
    assert warning_msg not in caplog.text
