"""Tests for the BSB-LAN climate platform."""

from datetime import timedelta
from typing import Any
from unittest.mock import AsyncMock, MagicMock

from bsblan import BSBLANError, HeatingCircuitStatus, StaticState
from freezegun.api import FrozenDateTimeFactory
import pytest
from syrupy.assertion import SnapshotAssertion

from homeassistant.components.bsblan.const import DOMAIN
from homeassistant.components.climate import (
    ATTR_HVAC_MODE,
    ATTR_PRESET_MODE,
    DEFAULT_MAX_TEMP,
    DEFAULT_MIN_TEMP,
    DOMAIN as CLIMATE_DOMAIN,
    PRESET_ECO,
    PRESET_NONE,
    SERVICE_SET_HVAC_MODE,
    SERVICE_SET_PRESET_MODE,
    SERVICE_SET_TEMPERATURE,
    HVACAction,
    HVACMode,
)
from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er

from . import setup_with_selected_platforms

from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform

ENTITY_ID = "climate.heating_circuit_1"


def _temp_param(value: str) -> dict[str, Any]:
    """Build a raw BSB-LAN temperature parameter payload."""
    return {
        "name": "",
        "value": value,
        "unit": "&deg;C",
        "desc": "",
        "dataType": 0,
        "readonly": 0,
        "error": 0,
    }


@pytest.mark.parametrize(
    ("static_data", "expected_min", "expected_max"),
    [
        pytest.param(
            {
                "heating_protective_setpoint": _temp_param("10.0"),
                "comfort_setpoint_max": _temp_param("26.0"),
            },
            10.0,
            26.0,
            id="standard_device",
        ),
        pytest.param(
            {
                "min_temp": _temp_param("8.0"),
                "max_temp": _temp_param("20.0"),
            },
            8.0,
            20.0,
            id="pps_device",
        ),
        pytest.param(
            {
                "heating_protective_setpoint": _temp_param("---"),
                "comfort_setpoint_max": _temp_param("---"),
                "min_temp": _temp_param("8.0"),
                "max_temp": _temp_param("20.0"),
            },
            8.0,
            20.0,
            id="inactive_preferred_source",
        ),
        pytest.param(
            {
                "heating_protective_setpoint": _temp_param("---"),
                "comfort_setpoint_max": _temp_param("---"),
            },
            DEFAULT_MIN_TEMP,
            DEFAULT_MAX_TEMP,
            id="all_sources_inactive",
        ),
    ],
)
async def test_climate_min_max_temperature(
    hass: HomeAssistant,
    mock_bsblan: AsyncMock,
    mock_config_entry: MockConfigEntry,
    static_data: dict[str, Any],
    expected_min: float,
    expected_max: float,
) -> None:
    """Test min/max temperature bounds resolved from per-circuit static values."""
    mock_bsblan.static_values.return_value = StaticState.model_validate(static_data)

    await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])

    state = hass.states.get(ENTITY_ID)
    assert state is not None
    assert state.attributes["min_temp"] == expected_min
    assert state.attributes["max_temp"] == expected_max


async def test_celsius_fahrenheit(
    hass: HomeAssistant,
    mock_bsblan: AsyncMock,
    mock_config_entry: MockConfigEntry,
    snapshot: SnapshotAssertion,
    entity_registry: er.EntityRegistry,
) -> None:
    """Test Celsius and Fahrenheit temperature units."""
    await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])

    await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)


async def test_climate_entity_loads_without_static_values(
    hass: HomeAssistant,
    mock_bsblan: AsyncMock,
    mock_config_entry: MockConfigEntry,
) -> None:
    """Test the climate entity still loads when static values are unavailable."""
    mock_bsblan.static_values.side_effect = BSBLANError("General error")

    await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])

    state = hass.states.get(ENTITY_ID)
    assert state is not None
    assert state.attributes["current_temperature"] is not None


