"""Test Google Smart Home."""
from unittest.mock import patch, Mock
import pytest

from homeassistant.core import State, EVENT_CALL_SERVICE
from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, __version__
from homeassistant.setup import async_setup_component
from homeassistant.components import camera
from homeassistant.components.climate.const import (
    ATTR_MIN_TEMP,
    ATTR_MAX_TEMP,
    HVAC_MODE_HEAT,
)
from homeassistant.components.google_assistant import (
    const,
    trait,
    smart_home as sh,
    EVENT_COMMAND_RECEIVED,
    EVENT_QUERY_RECEIVED,
    EVENT_SYNC_RECEIVED,
)
from homeassistant.components.demo.binary_sensor import DemoBinarySensor
from homeassistant.components.demo.cover import DemoCover
from homeassistant.components.demo.light import DemoLight
from homeassistant.components.demo.media_player import AbstractDemoPlayer
from homeassistant.components.demo.switch import DemoSwitch

from homeassistant.helpers import device_registry
from tests.common import (
    mock_device_registry,
    mock_registry,
    mock_area_registry,
    mock_coro,
)

from . import BASIC_CONFIG, MockConfig

REQ_ID = "ff36a3cc-ec34-11e6-b1a0-64510650abcf"


@pytest.fixture
def registries(hass):
    """Registry mock setup."""
    from types import SimpleNamespace

    ret = SimpleNamespace()
    ret.entity = mock_registry(hass)
    ret.device = mock_device_registry(hass)
    ret.area = mock_area_registry(hass)
    return ret


async def test_sync_message(hass):
    """Test a sync message."""
    light = DemoLight(None, "Demo Light", state=False, hs_color=(180, 75))
    light.hass = hass
    light.entity_id = "light.demo_light"
    await light.async_update_ha_state()

    # This should not show up in the sync request
    hass.states.async_set("sensor.no_match", "something")

    # Excluded via config
    hass.states.async_set("light.not_expose", "on")

    config = MockConfig(
        should_expose=lambda state: state.entity_id != "light.not_expose",
        entity_config={
            "light.demo_light": {
                const.CONF_ROOM_HINT: "Living Room",
                const.CONF_ALIASES: ["Hello", "World"],
            }
        },
    )

    events = []
    hass.bus.async_listen(EVENT_SYNC_RECEIVED, events.append)

    result = await sh.async_handle_message(
        hass,
        config,
        "test-agent",
        {"requestId": REQ_ID, "inputs": [{"intent": "action.devices.SYNC"}]},
    )

    assert result == {
        "requestId": REQ_ID,
        "payload": {
            "agentUserId": "test-agent",
            "devices": [
                {
                    "id": "light.demo_light",
                    "name": {"name": "Demo Light", "nicknames": ["Hello", "World"]},
                    "traits": [
                        trait.TRAIT_BRIGHTNESS,
                        trait.TRAIT_ONOFF,
                        trait.TRAIT_COLOR_SETTING,
                    ],
                    "type": const.TYPE_LIGHT,
                    "willReportState": False,
                    "attributes": {
                        "colorModel": "hsv",
                        "colorTemperatureRange": {
                            "temperatureMinK": 2000,
                            "temperatureMaxK": 6535,
                        },
                    },
                    "roomHint": "Living Room",
                }
            ],
        },
    }
    await hass.async_block_till_done()

    assert len(events) == 1
    assert events[0].event_type == EVENT_SYNC_RECEIVED
    assert events[0].data == {"request_id": REQ_ID}


