"""Test deCONZ component setup process."""
from unittest.mock import Mock, patch

import asyncio
import pytest
import voluptuous as vol

from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.setup import async_setup_component
from homeassistant.components import deconz

from tests.common import mock_coro, MockConfigEntry

ENTRY1_HOST = "1.2.3.4"
ENTRY1_PORT = 80
ENTRY1_API_KEY = "1234567890ABCDEF"
ENTRY1_BRIDGEID = "12345ABC"

ENTRY2_HOST = "2.3.4.5"
ENTRY2_PORT = 80
ENTRY2_API_KEY = "1234567890ABCDEF"
ENTRY2_BRIDGEID = "23456DEF"


async def setup_entry(hass, entry):
    """Test that setup entry works."""
    with patch.object(
        deconz.DeconzGateway, "async_setup", return_value=mock_coro(True)
    ), patch.object(
        deconz.DeconzGateway,
        "async_update_device_registry",
        return_value=mock_coro(True),
    ):
        assert await deconz.async_setup_entry(hass, entry) is True


async def test_config_with_host_passed_to_config_entry(hass):
    """Test that configured options for a host are loaded via config entry."""
    with patch.object(hass.config_entries, "flow") as mock_config_flow:
        assert (
            await async_setup_component(
                hass,
                deconz.DOMAIN,
                {
                    deconz.DOMAIN: {
                        deconz.CONF_HOST: ENTRY1_HOST,
                        deconz.CONF_PORT: ENTRY1_PORT,
                    }
                },
            )
            is True
        )
    # Import flow started
    assert len(mock_config_flow.mock_calls) == 1


async def test_config_without_host_not_passed_to_config_entry(hass):
    """Test that a configuration without a host does not initiate an import."""
    MockConfigEntry(domain=deconz.DOMAIN, data={}).add_to_hass(hass)
    with patch.object(hass.config_entries, "flow") as mock_config_flow:
        assert (
            await async_setup_component(hass, deconz.DOMAIN, {deconz.DOMAIN: {}})
            is True
        )
    # No flow started
    assert len(mock_config_flow.mock_calls) == 0


async def test_config_import_entry_fails_when_entries_exist(hass):
    """Test that an already registered host does not initiate an import."""
    MockConfigEntry(domain=deconz.DOMAIN, data={}).add_to_hass(hass)
    with patch.object(hass.config_entries, "flow") as mock_config_flow:
        assert (
            await async_setup_component(
                hass,
                deconz.DOMAIN,
                {
                    deconz.DOMAIN: {
                        deconz.CONF_HOST: ENTRY1_HOST,
                        deconz.CONF_PORT: ENTRY1_PORT,
                    }
                },
            )
            is True
        )
    # No flow started
    assert len(mock_config_flow.mock_calls) == 0


async def test_config_discovery(hass):
    """Test that a discovered bridge does not initiate an import."""
    with patch.object(hass, "config_entries") as mock_config_entries:
        assert await async_setup_component(hass, deconz.DOMAIN, {}) is True
    # No flow started
    assert len(mock_config_entries.flow.mock_calls) == 0


async def test_setup_entry_fails(hass):
    """Test setup entry fails if deCONZ is not available."""
    entry = Mock()
    entry.data = {
        deconz.CONF_HOST: ENTRY1_HOST,
        deconz.CONF_PORT: ENTRY1_PORT,
        deconz.CONF_API_KEY: ENTRY1_API_KEY,
    }
    with patch("pydeconz.DeconzSession.async_load_parameters", side_effect=Exception):
        await deconz.async_setup_entry(hass, entry)


async def test_setup_entry_no_available_bridge(hass):
    """Test setup entry fails if deCONZ is not available."""
    entry = Mock()
    entry.data = {
        deconz.CONF_HOST: ENTRY1_HOST,
        deconz.CONF_PORT: ENTRY1_PORT,
        deconz.CONF_API_KEY: ENTRY1_API_KEY,
    }
    with patch(
        "pydeconz.DeconzSession.async_load_parameters", side_effect=asyncio.TimeoutError
    ), pytest.raises(ConfigEntryNotReady):
        await deconz.async_setup_entry(hass, entry)