async def test_climate_entity_properties(
    hass: HomeAssistant,
    mock_bsblan: AsyncMock,
    mock_config_entry: MockConfigEntry,
    snapshot: SnapshotAssertion,
    entity_registry: er.EntityRegistry,
    freezer: FrozenDateTimeFactory,
) -> None:
    """Test the climate entity properties."""
    await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])
    await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)

    # Test target_temperature
    mock_target_temp = MagicMock()
    mock_target_temp.value = 23.5
    mock_bsblan.state.return_value.target_temperature = mock_target_temp

    freezer.tick(timedelta(minutes=1))
    async_fire_time_changed(hass)
    await hass.async_block_till_done()

    state = hass.states.get(ENTITY_ID)
    assert state.attributes["temperature"] == 23.5

    # Test hvac_mode - BSB-LAN returns integer: 1=auto
    mock_hvac_mode = MagicMock()
    mock_hvac_mode.value = 1  # auto mode
    mock_bsblan.state.return_value.hvac_mode = mock_hvac_mode

    freezer.tick(timedelta(minutes=1))
    async_fire_time_changed(hass)
    await hass.async_block_till_done()

    state = hass.states.get(ENTITY_ID)
    assert state.state == HVACMode.AUTO

    # Test preset_mode - BSB-LAN mode 2 is eco/reduced
    mock_hvac_mode.value = 2  # eco mode

    freezer.tick(timedelta(minutes=1))
    async_fire_time_changed(hass)
    await hass.async_block_till_done()

    state = hass.states.get(ENTITY_ID)
    assert state.attributes["preset_mode"] == PRESET_ECO

    # Test hvac_action mapping
    mock_hvac_action = MagicMock()
    mock_hvac_action.value = HeatingCircuitStatus.COOLING_ACTIVE
    mock_bsblan.state.return_value.hvac_action = mock_hvac_action

    freezer.tick(timedelta(minutes=1))
    async_fire_time_changed(hass)
    await hass.async_block_till_done()

    state = hass.states.get(ENTITY_ID)
    assert state.attributes["hvac_action"] == HVACAction.COOLING


async def _async_set_hvac_action(
    hass: HomeAssistant,
    mock_bsblan: AsyncMock,
    freezer: FrozenDateTimeFactory,
    value: MagicMock | None,
) -> HVACAction | None:
    """Helper to push a new hvac_action value through coordinator."""
    mock_bsblan.state.return_value.hvac_action = value
    freezer.tick(timedelta(minutes=1))
    async_fire_time_changed(hass)
    await hass.async_block_till_done()
    state = hass.states.get(ENTITY_ID)
    return state.attributes.get("hvac_action")


async def test_hvac_action_handles_none_inputs(
    hass: HomeAssistant,
    mock_bsblan: AsyncMock,
    mock_config_entry: MockConfigEntry,
    freezer: FrozenDateTimeFactory,
) -> None:
    """Ensure hvac_action gracefully handles None values."""
    await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])

    assert await _async_set_hvac_action(hass, mock_bsblan, freezer, None) is None

    mock_action = MagicMock()
    mock_action.value = None
    assert await _async_set_hvac_action(hass, mock_bsblan, freezer, mock_action) is None


async def test_hvac_action_uses_library_mapping(
    hass: HomeAssistant,
    mock_bsblan: AsyncMock,
    mock_config_entry: MockConfigEntry,
    freezer: FrozenDateTimeFactory,
) -> None:
    """Verify hvac_action correctly uses library's status code mapping."""
    await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])

    # Test a known status code is converted to action
    mock_action = MagicMock()
    mock_action.value = HeatingCircuitStatus.HEATING_COMFORT
    result = await _async_set_hvac_action(hass, mock_bsblan, freezer, mock_action)
    assert result == HVACAction.HEATING

    # Test unknown status code defaults to IDLE
    mock_action.value = 1  # Unknown status code
    result = await _async_set_hvac_action(hass, mock_bsblan, freezer, mock_action)
    assert result == HVACAction.IDLE


async def test_climate_without_current_temperature_sensor(
    hass: HomeAssistant,
    mock_bsblan: AsyncMock,
    mock_config_entry: MockConfigEntry,
    freezer: FrozenDateTimeFactory,
) -> None:
    """Test climate entity when current temperature sensor is not available."""
    await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])

    # Set current_temperature to None to simulate no temperature sensor
    mock_bsblan.state.return_value.current_temperature = None

    freezer.tick(timedelta(minutes=1))
    async_fire_time_changed(hass)
    await hass.async_block_till_done()

    # Should not crash and current_temperature should be None in attributes
    state = hass.states.get(ENTITY_ID)
    assert state is not None
    assert state.attributes["current_temperature"] is None


