"""Tests for the BSB-LAN water heater platform."""

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

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

from homeassistant.components.water_heater import (
    ATTR_OPERATION_MODE,
    DOMAIN as WATER_HEATER_DOMAIN,
    SERVICE_SET_OPERATION_MODE,
    SERVICE_SET_TEMPERATURE,
    SERVICE_TURN_OFF,
    SERVICE_TURN_ON,
    STATE_ECO,
    STATE_OFF,
    STATE_PERFORMANCE,
)
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 = "water_heater.water_heater"


@pytest.fixture
def mock_dhw_config_none(mock_bsblan: AsyncMock) -> None:
    """Mock coordinator to return None for dhw_config."""
    mock_bsblan.hot_water_config.return_value = None


@pytest.fixture
def mock_dhw_config_missing_attributes(mock_bsblan: AsyncMock) -> None:
    """Mock config without the temperature limit attributes."""
    mock_config = MagicMock()
    mock_config.reduced_setpoint = None
    mock_config.nominal_setpoint_max = None
    mock_bsblan.hot_water_config.return_value = mock_config


@pytest.fixture
def mock_dhw_config_none_values(mock_bsblan: AsyncMock) -> None:
    """Mock config with temperature setpoint objects where value is None."""
    mock_config = MagicMock()
    mock_reduced_setpoint = MagicMock()
    mock_reduced_setpoint.value = None
    mock_nominal_setpoint_max = MagicMock()
    mock_nominal_setpoint_max.value = None
    mock_config.reduced_setpoint = mock_reduced_setpoint
    mock_config.nominal_setpoint_max = mock_nominal_setpoint_max
    mock_bsblan.hot_water_config.return_value = mock_config


@pytest.mark.parametrize(
    ("dhw_file"),
    [
        ("dhw_state.json"),
    ],
)
async def test_water_heater_states(
    hass: HomeAssistant,
    mock_bsblan: AsyncMock,
    mock_config_entry: MockConfigEntry,
    snapshot: SnapshotAssertion,
    entity_registry: er.EntityRegistry,
    dhw_file: str,
) -> None:
    """Test water heater states with different configurations."""
    await setup_with_selected_platforms(
        hass, mock_config_entry, [Platform.WATER_HEATER]
    )
    await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)


async def test_water_heater_no_dhw_capability(
    hass: HomeAssistant,
    mock_bsblan: AsyncMock,
    mock_config_entry: MockConfigEntry,
    entity_registry: er.EntityRegistry,
) -> None:
    """Test that no water heater entity is created when DHW capability is missing."""
    # Mock DHW data to simulate no water heater capability
    mock_bsblan.hot_water_state.return_value.operating_mode = None
    mock_bsblan.hot_water_state.return_value.nominal_setpoint = None
    mock_bsblan.hot_water_state.return_value.dhw_actual_value_top_temperature = None

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

    # Verify no water heater entity was created
    entities = er.async_entries_for_config_entry(
        entity_registry, mock_config_entry.entry_id
    )
    water_heater_entities = [
        entity for entity in entities if entity.domain == Platform.WATER_HEATER
    ]

    assert len(water_heater_entities) == 0


async def test_water_heater_entity_properties(
    hass: HomeAssistant,
    mock_bsblan: AsyncMock,
    mock_config_entry: MockConfigEntry,
    freezer: FrozenDateTimeFactory,
) -> None:
    """Test the water heater entity properties."""
    await setup_with_selected_platforms(
        hass, mock_config_entry, [Platform.WATER_HEATER]
    )

    state = hass.states.get(ENTITY_ID)
    assert state is not None

    # Test when nominal setpoint is "10"
    mock_setpoint = MagicMock()
    mock_setpoint.value = 10
    mock_bsblan.hot_water_state.return_value.nominal_setpoint = mock_setpoint

    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.get("temperature") == 10


@pytest.mark.parametrize(
    ("mode", "bsblan_mode"),
    [
        (STATE_ECO, "2"),  # Eco maps to numeric value 2
        (STATE_OFF, "0"),  # Off maps to numeric value 0
        (STATE_PERFORMANCE, "1"),  # Performance/comfort maps to numeric value 1
    ],
)
async def test_set_operation_mode(
    hass: HomeAssistant,
    mock_bsblan: AsyncMock,
    mock_config_entry: MockConfigEntry,
    mode: str,
    bsblan_mode: str,
) -> None:
    """Test setting operation mode."""
    await setup_with_selected_platforms(
        hass, mock_config_entry, [Platform.WATER_HEATER]
    )

    await hass.services.async_call(
        domain=WATER_HEATER_DOMAIN,
        service=SERVICE_SET_OPERATION_MODE,
        service_data={
            ATTR_ENTITY_ID: ENTITY_ID,
            ATTR_OPERATION_MODE: mode,
        },
        blocking=True,
    )

    mock_bsblan.set_hot_water.assert_called_once_with(
        SetHotWaterParam(operating_mode=bsblan_mode)
    )


