"""Test ESPHome update entities."""
import asyncio
import dataclasses
from unittest.mock import Mock, patch

import pytest

from homeassistant.components.esphome.dashboard import async_get_dashboard
from homeassistant.components.update import UpdateEntityFeature
from homeassistant.const import STATE_ON, STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_send


@pytest.fixture(autouse=True)
def stub_reconnect():
    """Stub reconnect."""
    with patch("homeassistant.components.esphome.ReconnectLogic.start"):
        yield


@pytest.mark.parametrize(
    ("devices_payload", "expected_state", "expected_attributes"),
    [
        (
            [
                {
                    "name": "test",
                    "current_version": "2023.2.0-dev",
                    "configuration": "test.yaml",
                }
            ],
            "on",
            {
                "latest_version": "2023.2.0-dev",
                "installed_version": "1.0.0",
                "supported_features": UpdateEntityFeature.INSTALL,
            },
        ),
        (
            [
                {
                    "name": "test",
                    "current_version": "1.0.0",
                },
            ],
            "off",
            {
                "latest_version": "1.0.0",
                "installed_version": "1.0.0",
                "supported_features": 0,
            },
        ),
        (
            [],
            "unavailable",
            {"supported_features": 0},
        ),
    ],
)
async def test_update_entity(
    hass: HomeAssistant,
    mock_config_entry,
    mock_device_info,
    mock_dashboard,
    devices_payload,
    expected_state,
    expected_attributes,
) -> None:
    """Test ESPHome update entity."""
    mock_dashboard["configured"] = devices_payload
    await async_get_dashboard(hass).async_refresh()

    with patch(
        "homeassistant.components.esphome.update.DomainData.get_entry_data",
        return_value=Mock(available=True, device_info=mock_device_info),
    ):
        assert await hass.config_entries.async_forward_entry_setup(
            mock_config_entry, "update"
        )

    state = hass.states.get("update.none_firmware")
    assert state is not None
    assert state.state == expected_state
    for key, expected_value in expected_attributes.items():
        assert state.attributes.get(key) == expected_value

    if expected_state != "on":
        return

    with patch(
        "esphome_dashboard_api.ESPHomeDashboardAPI.compile", return_value=True
    ) as mock_compile, patch(
        "esphome_dashboard_api.ESPHomeDashboardAPI.upload", return_value=True
    ) as mock_upload:
        await hass.services.async_call(
            "update",
            "install",
            {"entity_id": "update.none_firmware"},
            blocking=True,
        )

    assert len(mock_compile.mock_calls) == 1
    assert mock_compile.mock_calls[0][1][0] == "test.yaml"

    assert len(mock_upload.mock_calls) == 1
    assert mock_upload.mock_calls[0][1][0] == "test.yaml"


async def test_update_static_info(
    hass: HomeAssistant,
    mock_config_entry,
    mock_device_info,
    mock_dashboard,
) -> None:
    """Test ESPHome update entity."""
    mock_dashboard["configured"] = [
        {
            "name": "test",
            "current_version": "1.2.3",
        },
    ]
    await async_get_dashboard(hass).async_refresh()

    signal_static_info_updated = f"esphome_{mock_config_entry.entry_id}_on_list"
    runtime_data = Mock(
        available=True,
        device_info=mock_device_info,
        signal_static_info_updated=signal_static_info_updated,
    )

    with patch(
        "homeassistant.components.esphome.update.DomainData.get_entry_data",
        return_value=runtime_data,
    ):
        assert await hass.config_entries.async_forward_entry_setup(
            mock_config_entry, "update"
        )

    state = hass.states.get("update.none_firmware")
    assert state is not None
    assert state.state == "on"

    runtime_data.device_info = dataclasses.replace(
        runtime_data.device_info, esphome_version="1.2.3"
    )
    async_dispatcher_send(hass, signal_static_info_updated, [])

    state = hass.states.get("update.none_firmware")
    assert state.state == "off"


@pytest.mark.parametrize(
    "expected_disconnect_state", [(True, STATE_ON), (False, STATE_UNAVAILABLE)]
)
async def test_update_device_state_for_availability(
    hass: HomeAssistant,
    expected_disconnect_state: tuple[bool, str],
    mock_config_entry,
    mock_device_info,
    mock_dashboard,
) -> None:
    """Test ESPHome update entity changes availability with the device."""
    mock_dashboard["configured"] = [
        {
            "name": "test",
            "current_version": "1.2.3",
        },
    ]
    await async_get_dashboard(hass).async_refresh()

    signal_device_updated = f"esphome_{mock_config_entry.entry_id}_on_device_update"
    runtime_data = Mock(
        available=True,
        expected_disconnect=False,
        device_info=mock_device_info,
        signal_device_updated=signal_device_updated,
    )

    with patch(
        "homeassistant.components.esphome.update.DomainData.get_entry_data",
        return_value=runtime_data,
    ):
        assert await hass.config_entries.async_forward_entry_setup(
            mock_config_entry, "update"
        )

    state = hass.states.get("update.none_firmware")
    assert state is not None
    assert state.state == "on"

    expected_disconnect, expected_state = expected_disconnect_state

    runtime_data.available = False
    runtime_data.expected_disconnect = expected_disconnect
    async_dispatcher_send(hass, signal_device_updated)

    state = hass.states.get("update.none_firmware")
    assert state.state == expected_state

    # Deep sleep devices should still be available
    runtime_data.device_info = dataclasses.replace(
        runtime_data.device_info, has_deep_sleep=True
    )

    async_dispatcher_send(hass, signal_device_updated)

    state = hass.states.get("update.none_firmware")
    assert state.state == "on"


async def test_update_entity_dashboard_not_available_startup(
    hass: HomeAssistant, mock_config_entry, mock_device_info, mock_dashboard
) -> None:
    """Test ESPHome update entity when dashboard is not available at startup."""
    with patch(
        "homeassistant.components.esphome.update.DomainData.get_entry_data",
        return_value=Mock(available=True, device_info=mock_device_info),
    ), patch(
        "esphome_dashboard_api.ESPHomeDashboardAPI.get_devices",
        side_effect=asyncio.TimeoutError,
    ):
        await async_get_dashboard(hass).async_refresh()
        assert await hass.config_entries.async_forward_entry_setup(
            mock_config_entry, "update"
        )

    state = hass.states.get("update.none_firmware")
    assert state is None

    mock_dashboard["configured"] = [
        {
            "name": "test",
            "current_version": "2023.2.0-dev",
            "configuration": "test.yaml",
        }
    ]
    await async_get_dashboard(hass).async_refresh()
    await hass.async_block_till_done()

    state = hass.states.get("update.none_firmware")
    assert state.state == "on"
    expected_attributes = {
        "latest_version": "2023.2.0-dev",
        "installed_version": "1.0.0",
        "supported_features": UpdateEntityFeature.INSTALL,
    }
    for key, expected_value in expected_attributes.items():
        assert state.attributes.get(key) == expected_value