async def test_setup_entry_successful(hass):
    """Test setup entry is successful."""
    entry = MockConfigEntry(
        domain=deconz.DOMAIN,
        data={
            deconz.CONF_HOST: ENTRY1_HOST,
            deconz.CONF_PORT: ENTRY1_PORT,
            deconz.CONF_API_KEY: ENTRY1_API_KEY,
            deconz.CONF_BRIDGEID: ENTRY1_BRIDGEID,
        },
    )
    entry.add_to_hass(hass)

    await setup_entry(hass, entry)

    assert ENTRY1_BRIDGEID in hass.data[deconz.DOMAIN]
    assert hass.data[deconz.DOMAIN][ENTRY1_BRIDGEID].master


async def test_setup_entry_multiple_gateways(hass):
    """Test setup entry is successful with multiple gateways."""
    entry = MockConfigEntry(
        domain=deconz.DOMAIN,
        data={
            deconz.CONF_HOST: ENTRY1_HOST,
            deconz.CONF_PORT: ENTRY1_PORT,
            deconz.CONF_API_KEY: ENTRY1_API_KEY,
            deconz.CONF_BRIDGEID: ENTRY1_BRIDGEID,
        },
    )
    entry.add_to_hass(hass)

    entry2 = MockConfigEntry(
        domain=deconz.DOMAIN,
        data={
            deconz.CONF_HOST: ENTRY2_HOST,
            deconz.CONF_PORT: ENTRY2_PORT,
            deconz.CONF_API_KEY: ENTRY2_API_KEY,
            deconz.CONF_BRIDGEID: ENTRY2_BRIDGEID,
        },
    )
    entry2.add_to_hass(hass)

    await setup_entry(hass, entry)
    await setup_entry(hass, entry2)

    assert ENTRY1_BRIDGEID in hass.data[deconz.DOMAIN]
    assert hass.data[deconz.DOMAIN][ENTRY1_BRIDGEID].master
    assert ENTRY2_BRIDGEID in hass.data[deconz.DOMAIN]
    assert not hass.data[deconz.DOMAIN][ENTRY2_BRIDGEID].master


async def test_unload_entry(hass):
    """Test being able to unload an entry."""
    entry = MockConfigEntry(
        domain=deconz.DOMAIN,
        data={
            deconz.CONF_HOST: ENTRY1_HOST,
            deconz.CONF_PORT: ENTRY1_PORT,
            deconz.CONF_API_KEY: ENTRY1_API_KEY,
            deconz.CONF_BRIDGEID: ENTRY1_BRIDGEID,
        },
    )
    entry.add_to_hass(hass)

    await setup_entry(hass, entry)

    with patch.object(
        deconz.DeconzGateway, "async_reset", return_value=mock_coro(True)
    ):
        assert await deconz.async_unload_entry(hass, entry)

    assert not hass.data[deconz.DOMAIN]


async def test_unload_entry_multiple_gateways(hass):
    """Test being able to unload an entry and master gateway gets moved."""
    entry = MockConfigEntry(
        domain=deconz.DOMAIN,
        data={
            deconz.CONF_HOST: ENTRY1_HOST,
            deconz.CONF_PORT: ENTRY1_PORT,
            deconz.CONF_API_KEY: ENTRY1_API_KEY,
            deconz.CONF_BRIDGEID: ENTRY1_BRIDGEID,
        },
    )
    entry.add_to_hass(hass)

    entry2 = MockConfigEntry(
        domain=deconz.DOMAIN,
        data={
            deconz.CONF_HOST: ENTRY2_HOST,
            deconz.CONF_PORT: ENTRY2_PORT,
            deconz.CONF_API_KEY: ENTRY2_API_KEY,
            deconz.CONF_BRIDGEID: ENTRY2_BRIDGEID,
        },
    )
    entry2.add_to_hass(hass)

    await setup_entry(hass, entry)
    await setup_entry(hass, entry2)

    with patch.object(
        deconz.DeconzGateway, "async_reset", return_value=mock_coro(True)
    ):
        assert await deconz.async_unload_entry(hass, entry)

    assert ENTRY2_BRIDGEID in hass.data[deconz.DOMAIN]
    assert hass.data[deconz.DOMAIN][ENTRY2_BRIDGEID].master


