"""Tests for Arrived/Departed binary sensors, Guest Arrived/Departed buttons, and coordinator arrived/departed flag lifecycle. Uses object.__new__ for pure-unit tests and hass fixtures for coordinator integration. """ from datetime import date, time from unittest.mock import AsyncMock, MagicMock, patch import pytest from homeassistant.core import HomeAssistant from pytest_homeassistant_custom_component.common import MockConfigEntry from custom_components.room_cal_ha.binary_sensor import ArrivedSensor, DepartedSensor from custom_components.room_cal_ha.button import ( GuestArrivedButton, GuestDepartedButton, GuestEnteredButton, ) from custom_components.room_cal_ha.const import ( CONF_SYNC_MODE, DOMAIN, STORE_CURRENT, STORE_NEXT, SYNC_MODE_CALDAV, SYNC_MODE_MANUAL, ) from custom_components.room_cal_ha.coordinator import CoordinatorData, RoomCalCoordinator from custom_components.room_cal_ha.models import ReservationData _ALICE = ReservationData( guest_name="Alice", check_in_date=date(2026, 7, 1), check_in_time=time(14, 0), check_out_date=date(2026, 7, 5), check_out_time=time(12, 0), ) _BOB = ReservationData( guest_name="Bob", check_in_date=date(2026, 7, 6), check_in_time=time(15, 0), check_out_date=date(2026, 7, 10), check_out_time=time(11, 0), ) _BASE_DATA = {"room_state_name": "Room State"} def _make_entry(hass, options: dict) -> MockConfigEntry: entry = MockConfigEntry(domain=DOMAIN, data=_BASE_DATA, options=options) entry.add_to_hass(hass) return entry # --------------------------------------------------------------------------- # ArrivedSensor / DepartedSensor unit tests # --------------------------------------------------------------------------- def _data(arrived=False, departed=False) -> CoordinatorData: return CoordinatorData( active=ReservationData(), current=ReservationData(), next=ReservationData(), current_last_refresh=None, next_last_refresh=None, active_arrived=arrived, active_departed=departed, ) def _arrived_sensor(arrived=False) -> ArrivedSensor: s = object.__new__(ArrivedSensor) s.coordinator = MagicMock() s.coordinator.data = _data(arrived=arrived) return s def _departed_sensor(departed=False) -> DepartedSensor: s = object.__new__(DepartedSensor) s.coordinator = MagicMock() s.coordinator.data = _data(departed=departed) return s def test_arrived_sensor_default_false(): assert _arrived_sensor(arrived=False).is_on is False def test_arrived_sensor_true_when_set(): assert _arrived_sensor(arrived=True).is_on is True def test_departed_sensor_default_false(): assert _departed_sensor(departed=False).is_on is False def test_departed_sensor_true_when_set(): assert _departed_sensor(departed=True).is_on is True # --------------------------------------------------------------------------- # GuestArrivedButton / GuestDepartedButton unit tests # --------------------------------------------------------------------------- def _arrived_button() -> GuestArrivedButton: btn = object.__new__(GuestArrivedButton) btn._coordinator = MagicMock() btn._coordinator.async_set_arrived = AsyncMock() return btn def _departed_button() -> GuestDepartedButton: btn = object.__new__(GuestDepartedButton) btn._coordinator = MagicMock() btn._coordinator.async_set_departed = AsyncMock() return btn async def test_arrived_button_calls_set_arrived_true(): btn = _arrived_button() await btn.async_press() btn._coordinator.async_set_arrived.assert_awaited_once_with(True) async def test_departed_button_calls_set_departed_true(): btn = _departed_button() await btn.async_press() btn._coordinator.async_set_departed.assert_awaited_once_with(True) # --------------------------------------------------------------------------- # GuestEnteredButton unit tests # --------------------------------------------------------------------------- def _entered_button(arrived=False) -> GuestEnteredButton: btn = object.__new__(GuestEnteredButton) btn._coordinator = MagicMock() btn._coordinator.data = _data(arrived=arrived) btn._coordinator.async_set_arrived = AsyncMock() return btn async def test_entered_button_sets_arrived_when_not_yet_arrived(): btn = _entered_button(arrived=False) await btn.async_press() btn._coordinator.async_set_arrived.assert_awaited_once_with(True) async def test_entered_button_does_not_re_set_arrived_when_already_arrived(): btn = _entered_button(arrived=True) await btn.async_press() btn._coordinator.async_set_arrived.assert_not_awaited() # --------------------------------------------------------------------------- # Coordinator: async_set_arrived / async_set_departed # --------------------------------------------------------------------------- @pytest.fixture async def coord(hass: HomeAssistant): entry = _make_entry(hass, {CONF_SYNC_MODE: SYNC_MODE_MANUAL}) c = RoomCalCoordinator(hass, entry) await c.async_setup() await c.async_refresh() return c async def test_set_arrived_sets_flag(coord: RoomCalCoordinator): await coord.async_set_arrived(True) assert coord._active_arrived is True assert coord.data.active_arrived is True async def test_set_arrived_persists_to_store(coord: RoomCalCoordinator): await coord.async_set_arrived(True) assert coord.store.get_arrived() is True async def test_set_departed_sets_flag(coord: RoomCalCoordinator): await coord.async_set_departed(True) assert coord._active_departed is True assert coord.data.active_departed is True async def test_set_departed_persists_to_store(coord: RoomCalCoordinator): await coord.async_set_departed(True) assert coord.store.get_departed() is True async def test_flags_load_from_store_on_startup(hass: HomeAssistant): entry = _make_entry(hass, {CONF_SYNC_MODE: SYNC_MODE_MANUAL}) c = RoomCalCoordinator(hass, entry) await c.async_setup() await c.store.async_set_arrived(True) await c.store.async_set_departed(True) c2 = RoomCalCoordinator(hass, entry) await c2.async_setup() assert c2._active_arrived is True assert c2._active_departed is True # --------------------------------------------------------------------------- # Coordinator: flag reset on reservation identity change # --------------------------------------------------------------------------- async def test_flags_reset_when_reservation_changes(hass: HomeAssistant): entry = _make_entry(hass, {CONF_SYNC_MODE: SYNC_MODE_CALDAV, "caldav_url": "https://x", "calendar_name": "R"}) c = RoomCalCoordinator(hass, entry) await c.async_setup() # Load Alice as current with patch("custom_components.room_cal_ha.coordinator.CalDavBackend") as M: M.return_value.async_fetch = AsyncMock(return_value=(_ALICE, None)) await c.async_refresh() await c.async_set_arrived(True) assert c._active_arrived is True # Bob becomes current — different check-in identity with patch("custom_components.room_cal_ha.coordinator.CalDavBackend") as M: M.return_value.async_fetch = AsyncMock(return_value=(_BOB, None)) await c.async_refresh() assert c._active_arrived is False assert c.data.active_arrived is False assert c.store.get_arrived() is False async def test_flags_not_reset_when_same_reservation(hass: HomeAssistant): entry = _make_entry(hass, {CONF_SYNC_MODE: SYNC_MODE_CALDAV, "caldav_url": "https://x", "calendar_name": "R"}) c = RoomCalCoordinator(hass, entry) await c.async_setup() with patch("custom_components.room_cal_ha.coordinator.CalDavBackend") as M: M.return_value.async_fetch = AsyncMock(return_value=(_ALICE, None)) await c.async_refresh() await c.async_set_arrived(True) # Same Alice again — no identity change with patch("custom_components.room_cal_ha.coordinator.CalDavBackend") as M: M.return_value.async_fetch = AsyncMock(return_value=(_ALICE, None)) await c.async_refresh() assert c._active_arrived is True assert c.data.active_arrived is True async def test_flags_reset_on_promote_from_next(coord: RoomCalCoordinator): await coord.store.async_set(STORE_CURRENT, _ALICE) await coord.store.async_set(STORE_NEXT, _BOB) await coord.async_refresh() await coord.async_set_arrived(True) await coord.async_promote_next_to_active() assert coord._active_arrived is False assert coord.data.active_arrived is False async def test_flags_reset_on_clear_current(coord: RoomCalCoordinator): await coord.store.async_set(STORE_CURRENT, _ALICE) await coord.async_refresh() await coord.async_set_arrived(True) await coord.async_clear_active() assert coord._active_arrived is False assert coord.data.active_arrived is False async def test_flags_reset_on_checkout(coord: RoomCalCoordinator): await coord.store.async_set(STORE_CURRENT, _ALICE) await coord.async_refresh() await coord.async_set_arrived(True) await coord._async_do_check_out() assert coord._active_arrived is False assert coord.data.active_arrived is False