"""The tests for the generic_thermostat."""

import datetime
from unittest.mock import patch

from freezegun import freeze_time
from freezegun.api import FrozenDateTimeFactory
import pytest
import voluptuous as vol

from homeassistant import config as hass_config, core as ha
from homeassistant.components import input_boolean, switch
from homeassistant.components.climate import (
    ATTR_PRESET_MODE,
    DOMAIN as CLIMATE_DOMAIN,
    PRESET_ACTIVITY,
    PRESET_AWAY,
    PRESET_COMFORT,
    PRESET_ECO,
    PRESET_HOME,
    PRESET_NONE,
    PRESET_SLEEP,
    HVACMode,
)
from homeassistant.components.generic_thermostat.const import DOMAIN
from homeassistant.const import (
    ATTR_TEMPERATURE,
    SERVICE_RELOAD,
    SERVICE_TURN_OFF,
    SERVICE_TURN_ON,
    STATE_OFF,
    STATE_ON,
    STATE_UNAVAILABLE,
    STATE_UNKNOWN,
    UnitOfTemperature,
)
from homeassistant.core import (
    DOMAIN as HOMEASSISTANT_DOMAIN,
    Context,
    CoreState,
    HomeAssistant,
    ServiceCall,
    State,
    callback,
)
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import (
    device_registry as dr,
    entity_platform,
    entity_registry as er,
)
from homeassistant.helpers.typing import StateType
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util
from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM

from tests.common import (
    MockConfigEntry,
    MockUser,
    assert_setup_component,
    async_fire_time_changed,
    async_mock_service,
    get_fixture_path,
    mock_restore_cache,
    setup_test_component_platform,
)
from tests.components.climate import common
from tests.components.switch.common import MockSwitch

ENTITY = "climate.test"
ENT_SENSOR = "sensor.test"
ENT_SWITCH = "switch.test"
HEAT_ENTITY = "climate.test_heat"
COOL_ENTITY = "climate.test_cool"
ATTR_AWAY_MODE = "away_mode"
MIN_TEMP = 3.0
MAX_TEMP = 65.0
TARGET_TEMP = 42.0
COLD_TOLERANCE = 0.5
HOT_TOLERANCE = 0.5
TARGET_TEMP_STEP = 0.5


async def test_setup_missing_conf(hass: HomeAssistant) -> None:
    """Test set up heat_control with missing config values."""
    config = {
        "platform": "generic_thermostat",
        "name": "test",
        "target_sensor": ENT_SENSOR,
    }
    with assert_setup_component(0):
        await async_setup_component(hass, "climate", {"climate": config})


async def test_valid_conf(hass: HomeAssistant) -> None:
    """Test set up generic_thermostat with valid config values."""
    assert await async_setup_component(
        hass,
        "climate",
        {
            "climate": {
                "platform": "generic_thermostat",
                "name": "test",
                "heater": ENT_SWITCH,
                "target_sensor": ENT_SENSOR,
            }
        },
    )


@pytest.fixture
async def setup_comp_1(hass: HomeAssistant) -> None:
    """Initialize components."""
    hass.config.units = METRIC_SYSTEM
    assert await async_setup_component(hass, "homeassistant", {})
    await hass.async_block_till_done()


@pytest.mark.usefixtures("setup_comp_1")
async def test_heater_input_boolean(hass: HomeAssistant) -> None:
    """Test heater switching input_boolean."""
    heater_switch = "input_boolean.test"
    assert await async_setup_component(
        hass, input_boolean.DOMAIN, {"input_boolean": {"test": None}}
    )

    assert await async_setup_component(
        hass,
        CLIMATE_DOMAIN,
        {
            "climate": {
                "platform": "generic_thermostat",
                "name": "test",
                "heater": heater_switch,
                "target_sensor": ENT_SENSOR,
                "initial_hvac_mode": HVACMode.HEAT,
            }
        },
    )
    await hass.async_block_till_done()

    assert hass.states.get(heater_switch).state == STATE_OFF

    _setup_sensor(hass, 18)
    await hass.async_block_till_done()
    await common.async_set_temperature(hass, 23)
    await hass.async_block_till_done()

    assert hass.states.get(heater_switch).state == STATE_ON


@pytest.mark.usefixtures("setup_comp_1")
async def test_heater_switch(
    hass: HomeAssistant, mock_switch_entities: list[MockSwitch]
) -> None:
    """Test heater switching test switch."""
    setup_test_component_platform(hass, switch.DOMAIN, mock_switch_entities)
    switch_1 = mock_switch_entities[1]
    assert await async_setup_component(
        hass, switch.DOMAIN, {"switch": {"platform": "test"}}
    )
    await hass.async_block_till_done()
    heater_switch = switch_1.entity_id

    assert await async_setup_component(
        hass,
        CLIMATE_DOMAIN,
        {
            "climate": {
                "platform": "generic_thermostat",
                "name": "test",
                "heater": heater_switch,
                "target_sensor": ENT_SENSOR,
                "initial_hvac_mode": HVACMode.HEAT,
            }
        },
    )

    await hass.async_block_till_done()
    assert hass.states.get(heater_switch).state == STATE_OFF

    _setup_sensor(hass, 18)
    await common.async_set_temperature(hass, 23)
    await hass.async_block_till_done()

    assert hass.states.get(heater_switch).state == STATE_ON


