"""Tests for the Duco integration setup."""

from datetime import timedelta
from unittest.mock import ANY, AsyncMock, patch

from duco_connectivity import (
    BoardInfo,
    ConfigNode,
    ConfigNodeOverview,
    ConfigValueString,
    DiagComponent,
    DucoConnectionError,
    DucoError,
    DucoResponseError,
    LanInfo,
    Node,
    NodeListActionItemList,
    VentilationTemperatureInfo,
)
from freezegun.api import FrozenDateTimeFactory
import pytest

from homeassistant.components.duco.const import BOX_NODE_ID, DOMAIN, SCAN_INTERVAL
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er

from . import setup_platform_integration
from .conftest import (
    TEST_HOST,
    TEST_MAC,
    UNSUPPORTED_BOARD_INFOS,
    node_configs_from_nodes,
)

from tests.common import MockConfigEntry, async_fire_time_changed


def _get_duco_node_device(
    device_registry: dr.DeviceRegistry, config_entry_id: str
) -> dr.DeviceEntry:
    """Return the primary Duco node device used in setup tests."""
    device = device_registry.async_get_device_by_identifier(
        ("duco", f"{TEST_MAC}_1"), config_entry_id
    )
    assert device is not None
    return device


def _node_configs_with_primary_name(
    mock_nodes: list[Node],
    primary_name: str,
) -> ConfigNodeOverview:
    """Return node configs with a custom name for the primary Duco node."""
    return ConfigNodeOverview(
        nodes=[
            ConfigNode(node_id=1, name=ConfigValueString(primary_name)),
            *node_configs_from_nodes(mock_nodes[1:]).nodes,
        ]
    )


@pytest.mark.parametrize(
    (
        "method",
        "exception",
        "expected_state",
        "expected_translation_key",
        "has_error_translation_placeholder",
    ),
    [
        (
            "async_get_board_info",
            DucoConnectionError("Connection refused"),
            ConfigEntryState.SETUP_RETRY,
            None,
            False,
        ),
        (
            "async_get_board_info",
            DucoError("Unexpected API error"),
            ConfigEntryState.SETUP_ERROR,
            "api_error",
            False,
        ),
        (
            "async_get_board_info",
            DucoResponseError(500, "/info"),
            ConfigEntryState.SETUP_ERROR,
            "api_error",
            False,
        ),
        (
            "async_get_nodes",
            DucoConnectionError("Connection refused"),
            ConfigEntryState.SETUP_RETRY,
            None,
            False,
        ),
        (
            "async_get_node_actions",
            DucoConnectionError("Connection refused"),
            ConfigEntryState.LOADED,
            None,
            False,
        ),
    ],
)
async def test_setup_entry_error(
    hass: HomeAssistant,
    mock_config_entry: MockConfigEntry,
    mock_duco_client: AsyncMock,
    method: str,
    exception: Exception,
    expected_state: ConfigEntryState,
    expected_translation_key: str | None,
    has_error_translation_placeholder: bool,
) -> None:
    """Test that fetch errors during setup result in the correct state."""
    getattr(mock_duco_client, method).side_effect = exception
    mock_config_entry.add_to_hass(hass)
    await hass.config_entries.async_setup(mock_config_entry.entry_id)
    await hass.async_block_till_done()

    assert mock_config_entry.state is expected_state
    assert mock_config_entry.error_reason_translation_key == expected_translation_key
    if has_error_translation_placeholder:
        assert mock_config_entry.error_reason_translation_placeholders == {
            "error": repr(exception)
        }
    else:
        assert mock_config_entry.error_reason_translation_placeholders is None


@pytest.mark.usefixtures("mock_duco_client")
async def test_setup_entry_success(
    hass: HomeAssistant,
    init_integration: MockConfigEntry,
) -> None:
    """Test successful setup of the Duco integration."""
    assert init_integration.state is ConfigEntryState.LOADED


async def test_device_via_device_links(
    hass: HomeAssistant,
    mock_config_entry: MockConfigEntry,
    mock_duco_client: AsyncMock,
    mock_nodes: list[Node],
    device_registry: dr.DeviceRegistry,
) -> None:
    """Test a sub-node added before the box still links to it via via_device_id."""
    # Force the child-before-parent race: return a sub-node ahead of the box node
    # and set up only the sensor platform (the fan platform would otherwise create
    # the box first). This only resolves because the box is pre-registered in
    # async_setup_entry before the platforms build their entities.
    box_node = next(node for node in mock_nodes if node.node_id == BOX_NODE_ID)
    child_node = next(node for node in mock_nodes if node.node_id != BOX_NODE_ID)
    mock_duco_client.async_get_nodes.return_value = [child_node, box_node]

    await setup_platform_integration(hass, mock_config_entry, [Platform.SENSOR])

    box_device = device_registry.async_get_device_by_identifier(
        (DOMAIN, f"{TEST_MAC}_{BOX_NODE_ID}"), mock_config_entry.entry_id
    )
    assert box_device is not None

    child_device = device_registry.async_get_device_by_identifier(
        (DOMAIN, f"{TEST_MAC}_{child_node.node_id}"), mock_config_entry.entry_id
    )
    assert child_device is not None
    assert child_device.via_device_id == box_device.id


