"""Tests for the Duco config flow."""

from ipaddress import IPv4Address
from unittest.mock import ANY, AsyncMock, patch

from duco_connectivity import (
    BoardInfo,
    DucoConnectionError,
    DucoError,
    DucoResponseError,
    LanInfo,
)
import pytest

from homeassistant.components.duco.const import DOMAIN
from homeassistant.config_entries import (
    SOURCE_DHCP,
    SOURCE_USER,
    SOURCE_ZEROCONF,
    ConfigFlowResult,
)
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo

from .conftest import TEST_HOST, TEST_MAC, UNSUPPORTED_BOARD_INFOS, USER_INPUT

from tests.common import MockConfigEntry

ZEROCONF_DISCOVERY = ZeroconfServiceInfo(
    ip_address=IPv4Address(TEST_HOST),
    ip_addresses=[IPv4Address(TEST_HOST)],
    port=80,
    hostname="duco_061293.local.",
    type="_http._tcp.local.",
    name="DUCO [a0dd6c061293]._http._tcp.local.",
    properties={},
)

DHCP_DISCOVERY = DhcpServiceInfo(
    ip=TEST_HOST,
    hostname="duco_ddeeff",
    macaddress="aabbccddeeff",
)

_SUPPORTED_BOARD_INFOS = [
    pytest.param(
        BoardInfo(
            box_name="ENERGY",
            box_sub_type_name="Eu",
            serial_board_box="ABC123",
            serial_board_comm="DEF456",
            serial_duco_box="GHI789",
            serial_duco_comm="JKL012",
            time=1700000000,
            public_api_version="2.5",
        ),
        id="energy-supported",
    ),
    pytest.param(
        BoardInfo(
            box_name="FOCUS",
            box_sub_type_name="Eu",
            serial_board_box="ABC123",
            serial_board_comm="DEF456",
            serial_duco_box="GHI789",
            serial_duco_comm="JKL012",
            time=1700000000,
            public_api_version="2.5",
        ),
        id="focus-supported",
    ),
    pytest.param(
        BoardInfo(
            box_name="SOMETHING_NEW",
            box_sub_type_name="Eu",
            serial_board_box="ABC123",
            serial_board_comm="DEF456",
            serial_duco_box="GHI789",
            serial_duco_comm="JKL012",
            time=1700000000,
            public_api_version="2.5",
        ),
        id="unknown-box-name-supported",
    ),
]


async def _start_user_flow(hass: HomeAssistant) -> ConfigFlowResult:
    """Start the user config flow and assert the initial form."""
    result = await hass.config_entries.flow.async_init(
        DOMAIN, context={"source": SOURCE_USER}
    )
    assert result["type"] is FlowResultType.FORM
    assert result["step_id"] == "user"
    assert result["errors"] == {}
    return result


async def _start_reconfigure_flow(
    hass: HomeAssistant, mock_config_entry: MockConfigEntry
) -> ConfigFlowResult:
    """Start the reconfigure flow for an existing config entry."""
    mock_config_entry.add_to_hass(hass)

    result = await mock_config_entry.start_reconfigure_flow(hass)
    assert result["type"] is FlowResultType.FORM
    assert result["step_id"] == "reconfigure"
    return result


def _set_board_info_value(mock_duco_client: AsyncMock, board_info: BoardInfo) -> None:
    """Set the board info returned by the next config flow step."""
    mock_duco_client.async_get_board_info.side_effect = None
    mock_duco_client.async_get_board_info.return_value = board_info


@pytest.mark.usefixtures("mock_setup_entry")
async def test_user_flow_success(
    hass: HomeAssistant, mock_duco_client: AsyncMock
) -> None:
    """Test a successful user flow."""
    result = await _start_user_flow(hass)

    result = await hass.config_entries.flow.async_configure(
        result["flow_id"], USER_INPUT
    )

    assert result["type"] is FlowResultType.CREATE_ENTRY
    assert result["title"] == "SILENT_CONNECT"
    assert result["data"] == USER_INPUT
    assert result["result"].unique_id == TEST_MAC


