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

from collections.abc import Generator
from datetime import timedelta
from http import HTTPStatus
import re
from typing import Any

from freezegun import freeze_time
import pytest
from syrupy.assertion import SnapshotAssertion
import voluptuous as vol

from homeassistant.components.calendar import (
    CREATE_EVENT_SERVICE,
    DOMAIN,
    SERVICE_GET_EVENTS,
    CalendarEntity,
    CalendarEntityDescription,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceNotSupported
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.typing import UNDEFINED
from homeassistant.util import dt as dt_util

from .conftest import MockCalendarEntity, MockConfigEntry

from tests.common import MockUser, async_fire_time_changed
from tests.typing import ClientSessionGenerator, WebSocketGenerator


@pytest.fixture(name="frozen_time")
def mock_frozen_time() -> str | None:
    """Fixture to set a frozen time used in tests.

    This is needed so that it can run before other fixtures.
    """
    return None


@pytest.fixture(autouse=True)
def mock_set_frozen_time(frozen_time: str | None) -> Generator[None]:
    """Fixture to freeze time that also can work for other fixtures."""
    if not frozen_time:
        yield
    else:
        with freeze_time(frozen_time):
            yield


@pytest.fixture(name="setup_platform", autouse=True)
async def mock_setup_platform(
    hass: HomeAssistant,
    set_time_zone: None,
    frozen_time: str | None,
    mock_setup_integration: None,
    config_entry: MockConfigEntry,
) -> None:
    """Fixture to set up platforms used in the test.

    Ensures fixtures are set up in the right order.
    """
    assert await hass.config_entries.async_setup(config_entry.entry_id)
    await hass.async_block_till_done()


async def test_events_http_api(
    hass: HomeAssistant, hass_client: ClientSessionGenerator
) -> None:
    """Test the calendar demo view."""
    client = await hass_client()
    start = dt_util.now()
    end = start + timedelta(days=1)
    response = await client.get(
        f"/api/calendars/calendar.calendar_1?start={start.isoformat()}&end={end.isoformat()}"
    )
    assert response.status == HTTPStatus.OK
    events = await response.json()
    assert events[0]["summary"] == "Future Event"


async def test_events_http_api_missing_fields(
    hass: HomeAssistant, hass_client: ClientSessionGenerator
) -> None:
    """Test the calendar demo view."""
    client = await hass_client()
    response = await client.get("/api/calendars/calendar.calendar_2")
    assert response.status == HTTPStatus.BAD_REQUEST


async def test_events_http_api_error(
    hass: HomeAssistant,
    hass_client: ClientSessionGenerator,
    test_entities: list[MockCalendarEntity],
) -> None:
    """Test the calendar demo view."""
    client = await hass_client()
    start = dt_util.now()
    end = start + timedelta(days=1)

    test_entities[0].async_get_events.side_effect = HomeAssistantError("Failure")

    response = await client.get(
        f"/api/calendars/calendar.calendar_1?start={start.isoformat()}&end={end.isoformat()}"
    )
    assert response.status == HTTPStatus.INTERNAL_SERVER_ERROR
    assert await response.json() == {"message": "Error reading events: Failure"}


async def test_events_http_api_dates_wrong_order(
    hass: HomeAssistant, hass_client: ClientSessionGenerator
) -> None:
    """Test the calendar demo view."""
    client = await hass_client()
    start = dt_util.now()
    end = start + timedelta(days=-1)
    response = await client.get(
        f"/api/calendars/calendar.calendar_1?start={start.isoformat()}&end={end.isoformat()}"
    )
    assert response.status == HTTPStatus.BAD_REQUEST


async def test_calendars_http_api(
    hass: HomeAssistant, hass_client: ClientSessionGenerator
) -> None:
    """Test the calendar demo view."""
    client = await hass_client()
    response = await client.get("/api/calendars")
    assert response.status == HTTPStatus.OK
    data = await response.json()
    assert data == [
        {"entity_id": "calendar.calendar_1", "name": "Calendar 1"},
        {"entity_id": "calendar.calendar_2", "name": "Calendar 2"},
        {"entity_id": "calendar.calendar_3", "name": "Calendar 3"},
    ]


@pytest.mark.parametrize(
    ("payload", "code"),
    [
        (
            {
                "type": "calendar/event/create",
                "entity_id": "calendar.calendar_1",
                "event": {
                    "summary": "Bastille Day Party",
                    "dtstart": "1997-07-14T17:00:00+00:00",
                    "dtend": "1997-07-15T04:00:00+00:00",
                },
            },
            "not_supported",
        ),
        (
            {
                "type": "calendar/event/create",
                "entity_id": "calendar.calendar_99",
                "event": {
                    "summary": "Bastille Day Party",
                    "dtstart": "1997-07-14T17:00:00+00:00",
                    "dtend": "1997-07-15T04:00:00+00:00",
                },
            },
            "not_found",
        ),
        (
            {
                "type": "calendar/event/delete",
                "entity_id": "calendar.calendar_1",
                "uid": "some-uid",
            },
            "not_supported",
        ),
        (
            {
                "type": "calendar/event/delete",
                "entity_id": "calendar.calendar_99",
                "uid": "some-uid",
            },
            "not_found",
        ),
        (
            {
                "type": "calendar/event/update",
                "entity_id": "calendar.calendar_1",
                "uid": "some-uid",
                "event": {
                    "summary": "Bastille Day Party",
                    "dtstart": "1997-07-14T17:00:00+00:00",
                    "dtend": "1997-07-15T04:00:00+00:00",
                },
            },
            "not_supported",
        ),
        (
            {
                "type": "calendar/event/update",
                "entity_id": "calendar.calendar_99",
                "uid": "some-uid",
                "event": {
                    "summary": "Bastille Day Party",
                    "dtstart": "1997-07-14T17:00:00+00:00",
                    "dtend": "1997-07-15T04:00:00+00:00",
                },
            },
            "not_found",
        ),
    ],
)
async def test_unsupported_websocket(
    hass: HomeAssistant, hass_ws_client: WebSocketGenerator, payload, code
) -> None:
    """Test unsupported websocket command."""
    client = await hass_ws_client(hass)
    await client.send_json(
        {
            "id": 1,
            **payload,
        }
    )
    resp = await client.receive_json()
    assert resp.get("id") == 1
    assert resp.get("error")
    assert resp["error"].get("code") == code


async def test_unsupported_create_event_service(hass: HomeAssistant) -> None:
    """Test unsupported service call."""
    with pytest.raises(
        ServiceNotSupported,
        match="Entity calendar.calendar_1 does not "
        "support action calendar.create_event",
    ):
        await hass.services.async_call(
            DOMAIN,
            CREATE_EVENT_SERVICE,
            {
                "start_date_time": "1997-07-14T17:00:00+00:00",
                "end_date_time": "1997-07-15T04:00:00+00:00",
                "summary": "Bastille Day Party",
            },
            target={"entity_id": "calendar.calendar_1"},
            blocking=True,
        )


@pytest.mark.parametrize(
    ("date_fields", "expected_error", "error_match"),
    [
        (
            {},
            vol.error.MultipleInvalid,
            "must contain at least one of start_date, start_date_time, in",
        ),
        (
            {
                "start_date": "2022-04-01",
            },
            vol.error.MultipleInvalid,
            "Start and end dates must both be specified",
        ),
        (
            {
                "end_date": "2022-04-02",
            },
            vol.error.MultipleInvalid,
            "must contain at least one of start_date, start_date_time, in.",
        ),
        (
            {
                "start_date_time": "2022-04-01T06:00:00",
            },
            vol.error.MultipleInvalid,
            "Start and end datetimes must both be specified",
        ),
        (
            {
                "end_date_time": "2022-04-02T07:00:00",
            },
            vol.error.MultipleInvalid,
            "must contain at least one of start_date, start_date_time, in.",
        ),
        (
            {
                "start_date": "2022-04-01",
                "start_date_time": "2022-04-01T06:00:00",
                "end_date_time": "2022-04-02T07:00:00",
            },
            vol.error.MultipleInvalid,
            "must contain at most one of start_date, start_date_time, in.",
        ),
        (
            {
                "start_date_time": "2022-04-01T06:00:00",
                "end_date_time": "2022-04-01T07:00:00",
                "end_date": "2022-04-02",
            },
            vol.error.MultipleInvalid,
            "Start and end dates must both be specified",
        ),
        (
            {
                "start_date": "2022-04-01",
                "end_date_time": "2022-04-02T07:00:00",
            },
            vol.error.MultipleInvalid,
            "Start and end dates must both be specified",
        ),
        (
            {
                "start_date_time": "2022-04-01T07:00:00",
                "end_date": "2022-04-02",
            },
            vol.error.MultipleInvalid,
            "Start and end dates must both be specified",
        ),
        (
            {
                "in": {
                    "days": 2,
                    "weeks": 2,
                }
            },
            vol.error.MultipleInvalid,
            "two or more values in the same group of exclusion 'event_types'",
        ),
        (
            {
                "start_date": "2022-04-01",
                "end_date": "2022-04-02",
                "in": {
                    "days": 2,
                },
            },
            vol.error.MultipleInvalid,
            "must contain at most one of start_date, start_date_time, in.",
        ),
        (
            {
                "start_date_time": "2022-04-01T07:00:00",
                "end_date_time": "2022-04-01T07:00:00",
                "in": {
                    "days": 2,
                },
            },
            vol.error.MultipleInvalid,
            "must contain at most one of start_date, start_date_time, in.",
        ),
        (
            {
                "start_date_time": "2022-04-01T06:00:00+00:00",
                "end_date_time": "2022-04-01T07:00:00+01:00",
            },
            vol.error.MultipleInvalid,
            "Expected all values to have the same timezone",
        ),
        (
            {
                "start_date_time": "2022-04-01T07:00:00",
                "end_date_time": "2022-04-01T06:00:00",
            },
            vol.error.MultipleInvalid,
            "Expected minimum event duration",
        ),
        (
            {
                "start_date": "2022-04-02",
                "end_date": "2022-04-01",
            },
            vol.error.MultipleInvalid,
            "Expected minimum event duration",
        ),
        (
            {
                "start_date": "2022-04-01",
                "end_date": "2022-04-01",
            },
            vol.error.MultipleInvalid,
            "Expected minimum event duration",
        ),
    ],
    ids=[
        "missing_all",
        "missing_end_date",
        "missing_start_date",
        "missing_end_datetime",
        "missing_start_datetime",
        "multiple_start",
        "multiple_end",
        "missing_end_date",
        "missing_end_date_time",
        "multiple_in",
        "unexpected_in_with_date",
        "unexpected_in_with_datetime",
        "inconsistent_timezone",
        "incorrect_date_order",
        "incorrect_datetime_order",
        "dates_not_exclusive",
    ],
)
async def test_create_event_service_invalid_params(
    hass: HomeAssistant,
    date_fields: dict[str, Any],
    expected_error: type[Exception],
    error_match: str | None,
) -> None:
    """Test creating an event using the create_event service."""

    with pytest.raises(expected_error, match=error_match):
        await hass.services.async_call(
            DOMAIN,
            CREATE_EVENT_SERVICE,
            {
                "summary": "Bastille Day Party",
                **date_fields,
            },
            target={"entity_id": "calendar.calendar_1"},
            blocking=True,
        )


@pytest.mark.parametrize(
    "frozen_time", ["2023-06-22 10:30:00+00:00"], ids=["frozen_time"]
)
@pytest.mark.parametrize(
    ("service", "expected"),
    [
        (
            SERVICE_GET_EVENTS,
            {
                "calendar.calendar_1": {
                    "events": [
                        {
                            "start": "2023-06-22T05:00:00-06:00",
                            "end": "2023-06-22T06:00:00-06:00",
                            "summary": "Future Event",
                            "description": "Future Description",
                            "location": "Future Location",
                        }
                    ]
                }
            },
        ),
    ],
)
@pytest.mark.parametrize(
    ("start_time", "end_time"),
    [
        ("2023-06-22T04:30:00-06:00", "2023-06-22T06:30:00-06:00"),
        ("2023-06-22T04:30:00", "2023-06-22T06:30:00"),
        ("2023-06-22T10:30:00Z", "2023-06-22T12:30:00Z"),
    ],
)
async def test_list_events_service(
    hass: HomeAssistant,
    start_time: str,
    end_time: str,
    service: str,
    expected: dict[str, Any],
) -> None:
    """Test listing events from the service call using explicit start and end time.

    This test uses a fixed date/time so that it can deterministically test the
    string output values.
    """

    response = await hass.services.async_call(
        DOMAIN,
        service,
        target={"entity_id": ["calendar.calendar_1"]},
        service_data={
            "entity_id": "calendar.calendar_1",
            "start_date_time": start_time,
            "end_date_time": end_time,
        },
        blocking=True,
        return_response=True,
    )
    assert response == expected


@pytest.mark.parametrize(
    ("service"),
    [
        SERVICE_GET_EVENTS,
    ],
)
@pytest.mark.parametrize(
    ("entity", "duration"),
    [
        # Calendar 1 has an hour long event starting in 30 minutes. No events in the
        # next 15 minutes, but it shows up an hour from now.
        ("calendar.calendar_1", "00:15:00"),
        ("calendar.calendar_1", "01:00:00"),
        # Calendar 2 has a active event right now
        ("calendar.calendar_2", "00:15:00"),
    ],
)
@pytest.mark.parametrize("frozen_time", ["2023-10-19 13:50:05"], ids=["frozen_time"])
async def test_list_events_service_duration(
    hass: HomeAssistant,
    entity: str,
    duration: str,
    service: str,
    snapshot: SnapshotAssertion,
) -> None:
    """Test listing events using a time duration."""
    response = await hass.services.async_call(
        DOMAIN,
        service,
        {
            "entity_id": entity,
            "duration": duration,
        },
        blocking=True,
        return_response=True,
    )
    assert response == snapshot


async def test_list_events_positive_duration(hass: HomeAssistant) -> None:
    """Test listing events requires a positive duration."""
    with pytest.raises(vol.Invalid, match="should be positive"):
        await hass.services.async_call(
            DOMAIN,
            SERVICE_GET_EVENTS,
            {
                "entity_id": "calendar.calendar_1",
                "duration": "-01:00:00",
            },
            blocking=True,
            return_response=True,
        )


async def test_list_events_exclusive_fields(hass: HomeAssistant) -> None:
    """Test listing events specifying fields that are exclusive."""
    end = dt_util.now() + timedelta(days=1)

    with pytest.raises(vol.Invalid, match="at most one of"):
        await hass.services.async_call(
            DOMAIN,
            SERVICE_GET_EVENTS,
            {
                "entity_id": "calendar.calendar_1",
                "end_date_time": end,
                "duration": "01:00:00",
            },
            blocking=True,
            return_response=True,
        )


async def test_list_events_missing_fields(hass: HomeAssistant) -> None:
    """Test listing events missing some required fields."""
    with pytest.raises(vol.Invalid, match="at least one of"):
        await hass.services.async_call(
            DOMAIN,
            SERVICE_GET_EVENTS,
            {
                "entity_id": "calendar.calendar_1",
            },
            blocking=True,
            return_response=True,
        )


@pytest.mark.parametrize(
    "frozen_time", ["2023-06-22 10:30:00+00:00"], ids=["frozen_time"]
)
@pytest.mark.parametrize(
    ("service_data", "error_msg"),
    [
        (
            {
                "start_date_time": "2023-06-22T04:30:00-06:00",
                "end_date_time": "2023-06-22T04:30:00-06:00",
            },
            "Expected end time to be after start time"
            " (2023-06-22 04:30:00-06:00,"
            " 2023-06-22 04:30:00-06:00)",
        ),
        (
            {
                "start_date_time": "2023-06-22T04:30:00",
                "end_date_time": "2023-06-22T04:30:00",
            },
            "Expected end time to be after start time"
            " (2023-06-22 04:30:00,"
            " 2023-06-22 04:30:00)",
        ),
        (
            {"start_date_time": "2023-06-22", "end_date_time": "2023-06-22"},
            "Expected end time to be after start time"
            " (2023-06-22 00:00:00,"
            " 2023-06-22 00:00:00)",
        ),
        (
            {"start_date_time": "2023-06-22 10:00:00", "duration": "0"},
            "Expected positive duration (0:00:00)",
        ),
    ],
)
async def test_list_events_service_same_dates(
    hass: HomeAssistant,
    service_data: dict[str, str],
    error_msg: str,
) -> None:
    """Test listing events from the service call using the same start and end time."""

    with pytest.raises(vol.error.MultipleInvalid, match=re.escape(error_msg)):
        await hass.services.async_call(
            DOMAIN,
            SERVICE_GET_EVENTS,
            service_data={
                "entity_id": "calendar.calendar_1",
                **service_data,
            },
            blocking=True,
            return_response=True,
        )


async def test_calendar_initial_color_valid(
    hass: HomeAssistant,
    entity_registry: er.EntityRegistry,
    test_entities: list[MockCalendarEntity],
) -> None:
    """Test that initial_color creates initial entity options."""
    # Entity 3 was created with an initial_color
    entity = test_entities[2]

    # Check that entity registry was populated with the initial_color
    entry = entity_registry.async_get(entity.entity_id)
    assert entry is not None
    assert entry.options.get(DOMAIN, {}).get("color") == "#FF0000"


@pytest.mark.parametrize(
    "invalid_initial_color",
    [
        "FF0000",  # Missing #
        "#FF00",  # Too short
        "#FF00000",  # Too long
        "#GGGGGG",  # Invalid hex
        "red",  # Not hex
        "",  # Empty
    ],
)
async def test_calendar_initial_color_invalid(
    hass: HomeAssistant,
    entity_registry: er.EntityRegistry,
    invalid_initial_color: str,
) -> None:
    """Test that invalid initial_color is ignored."""
    entity = MockCalendarEntity(
        "Invalid Color Test",
        [],
        initial_color=invalid_initial_color,
        unique_id=f"test_{invalid_initial_color}",
    )
    assert entity.get_initial_entity_options() is None


async def test_calendar_initial_color_none(
    hass: HomeAssistant,
    test_entities: list[MockCalendarEntity],
) -> None:
    """Test that entities without initial_color return None."""
    # Entities 1 and 2 were created without an initial_color
    entity = test_entities[0]
    assert entity.get_initial_entity_options() is None


@pytest.mark.parametrize(
    ("description_color", "attr_color", "expected_color"),
    [
        # no description and no attr_initial_color
        (UNDEFINED, UNDEFINED, None),
        # no description and attr_initial_color "A"
        (UNDEFINED, "#AAAAAA", "#AAAAAA"),
        # no description and attr_initial_color None
        (UNDEFINED, None, None),
        # description setting the color "B", and no attr_initial_color
        ("#BBBBBB", UNDEFINED, "#BBBBBB"),
        # description setting the color "B", but overridden by attr_initial_color "A"
        ("#BBBBBB", "#AAAAAA", "#AAAAAA"),
        # description setting the color "B", but overridden by attr_initial_color None
        ("#BBBBBB", None, None),
    ],
)
async def test_calendar_initial_color_precedence(
    description_color: str | object | None,
    attr_color: str | object | None,
    expected_color: str | None,
) -> None:
    """Test that _attr_initial_color takes precedence over entity_description."""

    class TestCalendarEntity(CalendarEntity):
        """Test entity for initial_color precedence tests."""

        _attr_has_entity_name = True

        def __init__(
            self,
            description_color: str | object | None,
            attr_color: str | object | None,
        ) -> None:
            """Initialize entity."""
            self._attr_name = "Test"
            self._attr_unique_id = "test_precedence"

            # Only set entity_description if description_color is not UNDEFINED
            if description_color is not UNDEFINED:
                self.entity_description = CalendarEntityDescription(
                    key="test",
                    initial_color=description_color,
                )

            # Only set _attr_initial_color if attr_color is not UNDEFINED
            if attr_color is not UNDEFINED:
                self._attr_initial_color = attr_color

    entity = TestCalendarEntity(description_color, attr_color)
    assert entity.initial_color == expected_color


async def test_websocket_handle_subscribe_calendar_events(
    hass: HomeAssistant,
    hass_ws_client: WebSocketGenerator,
    test_entities: list[MockCalendarEntity],
) -> None:
    """Test subscribing to calendar event updates via websocket."""
    client = await hass_ws_client(hass)

    start = dt_util.now()
    end = start + timedelta(days=1)

    await client.send_json_auto_id(
        {
            "type": "calendar/event/subscribe",
            "entity_id": "calendar.calendar_1",
            "start": start.isoformat(),
            "end": end.isoformat(),
        }
    )
    msg = await client.receive_json()
    assert msg["success"]
    subscription_id = msg["id"]

    # Should receive initial event list
    msg = await client.receive_json()
    assert msg["id"] == subscription_id
    assert msg["type"] == "event"
    assert "events" in msg["event"]
    events = msg["event"]["events"]
    assert len(events) == 1
    assert events[0]["summary"] == "Future Event"
    assert events[0]["uid"] == "calendar-event-uid-1"
    assert events[0]["rrule"] == "FREQ=WEEKLY;COUNT=3"
    assert events[0]["recurrence_id"] == "20260415"


async def test_websocket_subscribe_updates_on_state_change(
    hass: HomeAssistant,
    hass_ws_client: WebSocketGenerator,
    test_entities: list[MockCalendarEntity],
) -> None:
    """Test that subscribers receive updates when calendar state changes."""
    client = await hass_ws_client(hass)

    start = dt_util.now()
    end = start + timedelta(days=1)

    await client.send_json_auto_id(
        {
            "type": "calendar/event/subscribe",
            "entity_id": "calendar.calendar_1",
            "start": start.isoformat(),
            "end": end.isoformat(),
        }
    )
    msg = await client.receive_json()
    assert msg["success"]
    subscription_id = msg["id"]

    # Receive initial event list
    msg = await client.receive_json()
    assert msg["id"] == subscription_id

    # Add a new event and trigger state update
    entity = test_entities[0]
    entity.create_event(
        start=start + timedelta(hours=2),
        end=start + timedelta(hours=3),
        summary="New Event",
    )
    entity.async_write_ha_state()
    await hass.async_block_till_done()

    # Should receive updated event list
    msg = await client.receive_json()
    assert msg["id"] == subscription_id
    assert msg["type"] == "event"
    events = msg["event"]["events"]
    assert len(events) == 2
    summaries = {event["summary"] for event in events}
    assert "Future Event" in summaries
    assert "New Event" in summaries


async def test_websocket_subscribe_entity_not_found(
    hass: HomeAssistant,
    hass_ws_client: WebSocketGenerator,
) -> None:
    """Test subscribing to a non-existent calendar entity."""
    client = await hass_ws_client(hass)

    start = dt_util.now()
    end = start + timedelta(days=1)

    await client.send_json_auto_id(
        {
            "type": "calendar/event/subscribe",
            "entity_id": "calendar.nonexistent",
            "start": start.isoformat(),
            "end": end.isoformat(),
        }
    )
    msg = await client.receive_json()
    assert not msg["success"]
    assert msg["error"]["code"] == "not_found"
    assert "Calendar entity not found" in msg["error"]["message"]


async def test_websocket_subscribe_event_fetch_error(
    hass: HomeAssistant,
    hass_ws_client: WebSocketGenerator,
    test_entities: list[MockCalendarEntity],
) -> None:
    """Test subscription handles event fetch errors gracefully."""
    client = await hass_ws_client(hass)

    start = dt_util.now()
    end = start + timedelta(days=1)

    # Set up entity to fail on async_get_events
    test_entities[0].async_get_events.side_effect = HomeAssistantError("API Error")

    await client.send_json_auto_id(
        {
            "type": "calendar/event/subscribe",
            "entity_id": "calendar.calendar_1",
            "start": start.isoformat(),
            "end": end.isoformat(),
        }
    )
    msg = await client.receive_json()
    assert msg["success"]
    subscription_id = msg["id"]

    # Should receive None for events due to error
    msg = await client.receive_json()
    assert msg["id"] == subscription_id
    assert msg["type"] == "event"
    assert msg["event"]["events"] is None


async def test_websocket_subscribe_invalid_timespan(
    hass: HomeAssistant,
    hass_ws_client: WebSocketGenerator,
    test_entities: list[MockCalendarEntity],
) -> None:
    """Test subscribing with start after end returns an error."""
    client = await hass_ws_client(hass)

    now = dt_util.now()
    start = now + timedelta(days=1)
    end = now

    await client.send_json_auto_id(
        {
            "type": "calendar/event/subscribe",
            "entity_id": "calendar.calendar_1",
            "start": start.isoformat(),
            "end": end.isoformat(),
        }
    )
    msg = await client.receive_json()
    assert not msg["success"]
    assert msg["error"]["code"] == "invalid_format"
    assert "Start must be before end" in msg["error"]["message"]


async def test_websocket_subscribe_debounces_rapid_updates(
    hass: HomeAssistant,
    hass_ws_client: WebSocketGenerator,
    test_entities: list[MockCalendarEntity],
) -> None:
    """Test that rapid state writes are debounced for event listeners."""
    client = await hass_ws_client(hass)

    start = dt_util.now()
    end = start + timedelta(days=1)

    await client.send_json_auto_id(
        {
            "type": "calendar/event/subscribe",
            "entity_id": "calendar.calendar_1",
            "start": start.isoformat(),
            "end": end.isoformat(),
        }
    )
    msg = await client.receive_json()
    assert msg["success"]
    subscription_id = msg["id"]

    # Receive initial event list
    msg = await client.receive_json()
    assert msg["id"] == subscription_id

    entity = test_entities[0]
    entity.async_get_events.reset_mock()

    # Rapidly write state multiple times
    for i in range(5):
        entity.create_event(
            start=start + timedelta(hours=i + 2),
            end=start + timedelta(hours=i + 3),
            summary=f"Rapid Event {i}",
        )
        entity.async_write_ha_state()

    await hass.async_block_till_done()

    # The debouncer with immediate=True fires the first call immediately
    # and coalesces the rest into one call after the cooldown.
    # Without debouncing this would be 5 calls.
    assert entity.async_get_events.call_count == 1

    # Advance time past the debounce cooldown to fire the trailing call
    async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=2))
    await hass.async_block_till_done()

    # Should be exactly 2 total: immediate + one coalesced trailing call
    assert entity.async_get_events.call_count == 2

    # Drain messages: immediate update + trailing debounced update
    messages: list[dict] = []
    for _ in range(10):
        msg = await client.receive_json()
        assert msg["id"] == subscription_id
        assert msg["type"] == "event"
        messages.append(msg)
        if len(msg["event"]["events"]) == 6:  # 1 original + 5 rapid
            break
    else:
        pytest.fail("Did not receive expected calendar event list with 6 events")

    # The final message has all events
    assert len(messages[-1]["event"]["events"]) == 6


