"""Configuration flow tests for the Tailwind integration."""

from ipaddress import ip_address
from unittest.mock import MagicMock

from gotailwind import (
    TailwindAuthenticationError,
    TailwindConnectionError,
    TailwindUnsupportedFirmwareVersionError,
)
import pytest
from syrupy.assertion import SnapshotAssertion

from homeassistant.components.tailwind.const import DOMAIN
from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER, SOURCE_ZEROCONF
from homeassistant.const import CONF_HOST, CONF_TOKEN
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 tests.common import MockConfigEntry

pytestmark = pytest.mark.usefixtures("mock_setup_entry")


@pytest.mark.usefixtures("mock_tailwind")
async def test_user_flow(hass: HomeAssistant) -> None:
    """Test the full happy path user flow from start to finish."""
    result = await hass.config_entries.flow.async_init(
        DOMAIN,
        context={"source": SOURCE_USER},
    )

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

    result = await hass.config_entries.flow.async_configure(
        result["flow_id"],
        user_input={
            CONF_HOST: "127.0.0.1",
            CONF_TOKEN: "987654",
        },
    )

    assert result["type"] is FlowResultType.CREATE_ENTRY

    config_entry = result["result"]
    assert config_entry.unique_id == "3c:e9:0e:6d:21:84"
    assert config_entry.data == {
        CONF_HOST: "127.0.0.1",
        CONF_TOKEN: "987654",
    }
    assert not config_entry.options


@pytest.mark.parametrize(
    ("side_effect", "expected_error"),
    [
        (TailwindConnectionError, {CONF_HOST: "cannot_connect"}),
        (TailwindAuthenticationError, {CONF_TOKEN: "invalid_auth"}),
        (Exception, {"base": "unknown"}),
    ],
)
async def test_user_flow_errors(
    hass: HomeAssistant,
    mock_tailwind: MagicMock,
    side_effect: Exception,
    expected_error: dict[str, str],
) -> None:
    """Test we show user form on a connection error."""
    mock_tailwind.status.side_effect = side_effect

    result = await hass.config_entries.flow.async_init(
        DOMAIN,
        context={"source": SOURCE_USER},
        data={
            CONF_HOST: "127.0.0.1",
            CONF_TOKEN: "987654",
        },
    )

    assert result["type"] is FlowResultType.FORM
    assert result["step_id"] == "user"
    assert result["errors"] == expected_error

    mock_tailwind.status.side_effect = None
    result = await hass.config_entries.flow.async_configure(
        result["flow_id"],
        user_input={
            CONF_HOST: "127.0.0.2",
            CONF_TOKEN: "123456",
        },
    )
    assert result["type"] is FlowResultType.CREATE_ENTRY

    config_entry = result["result"]
    assert config_entry.unique_id == "3c:e9:0e:6d:21:84"
    assert config_entry.data == {
        CONF_HOST: "127.0.0.2",
        CONF_TOKEN: "123456",
    }
    assert not config_entry.options


async def test_user_flow_unsupported_firmware_version(
    hass: HomeAssistant, mock_tailwind: MagicMock
) -> None:
    """Test configuration flow aborts when the firmware version is not supported."""
    mock_tailwind.status.side_effect = TailwindUnsupportedFirmwareVersionError
    result = await hass.config_entries.flow.async_init(
        DOMAIN,
        context={"source": SOURCE_USER},
        data={
            CONF_HOST: "127.0.0.1",
            CONF_TOKEN: "987654",
        },
    )

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


@pytest.mark.usefixtures("mock_tailwind")
async def test_user_flow_already_configured(
    hass: HomeAssistant, mock_config_entry: MockConfigEntry
) -> None:
    """Test configuration flow aborts when the device is already configured.

    Also, ensures the existing config entry is updated with the new host.
    """
    mock_config_entry.add_to_hass(hass)
    assert mock_config_entry.data[CONF_HOST] == "127.0.0.127"

    result = await hass.config_entries.flow.async_init(
        DOMAIN,
        context={"source": SOURCE_USER},
        data={
            CONF_HOST: "127.0.0.1",
            CONF_TOKEN: "987654",
        },
    )

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


