"""Test for smart home alexa support."""
import json
from uuid import uuid4

import pytest

from homeassistant.core import Context, callback
from homeassistant.const import (
    TEMP_CELSIUS, TEMP_FAHRENHEIT, STATE_LOCKED,
    STATE_UNLOCKED, STATE_UNKNOWN)
from homeassistant.setup import async_setup_component
from homeassistant.components import alexa
from homeassistant.components.alexa import smart_home
from homeassistant.helpers import entityfilter

from tests.common import async_mock_service

DEFAULT_CONFIG = smart_home.Config(should_expose=lambda entity_id: True)


@pytest.fixture
def events(hass):
    """Fixture that catches alexa events."""
    events = []
    hass.bus.async_listen(
        smart_home.EVENT_ALEXA_SMART_HOME,
        callback(lambda e: events.append(e))
    )
    yield events


def get_new_request(namespace, name, endpoint=None):
    """Generate a new API message."""
    raw_msg = {
        'directive': {
            'header': {
                'namespace': namespace,
                'name': name,
                'messageId': str(uuid4()),
                'correlationToken': str(uuid4()),
                'payloadVersion': '3',
            },
            'endpoint': {
                'scope': {
                    'type': 'BearerToken',
                    'token': str(uuid4()),
                },
                'endpointId': endpoint,
            },
            'payload': {},
        }
    }

    if not endpoint:
        raw_msg['directive'].pop('endpoint')

    return raw_msg


def test_create_api_message_defaults():
    """Create a API message response of a request with defaults."""
    request = get_new_request('Alexa.PowerController', 'TurnOn', 'switch#xy')
    request = request['directive']

    msg = smart_home.api_message(request, payload={'test': 3})

    assert 'event' in msg
    msg = msg['event']

    assert msg['header']['messageId'] is not None
    assert msg['header']['messageId'] != request['header']['messageId']
    assert msg['header']['correlationToken'] == \
        request['header']['correlationToken']
    assert msg['header']['name'] == 'Response'
    assert msg['header']['namespace'] == 'Alexa'
    assert msg['header']['payloadVersion'] == '3'

    assert 'test' in msg['payload']
    assert msg['payload']['test'] == 3

    assert msg['endpoint'] == request['endpoint']


def test_create_api_message_special():
    """Create a API message response of a request with non defaults."""
    request = get_new_request('Alexa.PowerController', 'TurnOn')
    request = request['directive']

    request['header'].pop('correlationToken')

    msg = smart_home.api_message(request, 'testName', 'testNameSpace')

    assert 'event' in msg
    msg = msg['event']

    assert msg['header']['messageId'] is not None
    assert msg['header']['messageId'] != request['header']['messageId']
    assert 'correlationToken' not in msg['header']
    assert msg['header']['name'] == 'testName'
    assert msg['header']['namespace'] == 'testNameSpace'
    assert msg['header']['payloadVersion'] == '3'

    assert msg['payload'] == {}
    assert 'endpoint' not in msg


async def test_wrong_version(hass):
    """Test with wrong version."""
    msg = get_new_request('Alexa.PowerController', 'TurnOn')
    msg['directive']['header']['payloadVersion'] = '2'

    with pytest.raises(AssertionError):
        await smart_home.async_handle_message(hass, DEFAULT_CONFIG, msg)


async def discovery_test(device, hass, expected_endpoints=1):
    """Test alexa discovery request."""
    request = get_new_request('Alexa.Discovery', 'Discover')

    # setup test devices
    hass.states.async_set(*device)

    msg = await smart_home.async_handle_message(
        hass, DEFAULT_CONFIG, request)

    assert 'event' in msg
    msg = msg['event']

    assert msg['header']['name'] == 'Discover.Response'
    assert msg['header']['namespace'] == 'Alexa.Discovery'
    endpoints = msg['payload']['endpoints']
    assert len(endpoints) == expected_endpoints

    if expected_endpoints == 1:
        return endpoints[0]
    if expected_endpoints > 1:
        return endpoints
    return None


def assert_endpoint_capabilities(endpoint, *interfaces):
    """Assert the endpoint supports the given interfaces.

    Returns a set of capabilities, in case you want to assert more things about
    them.
    """
    capabilities = endpoint['capabilities']
    supported = set(
        feature['interface']
        for feature in capabilities)

    assert supported == set(interfaces)
    return capabilities


async def test_switch(hass, events):
    """Test switch discovery."""
    device = ('switch.test', 'on', {'friendly_name': "Test switch"})
    appliance = await discovery_test(device, hass)

    assert appliance['endpointId'] == 'switch#test'
    assert appliance['displayCategories'][0] == "SWITCH"
    assert appliance['friendlyName'] == "Test switch"
    assert_endpoint_capabilities(appliance, 'Alexa.PowerController')

    await assert_power_controller_works(
        'switch#test',
        'switch.turn_on',
        'switch.turn_off',
        hass)

    properties = await reported_properties(hass, 'switch#test')
    properties.assert_equal('Alexa.PowerController', 'powerState', 'ON')


async def test_light(hass):
    """Test light discovery."""
    device = ('light.test_1', 'on', {'friendly_name': "Test light 1"})
    appliance = await discovery_test(device, hass)

    assert appliance['endpointId'] == 'light#test_1'
    assert appliance['displayCategories'][0] == "LIGHT"
    assert appliance['friendlyName'] == "Test light 1"
    assert_endpoint_capabilities(appliance, 'Alexa.PowerController')

    await assert_power_controller_works(
        'light#test_1',
        'light.turn_on',
        'light.turn_off',
        hass)


