import datetime
import json
from secrets import choice
from unittest.mock import patch

import fakeredis
from unittest import mock
import pytest
from django.contrib.auth import get_user_model
from django.urls import reverse

from assets.models.assets import OperatingSystem, Asset
# noinspection PyUnresolvedReferences
from console.tests.test_utils import add_user_with_permissions, get_url, test_server
from core.utils import dtnow
from dashboard.models import DashboardLayout
# TODO: enable when correlator is ready
# from dashboard.tasks import update_incs_by_time_statistics, update_events_by_time_statistics
from dashboard.tasks import update_incs_by_time_statistics
from dashboard.utils import RedisInstances
from dashboard.views import check_user_widgets
from incident.models import Incident
from networkmap.api import DANGER_STATUSES
from perms.models import Perm
from users.models import UserInfo

# from dashboard.serializers import DashboardSerializer

User = get_user_model()

real_datetime_class = datetime.datetime
USERNAME = 'test_not_superuser'
PASSWORD = 'test_password'


def mock_datetime_now(target, dt):
    class DatetimeSubclassMeta(type):
        @classmethod
        def __instancecheck__(mcs, obj):
            return isinstance(obj, real_datetime_class)

    class BaseMockedDatetime(real_datetime_class):
        @classmethod
        def now(cls, tz=None):
            return target.replace(tzinfo=tz)

        @classmethod
        def utcnow(cls):
            return target

    # Python2 & Python3 compatible metaclass
    MockedDatetime = DatetimeSubclassMeta('datetime', (BaseMockedDatetime,), {})

    return mock.patch.object(dt, 'datetime', MockedDatetime)