# pylint: disable=redefined-outer-name
async def test_sync_in_area(hass, registries):
    """Test a sync message where room hint comes from area."""
    area = registries.area.async_create("Living Room")

    device = registries.device.async_get_or_create(
        config_entry_id="1234",
        connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
    )
    registries.device.async_update_device(device.id, area_id=area.id)

    entity = registries.entity.async_get_or_create(
        "light", "test", "1235", suggested_object_id="demo_light", device_id=device.id
    )

    light = DemoLight(None, "Demo Light", state=False, hs_color=(180, 75))
    light.hass = hass
    light.entity_id = entity.entity_id
    await light.async_update_ha_state()

    config = MockConfig(should_expose=lambda _: True, entity_config={})

    events = []
    hass.bus.async_listen(EVENT_SYNC_RECEIVED, events.append)

    result = await sh.async_handle_message(
        hass,
        config,
        "test-agent",
        {"requestId": REQ_ID, "inputs": [{"intent": "action.devices.SYNC"}]},
    )

    assert result == {
        "requestId": REQ_ID,
        "payload": {
            "agentUserId": "test-agent",
            "devices": [
                {
                    "id": "light.demo_light",
                    "name": {"name": "Demo Light"},
                    "traits": [
                        trait.TRAIT_BRIGHTNESS,
                        trait.TRAIT_ONOFF,
                        trait.TRAIT_COLOR_SETTING,
                    ],
                    "type": const.TYPE_LIGHT,
                    "willReportState": False,
                    "attributes": {
                        "colorModel": "hsv",
                        "colorTemperatureRange": {
                            "temperatureMinK": 2000,
                            "temperatureMaxK": 6535,
                        },
                    },
                    "roomHint": "Living Room",
                }
            ],
        },
    }
    await hass.async_block_till_done()

    assert len(events) == 1
    assert events[0].event_type == EVENT_SYNC_RECEIVED
    assert events[0].data == {"request_id": REQ_ID}


async def test_query_message(hass):
    """Test a sync message."""
    light = DemoLight(None, "Demo Light", state=False, hs_color=(180, 75))
    light.hass = hass
    light.entity_id = "light.demo_light"
    await light.async_update_ha_state()

    light2 = DemoLight(
        None, "Another Light", state=True, hs_color=(180, 75), ct=400, brightness=78
    )
    light2.hass = hass
    light2.entity_id = "light.another_light"
    await light2.async_update_ha_state()

    events = []
    hass.bus.async_listen(EVENT_QUERY_RECEIVED, events.append)

    result = await sh.async_handle_message(
        hass,
        BASIC_CONFIG,
        "test-agent",
        {
            "requestId": REQ_ID,
            "inputs": [
                {
                    "intent": "action.devices.QUERY",
                    "payload": {
                        "devices": [
                            {"id": "light.demo_light"},
                            {"id": "light.another_light"},
                            {"id": "light.non_existing"},
                        ]
                    },
                }
            ],
        },
    )

    assert result == {
        "requestId": REQ_ID,
        "payload": {
            "devices": {
                "light.non_existing": {"online": False},
                "light.demo_light": {"on": False, "online": True},
                "light.another_light": {
                    "on": True,
                    "online": True,
                    "brightness": 30,
                    "color": {
                        "spectrumHsv": {
                            "hue": 180,
                            "saturation": 0.75,
                            "value": 0.3058823529411765,
                        },
                        "temperatureK": 2500,
                    },
                },
            }
        },
    }

    assert len(events) == 3
    assert events[0].event_type == EVENT_QUERY_RECEIVED
    assert events[0].data == {"request_id": REQ_ID, "entity_id": "light.demo_light"}
    assert events[1].event_type == EVENT_QUERY_RECEIVED
    assert events[1].data == {"request_id": REQ_ID, "entity_id": "light.another_light"}
    assert events[2].event_type == EVENT_QUERY_RECEIVED
    assert events[2].data == {"request_id": REQ_ID, "entity_id": "light.non_existing"}