async def test_dimmable_light(hass):
    """Test dimmable light discovery."""
    device = (
        'light.test_2', 'on', {
            'brightness': 128,
            'friendly_name': "Test light 2", 'supported_features': 1
        })
    appliance = await discovery_test(device, hass)

    assert appliance['endpointId'] == 'light#test_2'
    assert appliance['displayCategories'][0] == "LIGHT"
    assert appliance['friendlyName'] == "Test light 2"

    assert_endpoint_capabilities(
        appliance,
        'Alexa.BrightnessController',
        'Alexa.PowerController',
    )

    properties = await reported_properties(hass, 'light#test_2')
    properties.assert_equal('Alexa.PowerController', 'powerState', 'ON')
    properties.assert_equal('Alexa.BrightnessController', 'brightness', 50)

    call, _ = await assert_request_calls_service(
        'Alexa.BrightnessController', 'SetBrightness', 'light#test_2',
        'light.turn_on',
        hass,
        payload={'brightness': '50'})
    assert call.data['brightness_pct'] == 50


async def test_color_light(hass):
    """Test color light discovery."""
    device = (
        'light.test_3',
        'on',
        {
            'friendly_name': "Test light 3",
            'supported_features': 19,
            'min_mireds': 142,
            'color_temp': '333',
        }
    )
    appliance = await discovery_test(device, hass)

    assert appliance['endpointId'] == 'light#test_3'
    assert appliance['displayCategories'][0] == "LIGHT"
    assert appliance['friendlyName'] == "Test light 3"

    assert_endpoint_capabilities(
        appliance,
        'Alexa.BrightnessController',
        'Alexa.PowerController',
        'Alexa.ColorController',
        'Alexa.ColorTemperatureController',
    )

    # IncreaseColorTemperature and DecreaseColorTemperature have their own
    # tests


async def test_script(hass):
    """Test script discovery."""
    device = ('script.test', 'off', {'friendly_name': "Test script"})
    appliance = await discovery_test(device, hass)

    assert appliance['endpointId'] == 'script#test'
    assert appliance['displayCategories'][0] == "ACTIVITY_TRIGGER"
    assert appliance['friendlyName'] == "Test script"

    (capability,) = assert_endpoint_capabilities(
        appliance,
        'Alexa.SceneController')
    assert not capability['supportsDeactivation']

    await assert_scene_controller_works(
        'script#test',
        'script.turn_on',
        None,
        hass)


async def test_cancelable_script(hass):
    """Test cancalable script discovery."""
    device = (
        'script.test_2',
        'off',
        {'friendly_name': "Test script 2", 'can_cancel': True},
    )
    appliance = await discovery_test(device, hass)

    assert appliance['endpointId'] == 'script#test_2'
    (capability,) = assert_endpoint_capabilities(
        appliance,
        'Alexa.SceneController')
    assert capability['supportsDeactivation']

    await assert_scene_controller_works(
        'script#test_2',
        'script.turn_on',
        'script.turn_off',
        hass)


async def test_input_boolean(hass):
    """Test input boolean discovery."""
    device = (
        'input_boolean.test',
        'off',
        {'friendly_name': "Test input boolean"},
    )
    appliance = await discovery_test(device, hass)

    assert appliance['endpointId'] == 'input_boolean#test'
    assert appliance['displayCategories'][0] == "OTHER"
    assert appliance['friendlyName'] == "Test input boolean"
    assert_endpoint_capabilities(appliance, 'Alexa.PowerController')

    await assert_power_controller_works(
        'input_boolean#test',
        'input_boolean.turn_on',
        'input_boolean.turn_off',
        hass)


async def test_scene(hass):
    """Test scene discovery."""
    device = ('scene.test', 'off', {'friendly_name': "Test scene"})
    appliance = await discovery_test(device, hass)

    assert appliance['endpointId'] == 'scene#test'
    assert appliance['displayCategories'][0] == "SCENE_TRIGGER"
    assert appliance['friendlyName'] == "Test scene"

    (capability,) = assert_endpoint_capabilities(
        appliance,
        'Alexa.SceneController')
    assert not capability['supportsDeactivation']

    await assert_scene_controller_works(
        'scene#test',
        'scene.turn_on',
        None,
        hass)


async def test_fan(hass):
    """Test fan discovery."""
    device = ('fan.test_1', 'off', {'friendly_name': "Test fan 1"})
    appliance = await discovery_test(device, hass)

    assert appliance['endpointId'] == 'fan#test_1'
    assert appliance['displayCategories'][0] == "OTHER"
    assert appliance['friendlyName'] == "Test fan 1"
    assert_endpoint_capabilities(appliance, 'Alexa.PowerController')


async def test_variable_fan(hass):
    """Test fan discovery.

    This one has variable speed.
    """
    device = (
        'fan.test_2',
        'off', {
            'friendly_name': "Test fan 2",
            'supported_features': 1,
            'speed_list': ['low', 'medium', 'high'],
            'speed': 'high',
        }
    )
    appliance = await discovery_test(device, hass)

    assert appliance['endpointId'] == 'fan#test_2'
    assert appliance['displayCategories'][0] == "OTHER"
    assert appliance['friendlyName'] == "Test fan 2"

    assert_endpoint_capabilities(
        appliance,
        'Alexa.PercentageController',
        'Alexa.PowerController',
    )

    call, _ = await assert_request_calls_service(
        'Alexa.PercentageController', 'SetPercentage', 'fan#test_2',
        'fan.set_speed',
        hass,
        payload={'percentage': '50'})
    assert call.data['speed'] == 'medium'

    await assert_percentage_changes(
        hass,
        [('high', '-5'), ('off', '5'), ('low', '-80')],
        'Alexa.PercentageController', 'AdjustPercentage', 'fan#test_2',
        'percentageDelta',
        'fan.set_speed',
        'speed')