@pytest.mark.parametrize(
    "exception",
    [
        pytest.param(DucoError("lan info error"), id="duco_error"),
        pytest.param(DucoConnectionError("lan info offline"), id="connection_error"),
    ],
)
async def test_setup_entry_ignores_lan_info_failures(
    hass: HomeAssistant,
    mock_config_entry: MockConfigEntry,
    mock_duco_client: AsyncMock,
    exception: Exception,
) -> None:
    """Test setup succeeds when the supplemental LAN info endpoint fails."""
    mock_duco_client.async_get_lan_info.side_effect = exception
    mock_config_entry.add_to_hass(hass)

    await hass.config_entries.async_setup(mock_config_entry.entry_id)
    await hass.async_block_till_done()

    assert mock_config_entry.state is ConfigEntryState.LOADED


@pytest.mark.parametrize(
    "exception",
    [
        pytest.param(DucoError("API error"), id="duco_error"),
        pytest.param(DucoConnectionError("Connection refused"), id="connection_error"),
    ],
)
async def test_setup_entry_recovers_from_optional_temperature_capability_failure(
    hass: HomeAssistant,
    freezer: FrozenDateTimeFactory,
    mock_config_entry: MockConfigEntry,
    mock_duco_client: AsyncMock,
    exception: Exception,
) -> None:
    """Test an optional temperature capability is retried after a setup failure."""
    mock_duco_client.async_get_ventilation_temperature_info.side_effect = [
        exception,
        VentilationTemperatureInfo(temp_oda=5.5),
    ]
    mock_config_entry.add_to_hass(hass)

    await hass.config_entries.async_setup(mock_config_entry.entry_id)
    await hass.async_block_till_done()

    assert mock_config_entry.state is ConfigEntryState.LOADED
    assert hass.states.get("sensor.living_outdoor_air_temperature") is None

    freezer.tick(SCAN_INTERVAL)
    async_fire_time_changed(hass)
    await hass.async_block_till_done(wait_background_tasks=True)

    state = hass.states.get("sensor.living_outdoor_air_temperature")
    assert state is not None
    assert state.state == "5.5"


async def test_setup_entry_ignores_node_name_config_failures(
    hass: HomeAssistant,
    mock_config_entry: MockConfigEntry,
    mock_duco_client: AsyncMock,
    mock_nodes: list[Node],
    device_registry: dr.DeviceRegistry,
) -> None:
    """Test setup falls back to API node names when node config fetch fails."""
    mock_duco_client.async_get_node_configs.side_effect = DucoError("node config error")
    mock_config_entry.add_to_hass(hass)

    await hass.config_entries.async_setup(mock_config_entry.entry_id)
    await hass.async_block_till_done()

    assert mock_config_entry.state is ConfigEntryState.LOADED

    device = _get_duco_node_device(device_registry, mock_config_entry.entry_id)
    assert device.name == mock_nodes[0].general.name
    assert mock_duco_client.async_get_node_configs.call_count == 1


@pytest.mark.parametrize("unsupported_board_info", UNSUPPORTED_BOARD_INFOS)
async def test_setup_entry_unsupported_board_info(
    hass: HomeAssistant,
    mock_config_entry: MockConfigEntry,
    mock_duco_client: AsyncMock,
    unsupported_board_info: BoardInfo,
) -> None:
    """Test that unsupported board info blocks setup for existing entries."""
    mock_duco_client.async_get_board_info.return_value = unsupported_board_info
    mock_config_entry.add_to_hass(hass)

    await hass.config_entries.async_setup(mock_config_entry.entry_id)
    await hass.async_block_till_done()

    assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
    assert mock_config_entry.error_reason_translation_key == "unsupported_board"
    assert mock_config_entry.error_reason_translation_placeholders is None


async def test_setup_entry_unsupported_board_without_info_endpoint(
    hass: HomeAssistant,
    mock_config_entry: MockConfigEntry,
    mock_duco_client: AsyncMock,
) -> None:
    """Test that setup fails when the board does not expose /info."""
    mock_duco_client.async_get_board_info.side_effect = DucoResponseError(404, "/info")
    mock_config_entry.add_to_hass(hass)

    await hass.config_entries.async_setup(mock_config_entry.entry_id)
    await hass.async_block_till_done()

    assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
    assert mock_config_entry.error_reason_translation_key == "unsupported_board"
    assert mock_config_entry.error_reason_translation_placeholders is None


async def test_unload_entry(
    hass: HomeAssistant,
    init_integration: MockConfigEntry,
) -> None:
    """Test unloading the Duco integration."""
    assert init_integration.state is ConfigEntryState.LOADED

    await hass.config_entries.async_unload(init_integration.entry_id)
    await hass.async_block_till_done()

    assert init_integration.state is ConfigEntryState.NOT_LOADED