@pytest.mark.django_db
class TestApi(object):

    @pytest.fixture(autouse=True)
    def testing_redis(self, django_user_model):
        server = fakeredis.FakeServer()
        test_redis = fakeredis.FakeStrictRedis(server=server)
        test_redis.set(RedisInstances.WIDGET_INCS_BY_TIME_DAY, json.dumps([0] * 24))
        test_redis.set(RedisInstances.WIDGET_INCS_BY_TIME_WEEK, json.dumps([0] * 7))
        test_redis.set(RedisInstances.WIDGET_INCS_BY_TIME_MONTH, json.dumps([0] * 30))
        test_redis.set(RedisInstances.WIDGET_INCS_BY_TIME_YEAR, json.dumps([0] * 12))
        test_redis.set(RedisInstances.WIDGET_EVENTS_BY_TIME_DAY, json.dumps([0] * 24))
        test_redis.set(RedisInstances.WIDGET_EVENTS_BY_TIME_WEEK, json.dumps([0] * 7))
        test_redis.set(RedisInstances.WIDGET_EVENTS_BY_TIME_MONTH, json.dumps([0] * 30))
        test_redis.set(RedisInstances.WIDGET_EVENTS_BY_TIME_YEAR, json.dumps([0] * 12))
        redis_patcher = patch('dashboard.tasks.redis_instance', test_redis)
        self.redis = redis_patcher.start()

        user1 = django_user_model.objects.create_superuser(username='admin400', password='nimda')
        user2 = django_user_model.objects.create_user(username=USERNAME, password=PASSWORD)
        UserInfo(user=user1).save()
        UserInfo(user=user2).save()

        return test_redis

    @pytest.mark.unit
    def test_inccidents_by_time_hour_task(self, testing_redis):
        """ Test for checking the celery task for incidents by time widget past hour incident sum
        Steps of the test:
            1. Get REDIS data before invoking task
            2. Create 2 incidents
            3. Run task again
            4. Check if value of day stats in REDIS changed
        :param testing_redis: fixture for initializing testing REDIS instance
        """
        day_data_before = json.loads(testing_redis.get(RedisInstances.WIDGET_INCS_BY_TIME_DAY))
        Incident.objects.create(timestamp=dtnow(), title='test_inc_1', importance=1, status=2, event_count=1, events='')
        update_incs_by_time_statistics()
        day_data_after = json.loads(testing_redis.get(RedisInstances.WIDGET_INCS_BY_TIME_DAY))
        assert day_data_before != day_data_after

    @pytest.mark.unit
    def test_incidents_by_time_week_update(self, testing_redis):
        """ Test for checking the data for incidents by time widget (week data), when day changes
        Steps of the test:
            1. Create 3 incidents with the last of which occurs the day after the first two
            2. Every time incidents created, call update widget data function
            3. Compare last value of week data with 3 (sum of incidents)
        :param testing_redis:
        """
        for i in range(3):
            incident_datetime = datetime.datetime(2020, 1, 1, 21, 59, 0, 0, None) + datetime.timedelta(hours=i)

            def dtnow_for_widgets(minutes=0):
                hours_from_dtnow_call = minutes
                return incident_datetime + datetime.timedelta(minutes=1, hours=hours_from_dtnow_call)

            with patch('dashboard.tasks.dtnow', dtnow_for_widgets):
                created_inc = Incident.objects.create(timestamp=incident_datetime,
                                                      title=f"test_inc{i}",
                                                      importance=1,
                                                      status=2,
                                                      event_count=1,
                                                      events='')
                update_incs_by_time_statistics()
        week_stats = json.loads(testing_redis.get(RedisInstances.WIDGET_INCS_BY_TIME_WEEK))
        assert week_stats[-1] == 1

    @pytest.mark.unit
    def test_incidents_by_time_month_update(self, testing_redis):
        """ Test for checking the data for incidents by time widget (month data), when day changes
        Steps of the test:
            1. Create 3 incidents with the last of which occurs the day after the first two
            2. Every time incidents created, call update widget data function
            3. Compare last value of week data with 3 (sum of incidents)
        :param testing_redis:
        """
        for i in range(3):
            incident_datetime = datetime.datetime(2020, 1, 1, 21, 59, 0, 0, None) + datetime.timedelta(hours=i)

            def dtnow_for_widgets(minutes=0):
                hours_from_dtnow_call = minutes
                return incident_datetime + datetime.timedelta(minutes=1, hours=hours_from_dtnow_call)

            with patch('dashboard.tasks.dtnow', dtnow_for_widgets):
                created_inc = Incident.objects.create(timestamp=incident_datetime,
                                                      title=f"test_inc{i}",
                                                      importance=1,
                                                      status=2,
                                                      event_count=1,
                                                      events='')
                update_incs_by_time_statistics()
        month_stats = json.loads(testing_redis.get(RedisInstances.WIDGET_INCS_BY_TIME_MONTH))
        assert month_stats[-1] == 1

    @pytest.mark.unit
    def test_incidents_by_time_year_update(self, testing_redis):
        """ Test for checking the data for incidents by time widget (month data), when day changes
        Steps of the test:
            1. Create 3 incidents with the last of which occurs the day after the first two and on next month
            2. Every time incidents created, call update widget data function
            3. Compare last value of week data with 3 (sum of incidents)
        :param testing_redis: fixture for initializing testing REDIS instance
        """
        for i in range(3):
            incident_datetime = datetime.datetime(2020, 1, 31, 21, 59, 0, 0, None) + datetime.timedelta(hours=i)

            def dtnow_for_widgets(minutes=0):
                hours_from_dtnow_call = minutes
                return incident_datetime + datetime.timedelta(minutes=1, hours=hours_from_dtnow_call)

            with patch('dashboard.tasks.dtnow', dtnow_for_widgets):
                created_inc = Incident.objects.create(timestamp=incident_datetime,
                                                      title=f"test_inc{i}",
                                                      importance=1,
                                                      status=2,
                                                      event_count=1,
                                                      events='')
                update_incs_by_time_statistics()
        year_stats = json.loads(testing_redis.get(RedisInstances.WIDGET_INCS_BY_TIME_YEAR))
        assert year_stats[-1] == 1

    @pytest.mark.unit
    def test_assets_by_incs_widget_counting_right_incidents(self, get_url, client):
        """
        Test for checking if widget backend correctly counts right incidents
        Steps of the test:
            1. Create asset for testing purposes
            2. Create 3 incidents with statuses within DANGER_STATUSES list and assign them to the asset from step 1
            3. Run widget backend
            4. Change status of one created incident to RESOLVED
            5. Run widget backend again
            6. Compare results of step 2 and 4
        :return:
            PASSED if values are not equal
            FAILED if not
        """
        test_os = OperatingSystem.objects.create(name='test_OS')
        test_asset = Asset.objects.create(name='test_asset', ip='1.1.1.1', os=test_os)
        for i in range(3):
            test_inc = Incident.objects.create(timestamp=dtnow(),
                                               title=f"test_inc{i}",
                                               importance=1,
                                               status=choice(DANGER_STATUSES),
                                               event_count=1,
                                               events='')
            test_asset.incidents.add(test_inc)
        url = reverse('assets_by_incs-list')
        client.force_login(User.objects.get(username='admin400'))
        response = client.get(url)
        response_result_before = json.loads(response.content.decode('utf-8'))
        inc_count_before = response_result_before[0].get('inc_count')
        changing_inc = Incident.objects.get(title='test_inc1')
        changing_inc.status = Incident.Status.RESOLVED
        changing_inc.save()
        response = client.get(url)
        response_result_after = json.loads(response.content.decode('utf-8'))
        inc_count_after = response_result_after[0].get('inc_count')
        assert inc_count_after != inc_count_before

    @pytest.mark.unit
    def test_opened_incs_widget_counter(self, client):
        """
        Test for checking if widget backend correctly counts opened widgets
        Steps of the test:
            1. Create incidents with statuses within DANGER_STATUSES list
            2. Run widget backend
            3. Change status of 1 incident from step 1 to RESOLVED
            4. Run backend again
            5. Compare results of step 2 and step 4
        :param client: django testing client
        :return:
            PASSED if values are not equal
            FAILED othervise
        """
        for i in range(3):
            Incident.objects.create(timestamp=dtnow(),
                                    title=f"test_inc{i}",
                                    importance=1,
                                    status=choice(DANGER_STATUSES),
                                    event_count=1,
                                    events='')
        url = reverse('opened_incs')
        client.force_login(User.objects.get(username='admin400'))
        response = client.get(url)
        response_result_before = json.loads(response.content.decode('utf-8'))
        count_before = response_result_before.get('inc_count')
        changing_inc = Incident.objects.get(title='test_inc1')
        changing_inc.status = Incident.Status.RESOLVED
        changing_inc.save()
        response = client.get(url)
        response_result_after = json.loads(response.content.decode('utf-8'))
        count_after = response_result_after.get('inc_count')
        assert count_before != count_after

    @pytest.mark.unit
    @pytest.mark.parametrize('user_perms, result_count, widget_name', [
        ([], 0, ''),
        ([Perm.can_view_correlation_rules_list], 1, 'correlator-info'),
        ([[Perm.can_view_sys_info], 2, 'sys-info']),
    ])
    def test_user_allowed_widgets(self, client, user_perms: list, result_count: int, widget_name: str):
        """ Test for checking the filtration of user system information and services widget,
            depending on his permissions
        """
        # Getting testing user
        test_user = User.objects.get(username=USERNAME)
        # Getting user dashboard instance
        widgets_before = DashboardLayout.objects.get(user=test_user)
        # Adding testing widgets to users dashboard
        widgets_before.widgets = [
            {
                "x": "0",
                "y": "0",
                "id": "sys-info",
                "width": "2",
                "height": "5"
            },
            {
                "x": "2",
                "y": "5",
                "id": "services",
                "width": "2",
                "height": "5"
            },
            {
                "x": "4",
                "y": "10",
                "id": "correlator-info",
                "width": "2",
                "height": "5"
            },
            {
                "x": "6",
                "y": "15",
                "id": "incs-by-category",
                "width": "2",
                "height": "5"
            },
        ]
        widgets_before.save()

        [test_user.user_permissions.add(Perm.get_rights(perm)) for perm in user_perms]
        check_user_widgets(test_user)
        widgets_after = DashboardLayout.objects.get(user=test_user)

        assert len(widgets_after.widgets) == result_count
        if result_count:
            assert widgets_after.widgets[0]['id'] == widget_name