async def test_lock(hass):
    """Test lock discovery."""
    device = ('lock.test', 'off', {'friendly_name': "Test lock"})
    appliance = await discovery_test(device, hass)

    assert appliance['endpointId'] == 'lock#test'
    assert appliance['displayCategories'][0] == "SMARTLOCK"
    assert appliance['friendlyName'] == "Test lock"
    assert_endpoint_capabilities(appliance, 'Alexa.LockController')

    _, msg = await assert_request_calls_service(
        'Alexa.LockController', 'Lock', 'lock#test',
        'lock.lock',
        hass)

    # always return LOCKED for now
    properties = msg['context']['properties'][0]
    assert properties['name'] == 'lockState'
    assert properties['namespace'] == 'Alexa.LockController'
    assert properties['value'] == 'LOCKED'


async def test_media_player(hass):
    """Test media player discovery."""
    device = (
        'media_player.test',
        'off', {
            'friendly_name': "Test media player",
            'supported_features': 0x59bd,
            'volume_level': 0.75
        }
    )
    appliance = await discovery_test(device, hass)

    assert appliance['endpointId'] == 'media_player#test'
    assert appliance['displayCategories'][0] == "TV"
    assert appliance['friendlyName'] == "Test media player"

    assert_endpoint_capabilities(
        appliance,
        'Alexa.InputController',
        'Alexa.PowerController',
        'Alexa.Speaker',
        'Alexa.StepSpeaker',
        'Alexa.PlaybackController',
    )

    await assert_power_controller_works(
        'media_player#test',
        'media_player.turn_on',
        'media_player.turn_off',
        hass)

    await assert_request_calls_service(
        'Alexa.PlaybackController', 'Play', 'media_player#test',
        'media_player.media_play',
        hass)

    await assert_request_calls_service(
        'Alexa.PlaybackController', 'Pause', 'media_player#test',
        'media_player.media_pause',
        hass)

    await assert_request_calls_service(
        'Alexa.PlaybackController', 'Stop', 'media_player#test',
        'media_player.media_stop',
        hass)

    await assert_request_calls_service(
        'Alexa.PlaybackController', 'Next', 'media_player#test',
        'media_player.media_next_track',
        hass)

    await assert_request_calls_service(
        'Alexa.PlaybackController', 'Previous', 'media_player#test',
        'media_player.media_previous_track',
        hass)

    call, _ = await assert_request_calls_service(
        'Alexa.Speaker', 'SetVolume', 'media_player#test',
        'media_player.volume_set',
        hass,
        payload={'volume': 50})
    assert call.data['volume_level'] == 0.5

    call, _ = await assert_request_calls_service(
        'Alexa.Speaker', 'SetMute', 'media_player#test',
        'media_player.volume_mute',
        hass,
        payload={'mute': True})
    assert call.data['is_volume_muted']

    call, _, = await assert_request_calls_service(
        'Alexa.Speaker', 'SetMute', 'media_player#test',
        'media_player.volume_mute',
        hass,
        payload={'mute': False})
    assert not call.data['is_volume_muted']

    await assert_percentage_changes(
        hass,
        [(0.7, '-5'), (0.8, '5'), (0, '-80')],
        'Alexa.Speaker', 'AdjustVolume', 'media_player#test',
        'volume',
        'media_player.volume_set',
        'volume_level')

    call, _ = await assert_request_calls_service(
        'Alexa.StepSpeaker', 'SetMute', 'media_player#test',
        'media_player.volume_mute',
        hass,
        payload={'mute': True})
    assert call.data['is_volume_muted']

    call, _, = await assert_request_calls_service(
        'Alexa.StepSpeaker', 'SetMute', 'media_player#test',
        'media_player.volume_mute',
        hass,
        payload={'mute': False})
    assert not call.data['is_volume_muted']

    call, _ = await assert_request_calls_service(
        'Alexa.StepSpeaker', 'AdjustVolume', 'media_player#test',
        'media_player.volume_up',
        hass,
        payload={'volumeSteps': 20})

    call, _ = await assert_request_calls_service(
        'Alexa.StepSpeaker', 'AdjustVolume', 'media_player#test',
        'media_player.volume_down',
        hass,
        payload={'volumeSteps': -20})


async def test_alert(hass):
    """Test alert discovery."""
    device = ('alert.test', 'off', {'friendly_name': "Test alert"})
    appliance = await discovery_test(device, hass)

    assert appliance['endpointId'] == 'alert#test'
    assert appliance['displayCategories'][0] == "OTHER"
    assert appliance['friendlyName'] == "Test alert"
    assert_endpoint_capabilities(appliance, 'Alexa.PowerController')

    await assert_power_controller_works(
        'alert#test',
        'alert.turn_on',
        'alert.turn_off',
        hass)


async def test_automation(hass):
    """Test automation discovery."""
    device = ('automation.test', 'off', {'friendly_name': "Test automation"})
    appliance = await discovery_test(device, hass)

    assert appliance['endpointId'] == 'automation#test'
    assert appliance['displayCategories'][0] == "OTHER"
    assert appliance['friendlyName'] == "Test automation"
    assert_endpoint_capabilities(appliance, 'Alexa.PowerController')

    await assert_power_controller_works(
        'automation#test',
        'automation.turn_on',
        'automation.turn_off',
        hass)


async def test_group(hass):
    """Test group discovery."""
    device = ('group.test', 'off', {'friendly_name': "Test group"})
    appliance = await discovery_test(device, hass)

    assert appliance['endpointId'] == 'group#test'
    assert appliance['displayCategories'][0] == "OTHER"
    assert appliance['friendlyName'] == "Test group"
    assert_endpoint_capabilities(appliance, 'Alexa.PowerController')

    await assert_power_controller_works(
        'group#test',
        'homeassistant.turn_on',
        'homeassistant.turn_off',
        hass)