async def test_climate_without_target_temperature_sensor(
    hass: HomeAssistant,
    mock_bsblan: AsyncMock,
    mock_config_entry: MockConfigEntry,
    freezer: FrozenDateTimeFactory,
) -> None:
    """Test climate entity when target temperature sensor is not available."""
    await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])

    # Set target_temperature to None to simulate no temperature sensor
    mock_bsblan.state.return_value.target_temperature = None

    freezer.tick(timedelta(minutes=1))
    async_fire_time_changed(hass)
    await hass.async_block_till_done()

    # Should not crash and target temperature should be None in attributes
    state = hass.states.get(ENTITY_ID)
    assert state is not None
    assert state.attributes["temperature"] is None


async def test_climate_hvac_mode_object_none(
    hass: HomeAssistant,
    mock_bsblan: AsyncMock,
    mock_config_entry: MockConfigEntry,
    freezer: FrozenDateTimeFactory,
) -> None:
    """Test climate entity when hvac_mode object itself is None."""
    await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])

    # Set hvac_mode to None (the object itself, not just the value)
    mock_bsblan.state.return_value.hvac_mode = None

    freezer.tick(timedelta(minutes=1))
    async_fire_time_changed(hass)
    await hass.async_block_till_done()

    # State should be unknown when hvac_mode object is None
    state = hass.states.get(ENTITY_ID)
    assert state is not None
    assert state.state == "unknown"
    # preset_mode should be "none" when hvac_mode object is None
    assert state.attributes["preset_mode"] == PRESET_NONE


# Mapping from HA HVACMode to BSB-Lan integer values for test assertions
HA_TO_BSBLAN_HVAC_MODE_TEST: dict[HVACMode, int] = {
    HVACMode.OFF: 0,
    HVACMode.AUTO: 1,
    HVACMode.HEAT: 3,
}


@pytest.mark.parametrize(
    "mode",
    [HVACMode.HEAT, HVACMode.AUTO, HVACMode.OFF],
)
async def test_async_set_hvac_mode(
    hass: HomeAssistant,
    mock_bsblan: AsyncMock,
    mock_config_entry: MockConfigEntry,
    mode: HVACMode,
) -> None:
    """Test setting HVAC mode via service call."""
    await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])

    # Call the service to set HVAC mode
    await hass.services.async_call(
        domain=CLIMATE_DOMAIN,
        service=SERVICE_SET_HVAC_MODE,
        service_data={ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: mode},
        blocking=True,
    )

    # Assert that the thermostat method was called with integer value
    expected_int = HA_TO_BSBLAN_HVAC_MODE_TEST[mode]
    mock_bsblan.thermostat.assert_called_once_with(hvac_mode=expected_int, circuit=1)
    mock_bsblan.thermostat.reset_mock()


@pytest.mark.parametrize(
    ("hvac_mode_int", "preset_mode"),
    [
        (1, PRESET_ECO),  # 1 = auto mode
        (1, PRESET_NONE),  # 1 = auto mode
        (3, PRESET_ECO),  # 3 = heat mode - can also set eco preset
        (0, PRESET_ECO),  # 0 = off mode - can also set eco preset
    ],
)
async def test_async_set_preset_mode_success(
    hass: HomeAssistant,
    mock_bsblan: AsyncMock,
    mock_config_entry: MockConfigEntry,
    hvac_mode_int: int,
    preset_mode: str,
) -> None:
    """Test setting preset mode via service call."""
    await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])

    # patch hvac_mode with integer value (BSB-LAN returns integers)
    mock_hvac_mode = MagicMock()
    mock_hvac_mode.value = hvac_mode_int
    mock_bsblan.state.return_value.hvac_mode = mock_hvac_mode

    # Attempt to set the preset mode
    await hass.services.async_call(
        CLIMATE_DOMAIN,
        SERVICE_SET_PRESET_MODE,
        {ATTR_ENTITY_ID: ENTITY_ID, ATTR_PRESET_MODE: preset_mode},
        blocking=True,
    )
    await hass.async_block_till_done()