@pytest.mark.parametrize(
    ("exception", "expected_error"),
    [
        (DucoConnectionError("Connection refused"), "cannot_connect"),
        (DucoError("Unexpected error"), "unknown"),
        (DucoResponseError(500, "/info"), "unknown"),
        (DucoResponseError(404, "/info"), "unsupported_board"),
    ],
)
@pytest.mark.usefixtures("mock_setup_entry")
async def test_user_flow_error(
    hass: HomeAssistant,
    mock_duco_client: AsyncMock,
    exception: Exception,
    expected_error: str,
) -> None:
    """Test handling of connection and unknown errors in the user flow."""
    result = await _start_user_flow(hass)

    mock_duco_client.async_get_board_info.side_effect = exception
    result = await hass.config_entries.flow.async_configure(
        result["flow_id"], USER_INPUT
    )

    assert result["type"] is FlowResultType.FORM
    assert result["step_id"] == "user"
    assert result["errors"] == {"base": expected_error}

    mock_duco_client.async_get_board_info.side_effect = None
    result = await hass.config_entries.flow.async_configure(
        result["flow_id"], USER_INPUT
    )

    assert result["type"] is FlowResultType.CREATE_ENTRY


async def test_user_flow_duplicate(
    hass: HomeAssistant,
    mock_duco_client: AsyncMock,
    mock_config_entry: MockConfigEntry,
) -> None:
    """Test that a duplicate config entry is aborted."""
    mock_config_entry.add_to_hass(hass)

    # Second attempt for the same device
    result = await _start_user_flow(hass)
    result = await hass.config_entries.flow.async_configure(
        result["flow_id"], USER_INPUT
    )

    assert result["type"] is FlowResultType.ABORT
    assert result["reason"] == "already_configured"


@pytest.mark.usefixtures("mock_setup_entry")
async def test_zeroconf_discovery_new_device(
    hass: HomeAssistant, mock_duco_client: AsyncMock
) -> None:
    """Test zeroconf discovery shows confirmation form and creates entry."""
    result = await hass.config_entries.flow.async_init(
        DOMAIN,
        context={"source": SOURCE_ZEROCONF},
        data=ZEROCONF_DISCOVERY,
    )

    assert result["type"] is FlowResultType.FORM
    assert result["step_id"] == "discovery_confirm"

    result = await hass.config_entries.flow.async_configure(
        result["flow_id"], user_input={}
    )

    assert result["type"] is FlowResultType.CREATE_ENTRY
    assert result["title"] == "SILENT_CONNECT"
    assert result["data"] == USER_INPUT
    assert result["result"].unique_id == TEST_MAC


async def test_zeroconf_discovery_updates_host(
    hass: HomeAssistant,
    mock_duco_client: AsyncMock,
    mock_config_entry: MockConfigEntry,
) -> None:
    """Test zeroconf discovery updates the host of an existing entry."""
    mock_config_entry.add_to_hass(hass)

    new_ip = "192.168.1.200"
    discovery = ZeroconfServiceInfo(
        ip_address=IPv4Address(new_ip),
        ip_addresses=[IPv4Address(new_ip)],
        port=80,
        hostname="duco_061293.local.",
        type="_http._tcp.local.",
        name="DUCO [a0dd6c061293]._http._tcp.local.",
        properties={},
    )

    result = await hass.config_entries.flow.async_init(
        DOMAIN,
        context={"source": SOURCE_ZEROCONF},
        data=discovery,
    )

    assert result["type"] is FlowResultType.ABORT
    assert result["reason"] == "already_configured"
    assert mock_config_entry.data[CONF_HOST] == new_ip


async def test_zeroconf_discovery_already_configured_same_ip(
    hass: HomeAssistant,
    mock_duco_client: AsyncMock,
    mock_config_entry: MockConfigEntry,
) -> None:
    """Test zeroconf discovery with unchanged IP aborts as already_configured."""
    mock_config_entry.add_to_hass(hass)

    result = await hass.config_entries.flow.async_init(
        DOMAIN,
        context={"source": SOURCE_ZEROCONF},
        data=ZEROCONF_DISCOVERY,
    )

    assert result["type"] is FlowResultType.ABORT
    assert result["reason"] == "already_configured"