async def test_cover(hass):
    """Test cover discovery."""
    device = (
        'cover.test',
        'off', {
            'friendly_name': "Test cover",
            'supported_features': 255,
            'position': 30,
        }
    )
    appliance = await discovery_test(device, hass)

    assert appliance['endpointId'] == 'cover#test'
    assert appliance['displayCategories'][0] == "DOOR"
    assert appliance['friendlyName'] == "Test cover"

    assert_endpoint_capabilities(
        appliance,
        'Alexa.PercentageController',
        'Alexa.PowerController',
    )

    await assert_power_controller_works(
        'cover#test',
        'cover.open_cover',
        'cover.close_cover',
        hass)

    call, _ = await assert_request_calls_service(
        'Alexa.PercentageController', 'SetPercentage', 'cover#test',
        'cover.set_cover_position',
        hass,
        payload={'percentage': '50'})
    assert call.data['position'] == 50

    await assert_percentage_changes(
        hass,
        [(25, '-5'), (35, '5'), (0, '-80')],
        'Alexa.PercentageController', 'AdjustPercentage', 'cover#test',
        'percentageDelta',
        'cover.set_cover_position',
        'position')


async def assert_percentage_changes(
        hass,
        adjustments,
        namespace,
        name,
        endpoint,
        parameter,
        service,
        changed_parameter):
    """Assert an API request making percentage changes works.

    AdjustPercentage, AdjustBrightness, etc. are examples of such requests.
    """
    for result_volume, adjustment in adjustments:
        if parameter:
            payload = {parameter: adjustment}
        else:
            payload = {}

        call, _ = await assert_request_calls_service(
            namespace, name, endpoint, service,
            hass,
            payload=payload)
        assert call.data[changed_parameter] == result_volume


async def test_temp_sensor(hass):
    """Test temperature sensor discovery."""
    device = (
        'sensor.test_temp',
        '42',
        {
            'friendly_name': "Test Temp Sensor",
            'unit_of_measurement': TEMP_FAHRENHEIT,
        }
    )
    appliance = await discovery_test(device, hass)

    assert appliance['endpointId'] == 'sensor#test_temp'
    assert appliance['displayCategories'][0] == 'TEMPERATURE_SENSOR'
    assert appliance['friendlyName'] == 'Test Temp Sensor'

    (capability,) = assert_endpoint_capabilities(
        appliance,
        'Alexa.TemperatureSensor')
    assert capability['interface'] == 'Alexa.TemperatureSensor'
    properties = capability['properties']
    assert properties['retrievable'] is True
    assert {'name': 'temperature'} in properties['supported']

    properties = await reported_properties(hass, 'sensor#test_temp')
    properties.assert_equal('Alexa.TemperatureSensor', 'temperature',
                            {'value': 42.0, 'scale': 'FAHRENHEIT'})


async def test_contact_sensor(hass):
    """Test contact sensor discovery."""
    device = (
        'binary_sensor.test_contact',
        'on',
        {
            'friendly_name': "Test Contact Sensor",
            'device_class': 'door',
        }
    )
    appliance = await discovery_test(device, hass)

    assert appliance['endpointId'] == 'binary_sensor#test_contact'
    assert appliance['displayCategories'][0] == 'CONTACT_SENSOR'
    assert appliance['friendlyName'] == 'Test Contact Sensor'

    (capability,) = assert_endpoint_capabilities(
        appliance,
        'Alexa.ContactSensor')
    assert capability['interface'] == 'Alexa.ContactSensor'
    properties = capability['properties']
    assert properties['retrievable'] is True
    assert {'name': 'detectionState'} in properties['supported']

    properties = await reported_properties(hass,
                                           'binary_sensor#test_contact')
    properties.assert_equal('Alexa.ContactSensor', 'detectionState',
                            'DETECTED')


async def test_motion_sensor(hass):
    """Test motion sensor discovery."""
    device = (
        'binary_sensor.test_motion',
        'on',
        {
            'friendly_name': "Test Motion Sensor",
            'device_class': 'motion',
        }
    )
    appliance = await discovery_test(device, hass)

    assert appliance['endpointId'] == 'binary_sensor#test_motion'
    assert appliance['displayCategories'][0] == 'MOTION_SENSOR'
    assert appliance['friendlyName'] == 'Test Motion Sensor'

    (capability,) = assert_endpoint_capabilities(
        appliance,
        'Alexa.MotionSensor')
    assert capability['interface'] == 'Alexa.MotionSensor'
    properties = capability['properties']
    assert properties['retrievable'] is True
    assert {'name': 'detectionState'} in properties['supported']

    properties = await reported_properties(hass,
                                           'binary_sensor#test_motion')
    properties.assert_equal('Alexa.MotionSensor', 'detectionState',
                            'DETECTED')


async def test_unknown_sensor(hass):
    """Test sensors of unknown quantities are not discovered."""
    device = (
        'sensor.test_sickness', '0.1', {
            'friendly_name': "Test Space Sickness Sensor",
            'unit_of_measurement': 'garn',
        })
    await discovery_test(device, hass, expected_endpoints=0)