@pytest.mark.usefixtures("setup_comp_1")
async def test_unique_id(
    hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
    """Test setting a unique ID."""
    unique_id = "some_unique_id"
    _setup_sensor(hass, 18)
    _setup_switch(hass, True)
    assert await async_setup_component(
        hass,
        CLIMATE_DOMAIN,
        {
            "climate": {
                "platform": "generic_thermostat",
                "name": "test",
                "heater": ENT_SWITCH,
                "target_sensor": ENT_SENSOR,
                "unique_id": unique_id,
            }
        },
    )
    await hass.async_block_till_done()

    entry = entity_registry.async_get(ENTITY)
    assert entry
    assert entry.unique_id == unique_id


def _setup_sensor(hass: HomeAssistant, temp: StateType) -> None:
    """Set up the test sensor."""
    hass.states.async_set(ENT_SENSOR, temp)


@pytest.fixture
async def setup_comp_2(hass: HomeAssistant) -> None:
    """Initialize components."""
    hass.config.units = METRIC_SYSTEM
    assert await async_setup_component(
        hass,
        CLIMATE_DOMAIN,
        {
            "climate": {
                "platform": "generic_thermostat",
                "name": "test",
                "cold_tolerance": 2,
                "hot_tolerance": 4,
                "heater": ENT_SWITCH,
                "target_sensor": ENT_SENSOR,
                "away_temp": 16,
                "sleep_temp": 17,
                "home_temp": 19,
                "comfort_temp": 20,
                "eco_temp": 18,
                "activity_temp": 21,
                "initial_hvac_mode": HVACMode.HEAT,
            }
        },
    )
    await hass.async_block_till_done()


async def test_setup_defaults_to_unknown(hass: HomeAssistant) -> None:
    """Test the setting of defaults to unknown."""
    hass.config.units = METRIC_SYSTEM
    await async_setup_component(
        hass,
        CLIMATE_DOMAIN,
        {
            "climate": {
                "platform": "generic_thermostat",
                "name": "test",
                "cold_tolerance": 2,
                "hot_tolerance": 4,
                "heater": ENT_SWITCH,
                "target_sensor": ENT_SENSOR,
                "away_temp": 16,
            }
        },
    )
    await hass.async_block_till_done()
    assert hass.states.get(ENTITY).state == HVACMode.OFF


async def test_setup_gets_current_temp_from_sensor(hass: HomeAssistant) -> None:
    """Test that current temperature is updated on entity addition."""
    hass.config.units = METRIC_SYSTEM
    _setup_sensor(hass, 18)
    await hass.async_block_till_done()
    await async_setup_component(
        hass,
        CLIMATE_DOMAIN,
        {
            "climate": {
                "platform": "generic_thermostat",
                "name": "test",
                "cold_tolerance": 2,
                "hot_tolerance": 4,
                "heater": ENT_SWITCH,
                "target_sensor": ENT_SENSOR,
                "away_temp": 16,
            }
        },
    )
    await hass.async_block_till_done()
    assert hass.states.get(ENTITY).attributes["current_temperature"] == 18


@pytest.mark.usefixtures("setup_comp_2")
async def test_default_setup_params(hass: HomeAssistant) -> None:
    """Test the setup with default parameters."""
    state = hass.states.get(ENTITY)
    assert state.attributes.get("min_temp") == 7
    assert state.attributes.get("max_temp") == 35
    assert state.attributes.get("temperature") == 7
    assert state.attributes.get("target_temp_step") == 0.1


@pytest.mark.usefixtures("setup_comp_2")
async def test_get_hvac_modes(hass: HomeAssistant) -> None:
    """Test that the operation list returns the correct modes."""
    state = hass.states.get(ENTITY)
    modes = state.attributes.get("hvac_modes")
    assert modes == [HVACMode.HEAT, HVACMode.OFF]


@pytest.mark.usefixtures("setup_comp_2")
async def test_set_target_temp(hass: HomeAssistant) -> None:
    """Test the setting of the target temperature."""
    await common.async_set_temperature(hass, 30)
    state = hass.states.get(ENTITY)
    assert state.attributes.get("temperature") == 30.0
    with pytest.raises(vol.Invalid):
        await common.async_set_temperature(hass, None)
    state = hass.states.get(ENTITY)
    assert state.attributes.get("temperature") == 30.0


@pytest.mark.usefixtures("setup_comp_2")
async def test_set_target_temp_change_preset(hass: HomeAssistant) -> None:
    """Test the setting of the target temperature.

    Verify that preset is changed.
    """
    await common.async_set_temperature(hass, 30)
    state = hass.states.get(ENTITY)
    assert state.attributes.get("preset_mode") == PRESET_NONE
    await common.async_set_temperature(hass, 20)
    state = hass.states.get(ENTITY)
    assert state.attributes.get("preset_mode") == PRESET_COMFORT


@pytest.mark.parametrize(
    ("preset", "temp"),
    [
        (PRESET_NONE, 23),
        (PRESET_AWAY, 16),
        (PRESET_COMFORT, 20),
        (PRESET_ECO, 18),
        (PRESET_HOME, 19),
        (PRESET_SLEEP, 17),
        (PRESET_ACTIVITY, 21),
    ],
)
@pytest.mark.usefixtures("setup_comp_2")
async def test_set_away_mode(hass: HomeAssistant, preset, temp) -> None:
    """Test the setting away mode."""
    await common.async_set_temperature(hass, 23)
    await common.async_set_preset_mode(hass, preset)
    state = hass.states.get(ENTITY)
    assert state.attributes.get("temperature") == temp


@pytest.mark.parametrize(
    ("preset", "temp"),
    [
        (PRESET_NONE, 23),
        (PRESET_AWAY, 16),
        (PRESET_COMFORT, 20),
        (PRESET_ECO, 18),
        (PRESET_HOME, 19),
        (PRESET_SLEEP, 17),
        (PRESET_ACTIVITY, 21),
    ],
)
@pytest.mark.usefixtures("setup_comp_2")
async def test_set_away_mode_and_restore_prev_temp(
    hass: HomeAssistant, preset, temp
) -> None:
    """Test the setting and removing away mode.

    Verify original temperature is restored.
    """
    await common.async_set_temperature(hass, 23)
    await common.async_set_preset_mode(hass, preset)
    state = hass.states.get(ENTITY)
    assert state.attributes.get("temperature") == temp
    await common.async_set_preset_mode(hass, PRESET_NONE)
    state = hass.states.get(ENTITY)
    assert state.attributes.get("temperature") == 23


@pytest.mark.parametrize(
    ("preset", "temp"),
    [
        (PRESET_NONE, 23),
        (PRESET_AWAY, 16),
        (PRESET_COMFORT, 20),
        (PRESET_ECO, 18),
        (PRESET_HOME, 19),
        (PRESET_SLEEP, 17),
        (PRESET_ACTIVITY, 21),
    ],
)
@pytest.mark.usefixtures("setup_comp_2")
async def test_set_away_mode_twice_and_restore_prev_temp(
    hass: HomeAssistant, preset, temp
) -> None:
    """Test the setting away mode twice in a row.

    Verify original temperature is restored.
    """
    await common.async_set_temperature(hass, 23)
    await common.async_set_preset_mode(hass, preset)
    await common.async_set_preset_mode(hass, preset)
    state = hass.states.get(ENTITY)
    assert state.attributes.get("temperature") == temp
    await common.async_set_preset_mode(hass, PRESET_NONE)
    state = hass.states.get(ENTITY)
    assert state.attributes.get("temperature") == 23


@pytest.mark.usefixtures("setup_comp_2")
async def test_set_preset_mode_invalid(hass: HomeAssistant) -> None:
    """Test an invalid mode raises an error and ignore case when checking modes."""
    await common.async_set_temperature(hass, 23)
    await common.async_set_preset_mode(hass, "away")
    state = hass.states.get(ENTITY)
    assert state.attributes.get("preset_mode") == "away"
    await common.async_set_preset_mode(hass, "none")
    state = hass.states.get(ENTITY)
    assert state.attributes.get("preset_mode") == "none"
    with pytest.raises(ServiceValidationError):
        await common.async_set_preset_mode(hass, "Sleep")
    state = hass.states.get(ENTITY)
    assert state.attributes.get("preset_mode") == "none"


@pytest.mark.usefixtures("setup_comp_2")
async def test_sensor_bad_value(hass: HomeAssistant) -> None:
    """Test sensor that have None as state."""
    state = hass.states.get(ENTITY)
    temp = state.attributes.get("current_temperature")

    _setup_sensor(hass, None)
    await hass.async_block_till_done()
    state = hass.states.get(ENTITY)
    assert state.attributes.get("current_temperature") == temp

    _setup_sensor(hass, "inf")
    await hass.async_block_till_done()
    state = hass.states.get(ENTITY)
    assert state.attributes.get("current_temperature") == temp

    _setup_sensor(hass, "nan")
    await hass.async_block_till_done()
    state = hass.states.get(ENTITY)
    assert state.attributes.get("current_temperature") == temp


async def test_sensor_unknown(hass: HomeAssistant) -> None:
    """Test when target sensor is Unknown."""
    hass.states.async_set("sensor.unknown", STATE_UNKNOWN)
    assert await async_setup_component(
        hass,
        "climate",
        {
            "climate": {
                "platform": "generic_thermostat",
                "name": "unknown",
                "heater": ENT_SWITCH,
                "target_sensor": "sensor.unknown",
            }
        },
    )
    await hass.async_block_till_done()
    state = hass.states.get("climate.unknown")
    assert state.attributes.get("current_temperature") is None


async def test_sensor_unavailable(hass: HomeAssistant) -> None:
    """Test when target sensor is Unavailable."""
    hass.states.async_set("sensor.unavailable", STATE_UNAVAILABLE)
    assert await async_setup_component(
        hass,
        "climate",
        {
            "climate": {
                "platform": "generic_thermostat",
                "name": "unavailable",
                "heater": ENT_SWITCH,
                "target_sensor": "sensor.unavailable",
            }
        },
    )
    await hass.async_block_till_done()
    state = hass.states.get("climate.unavailable")
    assert state.attributes.get("current_temperature") is None


@pytest.mark.usefixtures("setup_comp_2")
async def test_set_target_temp_heater_on(hass: HomeAssistant) -> None:
    """Test if target temperature turn heater on."""
    calls = _setup_switch(hass, False)
    _setup_sensor(hass, 25)
    await hass.async_block_till_done()
    await common.async_set_temperature(hass, 30)
    assert len(calls) == 1
    call = calls[0]
    assert call.domain == HOMEASSISTANT_DOMAIN
    assert call.service == SERVICE_TURN_ON
    assert call.data["entity_id"] == ENT_SWITCH


@pytest.mark.usefixtures("setup_comp_2")
async def test_set_target_temp_heater_off(hass: HomeAssistant) -> None:
    """Test if target temperature turn heater off."""
    calls = _setup_switch(hass, True)
    _setup_sensor(hass, 30)
    await hass.async_block_till_done()
    await common.async_set_temperature(hass, 25)
    assert len(calls) == 2
    call = calls[0]
    assert call.domain == HOMEASSISTANT_DOMAIN
    assert call.service == SERVICE_TURN_OFF
    assert call.data["entity_id"] == ENT_SWITCH


@pytest.mark.usefixtures("setup_comp_2")
async def test_temp_change_heater_on_within_tolerance(hass: HomeAssistant) -> None:
    """Test if temperature change doesn't turn on within tolerance."""
    calls = _setup_switch(hass, False)
    await common.async_set_temperature(hass, 30)
    _setup_sensor(hass, 29)
    await hass.async_block_till_done()
    assert len(calls) == 0


@pytest.mark.usefixtures("setup_comp_2")
async def test_temp_change_heater_on_outside_tolerance(hass: HomeAssistant) -> None:
    """Test if temperature change turn heater on outside cold tolerance."""
    calls = _setup_switch(hass, False)
    await common.async_set_temperature(hass, 30)
    _setup_sensor(hass, 27)
    await hass.async_block_till_done()
    assert len(calls) == 1
    call = calls[0]
    assert call.domain == HOMEASSISTANT_DOMAIN
    assert call.service == SERVICE_TURN_ON
    assert call.data["entity_id"] == ENT_SWITCH


async def test_external_toggle_resets_min_cycle(
    hass: HomeAssistant, freezer: FrozenDateTimeFactory
) -> None:
    """Test that an external toggle cancels the min_cycle scheduled check."""
    # Set up thermostat with min cycle duration and cooldown
    await _setup_thermostat_with_min_cycle_duration(hass, False, HVACMode.HEAT)

    fake_changed = dt_util.utcnow()
    # Perform initial actions at the same frozen time so the cycle timer is recent
    freezer.move_to(fake_changed)
    # Start with switch on and record service call registrations
    calls = _setup_switch(hass, True)

    # Cause condition to try to turn off (inside min cycle)
    await common.async_set_temperature(hass, 25)
    _setup_sensor(hass, 30)
    await hass.async_block_till_done()

    # No service calls should have been made because we're within min_cycle
    assert len(calls) == 0

    # Simulate an external toggle shortly after (resets internals)
    freezer.move_to(fake_changed + datetime.timedelta(minutes=1))
    hass.states.async_set(ENT_SWITCH, STATE_OFF)
    await hass.async_block_till_done()

    # Advance past the original min_cycle; since callbacks were cancelled by
    # the external toggle, no automatic turn_off should occur
    async_fire_time_changed(hass, fake_changed + datetime.timedelta(minutes=11))
    await hass.async_block_till_done()

    assert len(calls) == 0


@pytest.mark.usefixtures("setup_comp_2")
async def test_temp_change_heater_off_within_tolerance(hass: HomeAssistant) -> None:
    """Test if temperature change doesn't turn off within tolerance."""
    calls = _setup_switch(hass, True)
    await common.async_set_temperature(hass, 30)
    _setup_sensor(hass, 33)
    await hass.async_block_till_done()
    assert len(calls) == 0


@pytest.mark.usefixtures("setup_comp_2")
async def test_temp_change_heater_off_outside_tolerance(hass: HomeAssistant) -> None:
    """Test if temperature change turn heater off outside hot tolerance."""
    calls = _setup_switch(hass, True)
    await common.async_set_temperature(hass, 30)
    _setup_sensor(hass, 35)
    await hass.async_block_till_done()
    assert len(calls) == 1
    call = calls[0]
    assert call.domain == HOMEASSISTANT_DOMAIN
    assert call.service == SERVICE_TURN_OFF
    assert call.data["entity_id"] == ENT_SWITCH


@pytest.mark.usefixtures("setup_comp_2")
async def test_running_when_hvac_mode_is_off(hass: HomeAssistant) -> None:
    """Test that the switch turns off when enabled is set False."""
    calls = _setup_switch(hass, True)
    await common.async_set_temperature(hass, 30)
    await common.async_set_hvac_mode(hass, HVACMode.OFF)
    assert len(calls) == 1
    call = calls[0]
    assert call.domain == HOMEASSISTANT_DOMAIN
    assert call.service == SERVICE_TURN_OFF
    assert call.data["entity_id"] == ENT_SWITCH


@pytest.mark.usefixtures("setup_comp_2")
async def test_no_state_change_when_hvac_mode_off(hass: HomeAssistant) -> None:
    """Test that the switch doesn't turn on when enabled is False."""
    calls = _setup_switch(hass, False)
    await common.async_set_temperature(hass, 30)
    await common.async_set_hvac_mode(hass, HVACMode.OFF)
    _setup_sensor(hass, 25)
    await hass.async_block_till_done()
    assert len(calls) == 0


@pytest.mark.usefixtures("setup_comp_2")
async def test_hvac_mode_heat(hass: HomeAssistant) -> None:
    """Test change mode from OFF to HEAT.

    Switch turns on when temp below setpoint and mode changes.
    """
    await common.async_set_hvac_mode(hass, HVACMode.OFF)
    await common.async_set_temperature(hass, 30)
    _setup_sensor(hass, 25)
    await hass.async_block_till_done()
    calls = _setup_switch(hass, False)
    await common.async_set_hvac_mode(hass, HVACMode.HEAT)
    assert len(calls) == 1
    call = calls[0]
    assert call.domain == HOMEASSISTANT_DOMAIN
    assert call.service == SERVICE_TURN_ON
    assert call.data["entity_id"] == ENT_SWITCH


def _setup_switch(hass: HomeAssistant, is_on: bool) -> list[ServiceCall]:
    """Set up the test switch."""
    hass.states.async_set(ENT_SWITCH, STATE_ON if is_on else STATE_OFF)
    calls = []

    @callback
    def log_call(call: ServiceCall) -> None:
        """Log service calls."""
        calls.append(call)

    hass.services.async_register(ha.DOMAIN, SERVICE_TURN_ON, log_call)
    hass.services.async_register(ha.DOMAIN, SERVICE_TURN_OFF, log_call)

    return calls


@pytest.fixture
async def setup_comp_3(hass: HomeAssistant) -> None:
    """Initialize components."""
    hass.config.temperature_unit = UnitOfTemperature.CELSIUS
    assert await async_setup_component(
        hass,
        CLIMATE_DOMAIN,
        {
            "climate": {
                "platform": "generic_thermostat",
                "name": "test",
                "cold_tolerance": 2,
                "hot_tolerance": 4,
                "away_temp": 30,
                "heater": ENT_SWITCH,
                "target_sensor": ENT_SENSOR,
                "ac_mode": True,
                "initial_hvac_mode": HVACMode.COOL,
            }
        },
    )
    await hass.async_block_till_done()


@pytest.mark.usefixtures("setup_comp_3")
async def test_set_target_temp_ac_off(hass: HomeAssistant) -> None:
    """Test if target temperature turn ac off."""
    calls = _setup_switch(hass, True)
    _setup_sensor(hass, 25)
    await hass.async_block_till_done()
    await common.async_set_temperature(hass, 30)
    assert len(calls) == 2
    call = calls[0]
    assert call.domain == HOMEASSISTANT_DOMAIN
    assert call.service == SERVICE_TURN_OFF
    assert call.data["entity_id"] == ENT_SWITCH


@pytest.mark.usefixtures("setup_comp_3")
async def test_turn_away_mode_on_cooling(hass: HomeAssistant) -> None:
    """Test the setting away mode when cooling."""
    _setup_switch(hass, True)
    _setup_sensor(hass, 25)
    await hass.async_block_till_done()
    await common.async_set_temperature(hass, 19)
    await common.async_set_preset_mode(hass, PRESET_AWAY)
    state = hass.states.get(ENTITY)
    assert state.attributes.get("temperature") == 30


@pytest.mark.usefixtures("setup_comp_3")
async def test_hvac_mode_cool(hass: HomeAssistant) -> None:
    """Test change mode from OFF to COOL.

    Switch turns on when temp below setpoint and mode changes.
    """
    await common.async_set_hvac_mode(hass, HVACMode.OFF)
    await common.async_set_temperature(hass, 25)
    _setup_sensor(hass, 30)
    await hass.async_block_till_done()
    calls = _setup_switch(hass, False)
    await common.async_set_hvac_mode(hass, HVACMode.COOL)
    assert len(calls) == 1
    call = calls[0]
    assert call.domain == HOMEASSISTANT_DOMAIN
    assert call.service == SERVICE_TURN_ON
    assert call.data["entity_id"] == ENT_SWITCH


@pytest.mark.usefixtures("setup_comp_3")
async def test_set_target_temp_ac_on(hass: HomeAssistant) -> None:
    """Test if target temperature turn ac on."""
    calls = _setup_switch(hass, False)
    _setup_sensor(hass, 30)
    await hass.async_block_till_done()
    await common.async_set_temperature(hass, 25)
    assert len(calls) == 1
    call = calls[0]
    assert call.domain == HOMEASSISTANT_DOMAIN
    assert call.service == SERVICE_TURN_ON
    assert call.data["entity_id"] == ENT_SWITCH


@pytest.mark.usefixtures("setup_comp_3")
async def test_temp_change_ac_off_within_tolerance(hass: HomeAssistant) -> None:
    """Test if temperature change doesn't turn ac off within tolerance."""
    calls = _setup_switch(hass, True)
    await common.async_set_temperature(hass, 30)
    _setup_sensor(hass, 29.8)
    await hass.async_block_till_done()
    assert len(calls) == 0


@pytest.mark.usefixtures("setup_comp_3")
async def test_set_temp_change_ac_off_outside_tolerance(hass: HomeAssistant) -> None:
    """Test if temperature change turn ac off."""
    calls = _setup_switch(hass, True)
    await common.async_set_temperature(hass, 30)
    _setup_sensor(hass, 27)
    await hass.async_block_till_done()
    assert len(calls) == 1
    call = calls[0]
    assert call.domain == HOMEASSISTANT_DOMAIN
    assert call.service == SERVICE_TURN_OFF
    assert call.data["entity_id"] == ENT_SWITCH


@pytest.mark.usefixtures("setup_comp_3")
async def test_temp_change_ac_on_within_tolerance(hass: HomeAssistant) -> None:
    """Test if temperature change doesn't turn ac on within tolerance."""
    calls = _setup_switch(hass, False)
    await common.async_set_temperature(hass, 25)
    _setup_sensor(hass, 25.2)
    await hass.async_block_till_done()
    assert len(calls) == 0


@pytest.mark.usefixtures("setup_comp_3")
async def test_temp_change_ac_on_outside_tolerance(hass: HomeAssistant) -> None:
    """Test if temperature change turn ac on."""
    calls = _setup_switch(hass, False)
    await common.async_set_temperature(hass, 25)
    _setup_sensor(hass, 30)
    await hass.async_block_till_done()
    assert len(calls) == 1
    call = calls[0]
    assert call.domain == HOMEASSISTANT_DOMAIN
    assert call.service == SERVICE_TURN_ON
    assert call.data["entity_id"] == ENT_SWITCH


@pytest.mark.usefixtures("setup_comp_3")
async def test_running_when_operating_mode_is_off_2(hass: HomeAssistant) -> None:
    """Test that the switch turns off when enabled is set False."""
    calls = _setup_switch(hass, True)
    await common.async_set_temperature(hass, 30)
    await common.async_set_hvac_mode(hass, HVACMode.OFF)
    assert len(calls) == 1
    call = calls[0]
    assert call.domain == HOMEASSISTANT_DOMAIN
    assert call.service == SERVICE_TURN_OFF
    assert call.data["entity_id"] == ENT_SWITCH


@pytest.mark.usefixtures("setup_comp_3")
async def test_no_state_change_when_operation_mode_off_2(hass: HomeAssistant) -> None:
    """Test that the switch doesn't turn on when enabled is False."""
    calls = _setup_switch(hass, False)
    await common.async_set_temperature(hass, 30)
    await common.async_set_hvac_mode(hass, HVACMode.OFF)
    _setup_sensor(hass, 35)
    await hass.async_block_till_done()
    assert len(calls) == 0


async def test_set_target_temp_with_hvac_mode_heat(hass: HomeAssistant) -> None:
    """Test setting the target temperature and HVAC mode together."""
    hass.config.units = METRIC_SYSTEM
    assert await async_setup_component(
        hass,
        CLIMATE_DOMAIN,
        {
            "climate": {
                "platform": "generic_thermostat",
                "name": "test",
                "cold_tolerance": 2,
                "hot_tolerance": 4,
                "heater": ENT_SWITCH,
                "target_sensor": ENT_SENSOR,
                "initial_hvac_mode": HVACMode.OFF,
                "target_temp": 20,
            }
        },
    )
    await hass.async_block_till_done()
    await common.async_set_temperature(hass, temperature=30, hvac_mode=HVACMode.HEAT)
    state = hass.states.get(ENTITY)
    assert state.attributes.get(ATTR_TEMPERATURE) == 30.0
    assert state.state == HVACMode.HEAT


async def test_set_target_temp_with_hvac_mode_off(hass: HomeAssistant) -> None:
    """Test setting a temperature while switching the climate mode to OFF."""
    hass.config.units = METRIC_SYSTEM
    assert await async_setup_component(
        hass,
        CLIMATE_DOMAIN,
        {
            "climate": {
                "platform": "generic_thermostat",
                "name": "test",
                "cold_tolerance": 2,
                "hot_tolerance": 4,
                "heater": ENT_SWITCH,
                "target_sensor": ENT_SENSOR,
                "initial_hvac_mode": HVACMode.HEAT,
                "target_temp": 20,
            }
        },
    )
    await hass.async_block_till_done()

    await common.async_set_temperature(hass, temperature=30, hvac_mode=HVACMode.OFF)

    state = hass.states.get(ENTITY)
    assert state.attributes.get(ATTR_TEMPERATURE) == 30.0
    assert state.state == HVACMode.OFF


async def test_set_target_temp_with_hvac_mode_cool_when_ac_mode_enabled(
    hass: HomeAssistant,
) -> None:
    """Test that COOL is accepted from set_temperature when ac_mode is enabled."""
    hass.config.units = METRIC_SYSTEM
    assert await async_setup_component(
        hass,
        CLIMATE_DOMAIN,
        {
            "climate": {
                "platform": "generic_thermostat",
                "name": "test",
                "cold_tolerance": 2,
                "hot_tolerance": 4,
                "heater": ENT_SWITCH,
                "target_sensor": ENT_SENSOR,
                "initial_hvac_mode": HVACMode.OFF,
                "target_temp": 20,
                "ac_mode": True,
            }
        },
    )
    await hass.async_block_till_done()

    await common.async_set_temperature(hass, temperature=30, hvac_mode=HVACMode.COOL)

    state = hass.states.get(ENTITY)
    assert state.attributes.get(ATTR_TEMPERATURE) == 30.0
    assert state.state == HVACMode.COOL


async def _setup_thermostat_with_min_cycle_duration(
    hass: HomeAssistant, ac_mode: bool, initial_hvac_mode: HVACMode
):
    """Initialize components."""
    hass.config.temperature_unit = UnitOfTemperature.CELSIUS
    assert await async_setup_component(
        hass,
        CLIMATE_DOMAIN,
        {
            "climate": {
                "platform": "generic_thermostat",
                "name": "test",
                "cold_tolerance": 0.3,
                "hot_tolerance": 0.3,
                "heater": ENT_SWITCH,
                "target_sensor": ENT_SENSOR,
                "ac_mode": ac_mode,
                # cycle_cooldown ensures switch stays off for n minutes
                "cycle_cooldown": datetime.timedelta(minutes=10),
                # min_cycle_duration only ensures switch stays on for n minutes
                "min_cycle_duration": datetime.timedelta(minutes=10),
                "initial_hvac_mode": initial_hvac_mode,
            }
        },
    )
    await hass.async_block_till_done()


@pytest.mark.parametrize(
    (
        "ac_mode",
        "initial_hvac_mode",
        "initial_switch_state",
        "sensor_temperature",
        "target_temperature",
    ),
    [
        (True, HVACMode.COOL, False, 30, 25),
        (True, HVACMode.COOL, True, 25, 30),
        (False, HVACMode.HEAT, True, 25, 30),
        (False, HVACMode.HEAT, False, 30, 25),
    ],
)
async def test_heating_cooling_switch_does_not_toggle_when_within_min_cycle_duration(
    hass: HomeAssistant,
    ac_mode: bool,
    initial_hvac_mode: HVACMode,
    initial_switch_state: bool,
    sensor_temperature: int,
    target_temperature: int,
) -> None:
    """Test if heating/cooling does not toggle when inside minimum cycle."""
    # Given
    await _setup_thermostat_with_min_cycle_duration(hass, ac_mode, initial_hvac_mode)
    calls = _setup_switch(hass, initial_switch_state)

    # When
    await common.async_set_temperature(hass, target_temperature)
    _setup_sensor(hass, sensor_temperature)
    await hass.async_block_till_done()

    # Then
    assert len(calls) == 0


@pytest.mark.parametrize(
    (
        "ac_mode",
        "initial_hvac_mode",
        "initial_switch_state",
        "sensor_temperature",
        "target_temperature",
        "expected_triggered_service_call",
    ),
    [
        (True, HVACMode.COOL, False, 30, 25, SERVICE_TURN_ON),
        (True, HVACMode.COOL, True, 25, 30, SERVICE_TURN_OFF),
        (False, HVACMode.HEAT, False, 25, 30, SERVICE_TURN_ON),
        (False, HVACMode.HEAT, True, 30, 25, SERVICE_TURN_OFF),
    ],
)
async def test_heating_cooling_switch_toggles_when_outside_min_cycle_duration(
    hass: HomeAssistant,
    ac_mode: bool,
    initial_hvac_mode: HVACMode,
    initial_switch_state: bool,
    sensor_temperature: int,
    target_temperature: int,
    expected_triggered_service_call: str,
) -> None:
    """Test if heating/cooling toggles when outside minimum cycle."""
    # Given
    await _setup_thermostat_with_min_cycle_duration(hass, ac_mode, initial_hvac_mode)
    fake_changed = datetime.datetime(1970, 11, 11, 11, 11, 11, tzinfo=dt_util.UTC)
    with freeze_time(fake_changed):
        calls = _setup_switch(hass, initial_switch_state)

    # When
    await common.async_set_temperature(hass, target_temperature)
    _setup_sensor(hass, sensor_temperature)
    await hass.async_block_till_done()

    # Then
    assert len(calls) == 1
    call = calls[0]
    assert call.domain == HOMEASSISTANT_DOMAIN
    assert call.service == expected_triggered_service_call
    assert call.data["entity_id"] == ENT_SWITCH


@pytest.mark.parametrize(
    (
        "ac_mode",
        "initial_hvac_mode",
        "initial_switch_state",
        "sensor_temperature",
        "target_temperature",
        "changed_hvac_mode",
        "expected_triggered_service_call",
    ),
    [
        (True, HVACMode.COOL, False, 30, 25, HVACMode.COOL, SERVICE_TURN_ON),
        (True, HVACMode.COOL, True, 25, 30, HVACMode.OFF, SERVICE_TURN_OFF),
        (False, HVACMode.HEAT, False, 25, 30, HVACMode.HEAT, SERVICE_TURN_ON),
        (False, HVACMode.HEAT, True, 30, 25, HVACMode.OFF, SERVICE_TURN_OFF),
    ],
)
async def test_hvac_mode_change_toggles_switch_within_min_cycle_duration(
    hass: HomeAssistant,
    ac_mode: bool,
    initial_hvac_mode: HVACMode,
    initial_switch_state: bool,
    sensor_temperature: int,
    target_temperature: int,
    changed_hvac_mode: HVACMode,
    expected_triggered_service_call: str,
) -> None:
    """Test if mode change toggles heating/cooling despite minimum cycle."""
    # Given
    await _setup_thermostat_with_min_cycle_duration(hass, ac_mode, initial_hvac_mode)
    calls = _setup_switch(hass, initial_switch_state)

    # When
    await common.async_set_temperature(hass, target_temperature)
    _setup_sensor(hass, sensor_temperature)
    await hass.async_block_till_done()

    # Then
    assert len(calls) == 0
    await common.async_set_hvac_mode(hass, changed_hvac_mode)
    assert len(calls) == 1
    call = calls[0]
    assert call.domain == "homeassistant"
    assert call.service == expected_triggered_service_call
    assert call.data["entity_id"] == ENT_SWITCH


@pytest.fixture
async def setup_comp_7(hass: HomeAssistant) -> None:
    """Initialize components."""
    hass.config.temperature_unit = UnitOfTemperature.CELSIUS
    assert await async_setup_component(
        hass,
        CLIMATE_DOMAIN,
        {
            "climate": {
                "platform": "generic_thermostat",
                "name": "test",
                "cold_tolerance": 0.3,
                "hot_tolerance": 0.3,
                "heater": ENT_SWITCH,
                "target_temp": 25,
                "target_sensor": ENT_SENSOR,
                "ac_mode": True,
                "min_cycle_duration": datetime.timedelta(minutes=15),
                # cycle_cooldown ensures switch stays off for n minutes
                "cycle_cooldown": datetime.timedelta(minutes=15),
                "keep_alive": datetime.timedelta(minutes=10),
                "initial_hvac_mode": HVACMode.COOL,
            }
        },
    )

    await hass.async_block_till_done()


@pytest.mark.usefixtures("setup_comp_7")
async def test_temp_change_ac_trigger_on_long_enough_3(hass: HomeAssistant) -> None:
    """Test if turn on signal is sent at keep-alive intervals."""
    calls = _setup_switch(hass, True)
    await hass.async_block_till_done()
    _setup_sensor(hass, 30)
    await hass.async_block_till_done()
    await common.async_set_temperature(hass, 25)
    test_time = dt_util.utcnow()
    async_fire_time_changed(hass, test_time)
    await hass.async_block_till_done()
    assert len(calls) == 0
    async_fire_time_changed(hass, test_time + datetime.timedelta(minutes=5))
    await hass.async_block_till_done()
    assert len(calls) == 0
    async_fire_time_changed(hass, test_time + datetime.timedelta(minutes=10))
    await hass.async_block_till_done()
    assert len(calls) == 1
    call = calls[0]
    assert call.domain == HOMEASSISTANT_DOMAIN
    assert call.service == SERVICE_TURN_ON
    assert call.data["entity_id"] == ENT_SWITCH


@pytest.mark.usefixtures("setup_comp_7")
async def test_temp_change_ac_trigger_off_long_enough_3(hass: HomeAssistant) -> None:
    """Test if turn on signal is sent at keep-alive intervals."""
    calls = _setup_switch(hass, False)
    await hass.async_block_till_done()
    _setup_sensor(hass, 20)
    await hass.async_block_till_done()
    await common.async_set_temperature(hass, 25)
    test_time = dt_util.utcnow()
    async_fire_time_changed(hass, test_time)
    await hass.async_block_till_done()
    assert len(calls) == 0
    async_fire_time_changed(hass, test_time + datetime.timedelta(minutes=5))
    await hass.async_block_till_done()
    assert len(calls) == 0
    async_fire_time_changed(hass, test_time + datetime.timedelta(minutes=10))
    await hass.async_block_till_done()
    assert len(calls) == 1
    call = calls[0]
    assert call.domain == HOMEASSISTANT_DOMAIN
    assert call.service == SERVICE_TURN_OFF
    assert call.data["entity_id"] == ENT_SWITCH


@pytest.fixture
async def setup_comp_8(hass: HomeAssistant) -> None:
    """Initialize components."""
    hass.config.temperature_unit = UnitOfTemperature.CELSIUS
    assert await async_setup_component(
        hass,
        CLIMATE_DOMAIN,
        {
            "climate": {
                "platform": "generic_thermostat",
                "name": "test",
                "cold_tolerance": 0.3,
                "hot_tolerance": 0.3,
                "target_temp": 25,
                "heater": ENT_SWITCH,
                "target_sensor": ENT_SENSOR,
                "min_cycle_duration": datetime.timedelta(minutes=15),
                # cycle_cooldown ensures switch stays off for n minutes
                "cycle_cooldown": datetime.timedelta(minutes=15),
                "keep_alive": datetime.timedelta(minutes=10),
                "initial_hvac_mode": HVACMode.HEAT,
            }
        },
    )
    await hass.async_block_till_done()


@pytest.mark.usefixtures("setup_comp_8")
async def test_temp_change_heater_trigger_on_long_enough_2(hass: HomeAssistant) -> None:
    """Test if turn on signal is sent at keep-alive intervals."""
    calls = _setup_switch(hass, True)
    await hass.async_block_till_done()
    _setup_sensor(hass, 20)
    await hass.async_block_till_done()
    await common.async_set_temperature(hass, 25)
    test_time = dt_util.utcnow()
    async_fire_time_changed(hass, test_time)
    await hass.async_block_till_done()
    assert len(calls) == 0
    async_fire_time_changed(hass, test_time + datetime.timedelta(minutes=5))
    await hass.async_block_till_done()
    assert len(calls) == 0
    async_fire_time_changed(hass, test_time + datetime.timedelta(minutes=10))
    await hass.async_block_till_done()
    assert len(calls) == 1
    call = calls[0]
    assert call.domain == HOMEASSISTANT_DOMAIN
    assert call.service == SERVICE_TURN_ON
    assert call.data["entity_id"] == ENT_SWITCH


@pytest.mark.usefixtures("setup_comp_8")
async def test_temp_change_heater_trigger_off_long_enough_2(
    hass: HomeAssistant,
) -> None:
    """Test if turn on signal is sent at keep-alive intervals."""
    calls = _setup_switch(hass, False)
    await hass.async_block_till_done()
    _setup_sensor(hass, 30)
    await hass.async_block_till_done()
    await common.async_set_temperature(hass, 25)
    test_time = dt_util.utcnow()
    async_fire_time_changed(hass, test_time)
    await hass.async_block_till_done()
    assert len(calls) == 0
    async_fire_time_changed(hass, test_time + datetime.timedelta(minutes=5))
    await hass.async_block_till_done()
    assert len(calls) == 0
    async_fire_time_changed(hass, test_time + datetime.timedelta(minutes=10))
    await hass.async_block_till_done()
    assert len(calls) == 1
    call = calls[0]
    assert call.domain == HOMEASSISTANT_DOMAIN
    assert call.service == SERVICE_TURN_OFF
    assert call.data["entity_id"] == ENT_SWITCH


async def test_max_cycle_duration_turns_off(hass: HomeAssistant) -> None:
    """Test that max_cycle_duration forces the heater off after the duration."""
    hass.config.temperature_unit = UnitOfTemperature.CELSIUS
    assert await async_setup_component(
        hass,
        CLIMATE_DOMAIN,
        {
            "climate": {
                "platform": "generic_thermostat",
                "name": "test",
                "cold_tolerance": 0.3,
                "hot_tolerance": 0.3,
                "target_temp": 25,
                "heater": ENT_SWITCH,
                "target_sensor": ENT_SENSOR,
                "min_cycle_duration": datetime.timedelta(minutes=0),
                "max_cycle_duration": datetime.timedelta(minutes=10),
                "initial_hvac_mode": HVACMode.HEAT,
            }
        },
    )
    await hass.async_block_till_done()

    calls = _setup_switch(hass, False)
    # Ensure sensor indicates below target so heater will turn on
    _setup_sensor(hass, 20)
    await hass.async_block_till_done()

    # Heater should have been turned on
    assert len(calls) == 1
    call = calls[0]
    assert call.service == SERVICE_TURN_ON

    # Advance time to trigger max cycle shut-off
    test_time = dt_util.utcnow()
    async_fire_time_changed(hass, test_time)
    await hass.async_block_till_done()
    async_fire_time_changed(hass, test_time + datetime.timedelta(minutes=10))
    await hass.async_block_till_done()

    # One additional turn_off call should have occurred
    assert len(calls) == 2
    assert calls[1].service == SERVICE_TURN_OFF


async def test_external_toggle_resets_max_cycle(
    hass: HomeAssistant, freezer: FrozenDateTimeFactory
) -> None:
    """Test that an external toggle cancels the max_cycle scheduled check."""
    hass.config.temperature_unit = UnitOfTemperature.CELSIUS
    assert await async_setup_component(
        hass,
        CLIMATE_DOMAIN,
        {
            "climate": {
                "platform": "generic_thermostat",
                "name": "test",
                "cold_tolerance": 0.3,
                "hot_tolerance": 0.3,
                "target_temp": 25,
                "heater": ENT_SWITCH,
                "target_sensor": ENT_SENSOR,
                "min_cycle_duration": datetime.timedelta(minutes=0),
                "max_cycle_duration": datetime.timedelta(minutes=10),
                "initial_hvac_mode": HVACMode.HEAT,
            }
        },
    )
    await hass.async_block_till_done()

    calls = _setup_switch(hass, False)
    # Trigger heater to turn on
    _setup_sensor(hass, 20)
    await hass.async_block_till_done()
    assert len(calls) == 1

    # Simulate an external toggle event shortly after (resets internals)
    test_time = dt_util.utcnow()
    async_fire_time_changed(hass, test_time)
    freezer.move_to(test_time + datetime.timedelta(minutes=1))
    hass.states.async_set(ENT_SWITCH, STATE_ON)
    await hass.async_block_till_done()

    # Advance past the original max duration; since callbacks were cancelled by
    # the external toggle, no automatic turn_off should occur
    async_fire_time_changed(hass, test_time + datetime.timedelta(minutes=11))
    await hass.async_block_till_done()

    # Only the original turn_on call should be present
    assert len(calls) == 1


async def test_default_cycle_cooldown_allows_immediate_restart(
    hass: HomeAssistant,
) -> None:
    """Test default `cycle_cooldown` allows immediate restart when omitted."""
    hass.config.temperature_unit = UnitOfTemperature.CELSIUS
    # Do not provide `cycle_cooldown` here; default should be zero timedelta
    assert await async_setup_component(
        hass,
        CLIMATE_DOMAIN,
        {
            "climate": {
                "platform": "generic_thermostat",
                "name": "test",
                "cold_tolerance": 0.3,
                "hot_tolerance": 0.3,
                "target_temp": 25,
                "heater": ENT_SWITCH,
                "target_sensor": ENT_SENSOR,
                "min_cycle_duration": datetime.timedelta(minutes=0),
                "initial_hvac_mode": HVACMode.HEAT,
            }
        },
    )
    await hass.async_block_till_done()

    # Start with the switch ON so the thermostat can issue a turn_off
    calls = _setup_switch(hass, True)

    # Trigger off
    _setup_sensor(hass, 30)
    await hass.async_block_till_done()
    assert len(calls) == 1
    assert calls[0].service == SERVICE_TURN_OFF

    # Reflect the physical device change (services are not changing state in
    # this test harness). Update the entity to OFF so the thermostat sees the
    # device as inactive and can attempt to turn it on again.
    hass.states.async_set(ENT_SWITCH, STATE_OFF)
    await hass.async_block_till_done()

    # Immediately trigger on again; with default cooldown=0 this should be allowed
    _setup_sensor(hass, 20)
    await hass.async_block_till_done()
    assert len(calls) == 2
    assert calls[1].service == SERVICE_TURN_ON


async def test_cycle_cooldown_schedules_restart_after_cooldown(
    hass: HomeAssistant, freezer: FrozenDateTimeFactory
) -> None:
    """Test that cooldown blocks restart and schedules a restart check."""
    hass.config.temperature_unit = UnitOfTemperature.CELSIUS
    now = dt_util.utcnow()
    freezer.move_to(now)

    assert await async_setup_component(
        hass,
        CLIMATE_DOMAIN,
        {
            "climate": {
                "platform": "generic_thermostat",
                "name": "test",
                "cold_tolerance": 0.3,
                "hot_tolerance": 0.3,
                "target_temp": 25,
                "heater": ENT_SWITCH,
                "target_sensor": ENT_SENSOR,
                "min_cycle_duration": datetime.timedelta(minutes=0),
                "cycle_cooldown": datetime.timedelta(minutes=15),
                "initial_hvac_mode": HVACMode.HEAT,
            }
        },
    )
    await hass.async_block_till_done()

    # Force the thermostat into cooldown by faking a recent toggle time.
    thermostats = hass.data[entity_platform.DATA_DOMAIN_PLATFORM_ENTITIES][
        (CLIMATE_DOMAIN, "generic_thermostat")
    ]
    thermostat = thermostats[ENTITY]
    thermostat._last_toggled_time = now

    # Ensure turning on is blocked while in cooldown
    calls = _setup_switch(hass, False)
    _setup_sensor(hass, 20)
    await hass.async_block_till_done()
    assert len(calls) == 0

    # Advance to end of cooldown and trigger the scheduled check
    freezer.move_to(now + datetime.timedelta(minutes=15))
    async_fire_time_changed(hass, now + datetime.timedelta(minutes=15))
    await hass.async_block_till_done()

    assert len(calls) == 1
    assert calls[0].service == SERVICE_TURN_ON


@pytest.fixture
async def setup_comp_9(hass: HomeAssistant) -> None:
    """Initialize components."""
    assert await async_setup_component(
        hass,
        CLIMATE_DOMAIN,
        {
            "climate": {
                "platform": "generic_thermostat",
                "name": "test",
                "cold_tolerance": 0.3,
                "hot_tolerance": 0.3,
                "target_temp": 25,
                "heater": ENT_SWITCH,
                "target_sensor": ENT_SENSOR,
                "min_cycle_duration": datetime.timedelta(minutes=15),
                # cycle_cooldown ensures switch stays off for n minutes
                "cycle_cooldown": datetime.timedelta(minutes=15),
                "keep_alive": datetime.timedelta(minutes=10),
                "precision": 0.1,
            }
        },
    )
    await hass.async_block_till_done()


@pytest.mark.usefixtures("setup_comp_9")
async def test_precision(hass: HomeAssistant) -> None:
    """Test that setting precision to tenths works as intended."""
    hass.config.units = US_CUSTOMARY_SYSTEM
    await common.async_set_temperature(hass, 55.27)
    state = hass.states.get(ENTITY)
    assert state.attributes.get("temperature") == 55.3
    # check that target_temp_step defaults to precision
    assert state.attributes.get("target_temp_step") == 0.1


@pytest.fixture(
    params=[
        HVACMode.HEAT,
        HVACMode.COOL,
    ]
)
async def setup_comp_10(hass: HomeAssistant, request: pytest.FixtureRequest) -> None:
    """Initialize components."""
    assert await async_setup_component(
        hass,
        CLIMATE_DOMAIN,
        {
            "climate": {
                "platform": "generic_thermostat",
                "name": "test",
                "cold_tolerance": 0,
                "hot_tolerance": 0,
                "target_temp": 25,
                "heater": ENT_SWITCH,
                "target_sensor": ENT_SENSOR,
                "initial_hvac_mode": request.param,
            }
        },
    )
    await hass.async_block_till_done()


@pytest.mark.usefixtures("setup_comp_10")
async def test_zero_tolerances(hass: HomeAssistant) -> None:
    """Test that having a zero tolerance doesn't cause the switch to flip-flop."""

    # if the switch is off, it should remain off
    calls = _setup_switch(hass, False)
    _setup_sensor(hass, 25)
    await hass.async_block_till_done()
    await common.async_set_temperature(hass, 25)
    assert len(calls) == 0

    # if the switch is on, it should remain on
    calls = _setup_switch(hass, True)
    _setup_sensor(hass, 25)
    await hass.async_block_till_done()
    await common.async_set_temperature(hass, 25)
    assert len(calls) == 0


async def test_custom_setup_params(hass: HomeAssistant) -> None:
    """Test the setup with custom parameters."""
    result = await async_setup_component(
        hass,
        CLIMATE_DOMAIN,
        {
            "climate": {
                "platform": "generic_thermostat",
                "name": "test",
                "heater": ENT_SWITCH,
                "target_sensor": ENT_SENSOR,
                "min_temp": MIN_TEMP,
                "max_temp": MAX_TEMP,
                "target_temp": TARGET_TEMP,
                "target_temp_step": 0.5,
            }
        },
    )
    assert result
    await hass.async_block_till_done()
    state = hass.states.get(ENTITY)
    assert state.attributes.get("min_temp") == MIN_TEMP
    assert state.attributes.get("max_temp") == MAX_TEMP
    assert state.attributes.get("temperature") == TARGET_TEMP
    assert state.attributes.get("target_temp_step") == TARGET_TEMP_STEP


@pytest.mark.parametrize("hvac_mode", [HVACMode.OFF, HVACMode.HEAT, HVACMode.COOL])
async def test_restore_state(hass: HomeAssistant, hvac_mode) -> None:
    """Ensure states are restored on startup."""
    mock_restore_cache(
        hass,
        (
            State(
                "climate.test_thermostat",
                hvac_mode,
                {ATTR_TEMPERATURE: "20", ATTR_PRESET_MODE: PRESET_AWAY},
            ),
        ),
    )

    hass.set_state(CoreState.starting)

    await async_setup_component(
        hass,
        CLIMATE_DOMAIN,
        {
            "climate": {
                "platform": "generic_thermostat",
                "name": "test_thermostat",
                "heater": ENT_SWITCH,
                "target_sensor": ENT_SENSOR,
                "away_temp": 14,
            }
        },
    )
    await hass.async_block_till_done()
    state = hass.states.get("climate.test_thermostat")
    assert state.attributes[ATTR_TEMPERATURE] == 20
    assert state.attributes[ATTR_PRESET_MODE] == PRESET_AWAY
    assert state.state == hvac_mode


async def test_no_restore_state(hass: HomeAssistant) -> None:
    """Ensure states are restored on startup if they exist.

    Allows for graceful reboot.
    """
    mock_restore_cache(
        hass,
        (
            State(
                "climate.test_thermostat",
                HVACMode.OFF,
                {ATTR_TEMPERATURE: "20", ATTR_PRESET_MODE: PRESET_AWAY},
            ),
        ),
    )

    hass.set_state(CoreState.starting)

    await async_setup_component(
        hass,
        CLIMATE_DOMAIN,
        {
            "climate": {
                "platform": "generic_thermostat",
                "name": "test_thermostat",
                "heater": ENT_SWITCH,
                "target_sensor": ENT_SENSOR,
                "target_temp": 22,
            }
        },
    )
    await hass.async_block_till_done()
    state = hass.states.get("climate.test_thermostat")
    assert state.attributes[ATTR_TEMPERATURE] == 22
    assert state.state == HVACMode.OFF


async def test_initial_hvac_off_force_heater_off(hass: HomeAssistant) -> None:
    """Ensure that restored state is coherent with real situation.

    'initial_hvac_mode: off' will force HVAC status, but we must be sure
    that heater don't keep on.
    """
    # switch is on
    calls = _setup_switch(hass, True)
    assert hass.states.get(ENT_SWITCH).state == STATE_ON

    _setup_sensor(hass, 16)

    await async_setup_component(
        hass,
        CLIMATE_DOMAIN,
        {
            "climate": {
                "platform": "generic_thermostat",
                "name": "test_thermostat",
                "heater": ENT_SWITCH,
                "target_sensor": ENT_SENSOR,
                "target_temp": 20,
                "initial_hvac_mode": HVACMode.OFF,
            }
        },
    )
    await hass.async_block_till_done()
    state = hass.states.get("climate.test_thermostat")
    # 'initial_hvac_mode' will force state but must prevent heather keep working
    assert state.state == HVACMode.OFF
    # heater must be switched off
    assert len(calls) == 1
    call = calls[0]
    assert call.domain == HOMEASSISTANT_DOMAIN
    assert call.service == SERVICE_TURN_OFF
    assert call.data["entity_id"] == ENT_SWITCH


async def test_restore_will_turn_off_(hass: HomeAssistant) -> None:
    """Ensure that restored state is coherent with real situation.

    Thermostat status must trigger heater event if temp raises the target .
    """
    heater_switch = "input_boolean.test"
    mock_restore_cache(
        hass,
        (
            State(
                "climate.test_thermostat",
                HVACMode.HEAT,
                {ATTR_TEMPERATURE: "18", ATTR_PRESET_MODE: PRESET_NONE},
            ),
            State(heater_switch, STATE_ON, {}),
        ),
    )

    hass.set_state(CoreState.starting)

    assert await async_setup_component(
        hass, input_boolean.DOMAIN, {"input_boolean": {"test": None}}
    )
    await hass.async_block_till_done()
    assert hass.states.get(heater_switch).state == STATE_ON

    _setup_sensor(hass, 22)

    await async_setup_component(
        hass,
        CLIMATE_DOMAIN,
        {
            "climate": {
                "platform": "generic_thermostat",
                "name": "test_thermostat",
                "heater": heater_switch,
                "target_sensor": ENT_SENSOR,
                "target_temp": 20,
            }
        },
    )
    await hass.async_block_till_done()
    state = hass.states.get("climate.test_thermostat")
    assert state.attributes[ATTR_TEMPERATURE] == 20
    assert state.state == HVACMode.HEAT
    assert hass.states.get(heater_switch).state == STATE_ON


async def test_restore_will_turn_off_when_loaded_second(hass: HomeAssistant) -> None:
    """Ensure that restored state is coherent with real situation.

    Switch is not available until after component is loaded
    """
    heater_switch = "input_boolean.test"
    mock_restore_cache(
        hass,
        (
            State(
                "climate.test_thermostat",
                HVACMode.HEAT,
                {ATTR_TEMPERATURE: "18", ATTR_PRESET_MODE: PRESET_NONE},
            ),
            State(heater_switch, STATE_ON, {}),
        ),
    )

    hass.set_state(CoreState.starting)

    await hass.async_block_till_done()
    assert hass.states.get(heater_switch) is None

    _setup_sensor(hass, 16)

    await async_setup_component(
        hass,
        CLIMATE_DOMAIN,
        {
            "climate": {
                "platform": "generic_thermostat",
                "name": "test_thermostat",
                "heater": heater_switch,
                "target_sensor": ENT_SENSOR,
                "target_temp": 20,
                "initial_hvac_mode": HVACMode.OFF,
            }
        },
    )
    await hass.async_block_till_done()
    state = hass.states.get("climate.test_thermostat")
    assert state.attributes[ATTR_TEMPERATURE] == 20
    assert state.state == HVACMode.OFF

    calls_on = async_mock_service(hass, ha.DOMAIN, SERVICE_TURN_ON)
    calls_off = async_mock_service(hass, ha.DOMAIN, SERVICE_TURN_OFF)

    assert await async_setup_component(
        hass, input_boolean.DOMAIN, {"input_boolean": {"test": None}}
    )
    await hass.async_block_till_done()
    # heater must be switched off
    assert len(calls_on) == 0
    assert len(calls_off) == 1
    call = calls_off[0]
    assert call.domain == HOMEASSISTANT_DOMAIN
    assert call.service == SERVICE_TURN_OFF
    assert call.data["entity_id"] == "input_boolean.test"


async def test_restore_state_uncoherence_case(hass: HomeAssistant) -> None:
    """Test restore from a strange state.

    - Turn the generic thermostat off
    - Restart HA and restore state from DB
    """
    _mock_restore_cache(hass, temperature=20)

    calls = _setup_switch(hass, False)
    _setup_sensor(hass, 15)
    await _setup_climate(hass)
    await hass.async_block_till_done()

    state = hass.states.get(ENTITY)
    assert state.attributes[ATTR_TEMPERATURE] == 20
    assert state.state == HVACMode.OFF
    assert len(calls) == 0

    calls = _setup_switch(hass, False)
    await hass.async_block_till_done()
    state = hass.states.get(ENTITY)
    assert state.state == HVACMode.OFF


async def _setup_climate(hass: HomeAssistant) -> None:
    assert await async_setup_component(
        hass,
        CLIMATE_DOMAIN,
        {
            "climate": {
                "platform": "generic_thermostat",
                "name": "test",
                "cold_tolerance": 2,
                "hot_tolerance": 4,
                "away_temp": 30,
                "heater": ENT_SWITCH,
                "target_sensor": ENT_SENSOR,
                "ac_mode": True,
            }
        },
    )


def _mock_restore_cache(
    hass: HomeAssistant, temperature: int = 20, hvac_mode: HVACMode = HVACMode.OFF
) -> None:
    mock_restore_cache(
        hass,
        (
            State(
                ENTITY,
                hvac_mode,
                {ATTR_TEMPERATURE: str(temperature), ATTR_PRESET_MODE: PRESET_AWAY},
            ),
        ),
    )


async def test_reload(hass: HomeAssistant) -> None:
    """Test we can reload."""

    assert await async_setup_component(
        hass,
        CLIMATE_DOMAIN,
        {
            "climate": {
                "platform": "generic_thermostat",
                "name": "test",
                "heater": "switch.any",
                "target_sensor": "sensor.any",
            }
        },
    )

    await hass.async_block_till_done()
    assert len(hass.states.async_all()) == 1
    assert hass.states.get("climate.test") is not None

    yaml_path = get_fixture_path("configuration.yaml", "generic_thermostat")
    with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path):
        await hass.services.async_call(
            DOMAIN,
            SERVICE_RELOAD,
            {},
            blocking=True,
        )
        await hass.async_block_till_done()

    assert len(hass.states.async_all()) == 1
    assert hass.states.get("climate.test") is None
    assert hass.states.get("climate.reload")


