"""The tests for the Template number platform."""

from typing import Any

import pytest
from syrupy.assertion import SnapshotAssertion

from homeassistant.components import number
from homeassistant.components.number import (
    ATTR_MAX,
    ATTR_MIN,
    ATTR_STEP,
    ATTR_VALUE as NUMBER_ATTR_VALUE,
    DOMAIN as NUMBER_DOMAIN,
    SERVICE_SET_VALUE as NUMBER_SERVICE_SET_VALUE,
)
from homeassistant.components.template import DOMAIN
from homeassistant.components.template.const import CONF_PICTURE
from homeassistant.const import (
    ATTR_ENTITY_ID,
    ATTR_ENTITY_PICTURE,
    ATTR_ICON,
    CONF_ENTITY_ID,
    CONF_ICON,
    CONF_UNIT_OF_MEASUREMENT,
    STATE_UNAVAILABLE,
    STATE_UNKNOWN,
)
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.typing import ConfigType

from .conftest import (
    ConfigurationStyle,
    TemplatePlatformSetup,
    assert_action,
    assert_state_and_attributes,
    async_get_flow_preview_state,
    async_trigger,
    make_test_action,
    make_test_trigger,
    setup_and_test_nested_unique_id,
    setup_and_test_unique_id,
    setup_entity,
    setup_mock_template_entity_restore_state,
    setup_restore_template_entity,
)

from tests.common import MockConfigEntry
from tests.typing import WebSocketGenerator

TEST_AVAILABILITY_ENTITY_ID = "binary_sensor.test_availability"
TEST_MAXIMUM_ENTITY_ID = "sensor.maximum"
TEST_MINIMUM_ENTITY_ID = "sensor.minimum"
TEST_STATE_ENTITY_ID = "number.test_state"
TEST_STEP_ENTITY_ID = "sensor.step"
TEST_NUMBER = TemplatePlatformSetup(
    number.DOMAIN,
    "template_number",
    make_test_trigger(
        TEST_AVAILABILITY_ENTITY_ID,
        TEST_MAXIMUM_ENTITY_ID,
        TEST_MINIMUM_ENTITY_ID,
        TEST_STATE_ENTITY_ID,
        TEST_STEP_ENTITY_ID,
    ),
)
TEST_SET_VALUE_ACTION = make_test_action("set_value", {"value": "{{ value }}"})
TEST_REQUIRED = {"state": "0", "step": "1", "set_value": []}


@pytest.fixture
async def setup_number(
    hass: HomeAssistant,
    count: int,
    style: ConfigurationStyle,
    config: dict[str, Any],
) -> None:
    """Do setup of number integration."""
    await setup_entity(hass, TEST_NUMBER, style, count, config)


async def test_setup_config_entry(
    hass: HomeAssistant,
    snapshot: SnapshotAssertion,
) -> None:
    """Test the config flow."""

    template_config_entry = MockConfigEntry(
        data={},
        domain=DOMAIN,
        options={
            "name": "My template",
            "template_type": "number",
            "state": "{{ 10 }}",
            "min": 0,
            "max": 100,
            "step": 0.1,
            "set_value": {
                "action": "input_number.set_value",
                "target": {"entity_id": "input_number.test"},
                "data": {"value": "{{ value }}"},
            },
        },
        title="My template",
    )
    template_config_entry.add_to_hass(hass)

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

    state = hass.states.get("number.my_template")
    assert state is not None
    assert state == snapshot


