from datetime import datetime, timezone

import pytest
from devolo_home_control_api.backend import MESSAGE_TYPES


@pytest.mark.usefixtures("home_control_instance")
@pytest.mark.usefixtures("mock_publisher_dispatch")
class TestUpdater:

    def test_hasattr(self):
        updater_functions = (func for func in dir(self.homecontrol.updater)
                             if callable(getattr(self.homecontrol.updater,
                                                 func)) and not func.startswith("__"))
        assert set(MESSAGE_TYPES.values()).difference(updater_functions) == set()

    @pytest.mark.usefixtures("mock_updater_binary_switch")
    def test_update_device(self, mocker):
        message = {
            "topic": "com/prosyst/mbs/services/fim/FunctionalItemEvent/PROPERTY_CHANGED",
            "properties": {
                "property.name": "state",
                "uid": f"devolo.BinarySwitch:{self.devices['mains']['uid']}",
            },
        }
        spy = mocker.spy(self.homecontrol.updater, '_binary_switch')
        self.homecontrol.updater.update(message=message)
        spy.assert_called_once_with(message)

    @pytest.mark.usefixtures("mock_updater_pending_operations")
    def test_update_pending_operations(self, mocker):
        message = {
            "topic": "com/prosyst/mbs/services/fim/FunctionalItemEvent/PROPERTY_CHANGED",
            "properties": {
                "property.name": "pendingOperations",
                "uid": "",
            },
        }
        spy = mocker.spy(self.homecontrol.updater, '_pending_operations')
        self.homecontrol.updater.update(message=message)
        spy.assert_called_once_with(message)

    def test_update_unwanted(self, mocker):
        message = {
            "topic": "com/prosyst/mbs/services/fim/FunctionalItemEvent/UNREGISTERED",
        }
        spy = mocker.spy(self.homecontrol.updater, '_unknown')
        self.homecontrol.updater.update(message=message)
        spy.assert_not_called()

    def test_update_smart_group(self, mocker):
        message = {
            "topic": "com/prosyst/mbs/services/fim/FunctionalItemEvent/PROPERTY_CHANGED",
            "properties": {
                "uid": "devolo.MultiLevelSwitch:devolo.smartGroup.Group",
                "property.name": "value",
            },
        }
        spy = mocker.spy(self.homecontrol.updater, '_multi_level_switch')
        self.homecontrol.updater.update(message=message)
        spy.assert_not_called()
        message = {
            "topic": "com/prosyst/mbs/services/fim/FunctionalItemEvent/PROPERTY_CHANGED",
            "properties": {
                "uid": "devolo.BinarySwitch:devolo.smartGroup.Group",
                "property.name": "value",
            },
        }
        spy = mocker.spy(self.homecontrol.updater, '_binary_switch')
        self.homecontrol.updater.update(message=message)
        spy.assert_not_called()

    def test__automatic_calibration(self):
        uid = self.devices['blinds']['uid']
        calibration_status = self.devices['blinds']['calibrationStatus']
        self.homecontrol.updater._automatic_calibration(
            message={"properties": {
                "uid": f"acs.{uid}",
                "property.value.new": {
                    "status": calibration_status,
                },
            }})
        assert self.homecontrol.devices[uid].settings_property['automatic_calibration'].calibration_status

    def test__automatic_calibration_key_error(self):
        uid = self.devices['blinds']['uid']
        calibration_status = self.devices['blinds']['calibrationStatus']
        self.homecontrol.updater._automatic_calibration(
            message={"properties": {
                "uid": f"acs.{uid}",
                "property.value.new": calibration_status,
            }})
        assert self.homecontrol.devices[uid].settings_property['automatic_calibration'].calibration_status

    def test__binary_async_blinds(self):
        uid = self.devices['blinds']['uid']
        self.homecontrol.devices[uid].settings_property['i2'].value = self.devices['blinds']['i2']
        self.homecontrol.updater._binary_async(
            message={"properties": {
                "uid": f"bas.{uid}#i2",
                "property.value.new": not self.devices['blinds']['i2'],
            }})
        assert not self.homecontrol.devices[uid].settings_property['i2'].value

    def test__binary_async_siren(self):
        uid = self.devices['siren']['uid']
        self.homecontrol.devices[uid].settings_property['muted'].value = self.devices['siren']['muted']
        self.homecontrol.updater._binary_async(
            message={"properties": {
                "uid": f"bas.{uid}",
                "property.value.new": not self.devices['siren']['muted'],
            }})
        assert not self.homecontrol.devices[uid].settings_property['muted'].value

    def test__binary_sensor(self):
        uid = self.devices['sensor']['uid']
        device = self.homecontrol.devices[uid].binary_sensor_property[f"devolo.BinarySensor:{uid}"]
        device.state = True
        state = device.state
        self.homecontrol.updater._binary_sensor(
            message={"properties": {
                "property.name": "state",
                "uid": f"devolo.BinarySensor:{uid}",
                "property.value.new": 0,
            }})
        state_new = device.state
        assert state != state_new
        assert device.last_activity != datetime.fromtimestamp(0)

    def test__binary_sync(self):
        uid = self.devices['blinds']['uid']
        self.homecontrol.updater._binary_sync(message={
            "properties": {
                "uid": f"bss.{uid}",
                "property.value.new": self.devices['blinds']['movement_direction'],
            },
        })
        assert self.homecontrol.devices[uid].settings_property["movement_direction"].direction is \
            bool(self.devices['blinds']['movement_direction'])

    def test__binary_switch(self):
        uid = self.devices.get("mains").get("uid")
        self.homecontrol.devices.get(uid).binary_switch_property.get(f"devolo.BinarySwitch:{uid}").state = True
        state = self.homecontrol.devices.get(uid).binary_switch_property.get(f"devolo.BinarySwitch:{uid}").state
        self.homecontrol.updater._binary_switch(message={
            "properties": {
                "property.name": "targetState",
                "uid": f"devolo.BinarySwitch:{uid}",
                "property.value.new": 0
            },
        })
        state_new = self.homecontrol.devices.get(uid).binary_switch_property.get(f"devolo.BinarySwitch:{uid}").state
        assert state != state_new

    def test__device_state(self):
        uid = self.devices.get("mains").get("uid")
        online_state = self.homecontrol.devices.get(uid).status
        self.homecontrol.updater._device_state(
            message={"properties": {
                "uid": uid,
                "property.name": "status",
                "property.value.new": 1
            }})
        assert self.homecontrol.devices.get(uid).status == 1
        assert online_state != self.homecontrol.devices.get(uid).status

    def test__general_device(self):
        uid = self.devices['mains']['uid']
        events_enabled = self.devices['mains']['properties']['eventsEnabled']
        self.homecontrol.devices[uid].settings_property['general_device_settings'].events_enabled = events_enabled
        self.homecontrol.updater._general_device(
            message={
                "properties": {
                    "uid": f"gds.{uid}",
                    "property.value.new": {
                        "eventsEnabled": not events_enabled,
                        "icon": self.devices['mains']['properties']['icon'],
                        "name": self.devices['mains']['properties']["itemName"],
                        "zoneID": self.devices['mains']['properties']["zoneId"],
                    },
                },
            })
        assert not self.homecontrol.devices[uid].settings_property['general_device_settings'].events_enabled

    def test__gateway_accessible(self):
        self.homecontrol.gateway.online = True
        self.homecontrol.gateway.sync = True
        accessible = self.homecontrol.gateway.online
        online_sync = self.homecontrol.gateway.sync
        self.homecontrol.updater._gateway_accessible(
            message={
                "properties": {
                    "property.name": "gatewayAccessible",
                    "property.value.new": {
                        "accessible": False,
                        "onlineSync": False,
                    },
                },
            })
        accessible_new = self.homecontrol.gateway.online
        online_sync_new = self.homecontrol.gateway.sync
        assert accessible != accessible_new
        assert online_sync != online_sync_new

    def test__grouping(self):
        zone_id = "hz_3"
        name = self.gateway['zones'][zone_id]
        self.homecontrol.updater._grouping(message={
            "properties": {
                "property.value.new": [{
                    "id": zone_id,
                    "name": self.devices['mains']['properties']['zone'],
                }],
            },
        })
        assert self.homecontrol.gateway.zones[zone_id] != name
        assert self.homecontrol.gateway.zones[zone_id] == self.devices['mains']['properties']['zone']

    def test__gui_enabled(self):
        uid = self.devices['mains']['uid']
        element_uids = self.devices['mains']['properties']['elementUIDs']
        enabled = self.devices['mains']['properties']['guiEnabled']
        self.homecontrol.devices[uid].binary_switch_property[element_uids[1]].enabled = enabled
        self.homecontrol.updater._gui_enabled(message={
            "uid": element_uids[0],
            "property.value.new": not enabled,
        })
        assert self.homecontrol.devices[uid].binary_switch_property[element_uids[1]].enabled != enabled

    def test__humidity_bar(self):
        uid = self.devices.get("humidity").get("uid")
        self.homecontrol.devices.get(uid).humidity_bar_property.get(f"devolo.HumidityBar:{uid}").value = 75
        self.homecontrol.devices.get(uid).humidity_bar_property.get(f"devolo.HumidityBar:{uid}").zone = 1
        current_value = self.homecontrol.devices.get(uid).humidity_bar_property.get(f"devolo.HumidityBar:{uid}").value
        current_zone = self.homecontrol.devices.get(uid).humidity_bar_property.get(f"devolo.HumidityBar:{uid}").zone
        self.homecontrol.updater._humidity_bar(
            message={"properties": {
                "uid": f"devolo.HumidityBarValue:{uid}",
                "property.value.new": 50,
            }})
        current_value_new = self.homecontrol.devices.get(uid).humidity_bar_property.get(f"devolo.HumidityBar:{uid}").value
        self.homecontrol.updater._humidity_bar(
            message={"properties": {
                "uid": f"devolo.HumidityBarZone:{uid}",
                "property.value.new": 0,
            }})
        current_zone_new = self.homecontrol.devices.get(uid).humidity_bar_property.get(f"devolo.HumidityBar:{uid}").zone

        assert current_value_new == 50
        assert current_zone_new == 0
        assert current_value != current_value_new
        assert current_zone != current_zone_new

    @pytest.mark.usefixtures("mock_publisher_add_event")
    @pytest.mark.usefixtures("mock_publisher_delete_event")
    @pytest.mark.parametrize("event", ["add", "delete"])
    def test__inspect_devices_valid(self, mocker, event):
        self.homecontrol.updater.on_device_change = lambda device_uids: (device_uids, event)
        spy = mocker.spy(self.homecontrol.updater._publisher, f"{event}_event")
        self.homecontrol.updater._inspect_devices(
            message={"properties": {
                "uid": "devolo.DevicesPage",
                "property.value.new": [],
            }})
        spy.assert_called_once()

    def test__inspect_devices_error(self, mocker):
        self.homecontrol.updater.on_device_change = None
        spy = mocker.spy(self.homecontrol.updater._logger, "error")
        self.homecontrol.updater._inspect_devices({})
        spy.assert_called_once_with("on_device_change is not set.")
        self.homecontrol.updater.on_device_change = lambda: None
        spy = mocker.spy(self.homecontrol.updater, "on_device_change")
        self.homecontrol.updater._inspect_devices(message={"properties": {
            "uid": "",
            "property.value.new": [],
        }})
        spy.assert_not_called()

    def test__meter(self):
        uid = self.devices.get("mains").get("uid")
        self.homecontrol.devices.get(uid).consumption_property.get(f"devolo.Meter:{uid}").current = 5
        self.homecontrol.devices.get(uid).consumption_property.get(f"devolo.Meter:{uid}").total = 230
        total = self.homecontrol.devices.get(uid).consumption_property.get(f"devolo.Meter:{uid}").total
        # Changing current value
        self.homecontrol.updater._meter(
            message={"properties": {
                "property.name": "currentValue",
                "uid": f"devolo.Meter:{uid}",
                "property.value.new": 7,
            }})
        current_new = self.homecontrol.devices.get(uid).consumption_property.get(f"devolo.Meter:{uid}").current
        # Check if current value has changed
        assert current_new == 7
        # Check if total has not changed
        assert total == self.homecontrol.devices.get(uid).consumption_property.get(f"devolo.Meter:{uid}").total
        # Changing total value
        self.homecontrol.updater._meter(
            message={"properties": {
                "property.name": "totalValue",
                "uid": f"devolo.Meter:{uid}",
                "property.value.new": 235
            }})
        total_new = self.homecontrol.devices.get(uid).consumption_property.get(f"devolo.Meter:{uid}").total
        # Check if total value has changed
        assert total_new == 235
        # Check if current value has not changed
        assert self.homecontrol.devices.get(uid).consumption_property.get(f"devolo.Meter:{uid}").current == current_new

    def test___multilevel_async(self):
        uid = self.devices['mains']['UID']
        element_uid = self.devices['mains']['properties']['settingUIDs'][4]
        value = self.devices['mains']['flashMode'] + 1
        self.homecontrol.updater._multilevel_async(message={"properties": {
            "uid": element_uid,
            "property.value.new": value
        }})
        assert self.homecontrol.devices[uid].settings_property['flash_mode'].value == value

    def test__multi_level_sensor(self):
        uid = self.devices['sensor']['uid']
        element_uid = f"devolo.MultiLevelSensor:{uid}#MultilevelSensor(1)"
        self.homecontrol.devices[uid].multi_level_sensor_property[element_uid].value = 100
        current = self.homecontrol.devices[uid].multi_level_sensor_property[element_uid].value
        self.homecontrol.updater._multi_level_sensor(message={"properties": {
            "uid": element_uid,
            "property.value.new": 50
        }})
        current_new = self.homecontrol.devices[uid].multi_level_sensor_property[element_uid].value

        assert current_new == 50
        assert current != current_new

    def test__multi_level_switch(self):
        device = self.devices.get("multi_level_switch")
        uid = device.get("uid")
        self.homecontrol.devices.get(uid).multi_level_switch_property.get(device.get("elementUIDs")[0]).value = \
            device.get("value")
        current = self.homecontrol.devices.get(uid).multi_level_switch_property.get(device.get("elementUIDs")[0]).value
        self.homecontrol.updater._multi_level_switch(
            message={"properties": {
                "uid": device.get("elementUIDs")[0],
                "property.value.new": device.get("max"),
            }})
        current_new = self.homecontrol.devices.get(uid).multi_level_switch_property.get(device.get("elementUIDs")[0]).value

        assert current_new == device.get("max")
        assert current != current_new

    def test__multilevel_sync_sensor(self):
        device = self.devices['sensor']
        uid = device['uid']
        value = device['properties']['value']
        self.homecontrol.devices[uid].settings_property['motion_sensitivity'].motion_sensitivity = value
        self.homecontrol.updater._multilevel_sync(
            message={"properties": {
                "uid": f"mss.{uid}",
                "property.value.new": value - 1,
            }})
        assert self.homecontrol.devices[uid].settings_property['motion_sensitivity'].motion_sensitivity == value - 1

    def test__multilevel_sync_shutter(self):
        device = self.devices['blinds']
        uid = device['uid']
        value = device['shutter_duration']
        self.homecontrol.devices[uid].settings_property['shutter_duration'].shutter_duration = value
        self.homecontrol.updater._multilevel_sync(
            message={"properties": {
                "uid": f"mss.{uid}",
                "property.value.new": value - 1,
            }})
        assert self.homecontrol.devices[uid].settings_property['shutter_duration'].shutter_duration == value - 1

    def test__multilevel_sync_siren(self):
        device = self.devices['siren']
        uid = device['uid']
        value = device['properties']['value']
        self.homecontrol.devices[uid].settings_property['tone'].tone = value
        self.homecontrol.updater._multilevel_sync(
            message={"properties": {
                "uid": f"mss.{uid}",
                "property.value.new": value - 1,
            }})
        assert self.homecontrol.devices[uid].settings_property['tone'].tone == value - 1

    def test__led(self):
        device = self.devices['mains']
        uid = device['uid']
        led_setting = device['properties']['led_setting']
        self.homecontrol.devices[uid].settings_property['led'].led_setting = led_setting
        self.homecontrol.updater._led(message={"properties": {
            "uid": f"lis.{uid}",
            "property.value.new": not led_setting,
        }})
        assert self.homecontrol.devices[uid].settings_property['led'].led_setting is not led_setting

    def test__parameter(self):
        device = self.devices['mains']
        uid = device['uid']
        param_changed = device['properties']['param_changed']
        self.homecontrol.devices[uid].settings_property['param_changed'].param_changed = param_changed
        self.homecontrol.updater._parameter(
            message={"properties": {
                "uid": f"cps.{uid}",
                "property.value.new": not param_changed,
            }})
        assert self.homecontrol.devices[uid].settings_property['param_changed'].param_changed is not param_changed

    def test__pending_operations_false(self):
        device = self.devices['mains']
        uid = device['uid']
        pending_operation = device['properties']['pending_operations']
        self.homecontrol.devices[uid].pending_operation = not pending_operation
        self.homecontrol.updater._pending_operations(message={"properties": {
            "uid": device['elementUIDs'][1]
        }})
        assert self.homecontrol.devices[uid].pending_operation

    def test__pending_operations_true(self):
        device = self.devices['mains']
        uid = device['uid']
        pending_operation = device['properties']['pending_operations']
        self.homecontrol.devices[uid].pending_operation = pending_operation
        self.homecontrol.updater._pending_operations(
            message={"properties": {
                "uid": device['elementUIDs'][1],
                "property.value.new": {
                    "status": 1
                }
            }})
        assert not self.homecontrol.devices[uid].pending_operation

    def test__pending_operations_setting(self):
        device = self.devices['mains']
        uid = device['uid']
        pending_operation = device['properties']['pending_operations']
        self.homecontrol.devices[uid].pending_operation = pending_operation
        self.homecontrol.updater._pending_operations(
            message={"properties": {
                "uid": device['settingUIDs'][3],
                "property.value.new": {
                    "status": 1,
                },
            }})
        assert not self.homecontrol.devices[uid].pending_operation

    def test__pending_operations_useless(self, mocker):
        spy = mocker.spy(self.homecontrol.updater._publisher, 'dispatch')
        self.homecontrol.updater._pending_operations(message={"properties": {
            "uid": "devolo.PairDevice",
        }})
        spy.assert_not_called()

    def test__protection_local(self):
        device = self.devices['mains']
        uid = device['uid']
        local_switch = device['properties']['local_switch']
        self.homecontrol.devices[uid].settings_property['protection'].local_switching = local_switch
        self.homecontrol.updater._protection(message={
            "properties": {
                "uid": f"ps.{uid}",
                "property.name": "targetLocalSwitch",
                "property.value.new": not local_switch,
            },
        })
        assert self.homecontrol.devices[uid].settings_property['protection'].local_switching is not local_switch

    def test__protection_remote(self):
        device = self.devices['mains']
        uid = device['uid']
        remote_switch = device['properties']['remote_switch']
        self.homecontrol.devices[uid].settings_property['protection'].remote_switching = remote_switch
        self.homecontrol.updater._protection(message={
            "properties": {
                "uid": f"ps.{uid}",
                "property.name": "targetRemoteSwitch",
                "property.value.new": not remote_switch,
            },
        })
        assert self.homecontrol.devices[uid].settings_property['protection'].remote_switching is not remote_switch

    def test__remote_control(self):
        device = self.devices.get("remote")
        uid = device.get("uid")
        self.homecontrol.devices.get(uid).remote_control_property \
            .get(device.get("elementUIDs")[0]).key_pressed = 0
        self.homecontrol.updater._remote_control(
            message={"properties": {
                "uid": device.get("elementUIDs")[0],
                "property.value.new": 1,
            }})

        assert self.homecontrol.devices.get(uid).remote_control_property \
            .get(device.get("elementUIDs")[0]).key_pressed == 1

    def test__since_time(self):
        device = self.devices['mains']
        uid = device['uid']
        now = datetime.now()
        total_since = self.homecontrol.devices[uid].consumption_property[f'devolo.Meter:{uid}'].total_since
        self.homecontrol.updater._since_time({
            "uid": f"devolo.Meter:{uid}",
            "property.value.new": now.replace(tzinfo=timezone.utc).timestamp() * 1000,
        })
        new_total_since = self.homecontrol.devices[uid].consumption_property[f'devolo.Meter:{uid}'].total_since
        assert total_since != new_total_since
        assert new_total_since == now

    def test__switch_type(self):
        device = self.devices['remote']
        uid = device['uid']
        self.homecontrol.updater._switch_type(
            message={"properties": {
                "uid": f"sts.{uid}",
                "property.value.new": device['key_count'] / 4,
            }})
        assert self.homecontrol.devices[uid].settings_property['switch_type'].value == device['key_count'] / 2

    def test__temperature_report(self):
        device = self.devices['sensor']
        uid = device['uid']
        self.homecontrol.devices[uid].settings_property['temperature_report'].temp_report = device['temp_report']
        self.homecontrol.updater._temperature_report(
            message={"properties": {
                "uid": f"trs.{uid}",
                "property.value.new": not device['temp_report'],
            }})
        assert self.homecontrol.devices[uid].settings_property['temperature_report'].temp_report is not device['temp_report']

    def test__voltage_multi_level_sensor(self):
        uid = self.devices.get("mains").get("uid")
        self.homecontrol.devices.get(uid).multi_level_sensor_property.get(f"devolo.VoltageMultiLevelSensor:{uid}").value = 231
        current = self.homecontrol.devices.get(uid).multi_level_sensor_property.get(
            f"devolo.VoltageMultiLevelSensor:{uid}").value
        self.homecontrol.updater._voltage_multi_level_sensor(
            message={"properties": {
                "uid": f"devolo.VoltageMultiLevelSensor:{uid}",
                "property.value.new": 234
            }})
        current_new = self.homecontrol.devices.get(uid).multi_level_sensor_property.get(
            f"devolo.VoltageMultiLevelSensor:{uid}").value

        assert current_new == 234
        assert current != current_new