async def test_events_http_api_unauthorized(
    hass: HomeAssistant,
    hass_client: ClientSessionGenerator,
    hass_admin_user: MockUser,
) -> None:
    """Test that the events HTTP API enforces per-entity read permission."""
    hass_admin_user.groups = []
    hass_admin_user.mock_policy(
        {"entities": {"entity_ids": {"calendar.calendar_2": True}}}
    )
    client = await hass_client()
    start = dt_util.now()
    end = start + timedelta(days=1)
    response = await client.get(
        f"/api/calendars/calendar.calendar_1?start={start.isoformat()}&end={end.isoformat()}"
    )
    assert response.status == HTTPStatus.UNAUTHORIZED
    # Allowed entity still works
    response = await client.get(
        f"/api/calendars/calendar.calendar_2?start={start.isoformat()}&end={end.isoformat()}"
    )
    assert response.status == HTTPStatus.OK


async def test_calendars_http_api_filters_unauthorized(
    hass: HomeAssistant,
    hass_client: ClientSessionGenerator,
    hass_admin_user: MockUser,
) -> None:
    """Test that the calendar list filters out entities the user cannot read."""
    hass_admin_user.groups = []
    hass_admin_user.mock_policy(
        {"entities": {"entity_ids": {"calendar.calendar_2": True}}}
    )
    client = await hass_client()
    response = await client.get("/api/calendars")
    assert response.status == HTTPStatus.OK
    data = await response.json()
    assert data == [{"entity_id": "calendar.calendar_2", "name": "Calendar 2"}]