async def test_device_id(
    hass: HomeAssistant,
    entity_registry: er.EntityRegistry,
    device_registry: dr.DeviceRegistry,
) -> None:
    """Test for source entity device."""

    source_config_entry = MockConfigEntry()
    source_config_entry.add_to_hass(hass)
    source_device_entry = device_registry.async_get_or_create(
        config_entry_id=source_config_entry.entry_id,
        identifiers={("switch", "identifier_test")},
        connections={("mac", "30:31:32:33:34:35")},
    )
    source_entity = entity_registry.async_get_or_create(
        "switch",
        "test",
        "source",
        config_entry=source_config_entry,
        device_id=source_device_entry.id,
    )
    await hass.async_block_till_done()
    assert entity_registry.async_get("switch.test_source") is not None

    helper_config_entry = MockConfigEntry(
        data={},
        domain=DOMAIN,
        options={
            "name": "Test",
            "heater": "switch.test_source",
            "target_sensor": ENT_SENSOR,
            "ac_mode": False,
            "cold_tolerance": 0.3,
            "hot_tolerance": 0.3,
        },
        title="Test",
    )
    helper_config_entry.add_to_hass(hass)

    assert await hass.config_entries.async_setup(helper_config_entry.entry_id)
    await hass.async_block_till_done()

    helper_entity = entity_registry.async_get("climate.test")
    assert helper_entity is not None
    assert helper_entity.device_id == source_entity.device_id