@pytest.mark.parametrize(
    ("target_temp"),
    [
        (8.0),  # Min temperature
        (15.0),  # Mid-range temperature
        (20.0),  # Max temperature
    ],
)
async def test_async_set_temperature(
    hass: HomeAssistant,
    mock_bsblan: AsyncMock,
    mock_config_entry: MockConfigEntry,
    target_temp: float,
) -> None:
    """Test setting temperature via service call."""
    await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])

    await hass.services.async_call(
        domain=CLIMATE_DOMAIN,
        service=SERVICE_SET_TEMPERATURE,
        service_data={ATTR_ENTITY_ID: ENTITY_ID, ATTR_TEMPERATURE: target_temp},
        blocking=True,
    )
    # Assert that the thermostat method was called with the correct temperature
    mock_bsblan.thermostat.assert_called_once_with(
        target_temperature=target_temp, circuit=1
    )


async def test_async_set_data(
    hass: HomeAssistant,
    mock_bsblan: AsyncMock,
    mock_config_entry: MockConfigEntry,
) -> None:
    """Test setting data via service calls."""
    await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])

    # Test setting temperature
    await hass.services.async_call(
        CLIMATE_DOMAIN,
        SERVICE_SET_TEMPERATURE,
        {ATTR_ENTITY_ID: ENTITY_ID, ATTR_TEMPERATURE: 19},
        blocking=True,
    )
    mock_bsblan.thermostat.assert_called_once_with(target_temperature=19, circuit=1)
    mock_bsblan.thermostat.reset_mock()

    # Test setting HVAC mode - should convert to integer (3=heat)
    await hass.services.async_call(
        CLIMATE_DOMAIN,
        SERVICE_SET_HVAC_MODE,
        {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVACMode.HEAT},
        blocking=True,
    )
    mock_bsblan.thermostat.assert_called_once_with(hvac_mode=3, circuit=1)  # 3 = heat
    mock_bsblan.thermostat.reset_mock()

    # Patch HVAC mode to AUTO (integer 1)
    mock_hvac_mode = MagicMock()
    mock_hvac_mode.value = 1  # 1 = auto mode
    mock_bsblan.state.return_value.hvac_mode = mock_hvac_mode

    # Test setting preset mode to ECO - should use integer 2
    await hass.services.async_call(
        CLIMATE_DOMAIN,
        SERVICE_SET_PRESET_MODE,
        {ATTR_ENTITY_ID: ENTITY_ID, ATTR_PRESET_MODE: PRESET_ECO},
        blocking=True,
    )
    mock_bsblan.thermostat.assert_called_once_with(
        hvac_mode=2, circuit=1
    )  # 2 = eco/reduced
    mock_bsblan.thermostat.reset_mock()

    # Test setting preset mode to NONE - should use integer 1 (auto)
    await hass.services.async_call(
        CLIMATE_DOMAIN,
        SERVICE_SET_PRESET_MODE,
        {ATTR_ENTITY_ID: ENTITY_ID, ATTR_PRESET_MODE: PRESET_NONE},
        blocking=True,
    )
    mock_bsblan.thermostat.assert_called_once_with(hvac_mode=1, circuit=1)  # 1 = auto
    mock_bsblan.thermostat.reset_mock()

    # Test error handling
    mock_bsblan.thermostat.side_effect = BSBLANError("Test error")
    with pytest.raises(HomeAssistantError) as exc:
        await hass.services.async_call(
            CLIMATE_DOMAIN,
            SERVICE_SET_TEMPERATURE,
            {ATTR_ENTITY_ID: ENTITY_ID, ATTR_TEMPERATURE: 20},
            blocking=True,
        )
    assert exc.value.translation_domain == DOMAIN
    assert exc.value.translation_key == "set_data_error"


async def test_dual_circuit_climate_entities(
    hass: HomeAssistant,
    mock_bsblan: AsyncMock,
    mock_config_entry_dual_circuit: MockConfigEntry,
) -> None:
    """Test that dual-circuit config creates two climate entities with correct IDs."""
    await setup_with_selected_platforms(
        hass, mock_config_entry_dual_circuit, [Platform.CLIMATE]
    )

    # Circuit 1 entity should exist
    state1 = hass.states.get("climate.heating_circuit_1")
    assert state1 is not None

    # Circuit 2 entity should exist
    state2 = hass.states.get("climate.heating_circuit_2")
    assert state2 is not None