async def test_thermostat(hass):
    """Test thermostat discovery."""
    hass.config.units.temperature_unit = TEMP_FAHRENHEIT
    device = (
        'climate.test_thermostat',
        'cool',
        {
            'operation_mode': 'cool',
            'temperature': 70.0,
            'target_temp_high': 80.0,
            'target_temp_low': 60.0,
            'current_temperature': 75.0,
            'friendly_name': "Test Thermostat",
            'supported_features': 1 | 2 | 4 | 128,
            'operation_list': ['heat', 'cool', 'auto', 'off'],
            'min_temp': 50,
            'max_temp': 90,
        }
    )
    appliance = await discovery_test(device, hass)

    assert appliance['endpointId'] == 'climate#test_thermostat'
    assert appliance['displayCategories'][0] == 'THERMOSTAT'
    assert appliance['friendlyName'] == "Test Thermostat"

    assert_endpoint_capabilities(
        appliance,
        'Alexa.ThermostatController',
        'Alexa.TemperatureSensor',
    )

    properties = await reported_properties(
        hass, 'climate#test_thermostat')
    properties.assert_equal(
        'Alexa.ThermostatController', 'thermostatMode', 'COOL')
    properties.assert_equal(
        'Alexa.ThermostatController', 'targetSetpoint',
        {'value': 70.0, 'scale': 'FAHRENHEIT'})
    properties.assert_equal(
        'Alexa.TemperatureSensor', 'temperature',
        {'value': 75.0, 'scale': 'FAHRENHEIT'})

    call, _ = await assert_request_calls_service(
        'Alexa.ThermostatController', 'SetTargetTemperature',
        'climate#test_thermostat', 'climate.set_temperature',
        hass,
        payload={'targetSetpoint': {'value': 69.0, 'scale': 'FAHRENHEIT'}}
    )
    assert call.data['temperature'] == 69.0

    msg = await assert_request_fails(
        'Alexa.ThermostatController', 'SetTargetTemperature',
        'climate#test_thermostat', 'climate.set_temperature',
        hass,
        payload={'targetSetpoint': {'value': 0.0, 'scale': 'CELSIUS'}}
    )
    assert msg['event']['payload']['type'] == 'TEMPERATURE_VALUE_OUT_OF_RANGE'

    call, _ = await assert_request_calls_service(
        'Alexa.ThermostatController', 'SetTargetTemperature',
        'climate#test_thermostat', 'climate.set_temperature',
        hass,
        payload={
            'targetSetpoint': {'value': 70.0, 'scale': 'FAHRENHEIT'},
            'lowerSetpoint': {'value': 293.15, 'scale': 'KELVIN'},
            'upperSetpoint': {'value': 30.0, 'scale': 'CELSIUS'},
        }
    )
    assert call.data['temperature'] == 70.0
    assert call.data['target_temp_low'] == 68.0
    assert call.data['target_temp_high'] == 86.0

    msg = await assert_request_fails(
        'Alexa.ThermostatController', 'SetTargetTemperature',
        'climate#test_thermostat', 'climate.set_temperature',
        hass,
        payload={
            'lowerSetpoint': {'value': 273.15, 'scale': 'KELVIN'},
            'upperSetpoint': {'value': 75.0, 'scale': 'FAHRENHEIT'},
        }
    )
    assert msg['event']['payload']['type'] == 'TEMPERATURE_VALUE_OUT_OF_RANGE'

    msg = await assert_request_fails(
        'Alexa.ThermostatController', 'SetTargetTemperature',
        'climate#test_thermostat', 'climate.set_temperature',
        hass,
        payload={
            'lowerSetpoint': {'value': 293.15, 'scale': 'FAHRENHEIT'},
            'upperSetpoint': {'value': 75.0, 'scale': 'CELSIUS'},
        }
    )
    assert msg['event']['payload']['type'] == 'TEMPERATURE_VALUE_OUT_OF_RANGE'

    call, _ = await assert_request_calls_service(
        'Alexa.ThermostatController', 'AdjustTargetTemperature',
        'climate#test_thermostat', 'climate.set_temperature',
        hass,
        payload={'targetSetpointDelta': {'value': -10.0, 'scale': 'KELVIN'}}
    )
    assert call.data['temperature'] == 52.0

    msg = await assert_request_fails(
        'Alexa.ThermostatController', 'AdjustTargetTemperature',
        'climate#test_thermostat', 'climate.set_temperature',
        hass,
        payload={'targetSetpointDelta': {'value': 20.0, 'scale': 'CELSIUS'}}
    )
    assert msg['event']['payload']['type'] == 'TEMPERATURE_VALUE_OUT_OF_RANGE'

    call, _ = await assert_request_calls_service(
        'Alexa.ThermostatController', 'SetThermostatMode',
        'climate#test_thermostat', 'climate.set_operation_mode',
        hass,
        payload={'thermostatMode': {'value': 'HEAT'}}
    )
    assert call.data['operation_mode'] == 'heat'

    call, _ = await assert_request_calls_service(
        'Alexa.ThermostatController', 'SetThermostatMode',
        'climate#test_thermostat', 'climate.set_operation_mode',
        hass,
        payload={'thermostatMode': 'HEAT'}
    )

    assert call.data['operation_mode'] == 'heat'
    msg = await assert_request_fails(
        'Alexa.ThermostatController', 'SetThermostatMode',
        'climate#test_thermostat', 'climate.set_operation_mode',
        hass,
        payload={'thermostatMode': {'value': 'INVALID'}}
    )
    assert msg['event']['payload']['type'] == 'UNSUPPORTED_THERMOSTAT_MODE'
    hass.config.units.temperature_unit = TEMP_CELSIUS

    call, _ = await assert_request_calls_service(
        'Alexa.ThermostatController', 'SetThermostatMode',
        'climate#test_thermostat', 'climate.set_operation_mode',
        hass,
        payload={'thermostatMode': 'OFF'}
    )
    assert call.data['operation_mode'] == 'off'