async def test_device_id_yaml(
    hass: HomeAssistant,
    entity_registry: er.EntityRegistry,
    device_registry: dr.DeviceRegistry,
    caplog: pytest.LogCaptureFixture,
) -> None:
    """Test no device is set for a YAML-configured thermostat."""
    source_config_entry = MockConfigEntry()
    source_config_entry.add_to_hass(hass)
    source_device_entry = device_registry.async_get_or_create(
        config_entry_id=source_config_entry.entry_id,
        identifiers={("switch", "identifier_test")},
        connections={("mac", "30:31:32:33:34:35")},
    )
    entity_registry.async_get_or_create(
        "switch",
        "test",
        "source",
        config_entry=source_config_entry,
        device_id=source_device_entry.id,
    )
    await hass.async_block_till_done()

    assert await async_setup_component(
        hass,
        CLIMATE_DOMAIN,
        {
            "climate": {
                "platform": "generic_thermostat",
                "name": "test",
                "heater": "switch.test_source",
                "target_sensor": ENT_SENSOR,
                "unique_id": "generic_thermostat_yaml",
            }
        },
    )
    await hass.async_block_till_done()

    helper_entity = entity_registry.async_get("climate.test")
    assert helper_entity is not None
    assert helper_entity.device_id is None
    assert "attempts to attach a device to an entity" not in caplog.text