@pytest.mark.parametrize(
    ("exception", "expected_reason"),
    [
        (DucoConnectionError("Connection refused"), "cannot_connect"),
        (DucoError("Unexpected error"), "unknown"),
        (DucoResponseError(404, "/info"), "unsupported_board"),
    ],
)
async def test_zeroconf_discovery_exceptions(
    hass: HomeAssistant,
    mock_duco_client: AsyncMock,
    exception: Exception,
    expected_reason: str,
) -> None:
    """Test zeroconf discovery aborts on connection and unknown errors."""
    mock_duco_client.async_get_board_info.side_effect = exception

    result = await hass.config_entries.flow.async_init(
        DOMAIN,
        context={"source": SOURCE_ZEROCONF},
        data=ZEROCONF_DISCOVERY,
    )

    assert result["type"] is FlowResultType.ABORT
    assert result["reason"] == expected_reason


@pytest.mark.usefixtures("mock_setup_entry")
async def test_reconfigure_flow_success(
    hass: HomeAssistant,
    mock_duco_client: AsyncMock,
    mock_config_entry: MockConfigEntry,
) -> None:
    """Test a successful reconfigure flow updates host and reloads."""
    result = await _start_reconfigure_flow(hass, mock_config_entry)

    mock_duco_client.async_get_board_info.side_effect = DucoConnectionError(
        "Connection refused"
    )
    result = await hass.config_entries.flow.async_configure(
        result["flow_id"], {CONF_HOST: "192.168.1.50"}
    )

    assert result["type"] is FlowResultType.FORM
    assert result["step_id"] == "reconfigure"
    assert result["errors"] == {"base": "cannot_connect"}

    mock_duco_client.async_get_board_info.side_effect = None
    new_host = "192.168.1.200"
    result = await hass.config_entries.flow.async_configure(
        result["flow_id"], {CONF_HOST: new_host}
    )

    assert result["type"] is FlowResultType.ABORT
    assert result["reason"] == "reconfigure_successful"
    assert mock_config_entry.data[CONF_HOST] == new_host


@pytest.mark.usefixtures("mock_setup_entry")
async def test_reconfigure_flow_wrong_device(
    hass: HomeAssistant,
    mock_duco_client: AsyncMock,
    mock_config_entry: MockConfigEntry,
) -> None:
    """Test reconfigure flow aborts when pointing to a different device."""
    result = await _start_reconfigure_flow(hass, mock_config_entry)

    # Simulate a different MAC returned by the new host
    different_mac = "11:22:33:44:55:66"
    mock_duco_client.async_get_lan_info.return_value = LanInfo(
        mode="WIFI_CLIENT",
        ip="192.168.1.200",
        net_mask="255.255.255.0",
        default_gateway="192.168.1.1",
        dns="8.8.8.8",
        mac=different_mac,
        host_name="duco-other",
        rssi_wifi=-60,
    )

    result = await hass.config_entries.flow.async_configure(
        result["flow_id"], {CONF_HOST: "192.168.1.200"}
    )

    assert result["type"] is FlowResultType.ABORT
    assert result["reason"] == "unique_id_mismatch"


@pytest.mark.usefixtures("mock_setup_entry")
@pytest.mark.parametrize(
    ("exception", "expected_error"),
    [
        (DucoConnectionError("Connection refused"), "cannot_connect"),
        (DucoError("Unexpected error"), "unknown"),
        (DucoResponseError(500, "/info"), "unknown"),
    ],
)
async def test_reconfigure_flow_error(
    hass: HomeAssistant,
    mock_duco_client: AsyncMock,
    mock_config_entry: MockConfigEntry,
    exception: Exception,
    expected_error: str,
) -> None:
    """Test reconfigure flow shows error on connection failure."""
    result = await _start_reconfigure_flow(hass, mock_config_entry)

    mock_duco_client.async_get_board_info.side_effect = exception
    result = await hass.config_entries.flow.async_configure(
        result["flow_id"], {CONF_HOST: "192.168.1.200"}
    )

    assert result["type"] is FlowResultType.FORM
    assert result["step_id"] == "reconfigure"
    assert result["errors"] == {"base": expected_error}

    mock_duco_client.async_get_board_info.side_effect = None
    result = await hass.config_entries.flow.async_configure(
        result["flow_id"], {CONF_HOST: "192.168.1.200"}
    )

    assert result["type"] is FlowResultType.ABORT
    assert result["reason"] == "reconfigure_successful"