@pytest.mark.parametrize(
    "command",
    [
        {
            "type": "calendar/event/create",
            "entity_id": "calendar.calendar_1",
            "event": {
                "summary": "Bastille Day Party",
                "dtstart": "1997-07-14T17:00:00+00:00",
                "dtend": "1997-07-15T04:00:00+00:00",
            },
        },
        {
            "type": "calendar/event/delete",
            "entity_id": "calendar.calendar_1",
            "uid": "some-uid",
        },
        {
            "type": "calendar/event/update",
            "entity_id": "calendar.calendar_1",
            "uid": "some-uid",
            "event": {
                "summary": "Bastille Day Party",
                "dtstart": "1997-07-14T17:00:00+00:00",
                "dtend": "1997-07-15T04:00:00+00:00",
            },
        },
    ],
)
async def test_websocket_event_mutate_unauthorized(
    hass: HomeAssistant,
    hass_ws_client: WebSocketGenerator,
    hass_admin_user: MockUser,
    command: dict[str, Any],
) -> None:
    """Test that mutating event WS commands enforce per-entity control permission."""
    hass_admin_user.groups = []
    hass_admin_user.mock_policy({})
    client = await hass_ws_client(hass)
    await client.send_json_auto_id(command)
    msg = await client.receive_json()
    assert not msg["success"]
    assert msg["error"]["code"] == "unauthorized"


async def test_websocket_subscribe_unauthorized(
    hass: HomeAssistant,
    hass_ws_client: WebSocketGenerator,
    hass_admin_user: MockUser,
) -> None:
    """Test calendar event subscription enforces per-entity read permission."""
    hass_admin_user.groups = []
    hass_admin_user.mock_policy({})
    client = await hass_ws_client(hass)
    start = dt_util.now()
    end = start + timedelta(days=1)
    await client.send_json_auto_id(
        {
            "type": "calendar/event/subscribe",
            "entity_id": "calendar.calendar_1",
            "start": start.isoformat(),
            "end": end.isoformat(),
        }
    )
    msg = await client.receive_json()
    assert not msg["success"]
    assert msg["error"]["code"] == "unauthorized"
