"""Tests for the ``mtp.apps.common.limpyd_database`` module"""

import json
from datetime import timedelta
from logging import ERROR, INFO, WARNING
from mock import patch

from django.test import TestCase
from django.utils.timezone import now

from mtp.apps.common.limpyd_base import DjangoBackedRedisModelWithLogs

from .limpyd_models import RedisTestModel
from .models import DjangoTestModel, DjangoTestModelWithLogs


class LimpydBaseTestCase(TestCase):
    """TestCase to hold tests about ``limpyd_database``"""

    def test_link_between_django_and_redis_models(self):
        """Test that the link between django an redis models works"""

        # create a django instance
        django_instance = DjangoTestModel.objects.create(
            some_field='django field'
        )

        # create a limpyd instance
        redis_instance = django_instance.redis_instance
        redis_instance.some_field.set('limpyd field')

        # check that the limpyd instance was correctly created
        self.assertEqual(redis_instance.pk.get(), django_instance.pk)
        redis_instance2 = RedisTestModel.get(django_instance.pk)
        self.assertEqual(redis_instance2.some_field.get(), 'limpyd field')

        # check that we can get back the django instance
        django_instance2 = redis_instance2.django_instance
        self.assertEqual(django_instance2.some_field, 'django field')


class RedisBackedDjangoModelWithLogsTestCase(TestCase):
    """TestCase to hold tests about ``RedisBackedDjangoModelWithLogs``"""

    @classmethod
    def setUpClass(cls):
        """Set up the django and limpyd instances"""

        super().setUpClass()

        # create a django instance
        cls.django_instance = DjangoTestModelWithLogs.objects.create()

        # Shortcut for the "logger" django instance attribute
        cls.logger = cls.django_instance.logger

        # Shortcut for the "logs" limpyd field
        cls.log_field = cls.logger.logs

    def setUp(self):
        """Clean the log entries"""

        super().setUp()

        self.log_field.delete()

    def test_decode_log_should_decode_a_log_entry(self):
        """Test that the ``_decode_log`` work correctly"""

        # We remove the microseconds since sometimes,
        # the timestamp encoding / decoding don't output exactly the same date
        # as we got one microsecond of difference between the input and the output...
        now_utc_datetime = now().replace(microsecond=0)

        # Test with a recognized log level (10 = INFO level)
        log_data = {
            'level': 10,
            'unix_timestamp': now_utc_datetime.timestamp(),
            'msg': 'This a debug message'
        }
        json_as_string = json.dumps(log_data)

        self.assertEqual(
            self.logger._decode_entry(json_as_string),
            {
                'date': now_utc_datetime,
                'level': 10,
                'level_str': 'DEBUG',
                'msg': 'This a debug message'
            }
        )

        # Test with a unrecognized log level
        log_data = {
            'level': 13,
            'unix_timestamp': now_utc_datetime.timestamp(),
            'msg': 'This a debug message'
        }
        json_as_string = json.dumps(log_data)

        self.assertEqual(
            self.logger._decode_entry(json_as_string),
            {
                'date': now_utc_datetime,
                'level': 13,
                'msg': 'This a debug message'
            }
        )

    def test_log_method_should_add_log_entry(self):
        """Test that the ``log`` method add a log entry"""

        # Add a log entry with a recognized level
        self.logger.log(10, 'This is a log entry')

        self.assertEqual(
            len(list(self.log_field.zrange(0, -1))),
            1
        )

        # Add another log entry with a unrecognized level
        self.logger.log(666, 'This is another log entry')

        self.assertEqual(
            len(list(self.log_field.zrange(0, -1))),
            2
        )

    def test_entries_by_index_should_return_the_logs(self):
        """Test that the ``entries_by_index`` method work correctly"""

        # We add an INFO entry
        self.logger.info('Info entry')

        # We add an ERROR entry
        self.logger.error('Error entry')

        logs = self.logger.entries_by_index(0, -1)

        # We should get the INFO entry first since it was added first
        self.assertEqual(
            next(logs)['level_str'],
            'INFO'
        )

        # The ERROR entry is second
        self.assertEqual(
            next(logs)['level_str'],
            'ERROR'
        )

        # We use the ``reverse`` flag...
        logs = self.logger.entries_by_index(0, -1, reverse=True)

        # ...so the ERROR entry should be first
        self.assertEqual(
            next(logs)['level_str'],
            'ERROR'
        )

        # ...and the INFO one, second
        self.assertEqual(
            next(logs)['level_str'],
            'INFO'
        )

    def test_last_entry_should_return_the_last_added_entry(self):
        """Test that the ``last_entry`` method work correctly"""

        # We add an INFO entry
        self.logger.info('Info entry')

        # We add an ERROR entry
        self.logger.error('Error entry')

        # The last entry is the ERROR one
        self.assertEqual(
            self.logger.last_entry()['level_str'],
            'ERROR'
        )

        # We add a WARNING entry
        self.logger.warning('Error entry')

        # The last entry is the WARNING one
        self.assertEqual(
            self.logger.last_entry()['level_str'],
            'WARNING'
        )

    def test_entries_by_date_should_filter_the_entries(self):
        """Test that the ``entries_by_date`` method should filter the entries"""

        now_utc = now().replace(microsecond=0)
        one_day_later_utc = now_utc + timedelta(days=1)
        one_day_ago_utc = now_utc - timedelta(days=1)
        two_days_ago_utc = now_utc - timedelta(days=2)
        three_days_ago_utc = now_utc - timedelta(days=3)

        # Add a log entry now
        self.log_field.zadd(
            now_utc.timestamp(),
            json.dumps({
                'level': 10,
                'msg': 'test',
                'unix_timestamp': now_utc.timestamp()
            })
        )

        # Add a log entry two days ago
        self.log_field.zadd(
            two_days_ago_utc.timestamp(),
            json.dumps({
                'level': 10,
                'msg': 'test',
                'unix_timestamp': two_days_ago_utc.timestamp()
            })
        )

        # We got 1 entry between now minus 1 day and now minus 3 days
        self.assertEqual(
            len(list(self.logger.entries_by_date(three_days_ago_utc, one_day_ago_utc))),
            1
        )

        # We got 1 entry between now plus 1 day and now minus 1 days
        self.assertEqual(
            len(list(self.logger.entries_by_date(one_day_ago_utc, one_day_later_utc))),
            1
        )

        # We got 2 entries between now plus 1 day and now minus 3 days
        self.assertEqual(
            len(list(self.logger.entries_by_date(three_days_ago_utc, one_day_later_utc))),
            2
        )

        # We manually add an entry at the given timestamp
        three_days_ago_utc_timestamp = three_days_ago_utc.timestamp()

        self.log_field.zadd(
            three_days_ago_utc_timestamp,
            json.dumps(
                {
                    'msg': 'test',
                    'level': 10,
                    'unix_timestamp': three_days_ago_utc_timestamp
                }
            )
        )

        # We got only one entry, and this is this one
        entries = self.logger.entries_by_date(three_days_ago_utc, three_days_ago_utc)

        self.assertEqual(
            next(entries),
            {
                'msg': 'test',
                'level': 10,
                'level_str': 'DEBUG',
                'date': three_days_ago_utc
            }
        )

    def test_info_method_should_call_log_method(self):
        """Test that the ``info`` method work correctly"""

        with patch.object(DjangoBackedRedisModelWithLogs, '_log') as patched:
            self.logger.info('Info %', 'message')

        patched.assert_called_once_with(INFO, 'Info %', ('message',))

    def test_warning_method_should_call_log_method(self):
        """Test that the ``warning`` method work correctly"""

        with patch.object(DjangoBackedRedisModelWithLogs, '_log') as patched:
            self.logger.warning('Warning %', 'message')

        patched.assert_called_once_with(WARNING, 'Warning %', ('message',))

    def test_error_method_should_call_log_method(self):
        """Test that the ``error`` method work correctly"""

        with patch.object(DjangoBackedRedisModelWithLogs, '_log') as patched:
            self.logger.error('Error %', 'message')

        patched.assert_called_once_with(ERROR, 'Error %', ('message',))