async def test_execute(hass):
    """Test an execute command."""
    await async_setup_component(hass, "light", {"light": {"platform": "demo"}})

    await hass.services.async_call(
        "light", "turn_off", {"entity_id": "light.ceiling_lights"}, blocking=True
    )

    events = []
    hass.bus.async_listen(EVENT_COMMAND_RECEIVED, events.append)

    service_events = []
    hass.bus.async_listen(EVENT_CALL_SERVICE, service_events.append)

    result = await sh.async_handle_message(
        hass,
        BASIC_CONFIG,
        None,
        {
            "requestId": REQ_ID,
            "inputs": [
                {
                    "intent": "action.devices.EXECUTE",
                    "payload": {
                        "commands": [
                            {
                                "devices": [
                                    {"id": "light.non_existing"},
                                    {"id": "light.ceiling_lights"},
                                ],
                                "execution": [
                                    {
                                        "command": "action.devices.commands.OnOff",
                                        "params": {"on": True},
                                    },
                                    {
                                        "command": "action.devices.commands.BrightnessAbsolute",
                                        "params": {"brightness": 20},
                                    },
                                ],
                            }
                        ]
                    },
                }
            ],
        },
    )

    assert result == {
        "requestId": REQ_ID,
        "payload": {
            "commands": [
                {
                    "ids": ["light.non_existing"],
                    "status": "ERROR",
                    "errorCode": "deviceOffline",
                },
                {
                    "ids": ["light.ceiling_lights"],
                    "status": "SUCCESS",
                    "states": {
                        "on": True,
                        "online": True,
                        "brightness": 20,
                        "color": {
                            "spectrumHsv": {
                                "hue": 56,
                                "saturation": 0.86,
                                "value": 0.2,
                            },
                            "temperatureK": 2631,
                        },
                    },
                },
            ]
        },
    }

    assert len(events) == 4
    assert events[0].event_type == EVENT_COMMAND_RECEIVED
    assert events[0].data == {
        "request_id": REQ_ID,
        "entity_id": "light.non_existing",
        "execution": {
            "command": "action.devices.commands.OnOff",
            "params": {"on": True},
        },
    }
    assert events[1].event_type == EVENT_COMMAND_RECEIVED
    assert events[1].data == {
        "request_id": REQ_ID,
        "entity_id": "light.non_existing",
        "execution": {
            "command": "action.devices.commands.BrightnessAbsolute",
            "params": {"brightness": 20},
        },
    }
    assert events[2].event_type == EVENT_COMMAND_RECEIVED
    assert events[2].data == {
        "request_id": REQ_ID,
        "entity_id": "light.ceiling_lights",
        "execution": {
            "command": "action.devices.commands.OnOff",
            "params": {"on": True},
        },
    }
    assert events[3].event_type == EVENT_COMMAND_RECEIVED
    assert events[3].data == {
        "request_id": REQ_ID,
        "entity_id": "light.ceiling_lights",
        "execution": {
            "command": "action.devices.commands.BrightnessAbsolute",
            "params": {"brightness": 20},
        },
    }

    assert len(service_events) == 2
    assert service_events[0].data == {
        "domain": "light",
        "service": "turn_on",
        "service_data": {"entity_id": "light.ceiling_lights"},
    }
    assert service_events[0].context == events[2].context
    assert service_events[1].data == {
        "domain": "light",
        "service": "turn_on",
        "service_data": {"brightness_pct": 20, "entity_id": "light.ceiling_lights"},
    }
    assert service_events[1].context == events[2].context
    assert service_events[1].context == events[3].context


async def test_raising_error_trait(hass):
    """Test raising an error while executing a trait command."""
    hass.states.async_set(
        "climate.bla",
        HVAC_MODE_HEAT,
        {ATTR_MIN_TEMP: 15, ATTR_MAX_TEMP: 30, ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS},
    )

    events = []
    hass.bus.async_listen(EVENT_COMMAND_RECEIVED, events.append)
    await hass.async_block_till_done()

    result = await sh.async_handle_message(
        hass,
        BASIC_CONFIG,
        "test-agent",
        {
            "requestId": REQ_ID,
            "inputs": [
                {
                    "intent": "action.devices.EXECUTE",
                    "payload": {
                        "commands": [
                            {
                                "devices": [{"id": "climate.bla"}],
                                "execution": [
                                    {
                                        "command": "action.devices.commands."
                                        "ThermostatTemperatureSetpoint",
                                        "params": {"thermostatTemperatureSetpoint": 10},
                                    }
                                ],
                            }
                        ]
                    },
                }
            ],
        },
    )

    assert result == {
        "requestId": REQ_ID,
        "payload": {
            "commands": [
                {
                    "ids": ["climate.bla"],
                    "status": "ERROR",
                    "errorCode": "valueOutOfRange",
                }
            ]
        },
    }

    assert len(events) == 1
    assert events[0].event_type == EVENT_COMMAND_RECEIVED
    assert events[0].data == {
        "request_id": REQ_ID,
        "entity_id": "climate.bla",
        "execution": {
            "command": "action.devices.commands.ThermostatTemperatureSetpoint",
            "params": {"thermostatTemperatureSetpoint": 10},
        },
    }