@pytest.mark.usefixtures("mock_setup_entry")
async def test_reconfigure_flow_without_info_endpoint(
    hass: HomeAssistant,
    mock_duco_client: AsyncMock,
    mock_config_entry: MockConfigEntry,
) -> None:
    """Test reconfigure flow rejects boards that do not expose the supported API."""
    result = await _start_reconfigure_flow(hass, mock_config_entry)

    mock_duco_client.async_get_board_info.side_effect = DucoResponseError(404, "/info")
    result = await hass.config_entries.flow.async_configure(
        result["flow_id"], {CONF_HOST: "192.168.1.50"}
    )

    assert result["type"] is FlowResultType.FORM
    assert result["step_id"] == "reconfigure"
    assert result["errors"] == {"base": "unsupported_board"}

    mock_duco_client.async_get_board_info.side_effect = None
    result = await hass.config_entries.flow.async_configure(
        result["flow_id"], {CONF_HOST: "192.168.1.200"}
    )

    assert result["type"] is FlowResultType.ABORT
    assert result["reason"] == "reconfigure_successful"


@pytest.mark.usefixtures("mock_setup_entry")
async def test_dhcp_discovery_new_device(
    hass: HomeAssistant, mock_duco_client: AsyncMock
) -> None:
    """Test DHCP discovery of a new device shows confirmation form and creates entry."""
    result = await hass.config_entries.flow.async_init(
        DOMAIN,
        context={"source": SOURCE_DHCP},
        data=DHCP_DISCOVERY,
    )

    assert result["type"] is FlowResultType.FORM
    assert result["step_id"] == "discovery_confirm"
    assert result["description_placeholders"] == {"name": "SILENT_CONNECT"}

    result = await hass.config_entries.flow.async_configure(
        result["flow_id"], user_input={}
    )

    assert result["type"] is FlowResultType.CREATE_ENTRY
    assert result["title"] == "SILENT_CONNECT"
    assert result["data"] == USER_INPUT
    assert result["result"].unique_id == TEST_MAC


async def test_dhcp_discovery_updates_host(
    hass: HomeAssistant,
    mock_duco_client: AsyncMock,
    mock_config_entry: MockConfigEntry,
) -> None:
    """Test DHCP discovery updates the host of an existing entry."""
    mock_config_entry.add_to_hass(hass)

    new_ip = "192.168.1.200"
    discovery = DhcpServiceInfo(
        ip=new_ip,
        hostname="duco_ddeeff",
        macaddress="aabbccddeeff",
    )

    result = await hass.config_entries.flow.async_init(
        DOMAIN,
        context={"source": SOURCE_DHCP},
        data=discovery,
    )

    assert result["type"] is FlowResultType.ABORT
    assert result["reason"] == "already_configured"
    assert mock_config_entry.data[CONF_HOST] == new_ip


async def test_dhcp_discovery_already_configured_same_ip(
    hass: HomeAssistant,
    mock_duco_client: AsyncMock,
    mock_config_entry: MockConfigEntry,
) -> None:
    """Test DHCP discovery with unchanged IP aborts as already_configured."""
    mock_config_entry.add_to_hass(hass)

    result = await hass.config_entries.flow.async_init(
        DOMAIN,
        context={"source": SOURCE_DHCP},
        data=DHCP_DISCOVERY,
    )

    assert result["type"] is FlowResultType.ABORT
    assert result["reason"] == "already_configured"


@pytest.mark.parametrize(
    ("exception", "expected_reason"),
    [
        (DucoConnectionError("Connection refused"), "cannot_connect"),
        (DucoError("Unexpected error"), "unknown"),
        (DucoResponseError(404, "/info"), "unsupported_board"),
    ],
)
async def test_dhcp_discovery_exceptions(
    hass: HomeAssistant,
    mock_duco_client: AsyncMock,
    exception: Exception,
    expected_reason: str,
) -> None:
    """Test DHCP discovery aborts on connection and unknown errors."""
    mock_duco_client.async_get_board_info.side_effect = exception

    result = await hass.config_entries.flow.async_init(
        DOMAIN,
        context={"source": SOURCE_DHCP},
        data=DHCP_DISCOVERY,
    )

    assert result["type"] is FlowResultType.ABORT
    assert result["reason"] == expected_reason


