import datetime
from typing import Dict
from unittest import mock
from unittest.mock import MagicMock

from logic import Helpers
from logic.tile.tiles.SensorLineChartTile import SensorLineChartTile, SensorType


def example_settings(showAxes: bool):
    return {
        "title": "My Room",
        "url": "http://127.0.0.1:10003",
        "sensorID": 1,
        "sensorIDsForMinMax": [2, 3, 4],
        "numberOfHoursToShow": 4,
        "decimals": 1,
        "lineColor": "rgba(254, 151, 0, 1)",
        "fillColor": "rgba(254, 151, 0, 0.2)",
        "showAxes": showAxes,
        "outdatedValueWarningLimitInSeconds": 300  # use -1 to disable warning
    }


def storage_leaf_service_mock(minValue, maxValue):
    storageLeafServiceMock = MagicMock()
    storageLeafServiceMock.get_data = MagicMock(return_value={
        'min': minValue,
        'max': maxValue
    })
    return storageLeafServiceMock


class TestGetMinMax:
    START_DATE = datetime.date(year=2021, month=2, day=8)
    END_DATE = datetime.date(year=2021, month=2, day=9)

    def test_humidity_returns_0_and_100(self):
        tile = SensorLineChartTile('mySensorTile', example_settings(False), 10)
        yValues = ['12.5', '10.5']

        result = tile._get_min_and_max('myPage',
                                       SensorType.HUMIDITY,
                                       self.START_DATE, self.END_DATE,
                                       storage_leaf_service_mock(15.0, 20.0),
                                       yValues)
        assert result == (0, 100)

    def test_temperature_no_values_returns_min_and_max_of_api(self):
        tile = SensorLineChartTile('mySensorTile', example_settings(False), 10)

        result = tile._get_min_and_max('myPage',
                                       SensorType.TEMPERATURE,
                                       self.START_DATE, self.END_DATE,
                                       storage_leaf_service_mock(15.0, 20.0),
                                       [])
        assert result == (-SensorLineChartTile.MAX_Y_AXIS_SPACING, 20 + SensorLineChartTile.MAX_Y_AXIS_SPACING)

    def test_temperature_min_max_data_is_none_returns_zero(self):
        tile = SensorLineChartTile('mySensorTile', example_settings(False), 10)

        result = tile._get_min_and_max('myPage',
                                       SensorType.TEMPERATURE,
                                       self.START_DATE, self.END_DATE,
                                       storage_leaf_service_mock(None, None),
                                       [])
        assert result == (-SensorLineChartTile.MAX_Y_AXIS_SPACING, SensorLineChartTile.MAX_Y_AXIS_SPACING)

    def test_temperature_min_is_above_zero_returns_zero_min(self):
        tile = SensorLineChartTile('mySensorTile', example_settings(False), 10)

        result = tile._get_min_and_max('myPage',
                                       SensorType.TEMPERATURE,
                                       self.START_DATE, self.END_DATE,
                                       storage_leaf_service_mock(5, 6),
                                       [])
        assert result == (-SensorLineChartTile.MAX_Y_AXIS_SPACING, 6 + SensorLineChartTile.MAX_Y_AXIS_SPACING)

    def test_temperature_min_is_below_zero_returns_min(self):
        tile = SensorLineChartTile('mySensorTile', example_settings(False), 10)

        result = tile._get_min_and_max('myPage',
                                       SensorType.TEMPERATURE,
                                       self.START_DATE, self.END_DATE,
                                       storage_leaf_service_mock(-3, 6),
                                       [])
        assert result == (-3 - SensorLineChartTile.MAX_Y_AXIS_SPACING, 6 + SensorLineChartTile.MAX_Y_AXIS_SPACING)

    def test_temperature_show_axes_no_values(self):
        tile = SensorLineChartTile('mySensorTile', example_settings(True), 10)

        result = tile._get_min_and_max('myPage',
                                       SensorType.TEMPERATURE,
                                       self.START_DATE, self.END_DATE,
                                       storage_leaf_service_mock(None, None),
                                       [])
        assert result == (-SensorLineChartTile.MAX_Y_AXIS_SPACING, SensorLineChartTile.MAX_Y_AXIS_SPACING)

    def test_temperature_show_axes_values_above_zero_return_zero_min(self):
        tile = SensorLineChartTile('mySensorTile', example_settings(True), 10)

        result = tile._get_min_and_max('myPage',
                                       SensorType.TEMPERATURE,
                                       self.START_DATE, self.END_DATE,
                                       storage_leaf_service_mock(None, None),
                                       [6.0, 12])
        assert result == (-SensorLineChartTile.MAX_Y_AXIS_SPACING, 12 + SensorLineChartTile.MAX_Y_AXIS_SPACING)

    def test_temperature_show_axes_values_below_zero_return_min(self):
        tile = SensorLineChartTile('mySensorTile', example_settings(True), 10)

        result = tile._get_min_and_max('myPage',
                                       SensorType.TEMPERATURE,
                                       self.START_DATE, self.END_DATE,
                                       storage_leaf_service_mock(None, None),
                                       [-6.0, 12])
        assert result == (-6 - SensorLineChartTile.MAX_Y_AXIS_SPACING, 12 + SensorLineChartTile.MAX_Y_AXIS_SPACING)