@pytest.mark.usefixtures("setup_comp_1")
async def test_hvac_mode_change_user_context(
    hass: HomeAssistant, hass_admin_user: MockUser
) -> None:
    """Test user context is preserved through the full chain.

    Full chain:
    1. User calls set_hvac_mode → parent context (has user_id)
    2. Generic thermostat calls homeassistant.turn_on → child context (no user_id)
    3. Switch state changes → child context
    4. Climate state updates in response → child context
    """
    heater_switch = "input_boolean.test"
    assert await async_setup_component(
        hass, input_boolean.DOMAIN, {"input_boolean": {"test": None}}
    )

    assert await async_setup_component(
        hass,
        CLIMATE_DOMAIN,
        {
            "climate": {
                "platform": "generic_thermostat",
                "name": "test",
                "heater": heater_switch,
                "target_sensor": ENT_SENSOR,
                "initial_hvac_mode": HVACMode.OFF,
                "cold_tolerance": 2,
                "hot_tolerance": 4,
            }
        },
    )
    await hass.async_block_till_done()

    # Set sensor below target so heating triggers on mode change
    _setup_sensor(hass, 18)
    await hass.async_block_till_done()
    await common.async_set_temperature(hass, 23)
    await hass.async_block_till_done()

    assert hass.states.get(heater_switch).state == STATE_OFF

    # Change HVAC mode with a user context
    user_context = Context(user_id=hass_admin_user.id)
    await hass.services.async_call(
        CLIMATE_DOMAIN,
        "set_hvac_mode",
        {"entity_id": ENTITY, "hvac_mode": HVACMode.HEAT},
        blocking=True,
        context=user_context,
    )
    await hass.async_block_till_done()

    # Step 2: The heater should have been turned on
    assert hass.states.get(heater_switch).state == STATE_ON

    # The switch state change should have a child context with the
    # user context as parent
    switch_state = hass.states.get(heater_switch)
    child_context = switch_state.context
    assert child_context.id != user_context.id
    assert child_context.parent_id == user_context.id

    # Step 4: The climate entity should keep the parent (user) context,
    # not the child context created for the switch service call
    climate_state = hass.states.get(ENTITY)
    assert climate_state.context.id == user_context.id
    assert climate_state.context.user_id == hass_admin_user.id