@pytest.mark.usefixtures("mock_setup_entry")
async def test_dhcp_discovery_exception_recovery(
    hass: HomeAssistant, mock_duco_client: AsyncMock
) -> None:
    """Test DHCP discovery recovers after an initial exception and creates the entry."""
    mock_duco_client.async_get_board_info.side_effect = DucoConnectionError(
        "Connection refused"
    )

    result = await hass.config_entries.flow.async_init(
        DOMAIN,
        context={"source": SOURCE_DHCP},
        data=DHCP_DISCOVERY,
    )

    assert result["type"] is FlowResultType.ABORT
    assert result["reason"] == "cannot_connect"

    mock_duco_client.async_get_board_info.side_effect = None

    result = await hass.config_entries.flow.async_init(
        DOMAIN,
        context={"source": SOURCE_DHCP},
        data=DHCP_DISCOVERY,
    )

    assert result["type"] is FlowResultType.FORM
    assert result["step_id"] == "discovery_confirm"

    result = await hass.config_entries.flow.async_configure(
        result["flow_id"], user_input={}
    )

    assert result["type"] is FlowResultType.CREATE_ENTRY
    assert result["result"].unique_id == TEST_MAC


@pytest.mark.usefixtures("mock_setup_entry")
@pytest.mark.parametrize("unsupported_board_info", UNSUPPORTED_BOARD_INFOS)
async def test_user_flow_unsupported_board_from_board_info(
    hass: HomeAssistant,
    mock_duco_client: AsyncMock,
    mock_board_info: BoardInfo,
    unsupported_board_info: BoardInfo,
) -> None:
    """Test user flow shows unsupported_board error when board validation fails."""
    result = await _start_user_flow(hass)

    _set_board_info_value(mock_duco_client, unsupported_board_info)
    result = await hass.config_entries.flow.async_configure(
        result["flow_id"], USER_INPUT
    )

    assert result["type"] is FlowResultType.FORM
    assert result["step_id"] == "user"
    assert result["errors"] == {"base": "unsupported_board"}

    _set_board_info_value(mock_duco_client, mock_board_info)
    result = await hass.config_entries.flow.async_configure(
        result["flow_id"], USER_INPUT
    )

    assert result["type"] is FlowResultType.CREATE_ENTRY


@pytest.mark.usefixtures("mock_setup_entry")
@pytest.mark.parametrize("supported_board_info", _SUPPORTED_BOARD_INFOS)
async def test_user_flow_allows_api_compatible_board_info(
    hass: HomeAssistant,
    mock_duco_client: AsyncMock,
    supported_board_info: BoardInfo,
) -> None:
    """Test user flow allows boards that expose a compatible API version."""
    result = await _start_user_flow(hass)

    _set_board_info_value(mock_duco_client, supported_board_info)
    result = await hass.config_entries.flow.async_configure(
        result["flow_id"], USER_INPUT
    )

    assert result["type"] is FlowResultType.CREATE_ENTRY
    assert result["title"] == str(supported_board_info.box_name)
    assert result["result"].unique_id == TEST_MAC


@pytest.mark.usefixtures("mock_setup_entry")
@pytest.mark.parametrize("unsupported_board_info", UNSUPPORTED_BOARD_INFOS)
async def test_reconfigure_flow_unsupported_board_from_board_info(
    hass: HomeAssistant,
    mock_duco_client: AsyncMock,
    mock_config_entry: MockConfigEntry,
    mock_board_info: BoardInfo,
    unsupported_board_info: BoardInfo,
) -> None:
    """Test reconfigure flow shows unsupported_board when board validation fails."""
    result = await _start_reconfigure_flow(hass, mock_config_entry)

    _set_board_info_value(mock_duco_client, unsupported_board_info)
    result = await hass.config_entries.flow.async_configure(
        result["flow_id"], {CONF_HOST: "192.168.1.50"}
    )

    assert result["type"] is FlowResultType.FORM
    assert result["step_id"] == "reconfigure"
    assert result["errors"] == {"base": "unsupported_board"}

    _set_board_info_value(mock_duco_client, mock_board_info)
    result = await hass.config_entries.flow.async_configure(
        result["flow_id"], {CONF_HOST: "192.168.1.200"}
    )

    assert result["type"] is FlowResultType.ABORT
    assert result["reason"] == "reconfigure_successful"