@pytest.mark.usefixtures("mock_tailwind")
async def test_zeroconf_flow(
    hass: HomeAssistant,
    snapshot: SnapshotAssertion,
) -> None:
    """Test the zeroconf happy flow from start to finish."""
    result = await hass.config_entries.flow.async_init(
        DOMAIN,
        context={"source": SOURCE_ZEROCONF},
        data=ZeroconfServiceInfo(
            ip_address=ip_address("127.0.0.1"),
            ip_addresses=[ip_address("127.0.0.1")],
            port=80,
            hostname="tailwind-3ce90e6d2184.local.",
            name="mock_name",
            properties={
                "device_id": "_3c_e9_e_6d_21_84_",
                "product": "iQ3",
                "SW ver": "10.10",
                "vendor": "tailwind",
            },
            type="mock_type",
        ),
    )

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

    progress = hass.config_entries.flow.async_progress()
    assert len(progress) == 1
    assert progress[0].get("flow_id") == result["flow_id"]

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

    assert result["type"] is FlowResultType.CREATE_ENTRY

    config_entry = result["result"]
    assert config_entry.unique_id == "3c:e9:0e:6d:21:84"
    assert config_entry.data == {
        CONF_HOST: "127.0.0.1",
        CONF_TOKEN: "987654",
    }
    assert not config_entry.options


@pytest.mark.parametrize(
    ("properties", "expected_reason"),
    [
        ({"SW ver": "10.10"}, "no_device_id"),
        ({"device_id": "_3c_e9_e_6d_21_84_", "SW ver": "0.0"}, "unsupported_firmware"),
    ],
)
async def test_zeroconf_flow_abort_incompatible_properties(
    hass: HomeAssistant, properties: dict[str, str], expected_reason: str
) -> None:
    """Test the zeroconf aborts when it advertises incompatible data."""
    result = await hass.config_entries.flow.async_init(
        DOMAIN,
        context={"source": SOURCE_ZEROCONF},
        data=ZeroconfServiceInfo(
            ip_address=ip_address("127.0.0.1"),
            ip_addresses=[ip_address("127.0.0.1")],
            port=80,
            hostname="tailwind-3ce90e6d2184.local.",
            name="mock_name",
            properties=properties,
            type="mock_type",
        ),
    )

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


@pytest.mark.parametrize(
    ("side_effect", "expected_error"),
    [
        (TailwindConnectionError, {"base": "cannot_connect"}),
        (TailwindAuthenticationError, {CONF_TOKEN: "invalid_auth"}),
        (Exception, {"base": "unknown"}),
    ],
)
async def test_zeroconf_flow_errors(
    hass: HomeAssistant,
    mock_tailwind: MagicMock,
    side_effect: Exception,
    expected_error: dict[str, str],
) -> None:
    """Test we show form on a error."""
    mock_tailwind.status.side_effect = side_effect

    result = await hass.config_entries.flow.async_init(
        DOMAIN,
        context={"source": SOURCE_ZEROCONF},
        data=ZeroconfServiceInfo(
            ip_address=ip_address("127.0.0.1"),
            ip_addresses=[ip_address("127.0.0.1")],
            port=80,
            hostname="tailwind-3ce90e6d2184.local.",
            name="mock_name",
            properties={
                "device_id": "_3c_e9_e_6d_21_84_",
                "product": "iQ3",
                "SW ver": "10.10",
                "vendor": "tailwind",
            },
            type="mock_type",
        ),
    )

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

    assert result["type"] is FlowResultType.FORM
    assert result["step_id"] == "zeroconf_confirm"
    assert result["errors"] == expected_error

    mock_tailwind.status.side_effect = None
    result = await hass.config_entries.flow.async_configure(
        result["flow_id"],
        user_input={
            CONF_TOKEN: "123456",
        },
    )
    assert result["type"] is FlowResultType.CREATE_ENTRY

    config_entry = result["result"]
    assert config_entry.unique_id == "3c:e9:0e:6d:21:84"
    assert config_entry.data == {
        CONF_HOST: "127.0.0.1",
        CONF_TOKEN: "123456",
    }
    assert not config_entry.options