async def test_service_configure(hass):
    """Test that service invokes pydeconz with the correct path and data."""
    entry = MockConfigEntry(
        domain=deconz.DOMAIN,
        data={
            deconz.CONF_HOST: ENTRY1_HOST,
            deconz.CONF_PORT: ENTRY1_PORT,
            deconz.CONF_API_KEY: ENTRY1_API_KEY,
            deconz.CONF_BRIDGEID: ENTRY1_BRIDGEID,
        },
    )
    entry.add_to_hass(hass)

    await setup_entry(hass, entry)

    hass.data[deconz.DOMAIN][ENTRY1_BRIDGEID].deconz_ids = {"light.test": "/light/1"}
    data = {"on": True, "attr1": 10, "attr2": 20}

    # only field
    with patch("pydeconz.DeconzSession.async_put_state", return_value=mock_coro(True)):
        await hass.services.async_call(
            "deconz", "configure", service_data={"field": "/light/42", "data": data}
        )
        await hass.async_block_till_done()

    # only entity
    with patch("pydeconz.DeconzSession.async_put_state", return_value=mock_coro(True)):
        await hass.services.async_call(
            "deconz", "configure", service_data={"entity": "light.test", "data": data}
        )
        await hass.async_block_till_done()

    # entity + field
    with patch("pydeconz.DeconzSession.async_put_state", return_value=mock_coro(True)):
        await hass.services.async_call(
            "deconz",
            "configure",
            service_data={"entity": "light.test", "field": "/state", "data": data},
        )
        await hass.async_block_till_done()

    # non-existing entity (or not from deCONZ)
    with patch("pydeconz.DeconzSession.async_put_state", return_value=mock_coro(True)):
        await hass.services.async_call(
            "deconz",
            "configure",
            service_data={
                "entity": "light.nonexisting",
                "field": "/state",
                "data": data,
            },
        )
        await hass.async_block_till_done()

    # field does not start with /
    with pytest.raises(vol.Invalid):
        with patch(
            "pydeconz.DeconzSession.async_put_state", return_value=mock_coro(True)
        ):
            await hass.services.async_call(
                "deconz",
                "configure",
                service_data={"entity": "light.test", "field": "state", "data": data},
            )
            await hass.async_block_till_done()


async def test_service_refresh_devices(hass):
    """Test that service can refresh devices."""
    entry = MockConfigEntry(
        domain=deconz.DOMAIN,
        data={
            deconz.CONF_HOST: ENTRY1_HOST,
            deconz.CONF_PORT: ENTRY1_PORT,
            deconz.CONF_API_KEY: ENTRY1_API_KEY,
            deconz.CONF_BRIDGEID: ENTRY1_BRIDGEID,
        },
    )
    entry.add_to_hass(hass)

    await setup_entry(hass, entry)

    with patch(
        "pydeconz.DeconzSession.async_load_parameters", return_value=mock_coro(True)
    ):
        await hass.services.async_call("deconz", "device_refresh", service_data={})
        await hass.async_block_till_done()

    with patch(
        "pydeconz.DeconzSession.async_load_parameters", return_value=mock_coro(False)
    ):
        await hass.services.async_call("deconz", "device_refresh", service_data={})
        await hass.async_block_till_done()