async def test_serialize_input_boolean(hass):
    """Test serializing an input boolean entity."""
    state = State("input_boolean.bla", "on")
    # pylint: disable=protected-access
    entity = sh.GoogleEntity(hass, BASIC_CONFIG, state)
    result = await entity.sync_serialize()
    assert result == {
        "id": "input_boolean.bla",
        "attributes": {},
        "name": {"name": "bla"},
        "traits": ["action.devices.traits.OnOff"],
        "type": "action.devices.types.SWITCH",
        "willReportState": False,
    }


async def test_unavailable_state_doesnt_sync(hass):
    """Test that an unavailable entity does not sync over."""
    light = DemoLight(None, "Demo Light", state=False)
    light.hass = hass
    light.entity_id = "light.demo_light"
    light._available = False  # pylint: disable=protected-access
    await light.async_update_ha_state()

    result = await sh.async_handle_message(
        hass,
        BASIC_CONFIG,
        "test-agent",
        {"requestId": REQ_ID, "inputs": [{"intent": "action.devices.SYNC"}]},
    )

    assert result == {
        "requestId": REQ_ID,
        "payload": {"agentUserId": "test-agent", "devices": []},
    }


@pytest.mark.parametrize(
    "device_class,google_type",
    [
        ("non_existing_class", "action.devices.types.SWITCH"),
        ("switch", "action.devices.types.SWITCH"),
        ("outlet", "action.devices.types.OUTLET"),
    ],
)
async def test_device_class_switch(hass, device_class, google_type):
    """Test that a cover entity syncs to the correct device type."""
    sensor = DemoSwitch(
        "Demo Sensor",
        state=False,
        icon="mdi:switch",
        assumed=False,
        device_class=device_class,
    )
    sensor.hass = hass
    sensor.entity_id = "switch.demo_sensor"
    await sensor.async_update_ha_state()

    result = await sh.async_handle_message(
        hass,
        BASIC_CONFIG,
        "test-agent",
        {"requestId": REQ_ID, "inputs": [{"intent": "action.devices.SYNC"}]},
    )

    assert result == {
        "requestId": REQ_ID,
        "payload": {
            "agentUserId": "test-agent",
            "devices": [
                {
                    "attributes": {},
                    "id": "switch.demo_sensor",
                    "name": {"name": "Demo Sensor"},
                    "traits": ["action.devices.traits.OnOff"],
                    "type": google_type,
                    "willReportState": False,
                }
            ],
        },
    }


@pytest.mark.parametrize(
    "device_class,google_type",
    [
        ("door", "action.devices.types.DOOR"),
        ("garage_door", "action.devices.types.GARAGE"),
        ("lock", "action.devices.types.SENSOR"),
        ("opening", "action.devices.types.SENSOR"),
        ("window", "action.devices.types.SENSOR"),
    ],
)
async def test_device_class_binary_sensor(hass, device_class, google_type):
    """Test that a binary entity syncs to the correct device type."""
    sensor = DemoBinarySensor("Demo Sensor", state=False, device_class=device_class)
    sensor.hass = hass
    sensor.entity_id = "binary_sensor.demo_sensor"
    await sensor.async_update_ha_state()

    result = await sh.async_handle_message(
        hass,
        BASIC_CONFIG,
        "test-agent",
        {"requestId": REQ_ID, "inputs": [{"intent": "action.devices.SYNC"}]},
    )

    assert result == {
        "requestId": REQ_ID,
        "payload": {
            "agentUserId": "test-agent",
            "devices": [
                {
                    "attributes": {"queryOnlyOpenClose": True},
                    "id": "binary_sensor.demo_sensor",
                    "name": {"name": "Demo Sensor"},
                    "traits": ["action.devices.traits.OpenClose"],
                    "type": google_type,
                    "willReportState": False,
                }
            ],
        },
    }