@pytest.mark.usefixtures("mock_duco_client")
async def test_cleanup_orphaned_temperature_entities(
    hass: HomeAssistant,
    mock_config_entry: MockConfigEntry,
    entity_registry: er.EntityRegistry,
) -> None:
    """Test stale temperature entities from prior versions are removed on setup."""
    mock_config_entry.add_to_hass(hass)

    old_unique_ids = [
        f"{TEST_MAC}_1_box_temperature",
        f"{TEST_MAC}_2_temperature",
    ]
    for unique_id in old_unique_ids:
        entity_registry.async_get_or_create(
            Platform.SENSOR,
            "duco",
            unique_id,
            config_entry=mock_config_entry,
        )

    await hass.config_entries.async_setup(mock_config_entry.entry_id)
    await hass.async_block_till_done()

    for unique_id in old_unique_ids:
        assert (
            entity_registry.async_get_entity_id(Platform.SENSOR, "duco", unique_id)
            is None
        )


async def test_setup_entry_creates_http_client(
    hass: HomeAssistant,
    mock_config_entry: MockConfigEntry,
    mock_board_info: BoardInfo,
    mock_lan_info: LanInfo,
    mock_node_actions: NodeListActionItemList,
    mock_nodes: list[Node],
) -> None:
    """Test that setup creates the Duco client with the provided host."""
    with patch(
        "homeassistant.components.duco.DucoClient",
        autospec=True,
    ) as mock_client_class:
        mock_client_class.return_value.async_get_board_info.return_value = (
            mock_board_info
        )
        mock_client_class.return_value.async_get_lan_info.return_value = mock_lan_info
        mock_client_class.return_value.async_get_nodes.return_value = mock_nodes
        mock_client_class.return_value.async_get_node_actions.return_value = (
            mock_node_actions
        )
        (
            mock_client_class.return_value.async_get_ventilation_temperature_info.return_value
        ) = VentilationTemperatureInfo()
        mock_client_class.return_value.async_get_diagnostics.return_value = [
            DiagComponent(component="Ventilation", status="Ok")
        ]
        (
            mock_client_class.return_value.async_get_write_requests_remaining
        ).return_value = 100
        mock_config_entry.add_to_hass(hass)
        await hass.config_entries.async_setup(mock_config_entry.entry_id)
        await hass.async_block_till_done()

    mock_client_class.assert_called_once_with(
        session=ANY,
        host=TEST_HOST,
    )


async def test_setup_entry_uses_configured_node_name(
    hass: HomeAssistant,
    mock_config_entry: MockConfigEntry,
    mock_duco_client: AsyncMock,
    mock_nodes: list[Node],
    device_registry: dr.DeviceRegistry,
) -> None:
    """Test setup uses the configurable Duco node name."""
    mock_duco_client.async_get_node_configs.return_value = (
        _node_configs_with_primary_name(mock_nodes, "Kitchen")
    )
    mock_config_entry.add_to_hass(hass)

    await hass.config_entries.async_setup(mock_config_entry.entry_id)
    await hass.async_block_till_done()

    device = _get_duco_node_device(device_registry, mock_config_entry.entry_id)
    assert device.name == "Kitchen"
    assert mock_duco_client.async_get_node_configs.call_count == 1


async def test_node_name_refresh_updates_device_registry_name(
    hass: HomeAssistant,
    freezer: FrozenDateTimeFactory,
    mock_duco_client: AsyncMock,
    mock_nodes: list[Node],
    mock_config_entry: MockConfigEntry,
    device_registry: dr.DeviceRegistry,
) -> None:
    """Test Duco node names update on reload, not during periodic polling."""
    mock_duco_client.async_get_node_configs.side_effect = [
        _node_configs_with_primary_name(mock_nodes, "Kitchen"),
        _node_configs_with_primary_name(mock_nodes, "Living Room"),
    ]
    mock_config_entry.add_to_hass(hass)

    await hass.config_entries.async_setup(mock_config_entry.entry_id)
    await hass.async_block_till_done()

    device = _get_duco_node_device(device_registry, mock_config_entry.entry_id)
    assert device.name == "Kitchen"
    assert mock_duco_client.async_get_node_configs.call_count == 1

    freezer.tick(timedelta(days=1))
    async_fire_time_changed(hass)
    await hass.async_block_till_done(wait_background_tasks=True)

    device = _get_duco_node_device(device_registry, mock_config_entry.entry_id)
    assert device.name == "Kitchen"
    assert mock_duco_client.async_get_node_configs.call_count == 1

    assert await hass.config_entries.async_reload(mock_config_entry.entry_id)
    await hass.async_block_till_done()

    device = _get_duco_node_device(device_registry, mock_config_entry.entry_id)
    assert device.name == "Living Room"
    assert mock_duco_client.async_get_node_configs.call_count == 2