@pytest.mark.usefixtures("setup_comp_1")
async def test_sensor_change_inherits_context(hass: HomeAssistant) -> None:
    """Test that sensor changes set the sensor's context on the thermostat.

    When the sensor updates, the thermostat should inherit the sensor's
    context so the resulting switch toggle has the sensor context as parent.
    """
    heater_switch = "input_boolean.test"
    assert await async_setup_component(
        hass, input_boolean.DOMAIN, {"input_boolean": {"test": None}}
    )

    assert await async_setup_component(
        hass,
        CLIMATE_DOMAIN,
        {
            "climate": {
                "platform": "generic_thermostat",
                "name": "test",
                "heater": heater_switch,
                "target_sensor": ENT_SENSOR,
                "initial_hvac_mode": HVACMode.HEAT,
                "cold_tolerance": 2,
                "hot_tolerance": 4,
            }
        },
    )
    await hass.async_block_till_done()

    await common.async_set_temperature(hass, 30)
    await hass.async_block_till_done()

    assert hass.states.get(heater_switch).state == STATE_OFF

    # Set sensor below target with a specific context
    sensor_context = Context()
    hass.states.async_set(ENT_SENSOR, "18", context=sensor_context)
    await hass.async_block_till_done()

    # The heater should have turned on
    assert hass.states.get(heater_switch).state == STATE_ON

    # The switch state change should have a child context with the
    # sensor context as parent
    switch_state = hass.states.get(heater_switch)
    assert switch_state.context.parent_id == sensor_context.id
    assert switch_state.context.id != sensor_context.id