@pytest.mark.parametrize(
    "device_class,google_type",
    [
        ("non_existing_class", "action.devices.types.BLINDS"),
        ("door", "action.devices.types.DOOR"),
        ("garage", "action.devices.types.GARAGE"),
    ],
)
async def test_device_class_cover(hass, device_class, google_type):
    """Test that a binary entity syncs to the correct device type."""
    sensor = DemoCover(hass, "Demo Sensor", device_class=device_class)
    sensor.hass = hass
    sensor.entity_id = "cover.demo_sensor"
    await sensor.async_update_ha_state()

    result = await sh.async_handle_message(
        hass,
        BASIC_CONFIG,
        "test-agent",
        {"requestId": REQ_ID, "inputs": [{"intent": "action.devices.SYNC"}]},
    )

    assert result == {
        "requestId": REQ_ID,
        "payload": {
            "agentUserId": "test-agent",
            "devices": [
                {
                    "attributes": {},
                    "id": "cover.demo_sensor",
                    "name": {"name": "Demo Sensor"},
                    "traits": ["action.devices.traits.OpenClose"],
                    "type": google_type,
                    "willReportState": False,
                }
            ],
        },
    }


@pytest.mark.parametrize(
    "device_class,google_type",
    [
        ("non_existing_class", "action.devices.types.SWITCH"),
        ("speaker", "action.devices.types.SPEAKER"),
        ("tv", "action.devices.types.TV"),
    ],
)
async def test_device_media_player(hass, device_class, google_type):
    """Test that a binary entity syncs to the correct device type."""
    sensor = AbstractDemoPlayer("Demo", device_class=device_class)
    sensor.hass = hass
    sensor.entity_id = "media_player.demo"
    await sensor.async_update_ha_state()

    result = await sh.async_handle_message(
        hass,
        BASIC_CONFIG,
        "test-agent",
        {"requestId": REQ_ID, "inputs": [{"intent": "action.devices.SYNC"}]},
    )

    assert result == {
        "requestId": REQ_ID,
        "payload": {
            "agentUserId": "test-agent",
            "devices": [
                {
                    "attributes": {},
                    "id": sensor.entity_id,
                    "name": {"name": sensor.name},
                    "traits": ["action.devices.traits.OnOff"],
                    "type": google_type,
                    "willReportState": False,
                }
            ],
        },
    }


async def test_query_disconnect(hass):
    """Test a disconnect message."""
    config = MockConfig(hass=hass)
    config.async_enable_report_state()
    assert config._unsub_report_state is not None
    with patch.object(
        config, "async_deactivate_report_state", side_effect=mock_coro
    ) as mock_deactivate:
        result = await sh.async_handle_message(
            hass,
            config,
            "test-agent",
            {"inputs": [{"intent": "action.devices.DISCONNECT"}], "requestId": REQ_ID},
        )
    assert result is None
    assert len(mock_deactivate.mock_calls) == 1


async def test_trait_execute_adding_query_data(hass):
    """Test a trait execute influencing query data."""
    hass.config.api = Mock(base_url="http://1.1.1.1:8123")
    hass.states.async_set(
        "camera.office", "idle", {"supported_features": camera.SUPPORT_STREAM}
    )

    with patch(
        "homeassistant.components.camera.async_request_stream",
        return_value=mock_coro("/api/streams/bla"),
    ):
        result = await sh.async_handle_message(
            hass,
            BASIC_CONFIG,
            None,
            {
                "requestId": REQ_ID,
                "inputs": [
                    {
                        "intent": "action.devices.EXECUTE",
                        "payload": {
                            "commands": [
                                {
                                    "devices": [{"id": "camera.office"}],
                                    "execution": [
                                        {
                                            "command": "action.devices.commands.GetCameraStream",
                                            "params": {
                                                "StreamToChromecast": True,
                                                "SupportedStreamProtocols": [
                                                    "progressive_mp4",
                                                    "hls",
                                                    "dash",
                                                    "smooth_stream",
                                                ],
                                            },
                                        }
                                    ],
                                }
                            ]
                        },
                    }
                ],
            },
        )

    assert result == {
        "requestId": REQ_ID,
        "payload": {
            "commands": [
                {
                    "ids": ["camera.office"],
                    "status": "SUCCESS",
                    "states": {
                        "online": True,
                        "cameraStreamAccessUrl": "http://1.1.1.1:8123/api/streams/bla",
                    },
                }
            ]
        },
    }