async def test_exclude_filters(hass):
    """Test exclusion filters."""
    request = get_new_request('Alexa.Discovery', 'Discover')

    # setup test devices
    hass.states.async_set(
        'switch.test', 'on', {'friendly_name': "Test switch"})

    hass.states.async_set(
        'script.deny', 'off', {'friendly_name': "Blocked script"})

    hass.states.async_set(
        'cover.deny', 'off', {'friendly_name': "Blocked cover"})

    config = smart_home.Config(should_expose=entityfilter.generate_filter(
        include_domains=[],
        include_entities=[],
        exclude_domains=['script'],
        exclude_entities=['cover.deny'],
    ))

    msg = await smart_home.async_handle_message(hass, config, request)
    await hass.async_block_till_done()

    msg = msg['event']

    assert len(msg['payload']['endpoints']) == 1


async def test_include_filters(hass):
    """Test inclusion filters."""
    request = get_new_request('Alexa.Discovery', 'Discover')

    # setup test devices
    hass.states.async_set(
        'switch.deny', 'on', {'friendly_name': "Blocked switch"})

    hass.states.async_set(
        'script.deny', 'off', {'friendly_name': "Blocked script"})

    hass.states.async_set(
        'automation.allow', 'off', {'friendly_name': "Allowed automation"})

    hass.states.async_set(
        'group.allow', 'off', {'friendly_name': "Allowed group"})

    config = smart_home.Config(should_expose=entityfilter.generate_filter(
        include_domains=['automation', 'group'],
        include_entities=['script.deny'],
        exclude_domains=[],
        exclude_entities=[],
    ))

    msg = await smart_home.async_handle_message(hass, config, request)
    await hass.async_block_till_done()

    msg = msg['event']

    assert len(msg['payload']['endpoints']) == 3


async def test_api_entity_not_exists(hass):
    """Test api turn on process without entity."""
    request = get_new_request('Alexa.PowerController', 'TurnOn', 'switch#test')

    call_switch = async_mock_service(hass, 'switch', 'turn_on')

    msg = await smart_home.async_handle_message(
        hass, DEFAULT_CONFIG, request)
    await hass.async_block_till_done()

    assert 'event' in msg
    msg = msg['event']

    assert not call_switch
    assert msg['header']['name'] == 'ErrorResponse'
    assert msg['header']['namespace'] == 'Alexa'
    assert msg['payload']['type'] == 'NO_SUCH_ENDPOINT'


async def test_api_function_not_implemented(hass):
    """Test api call that is not implemented to us."""
    request = get_new_request('Alexa.HAHAAH', 'Sweet')
    msg = await smart_home.async_handle_message(
        hass, DEFAULT_CONFIG, request)

    assert 'event' in msg
    msg = msg['event']

    assert msg['header']['name'] == 'ErrorResponse'
    assert msg['header']['namespace'] == 'Alexa'
    assert msg['payload']['type'] == 'INTERNAL_ERROR'


async def assert_request_fails(
        namespace,
        name,
        endpoint,
        service_not_called,
        hass,
        payload=None):
    """Assert an API request returns an ErrorResponse."""
    request = get_new_request(namespace, name, endpoint)
    if payload:
        request['directive']['payload'] = payload

    domain, service_name = service_not_called.split('.')
    call = async_mock_service(hass, domain, service_name)

    msg = await smart_home.async_handle_message(
        hass, DEFAULT_CONFIG, request)
    await hass.async_block_till_done()

    assert not call
    assert 'event' in msg
    assert msg['event']['header']['name'] == 'ErrorResponse'

    return msg


async def assert_request_calls_service(
        namespace,
        name,
        endpoint,
        service,
        hass,
        response_type='Response',
        payload=None):
    """Assert an API request calls a hass service."""
    context = Context()
    request = get_new_request(namespace, name, endpoint)
    if payload:
        request['directive']['payload'] = payload

    domain, service_name = service.split('.')
    calls = async_mock_service(hass, domain, service_name)

    msg = await smart_home.async_handle_message(
        hass, DEFAULT_CONFIG, request, context)
    await hass.async_block_till_done()

    assert len(calls) == 1
    call = calls[0]
    assert 'event' in msg
    assert call.data['entity_id'] == endpoint.replace('#', '.')
    assert msg['event']['header']['name'] == response_type
    assert call.context == context

    return call, msg


async def assert_power_controller_works(
    endpoint,
    on_service,
    off_service,
    hass
):
    """Assert PowerController API requests work."""
    await assert_request_calls_service(
        'Alexa.PowerController', 'TurnOn', endpoint,
        on_service, hass)

    await assert_request_calls_service(
        'Alexa.PowerController', 'TurnOff', endpoint,
        off_service, hass)


async def assert_scene_controller_works(
        endpoint,
        activate_service,
        deactivate_service,
        hass):
    """Assert SceneController API requests work."""
    _, response = await assert_request_calls_service(
        'Alexa.SceneController', 'Activate', endpoint,
        activate_service, hass,
        response_type='ActivationStarted')
    assert response['event']['payload']['cause']['type'] == 'VOICE_INTERACTION'
    assert 'timestamp' in response['event']['payload']

    if deactivate_service:
        await assert_request_calls_service(
            'Alexa.SceneController', 'Deactivate', endpoint,
            deactivate_service, hass,
            response_type='DeactivationStarted')
        cause_type = response['event']['payload']['cause']['type']
        assert cause_type == 'VOICE_INTERACTION'
        assert 'timestamp' in response['event']['payload']


@pytest.mark.parametrize(
    "result,adjust", [(25, '-5'), (35, '5'), (0, '-80')])
async def test_api_adjust_brightness(hass, result, adjust):
    """Test api adjust brightness process."""
    request = get_new_request(
        'Alexa.BrightnessController', 'AdjustBrightness', 'light#test')

    # add payload
    request['directive']['payload']['brightnessDelta'] = adjust

    # setup test devices
    hass.states.async_set(
        'light.test', 'off', {
            'friendly_name': "Test light", 'brightness': '77'
        })

    call_light = async_mock_service(hass, 'light', 'turn_on')

    msg = await smart_home.async_handle_message(
        hass, DEFAULT_CONFIG, request)
    await hass.async_block_till_done()

    assert 'event' in msg
    msg = msg['event']

    assert len(call_light) == 1
    assert call_light[0].data['entity_id'] == 'light.test'
    assert call_light[0].data['brightness_pct'] == result
    assert msg['header']['name'] == 'Response'