async def test_set_invalid_operation_mode(
    hass: HomeAssistant,
    mock_bsblan: AsyncMock,
    mock_config_entry: MockConfigEntry,
) -> None:
    """Test setting invalid operation mode."""
    await setup_with_selected_platforms(
        hass, mock_config_entry, [Platform.WATER_HEATER]
    )

    with pytest.raises(
        HomeAssistantError,
        match=(
            r"Operation mode invalid_mode is not valid for"
            r" water_heater\.water_heater\."
            r" Valid operation modes are: off, performance, eco"
        ),
    ):
        await hass.services.async_call(
            domain=WATER_HEATER_DOMAIN,
            service=SERVICE_SET_OPERATION_MODE,
            service_data={
                ATTR_ENTITY_ID: ENTITY_ID,
                ATTR_OPERATION_MODE: "invalid_mode",
            },
            blocking=True,
        )


async def test_set_temperature(
    hass: HomeAssistant,
    mock_bsblan: AsyncMock,
    mock_config_entry: MockConfigEntry,
) -> None:
    """Test setting temperature."""
    await setup_with_selected_platforms(
        hass, mock_config_entry, [Platform.WATER_HEATER]
    )

    await hass.services.async_call(
        domain=WATER_HEATER_DOMAIN,
        service=SERVICE_SET_TEMPERATURE,
        service_data={
            ATTR_ENTITY_ID: ENTITY_ID,
            ATTR_TEMPERATURE: 50,
        },
        blocking=True,
    )

    mock_bsblan.set_hot_water.assert_called_once_with(
        SetHotWaterParam(nominal_setpoint=50)
    )


async def test_set_temperature_failure(
    hass: HomeAssistant,
    mock_bsblan: AsyncMock,
    mock_config_entry: MockConfigEntry,
) -> None:
    """Test setting temperature with API failure."""
    await setup_with_selected_platforms(
        hass, mock_config_entry, [Platform.WATER_HEATER]
    )

    mock_bsblan.set_hot_water.side_effect = BSBLANError("Test error")

    with pytest.raises(
        HomeAssistantError, match="An error occurred while setting the temperature"
    ):
        await hass.services.async_call(
            domain=WATER_HEATER_DOMAIN,
            service=SERVICE_SET_TEMPERATURE,
            service_data={
                ATTR_ENTITY_ID: ENTITY_ID,
                ATTR_TEMPERATURE: 50,
            },
            blocking=True,
        )


async def test_operation_mode_error(
    hass: HomeAssistant,
    mock_bsblan: AsyncMock,
    mock_config_entry: MockConfigEntry,
) -> None:
    """Test operation mode setting with API failure."""
    await setup_with_selected_platforms(
        hass, mock_config_entry, [Platform.WATER_HEATER]
    )

    mock_bsblan.set_hot_water.side_effect = BSBLANError("Test error")

    with pytest.raises(
        HomeAssistantError, match="An error occurred while setting the operation mode"
    ):
        await hass.services.async_call(
            domain=WATER_HEATER_DOMAIN,
            service=SERVICE_SET_OPERATION_MODE,
            service_data={
                ATTR_ENTITY_ID: ENTITY_ID,
                ATTR_OPERATION_MODE: STATE_ECO,
            },
            blocking=True,
        )


async def test_water_heater_no_sensors(
    hass: HomeAssistant,
    mock_bsblan: AsyncMock,
    mock_config_entry: MockConfigEntry,
    freezer: FrozenDateTimeFactory,
) -> None:
    """Test water heater when sensors are not available."""
    await setup_with_selected_platforms(
        hass, mock_config_entry, [Platform.WATER_HEATER]
    )

    # Set all sensors to None to simulate missing sensors
    mock_bsblan.hot_water_state.return_value.operating_mode = None
    mock_bsblan.hot_water_state.return_value.dhw_actual_value_top_temperature = None
    mock_bsblan.hot_water_state.return_value.nominal_setpoint = None

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

    # Should not crash and properties should return None
    state = hass.states.get(ENTITY_ID)
    assert state is not None
    assert state.attributes.get("current_operation") is None
    assert state.attributes.get("current_temperature") is None
    assert state.attributes.get("temperature") is None


async def test_current_operation_none_value(
    hass: HomeAssistant,
    mock_bsblan: AsyncMock,
    mock_config_entry: MockConfigEntry,
    freezer: FrozenDateTimeFactory,
) -> None:
    """Test current_operation returns None when operating_mode value is None."""
    await setup_with_selected_platforms(
        hass, mock_config_entry, [Platform.WATER_HEATER]
    )

    mock_operating_mode = MagicMock()
    mock_operating_mode.value = None
    mock_bsblan.hot_water_state.return_value.operating_mode = mock_operating_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 is not None
    assert state.attributes.get("current_operation") is None