@pytest.mark.parametrize(
    ("count", "config"),
    [
        (
            1,
            {
                "state": "{{ 4 }}",
                "set_value": {"service": "script.set_value"},
                "step": "{{ 1 }}",
            },
        )
    ],
)
@pytest.mark.parametrize(
    "style",
    [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.usefixtures("setup_number")
async def test_missing_optional_config(hass: HomeAssistant) -> None:
    """Test: missing optional template is ok."""
    await async_trigger(hass, TEST_STATE_ENTITY_ID, "anything")
    _verify(hass, 4, 1, 0.0, 100.0, None)


@pytest.mark.parametrize(
    ("count", "config"),
    [
        (
            0,
            {
                "state": "{{ 4 }}",
            },
        )
    ],
)
@pytest.mark.parametrize(
    "style",
    [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.usefixtures("setup_number")
async def test_missing_required_keys(hass: HomeAssistant) -> None:
    """Test: missing required fields will fail."""
    assert hass.states.async_all("number") == []


@pytest.mark.parametrize(
    ("count", "config"),
    [
        (
            1,
            {
                "state": "{{ 4 }}",
                "set_value": {"service": "script.set_value"},
                "min": "{{ 3 }}",
                "max": "{{ 5 }}",
                "step": "{{ 1 }}",
                "unit_of_measurement": "beer",
            },
        )
    ],
)
@pytest.mark.parametrize(
    "style",
    [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.usefixtures("setup_number")
async def test_all_optional_config(hass: HomeAssistant) -> None:
    """Test: including all optional templates is ok."""
    await async_trigger(hass, TEST_STATE_ENTITY_ID, "anything")
    _verify(hass, 4, 1, 3, 5, "beer")


@pytest.mark.parametrize(
    ("count", "config"),
    [
        (
            1,
            {
                "state": f"{{{{ states('{TEST_STATE_ENTITY_ID}') | float(1.0) }}}}",
                "step": f"{{{{ states('{TEST_STEP_ENTITY_ID}') | float(5.0) }}}}",
                "min": f"{{{{ states('{TEST_MINIMUM_ENTITY_ID}') | float(0.0) }}}}",
                "max": f"{{{{ states('{TEST_MAXIMUM_ENTITY_ID}') | float(100.0) }}}}",
                **TEST_SET_VALUE_ACTION,
            },
        )
    ],
)
@pytest.mark.parametrize(
    "style",
    [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.usefixtures("setup_number")
async def test_template_number(
    hass: HomeAssistant, entity_registry: er.EntityRegistry, calls: list[ServiceCall]
) -> None:
    """Test templates with values from other entities."""
    await async_trigger(hass, TEST_STATE_ENTITY_ID, 4)
    await async_trigger(hass, TEST_STEP_ENTITY_ID, 1)
    await async_trigger(hass, TEST_MINIMUM_ENTITY_ID, 3)
    await async_trigger(hass, TEST_MAXIMUM_ENTITY_ID, 5)
    _verify(hass, 4, 1, 3, 5, None)

    await async_trigger(hass, TEST_STATE_ENTITY_ID, 5)
    _verify(hass, 5, 1, 3, 5, None)

    await async_trigger(hass, TEST_STEP_ENTITY_ID, 2)
    _verify(hass, 5, 2, 3, 5, None)

    await async_trigger(hass, TEST_MINIMUM_ENTITY_ID, 2)
    _verify(hass, 5, 2, 2, 5, None)

    await async_trigger(hass, TEST_MAXIMUM_ENTITY_ID, 6)
    _verify(hass, 5, 2, 2, 6, None)

    await hass.services.async_call(
        NUMBER_DOMAIN,
        NUMBER_SERVICE_SET_VALUE,
        {CONF_ENTITY_ID: TEST_NUMBER.entity_id, NUMBER_ATTR_VALUE: 2},
        blocking=True,
    )

    assert_action(TEST_NUMBER, calls, 1, "set_value", value=2)

    await async_trigger(hass, TEST_STATE_ENTITY_ID, 2)
    _verify(hass, 2, 2, 2, 6, None)


def _verify(
    hass: HomeAssistant,
    expected_value: int,
    expected_step: int,
    expected_minimum: int,
    expected_maximum: int,
    expected_unit_of_measurement: str | None,
) -> None:
    """Verify number's state."""
    state = hass.states.get(TEST_NUMBER.entity_id)
    attributes = state.attributes
    assert state.state == str(float(expected_value))
    assert attributes.get(ATTR_STEP) == float(expected_step)
    assert attributes.get(ATTR_MAX) == float(expected_maximum)
    assert attributes.get(ATTR_MIN) == float(expected_minimum)
    assert attributes.get(CONF_UNIT_OF_MEASUREMENT) == expected_unit_of_measurement


@pytest.mark.parametrize("count", [1])
@pytest.mark.parametrize(
    ("style", "initial_expected_state"),
    [(ConfigurationStyle.MODERN, ""), (ConfigurationStyle.TRIGGER, None)],
)
@pytest.mark.parametrize(
    ("config", "attribute", "expected"),
    [
        (
            {
                CONF_ICON: (
                    "{% if states.number.test_state.state == '1' %}mdi:check{% endif %}"
                ),
                **TEST_REQUIRED,
            },
            ATTR_ICON,
            "mdi:check",
        ),
        (
            {
                CONF_PICTURE: (
                    "{% if states.number.test_state.state == '1' %}check.jpg{% endif %}"
                ),
                **TEST_REQUIRED,
            },
            ATTR_ENTITY_PICTURE,
            "check.jpg",
        ),
    ],
)
@pytest.mark.usefixtures("setup_number")
async def test_templated_optional_config(
    hass: HomeAssistant,
    attribute: str,
    expected: str,
    initial_expected_state: str | None,
) -> None:
    """Test optional config templates."""
    state = hass.states.get(TEST_NUMBER.entity_id)
    assert state.attributes.get(attribute) == initial_expected_state

    await async_trigger(hass, TEST_STATE_ENTITY_ID, "1")

    state = hass.states.get(TEST_NUMBER.entity_id)

    assert state.attributes[attribute] == expected


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

    device_config_entry = MockConfigEntry()
    device_config_entry.add_to_hass(hass)
    device_entry = device_registry.async_get_or_create(
        config_entry_id=device_config_entry.entry_id,
        identifiers={("test", "identifier_test")},
        connections={("mac", "30:31:32:33:34:35")},
    )
    await hass.async_block_till_done()
    assert device_entry is not None
    assert device_entry.id is not None

    template_config_entry = MockConfigEntry(
        data={},
        domain=DOMAIN,
        options={
            "name": "My template",
            "template_type": "number",
            "state": "{{ 10 }}",
            "min": 0,
            "max": 100,
            "step": 0.1,
            "set_value": {
                "action": "input_number.set_value",
                "target": {"entity_id": "input_number.test"},
                "data": {"value": "{{ value }}"},
            },
            "device_id": device_entry.id,
        },
        title="My template",
    )
    template_config_entry.add_to_hass(hass)

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

    template_entity = entity_registry.async_get("number.my_template")
    assert template_entity is not None
    assert template_entity.device_id == device_entry.id


@pytest.mark.parametrize(
    ("count", "config"),
    [
        (
            1,
            {
                "set_value": [],
            },
        )
    ],
)
@pytest.mark.parametrize(
    "style",
    [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.usefixtures("setup_number")
async def test_optimistic(hass: HomeAssistant) -> None:
    """Test configuration with optimistic state."""
    await hass.services.async_call(
        number.DOMAIN,
        number.SERVICE_SET_VALUE,
        {ATTR_ENTITY_ID: TEST_NUMBER.entity_id, "value": 4},
        blocking=True,
    )

    state = hass.states.get(TEST_NUMBER.entity_id)
    assert float(state.state) == 4

    await hass.services.async_call(
        number.DOMAIN,
        number.SERVICE_SET_VALUE,
        {ATTR_ENTITY_ID: TEST_NUMBER.entity_id, "value": 2},
        blocking=True,
    )

    state = hass.states.get(TEST_NUMBER.entity_id)
    assert float(state.state) == 2


@pytest.mark.parametrize(
    ("count", "config"),
    [
        (
            1,
            {
                "state": "{{ states('sensor.test_state') }}",
                "optimistic": False,
                "set_value": [],
            },
        )
    ],
)
@pytest.mark.parametrize(
    "style",
    [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.usefixtures("setup_number")
async def test_not_optimistic(hass: HomeAssistant) -> None:
    """Test optimistic yaml option set to false."""
    await hass.services.async_call(
        number.DOMAIN,
        number.SERVICE_SET_VALUE,
        {ATTR_ENTITY_ID: TEST_NUMBER.entity_id, "value": 4},
        blocking=True,
    )

    state = hass.states.get(TEST_NUMBER.entity_id)
    assert state.state == STATE_UNKNOWN


@pytest.mark.parametrize(
    ("count", "config"),
    [
        (
            1,
            {
                "set_value": [],
                "state": "{{ states('number.test_state') }}",
                "availability": (
                    "{{ is_state('binary_sensor.test_availability', 'on') }}"
                ),
            },
        )
    ],
)
@pytest.mark.parametrize(
    "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER]
)
@pytest.mark.usefixtures("setup_number")
async def test_availability(hass: HomeAssistant) -> None:
    """Test configuration with optimistic state."""

    await async_trigger(hass, TEST_STATE_ENTITY_ID, "4.0")
    await async_trigger(hass, TEST_AVAILABILITY_ENTITY_ID, "on")
    state = hass.states.get(TEST_NUMBER.entity_id)
    assert float(state.state) == 4

    await async_trigger(hass, TEST_AVAILABILITY_ENTITY_ID, "off")

    state = hass.states.get(TEST_NUMBER.entity_id)
    assert state.state == STATE_UNAVAILABLE

    await async_trigger(hass, TEST_STATE_ENTITY_ID, "2.0")

    state = hass.states.get(TEST_NUMBER.entity_id)
    assert state.state == STATE_UNAVAILABLE

    await async_trigger(hass, TEST_AVAILABILITY_ENTITY_ID, "on")

    state = hass.states.get(TEST_NUMBER.entity_id)
    assert float(state.state) == 2


@pytest.mark.parametrize(
    ("count", "config"),
    [
        (
            1,
            {
                "set_value": [],
                "state": "{{ states('number.test_state') }}",
                "availability": "{{ x - 12 }}",
            },
        )
    ],
)
@pytest.mark.parametrize(
    "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER]
)
@pytest.mark.usefixtures("setup_number")
async def test_invalid_availability_template_keeps_component_available(
    hass: HomeAssistant, caplog_setup_text: str, caplog: pytest.LogCaptureFixture
) -> None:
    """Test that an invalid availability keeps the device available."""
    await async_trigger(hass, TEST_AVAILABILITY_ENTITY_ID, "anything")
    assert hass.states.get(TEST_NUMBER.entity_id).state != STATE_UNAVAILABLE
    error = "UndefinedError: 'x' is undefined"
    assert error in caplog_setup_text or error in caplog.text


@pytest.mark.parametrize(
    ("count", "config"),
    [
        (
            1,
            {
                "state": "{{ 1 }}",
                "set_value": [],
                "step": "{{ 1 }}",
                "optimistic": True,
            },
        )
    ],
)
@pytest.mark.parametrize(
    "style",
    [
        ConfigurationStyle.MODERN,
    ],
)
@pytest.mark.usefixtures("setup_number")
async def test_empty_action_config(hass: HomeAssistant) -> None:
    """Test configuration with empty script."""
    await hass.services.async_call(
        number.DOMAIN,
        number.SERVICE_SET_VALUE,
        {ATTR_ENTITY_ID: TEST_NUMBER.entity_id, "value": 4},
        blocking=True,
    )

    state = hass.states.get(TEST_NUMBER.entity_id)
    assert float(state.state) == 4


async def test_flow_preview(
    hass: HomeAssistant,
    hass_ws_client: WebSocketGenerator,
) -> None:
    """Test the config flow preview."""

    state = await async_get_flow_preview_state(
        hass,
        hass_ws_client,
        number.DOMAIN,
        {
            "name": "My template",
            "min": 0.0,
            "max": 100.0,
            **TEST_REQUIRED,
        },
    )

    assert state["state"] == "0.0"


@pytest.mark.parametrize(
    "style",
    [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
async def test_unique_id(
    hass: HomeAssistant,
    style: ConfigurationStyle,
) -> None:
    """Test unique_id option only creates one vacuum per id."""
    await setup_and_test_unique_id(hass, TEST_NUMBER, style, TEST_REQUIRED, "{{ 0 }}")


@pytest.mark.parametrize(
    "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER]
)
async def test_nested_unique_id(
    hass: HomeAssistant,
    style: ConfigurationStyle,
    entity_registry: er.EntityRegistry,
) -> None:
    """Test a template unique_id propagates to vacuum unique_ids."""
    await setup_and_test_nested_unique_id(
        hass, TEST_NUMBER, style, entity_registry, TEST_REQUIRED, "{{ 0 }}"
    )


@pytest.mark.parametrize("count", [1])
@pytest.mark.parametrize(
    "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER]
)
@pytest.mark.parametrize(
    ("config", "expected_device_class"),
    [
        (
            {
                **TEST_REQUIRED,
                "unit_of_measurement": "°C",
                "device_class": "temperature",
            },
            "temperature",
        ),
        (
            TEST_REQUIRED,
            None,
        ),
    ],
)
@pytest.mark.usefixtures("setup_number")
async def test_setup_valid_device_class(
    hass: HomeAssistant, expected_device_class: str | None
) -> None:
    """Test setup with valid device_class."""
    await async_trigger(hass, TEST_STATE_ENTITY_ID, "75")
    assert (
        hass.states.get(TEST_NUMBER.entity_id).attributes.get("device_class")
        == expected_device_class
    )


@pytest.mark.parametrize(
    "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER]
)
@pytest.mark.parametrize(
    (
        "saved_state",
        "saved_extra_data",
        "initial_state",
        "initial_attributes",
    ),
    [
        (
            "some_value",
            {
                "native_max_value": 80,
                "native_min_value": 2,
                "native_step": 2,
                "native_unit_of_measurement": "°F",
                "native_value": 10,
            },
            "10",
            {
                "step": 2,
                "min": 2,
                "max": 80,
                "unit_of_measurement": "°C",
            },
        ),
        (
            "some_value",
            {
                "native_max_value": None,
                "native_min_value": None,
                "native_step": None,
                "native_unit_of_measurement": "°F",
                "native_value": 10,
            },
            "10",
            {
                "step": 1,
                "min": 0,
                "max": 100,
                "unit_of_measurement": "°C",
            },
        ),
        (
            STATE_UNAVAILABLE,
            {
                "native_max_value": 80,
                "native_min_value": 2,
                "native_step": 2,
                "native_unit_of_measurement": "°F",
                "native_value": 10,
            },
            STATE_UNKNOWN,
            {
                "step": 1,
                "min": 0,
                "max": 100,
                "unit_of_measurement": "°C",
            },
        ),
        (
            STATE_UNKNOWN,
            {
                "native_max_value": 80,
                "native_min_value": 2,
                "native_step": 2,
                "native_unit_of_measurement": "°F",
                "native_value": 10,
            },
            STATE_UNKNOWN,
            {
                "step": 1,
                "min": 0,
                "max": 100,
                "unit_of_measurement": "°C",
            },
        ),
    ],
)
async def test_restore_state(
    hass: HomeAssistant,
    style: ConfigurationStyle,
    saved_state: str,
    saved_extra_data: dict | None,
    initial_state: str,
    initial_attributes: ConfigType,
) -> None:
    """Test restoring state."""

    setup_mock_template_entity_restore_state(
        hass,
        TEST_NUMBER,
        saved_state,
        saved_extra_data=saved_extra_data,
    )

    await setup_restore_template_entity(
        hass,
        TEST_NUMBER,
        style,
        {
            "max": "{{ state_attr('number.test_state', 'max') or 100 }}",
            "min": "{{ state_attr('number.test_state', 'min') or 0 }}",
            "state": "{{ state_attr('number.test_state', 'native_value') }}",
            "step": "{{ state_attr('number.test_state', 'step') or 1 }}",
            "set_value": [],
            "device_class": "temperature",
            "unit_of_measurement": "°C",
        },
        "state_attr('number.test_state', 'native_value') | float(0) > 6",
    )

    assert_state_and_attributes(
        hass,
        TEST_NUMBER,
        initial_state,
        initial_attributes,
    )

    await async_trigger(
        hass,
        "number.test_state",
        "anything",
        {
            "native_value": 30.0,
            "step": 3,
            "min": 3,
            "max": 60,
        },
    )

    assert_state_and_attributes(
        hass,
        TEST_NUMBER,
        "30.0",
        {"step": 3, "min": 3, "max": 60},
    )