async def test_api_set_color_rgb(hass):
    """Test api set color process."""
    request = get_new_request(
        'Alexa.ColorController', 'SetColor', 'light#test')

    # add payload
    request['directive']['payload']['color'] = {
        'hue': '120',
        'saturation': '0.612',
        'brightness': '0.342',
    }

    # setup test devices
    hass.states.async_set(
        'light.test', 'off', {
            'friendly_name': "Test light",
            'supported_features': 16,
        })

    call_light = async_mock_service(hass, 'light', 'turn_on')

    msg = await smart_home.async_handle_message(
        hass, DEFAULT_CONFIG, request)
    await hass.async_block_till_done()

    assert 'event' in msg
    msg = msg['event']

    assert len(call_light) == 1
    assert call_light[0].data['entity_id'] == 'light.test'
    assert call_light[0].data['rgb_color'] == (33, 87, 33)
    assert msg['header']['name'] == 'Response'


async def test_api_set_color_temperature(hass):
    """Test api set color temperature process."""
    request = get_new_request(
        'Alexa.ColorTemperatureController', 'SetColorTemperature',
        'light#test')

    # add payload
    request['directive']['payload']['colorTemperatureInKelvin'] = '7500'

    # setup test devices
    hass.states.async_set(
        'light.test', 'off', {'friendly_name': "Test light"})

    call_light = async_mock_service(hass, 'light', 'turn_on')

    msg = await smart_home.async_handle_message(
        hass, DEFAULT_CONFIG, request)
    await hass.async_block_till_done()

    assert 'event' in msg
    msg = msg['event']

    assert len(call_light) == 1
    assert call_light[0].data['entity_id'] == 'light.test'
    assert call_light[0].data['kelvin'] == 7500
    assert msg['header']['name'] == 'Response'


@pytest.mark.parametrize("result,initial", [(383, '333'), (500, '500')])
async def test_api_decrease_color_temp(hass, result, initial):
    """Test api decrease color temp process."""
    request = get_new_request(
        'Alexa.ColorTemperatureController', 'DecreaseColorTemperature',
        'light#test')

    # setup test devices
    hass.states.async_set(
        'light.test', 'off', {
            'friendly_name': "Test light", 'color_temp': initial,
            'max_mireds': 500,
        })

    call_light = async_mock_service(hass, 'light', 'turn_on')

    msg = await smart_home.async_handle_message(
        hass, DEFAULT_CONFIG, request)
    await hass.async_block_till_done()

    assert 'event' in msg
    msg = msg['event']

    assert len(call_light) == 1
    assert call_light[0].data['entity_id'] == 'light.test'
    assert call_light[0].data['color_temp'] == result
    assert msg['header']['name'] == 'Response'


@pytest.mark.parametrize("result,initial", [(283, '333'), (142, '142')])
async def test_api_increase_color_temp(hass, result, initial):
    """Test api increase color temp process."""
    request = get_new_request(
        'Alexa.ColorTemperatureController', 'IncreaseColorTemperature',
        'light#test')

    # setup test devices
    hass.states.async_set(
        'light.test', 'off', {
            'friendly_name': "Test light", 'color_temp': initial,
            'min_mireds': 142,
        })

    call_light = async_mock_service(hass, 'light', 'turn_on')

    msg = await smart_home.async_handle_message(
        hass, DEFAULT_CONFIG, request)
    await hass.async_block_till_done()

    assert 'event' in msg
    msg = msg['event']

    assert len(call_light) == 1
    assert call_light[0].data['entity_id'] == 'light.test'
    assert call_light[0].data['color_temp'] == result
    assert msg['header']['name'] == 'Response'


async def test_report_lock_state(hass):
    """Test LockController implements lockState property."""
    hass.states.async_set(
        'lock.locked', STATE_LOCKED, {})
    hass.states.async_set(
        'lock.unlocked', STATE_UNLOCKED, {})
    hass.states.async_set(
        'lock.unknown', STATE_UNKNOWN, {})

    properties = await reported_properties(hass, 'lock.locked')
    properties.assert_equal('Alexa.LockController', 'lockState', 'LOCKED')

    properties = await reported_properties(hass, 'lock.unlocked')
    properties.assert_equal('Alexa.LockController', 'lockState', 'UNLOCKED')

    properties = await reported_properties(hass, 'lock.unknown')
    properties.assert_equal('Alexa.LockController', 'lockState', 'JAMMED')


async def test_report_dimmable_light_state(hass):
    """Test BrightnessController reports brightness correctly."""
    hass.states.async_set(
        'light.test_on', 'on', {'friendly_name': "Test light On",
                                'brightness': 128, 'supported_features': 1})
    hass.states.async_set(
        'light.test_off', 'off', {'friendly_name': "Test light Off",
                                  'supported_features': 1})

    properties = await reported_properties(hass, 'light.test_on')
    properties.assert_equal('Alexa.BrightnessController', 'brightness', 50)

    properties = await reported_properties(hass, 'light.test_off')
    properties.assert_equal('Alexa.BrightnessController', 'brightness', 0)


async def reported_properties(hass, endpoint):
    """Use ReportState to get properties and return them.

    The result is a _ReportedProperties instance, which has methods to make
    assertions about the properties.
    """
    request = get_new_request('Alexa', 'ReportState', endpoint)
    msg = await smart_home.async_handle_message(
        hass, DEFAULT_CONFIG, request)
    await hass.async_block_till_done()
    return _ReportedProperties(msg['context']['properties'])