async def test_identify(hass):
    """Test identify message."""
    result = await sh.async_handle_message(
        hass,
        BASIC_CONFIG,
        None,
        {
            "requestId": REQ_ID,
            "inputs": [
                {
                    "intent": "action.devices.IDENTIFY",
                    "payload": {
                        "device": {
                            "mdnsScanData": {
                                "additionals": [
                                    {
                                        "type": "TXT",
                                        "class": "IN",
                                        "name": "devhome._home-assistant._tcp.local",
                                        "ttl": 4500,
                                        "data": [
                                            "version=0.101.0.dev0",
                                            "base_url=http://192.168.1.101:8123",
                                            "requires_api_password=true",
                                        ],
                                    }
                                ]
                            }
                        },
                        "structureData": {},
                    },
                }
            ],
            "devices": [
                {
                    "id": "light.ceiling_lights",
                    "customData": {
                        "httpPort": 8123,
                        "httpSSL": False,
                        "proxyDeviceId": BASIC_CONFIG.agent_user_id,
                        "webhookId": "dde3b9800a905e886cc4d38e226a6e7e3f2a6993d2b9b9f63d13e42ee7de3219",
                    },
                }
            ],
        },
    )

    assert result == {
        "requestId": REQ_ID,
        "payload": {
            "device": {
                "id": BASIC_CONFIG.agent_user_id,
                "isLocalOnly": True,
                "isProxy": True,
                "deviceInfo": {
                    "hwVersion": "UNKNOWN_HW_VERSION",
                    "manufacturer": "Home Assistant",
                    "model": "Home Assistant",
                    "swVersion": __version__,
                },
            }
        },
    }


async def test_reachable_devices(hass):
    """Test REACHABLE_DEVICES intent."""
    # Matching passed in device.
    hass.states.async_set("light.ceiling_lights", "on")

    # Unsupported entity
    hass.states.async_set("not_supported.entity", "something")

    # Excluded via config
    hass.states.async_set("light.not_expose", "on")

    # Not passed in as google_id
    hass.states.async_set("light.not_mentioned", "on")

    config = MockConfig(
        should_expose=lambda state: state.entity_id != "light.not_expose"
    )

    result = await sh.async_handle_message(
        hass,
        config,
        None,
        {
            "requestId": REQ_ID,
            "inputs": [
                {
                    "intent": "action.devices.REACHABLE_DEVICES",
                    "payload": {
                        "device": {
                            "proxyDevice": {
                                "id": "6a04f0f7-6125-4356-a846-861df7e01497",
                                "customData": "{}",
                                "proxyData": "{}",
                            }
                        },
                        "structureData": {},
                    },
                }
            ],
            "devices": [
                {
                    "id": "light.ceiling_lights",
                    "customData": {
                        "httpPort": 8123,
                        "httpSSL": False,
                        "proxyDeviceId": BASIC_CONFIG.agent_user_id,
                        "webhookId": "dde3b9800a905e886cc4d38e226a6e7e3f2a6993d2b9b9f63d13e42ee7de3219",
                    },
                },
                {
                    "id": "light.not_expose",
                    "customData": {
                        "httpPort": 8123,
                        "httpSSL": False,
                        "proxyDeviceId": BASIC_CONFIG.agent_user_id,
                        "webhookId": "dde3b9800a905e886cc4d38e226a6e7e3f2a6993d2b9b9f63d13e42ee7de3219",
                    },
                },
                {"id": BASIC_CONFIG.agent_user_id, "customData": {}},
            ],
        },
    )

    assert result == {
        "requestId": REQ_ID,
        "payload": {"devices": [{"verificationId": "light.ceiling_lights"}]},
    }