@pytest.mark.parametrize("supported_board_info", _SUPPORTED_BOARD_INFOS)
async def test_zeroconf_discovery_allows_api_compatible_board_info(
    hass: HomeAssistant,
    mock_duco_client: AsyncMock,
    supported_board_info: BoardInfo,
) -> None:
    """Test zeroconf discovery allows boards that expose a compatible API version."""
    _set_board_info_value(mock_duco_client, supported_board_info)

    result = await hass.config_entries.flow.async_init(
        DOMAIN,
        context={"source": SOURCE_ZEROCONF},
        data=ZEROCONF_DISCOVERY,
    )

    assert result["type"] is FlowResultType.FORM
    assert result["step_id"] == "discovery_confirm"
    assert result["description_placeholders"] == {
        "name": str(supported_board_info.box_name)
    }


@pytest.mark.parametrize("unsupported_board_info", UNSUPPORTED_BOARD_INFOS)
async def test_zeroconf_discovery_unsupported_board_from_board_info(
    hass: HomeAssistant,
    mock_duco_client: AsyncMock,
    unsupported_board_info: BoardInfo,
) -> None:
    """Test zeroconf discovery aborts with unsupported_board when board validation fails."""
    _set_board_info_value(mock_duco_client, unsupported_board_info)

    result = await hass.config_entries.flow.async_init(
        DOMAIN,
        context={"source": SOURCE_ZEROCONF},
        data=ZEROCONF_DISCOVERY,
    )

    assert result["type"] is FlowResultType.ABORT
    assert result["reason"] == "unsupported_board"


@pytest.mark.parametrize("supported_board_info", _SUPPORTED_BOARD_INFOS)
async def test_dhcp_discovery_allows_api_compatible_board_info(
    hass: HomeAssistant,
    mock_duco_client: AsyncMock,
    supported_board_info: BoardInfo,
) -> None:
    """Test DHCP discovery allows boards that expose a compatible API version."""
    _set_board_info_value(mock_duco_client, supported_board_info)

    result = await hass.config_entries.flow.async_init(
        DOMAIN,
        context={"source": SOURCE_DHCP},
        data=DHCP_DISCOVERY,
    )

    assert result["type"] is FlowResultType.FORM
    assert result["step_id"] == "discovery_confirm"
    assert result["description_placeholders"] == {
        "name": str(supported_board_info.box_name)
    }


@pytest.mark.parametrize("unsupported_board_info", UNSUPPORTED_BOARD_INFOS)
async def test_dhcp_discovery_unsupported_board_from_board_info(
    hass: HomeAssistant,
    mock_duco_client: AsyncMock,
    unsupported_board_info: BoardInfo,
) -> None:
    """Test DHCP discovery aborts with unsupported_board when board validation fails."""
    _set_board_info_value(mock_duco_client, unsupported_board_info)

    result = await hass.config_entries.flow.async_init(
        DOMAIN,
        context={"source": SOURCE_DHCP},
        data=DHCP_DISCOVERY,
    )

    assert result["type"] is FlowResultType.ABORT
    assert result["reason"] == "unsupported_board"


@pytest.mark.usefixtures("mock_setup_entry")
async def test_user_flow_initializes_client_with_host(
    hass: HomeAssistant, mock_board_info: BoardInfo, mock_lan_info: LanInfo
) -> None:
    """Test that the config flow initializes the Duco client with the host."""
    with patch(
        "homeassistant.components.duco.config_flow.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
        result = await hass.config_entries.flow.async_init(
            DOMAIN, context={"source": SOURCE_USER}
        )
        result = await hass.config_entries.flow.async_configure(
            result["flow_id"], USER_INPUT
        )

    assert result["type"] is FlowResultType.CREATE_ENTRY
    mock_client_class.assert_called_once_with(
        session=ANY,
        host=TEST_HOST,
    )