class _ReportedProperties:
    def __init__(self, properties):
        self.properties = properties

    def assert_equal(self, namespace, name, value):
        """Assert a property is equal to a given value."""
        for prop in self.properties:
            if prop['namespace'] == namespace and prop['name'] == name:
                assert prop['value'] == value
                return prop

        assert False, 'property %s:%s not in %r' % (
            namespace,
            name,
            self.properties,
        )


async def test_entity_config(hass):
    """Test that we can configure things via entity config."""
    request = get_new_request('Alexa.Discovery', 'Discover')

    hass.states.async_set(
        'light.test_1', 'on', {'friendly_name': "Test light 1"})

    config = smart_home.Config(
        should_expose=lambda entity_id: True,
        entity_config={
            'light.test_1': {
                'name': 'Config name',
                'display_categories': 'SWITCH',
                'description': 'Config description'
            }
        }
    )

    msg = await smart_home.async_handle_message(
        hass, config, request)

    assert 'event' in msg
    msg = msg['event']

    assert len(msg['payload']['endpoints']) == 1

    appliance = msg['payload']['endpoints'][0]
    assert appliance['endpointId'] == 'light#test_1'
    assert appliance['displayCategories'][0] == "SWITCH"
    assert appliance['friendlyName'] == "Config name"
    assert appliance['description'] == "Config description"
    assert len(appliance['capabilities']) == 1
    assert appliance['capabilities'][-1]['interface'] == \
        'Alexa.PowerController'


async def test_unsupported_domain(hass):
    """Discovery ignores entities of unknown domains."""
    request = get_new_request('Alexa.Discovery', 'Discover')

    hass.states.async_set(
        'woz.boop', 'on', {'friendly_name': "Boop Woz"})

    msg = await smart_home.async_handle_message(
        hass, DEFAULT_CONFIG, request)

    assert 'event' in msg
    msg = msg['event']

    assert not msg['payload']['endpoints']


async def do_http_discovery(config, hass, aiohttp_client):
    """Submit a request to the Smart Home HTTP API."""
    await async_setup_component(hass, alexa.DOMAIN, config)
    http_client = await aiohttp_client(hass.http.app)

    request = get_new_request('Alexa.Discovery', 'Discover')
    response = await http_client.post(
        smart_home.SMART_HOME_HTTP_ENDPOINT,
        data=json.dumps(request),
        headers={'content-type': 'application/json'})
    return response


async def test_http_api(hass, aiohttp_client):
    """With `smart_home:` HTTP API is exposed."""
    config = {
        'alexa': {
            'smart_home': None
        }
    }

    response = await do_http_discovery(config, hass, aiohttp_client)
    response_data = await response.json()

    # Here we're testing just the HTTP view glue -- details of discovery are
    # covered in other tests.
    assert response_data['event']['header']['name'] == 'Discover.Response'


async def test_http_api_disabled(hass, aiohttp_client):
    """Without `smart_home:`, the HTTP API is disabled."""
    config = {
        'alexa': {}
    }
    response = await do_http_discovery(config, hass, aiohttp_client)

    assert response.status == 404


@pytest.mark.parametrize(
    "domain,payload,source_list,idx", [
        ('media_player', 'GAME CONSOLE', ['tv', 'game console'], 1),
        ('media_player', 'SATELLITE TV', ['satellite-tv', 'game console'], 0),
        ('media_player', 'SATELLITE TV', ['satellite_tv', 'game console'], 0),
        ('media_player', 'BAD DEVICE', ['satellite_tv', 'game console'], None),
    ]
)
async def test_api_select_input(hass, domain, payload, source_list, idx):
    """Test api set input process."""
    hass.states.async_set(
        'media_player.test', 'off', {
            'friendly_name': "Test media player",
            'source': 'unknown',
            'source_list': source_list,
        })

    # test where no source matches
    if idx is None:
        await assert_request_fails(
            'Alexa.InputController', 'SelectInput', 'media_player#test',
            'media_player.select_source',
            hass,
            payload={'input': payload})
        return

    call, _ = await assert_request_calls_service(
        'Alexa.InputController', 'SelectInput', 'media_player#test',
        'media_player.select_source',
        hass,
        payload={'input': payload})
    assert call.data['source'] == source_list[idx]


async def test_logging_request(hass, events):
    """Test that we log requests."""
    context = Context()
    request = get_new_request('Alexa.Discovery', 'Discover')
    await smart_home.async_handle_message(
        hass, DEFAULT_CONFIG, request, context)

    # To trigger event listener
    await hass.async_block_till_done()

    assert len(events) == 1
    event = events[0]

    assert event.data['request'] == {
        'namespace': 'Alexa.Discovery',
        'name': 'Discover',
    }
    assert event.data['response'] == {
        'namespace': 'Alexa.Discovery',
        'name': 'Discover.Response'
    }
    assert event.context == context


async def test_logging_request_with_entity(hass, events):
    """Test that we log requests."""
    context = Context()
    request = get_new_request('Alexa.PowerController', 'TurnOn', 'switch#xy')
    await smart_home.async_handle_message(
        hass, DEFAULT_CONFIG, request, context)

    # To trigger event listener
    await hass.async_block_till_done()

    assert len(events) == 1
    event = events[0]

    assert event.data['request'] == {
        'namespace': 'Alexa.PowerController',
        'name': 'TurnOn',
        'entity_id': 'switch.xy'
    }
    # Entity doesn't exist
    assert event.data['response'] == {
        'namespace': 'Alexa',
        'name': 'ErrorResponse'
    }
    assert event.context == context