@pytest.mark.parametrize(
    ("fixture_name", "test_description"),
    [
        ("mock_dhw_config_none", "no DHW config"),
        (
            "mock_dhw_config_missing_attributes",
            "DHW config with missing temperature attributes",
        ),
        (
            "mock_dhw_config_none_values",
            "DHW config with None temperature values",
        ),
    ],
)
async def test_water_heater_default_temperature_limits(
    hass: HomeAssistant,
    mock_bsblan: AsyncMock,
    mock_config_entry: MockConfigEntry,
    fixture_name: str,
    test_description: str,
    request: pytest.FixtureRequest,
) -> None:
    """Test water heater uses default temperature limits when config is unavailable."""
    # Apply the fixture dynamically
    request.getfixturevalue(fixture_name)

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

    state = hass.states.get(ENTITY_ID)
    assert state is not None

    # Should use default temperature limits when config is not available
    assert state.attributes.get("min_temp") == 10.0  # Default minimum
    assert state.attributes.get("max_temp") == 65.0  # Default maximum


async def test_water_heater_custom_temperature_limits_from_config(
    hass: HomeAssistant,
    mock_bsblan: AsyncMock,
    mock_config_entry: MockConfigEntry,
) -> None:
    """Test water heater uses custom temperature limits from DHW config."""
    # Set custom temperature limit values directly on the mock
    mock_bsblan.hot_water_config.return_value.reduced_setpoint.value = 15.0
    mock_bsblan.hot_water_config.return_value.nominal_setpoint_max.value = 75.0

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

    state = hass.states.get(ENTITY_ID)
    assert state is not None

    # Should use custom temperature limits from config
    assert (
        state.attributes.get("min_temp") == 15.0
    )  # Custom minimum from reduced_setpoint
    assert (
        state.attributes.get("max_temp") == 75.0
    )  # Custom maximum from nominal_setpoint_max


async def test_turn_on(
    hass: HomeAssistant,
    mock_bsblan: AsyncMock,
    mock_config_entry: MockConfigEntry,
) -> None:
    """Test turning on the water heater."""
    await setup_with_selected_platforms(
        hass, mock_config_entry, [Platform.WATER_HEATER]
    )

    await hass.services.async_call(
        domain=WATER_HEATER_DOMAIN,
        service=SERVICE_TURN_ON,
        service_data={
            ATTR_ENTITY_ID: ENTITY_ID,
        },
        blocking=True,
    )

    mock_bsblan.set_hot_water.assert_called_once_with(
        SetHotWaterParam(
            operating_mode="1"
        )  # Performance/comfort maps to numeric value 1
    )


async def test_turn_off(
    hass: HomeAssistant,
    mock_bsblan: AsyncMock,
    mock_config_entry: MockConfigEntry,
) -> None:
    """Test turning off the water heater."""
    await setup_with_selected_platforms(
        hass, mock_config_entry, [Platform.WATER_HEATER]
    )

    await hass.services.async_call(
        domain=WATER_HEATER_DOMAIN,
        service=SERVICE_TURN_OFF,
        service_data={
            ATTR_ENTITY_ID: ENTITY_ID,
        },
        blocking=True,
    )

    mock_bsblan.set_hot_water.assert_called_once_with(
        SetHotWaterParam(operating_mode="0")  # Off maps to numeric value 0
    )


async def test_turn_on_error(
    hass: HomeAssistant,
    mock_bsblan: AsyncMock,
    mock_config_entry: MockConfigEntry,
) -> None:
    """Test turning on the water heater with API failure."""
    await setup_with_selected_platforms(
        hass, mock_config_entry, [Platform.WATER_HEATER]
    )

    mock_bsblan.set_hot_water.side_effect = BSBLANError("Test error")

    with pytest.raises(
        HomeAssistantError, match="An error occurred while setting the operation mode"
    ):
        await hass.services.async_call(
            domain=WATER_HEATER_DOMAIN,
            service=SERVICE_TURN_ON,
            service_data={
                ATTR_ENTITY_ID: ENTITY_ID,
            },
            blocking=True,
        )


async def test_turn_off_error(
    hass: HomeAssistant,
    mock_bsblan: AsyncMock,
    mock_config_entry: MockConfigEntry,
) -> None:
    """Test turning off the water heater with API failure."""
    await setup_with_selected_platforms(
        hass, mock_config_entry, [Platform.WATER_HEATER]
    )

    mock_bsblan.set_hot_water.side_effect = BSBLANError("Test error")

    with pytest.raises(
        HomeAssistantError, match="An error occurred while setting the operation mode"
    ):
        await hass.services.async_call(
            domain=WATER_HEATER_DOMAIN,
            service=SERVICE_TURN_OFF,
            service_data={
                ATTR_ENTITY_ID: ENTITY_ID,
            },
            blocking=True,
        )