class TestPrepareMeasurementData:
    def test_no_measurements(self):
        tile = SensorLineChartTile('mySensorTile', example_settings(False), 10)
        measurements = []

        assert tile._prepare_measurement_data(measurements) == ([], [])

    def test_should_return_rounded_values(self):
        tile = SensorLineChartTile('mySensorTile', example_settings(False), 10)
        measurements = [
            {'id': 409281, 'value': '-5.37', 'timestamp': '2021-02-09 17:47:55', 'sensor_id': 5}
        ]

        assert tile._prepare_measurement_data(measurements) == (['2021-02-09 17:47:55'], ['-5.4'])

    def test_multiple_measurements_should_return_timestamps_from_latest_to_oldest(self):
        tile = SensorLineChartTile('mySensorTile', example_settings(False), 10)

        timestamp1 = '2021-02-09 17:47:55'
        timestamp2 = '2021-02-09 17:48:55'
        measurements = [
            {'id': 409281, 'value': '-5.37', 'timestamp': timestamp1, 'sensor_id': 5},
            {'id': 409282, 'value': '-6.2', 'timestamp': timestamp2, 'sensor_id': 5}
        ]

        assert tile._prepare_measurement_data(measurements) == ([timestamp2, timestamp1], ['-6.2', '-5.4'])


class TestPrepareGhostTrace:
    def test_all_values_below_zero_returns_empty_lists(self):
        tile = SensorLineChartTile('mySensorTile', example_settings(False), 10)
        x = ['2021-02-09 17:47:55']
        y = [-12]
        assert tile._prepare_ghost_trace(-10, x, y) == ([], [])

    def test_all_values_above_zero_and_min_from_api_above_zero_returns_empty_lists(self):
        tile = SensorLineChartTile('mySensorTile', example_settings(False), 10)
        x = ['2021-02-09 17:47:55', '2021-02-09 17:48:55']
        y = [6, 8]
        assert tile._prepare_ghost_trace(10, x, y) == ([], [])

    def test_all_values_above_zero_and_min_from_api_below_zero_returns_ghost_trace(self):
        tile = SensorLineChartTile('mySensorTile', example_settings(False), 10)
        x = ['2021-02-09 17:47:55', '2021-02-09 17:48:55']
        y = [6, 8]
        assert tile._prepare_ghost_trace(-10, x, y) == (x, [-10, -10])

    def test_no_values_returns_empty_lists(self):
        tile = SensorLineChartTile('mySensorTile', example_settings(False), 10)
        x = []
        y = []
        assert tile._prepare_ghost_trace(-10, x, y) == ([], [])