@pytest.mark.usefixtures("setup_comp_1")
async def test_stale_context_not_used_as_parent(
    hass: HomeAssistant, freezer: FrozenDateTimeFactory
) -> None:
    """Test that an expired context is not used as parent for the switch call.

    When a keepalive timer fires long after the last user interaction,
    the thermostat should not link the switch service call to the old
    user context.
    """
    heater_switch = "input_boolean.test"
    assert await async_setup_component(
        hass, input_boolean.DOMAIN, {"input_boolean": {"test": None}}
    )

    assert await async_setup_component(
        hass,
        CLIMATE_DOMAIN,
        {
            "climate": {
                "platform": "generic_thermostat",
                "name": "test",
                "heater": heater_switch,
                "target_sensor": ENT_SENSOR,
                "initial_hvac_mode": HVACMode.HEAT,
                "cold_tolerance": 2,
                "hot_tolerance": 4,
                "keep_alive": datetime.timedelta(minutes=10),
            }
        },
    )
    await hass.async_block_till_done()

    # Set temperature and sensor so heater is on
    await common.async_set_temperature(hass, 30)
    _setup_sensor(hass, 18)
    await hass.async_block_till_done()
    assert hass.states.get(heater_switch).state == STATE_ON

    # Capture service calls to check the keepalive context
    calls = async_mock_service(hass, ha.DOMAIN, SERVICE_TURN_ON)

    # Advance time past keepalive interval (10 min) and context expiry (5s)
    freezer.tick(datetime.timedelta(minutes=10))
    async_fire_time_changed(hass)
    await hass.async_block_till_done()

    # The keepalive should have sent a turn_on call
    assert len(calls) == 1
    assert calls[0].data["entity_id"] == heater_switch

    # The keepalive call's context should have no parent,
    # because the original context expired long ago
    assert calls[0].context.parent_id is None