@pytest.mark.usefixtures("mock_tailwind")
async def test_zeroconf_flow_not_discovered_again(
    hass: HomeAssistant,
    mock_config_entry: MockConfigEntry,
) -> None:
    """Test the zeroconf doesn't re-discover an existing device.

    Also, ensures the existing config entry is updated with the new host.
    """
    mock_config_entry.add_to_hass(hass)
    assert mock_config_entry.data[CONF_HOST] == "127.0.0.127"

    result = await hass.config_entries.flow.async_init(
        DOMAIN,
        context={"source": SOURCE_ZEROCONF},
        data=ZeroconfServiceInfo(
            ip_address=ip_address("127.0.0.1"),
            ip_addresses=[ip_address("127.0.0.1")],
            port=80,
            hostname="tailwind-3ce90e6d2184.local.",
            name="mock_name",
            properties={
                "device_id": "_3c_e9_e_6d_21_84_",
                "product": "iQ3",
                "SW ver": "10.10",
                "vendor": "tailwind",
            },
            type="mock_type",
        ),
    )

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


@pytest.mark.usefixtures("mock_tailwind")
async def test_reauth_flow(
    hass: HomeAssistant,
    mock_config_entry: MockConfigEntry,
) -> None:
    """Test the reauthentication configuration flow."""
    mock_config_entry.add_to_hass(hass)
    assert mock_config_entry.data[CONF_TOKEN] == "123456"

    result = await mock_config_entry.start_reauth_flow(hass)
    assert result["type"] is FlowResultType.FORM
    assert result["step_id"] == "reauth_confirm"

    result = await hass.config_entries.flow.async_configure(
        result["flow_id"],
        {CONF_TOKEN: "987654"},
    )
    await hass.async_block_till_done()

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

    assert mock_config_entry.data[CONF_TOKEN] == "987654"


@pytest.mark.parametrize(
    ("side_effect", "expected_error"),
    [
        (TailwindConnectionError, {"base": "cannot_connect"}),
        (TailwindAuthenticationError, {CONF_TOKEN: "invalid_auth"}),
        (Exception, {"base": "unknown"}),
    ],
)
async def test_reauth_flow_errors(
    hass: HomeAssistant,
    mock_config_entry: MockConfigEntry,
    mock_tailwind: MagicMock,
    side_effect: Exception,
    expected_error: dict[str, str],
) -> None:
    """Test we show form on a error."""
    mock_config_entry.add_to_hass(hass)
    mock_tailwind.status.side_effect = side_effect

    result = await mock_config_entry.start_reauth_flow(hass)

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

    assert result["type"] is FlowResultType.FORM
    assert result["step_id"] == "reauth_confirm"
    assert result["errors"] == expected_error

    mock_tailwind.status.side_effect = None
    result = await hass.config_entries.flow.async_configure(
        result["flow_id"],
        user_input={
            CONF_TOKEN: "123456",
        },
    )

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


async def test_dhcp_discovery_updates_entry(
    hass: HomeAssistant,
    mock_config_entry: MockConfigEntry,
) -> None:
    """Test DHCP discovery updates config entries."""
    mock_config_entry.add_to_hass(hass)
    assert mock_config_entry.data[CONF_HOST] == "127.0.0.127"

    result = await hass.config_entries.flow.async_init(
        DOMAIN,
        context={"source": SOURCE_DHCP},
        data=DhcpServiceInfo(
            hostname="tailwind-3ce90e6d2184.local.",
            ip="127.0.0.1",
            macaddress="3ce90e6d2184",
        ),
    )

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


async def test_dhcp_discovery_ignores_unknown(hass: HomeAssistant) -> None:
    """Test DHCP discovery is only used for updates.

    Anything else will just abort the flow.
    """
    result = await hass.config_entries.flow.async_init(
        DOMAIN,
        context={"source": SOURCE_DHCP},
        data=DhcpServiceInfo(
            hostname="tailwind-3ce90e6d2184.local.",
            ip="127.0.0.1",
            macaddress="3ce90e6d2184",
        ),
    )

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