class TestGetTimeSinceLastValue:
    def __get_warning_settings(self, enable: bool):
        return {
            'enable': enable,
            'limitInSeconds': 10,
            'enableNotificationViaPushbullet': False,
            'pushbulletToken': None
        }

    def test_warnings_disabled_returns_empty_string(self):
        tile = SensorLineChartTile('mySensorTile', example_settings(False), 10)

        warningSettings = self.__get_warning_settings(False)
        data = {'latestTime': SensorLineChartTile.DATETIME_UNIX_TIMESTAMP_START}

        assert tile._get_time_since_last_value(warningSettings, data) == ''

    def test_warnings_enabled_no_outdated_value_returns_empty_string(self):
        tile = SensorLineChartTile('mySensorTile', example_settings(False), 10)

        warningSettings = self.__get_warning_settings(True)
        data = {'latestTime': datetime.datetime.now()}

        assert tile._get_time_since_last_value(warningSettings, data) == ''

    @mock.patch('logic.tile.tiles.SensorLineChartTile.datetime')
    def test_warnings_enabled_outdated_value_returns_human_readable_string(self, datetimeMock):
        tile = SensorLineChartTile('mySensorTile', example_settings(False), 10)

        datetimeMock.now.return_value = datetime.datetime(year=2021, month=1, day=1, hour=12, minute=00, second=00)

        warningSettings = self.__get_warning_settings(True)
        data = {'latestTime': datetime.datetime(year=2021, month=1, day=1, hour=11, minute=00, second=00)}

        assert tile._get_time_since_last_value(warningSettings, data) == '1 hour ago'


class TestSendNotification:
    def __get_warning_settings(self, enable: bool, enableNotification: bool) -> Dict:
        return {
            'enable': enable,
            'limitInSeconds': 10,
            'enableNotificationViaPushbullet': enableNotification,
            'pushbulletToken': 'myToken'
        }

    def __get_sensor_info(self) -> Dict[str, str]:
        return {
            'name': 'mySensor',
            'type': 'temperature'
        }

    def __get_device_info(self) -> Dict[str, str]:
        return {
            'name': 'myDevice'
        }

    @mock.patch('logic.tile.tiles.SensorLineChartTile.Helpers')
    def test_notification_disabled_should_do_nothing(self, helpersMock):
        tile = SensorLineChartTile('mySensorTile', example_settings(False), 10)

        warningSettings = self.__get_warning_settings(True, False)

        tile._send_notification(warningSettings, {}, {}, '1 hour ago')
        helpersMock.send_notification_via_pushbullet.assert_not_called()

    @mock.patch('logic.tile.tiles.SensorLineChartTile.Helpers')
    def test_notification_enabled_no_outdated_value_should_do_nothing(self, helpersMock):
        tile = SensorLineChartTile('mySensorTile', example_settings(False), 10)

        warningSettings = self.__get_warning_settings(True, True)

        tile._send_notification(warningSettings, {}, {}, '')
        helpersMock.send_notification_via_pushbullet.assert_not_called()

    @mock.patch('logic.Helpers.requests')
    def test_send_notification_should_call_pushbullet_api(self, requestsMock):
        tile = SensorLineChartTile('mySensorTile', example_settings(False), 10)

        warningSettings = self.__get_warning_settings(True, True)

        requestsMock.post.return_value.status_code = 200

        tile._send_notification(warningSettings, self.__get_sensor_info(), self.__get_device_info(), '1 hour ago')
        requestsMock.post.assert_called_once_with(Helpers.PUSHBULLET_PUSH_URL, data=mock.ANY, headers=mock.ANY)

    @mock.patch('logic.Helpers.requests')
    def test_already_sent_should_skip_sending(self, requestsMock):
        tile = SensorLineChartTile('mySensorTile', example_settings(False), 10)

        warningSettings = self.__get_warning_settings(True, True)

        requestsMock.post.return_value.status_code = 200

        tile._send_notification(warningSettings, self.__get_sensor_info(), self.__get_device_info(), '1 hour ago')
        tile._send_notification(warningSettings, self.__get_sensor_info(), self.__get_device_info(), '1 hour ago')
        requestsMock.post.assert_called_once()

    @mock.patch('logic.Helpers.requests')
    def test_already_sent_new_value_arrives_and_gets_outdated_should_call_pushbullet_api(self, requestsMock):
        tile = SensorLineChartTile('mySensorTile', example_settings(False), 10)

        warningSettings = self.__get_warning_settings(True, True)
        requestsMock.post.return_value.status_code = 200

        tile._send_notification(warningSettings, self.__get_sensor_info(), self.__get_device_info(), '1 hour ago')

        # a new valid value arrives
        tile._send_notification(warningSettings, self.__get_sensor_info(), self.__get_device_info(), '')

        # value is outdated again
        tile._send_notification(warningSettings, self.__get_sensor_info(), self.__get_device_info(), '1 hour ago')

        assert requestsMock.post.call_count == 2