"""This modules holds models tests for ``common`` app."""

import string

from mock import patch

from django.conf import settings
from django.core.exceptions import ValidationError
from django.test import TestCase

from mtp.apps.common.tests.common_testapp.constants import TOKEN_LENGTH
from mtp.apps.common.tests.common_testapp.factories import ObjectWithTokenFactory
from mtp.apps.common.tests.common_testapp.models import ObjectWithToken
from mtp.apps.common.tests.test_cases import run_tests_concurrently

from ..models import Tokenable


class TokenableTestCase(TestCase):
    """Test the ``Tokenable`` abstract class."""

    @classmethod
    def setUpTestData(cls):
        """Create an object with token."""

        super().setUpTestData()

        cls.object_with_token = ObjectWithTokenFactory()

    def assertTokenGeneration(self, token, patterns, length):
        """Check if the provided token is in accordance with patterns and length."""

        self.assertEqual(len(token), length)
        for element in token:
            self.assertTrue(element in patterns)

    def test_token_generation(self):
        """Test the token generation with different patterns and length."""

        patterns, length = string.ascii_uppercase, 5
        for __ in range(10):
            token = Tokenable._generate_token(patterns, length)
            with self.subTest(token=token):
                self.assertTokenGeneration(token, patterns, length)

        patterns, length = string.ascii_uppercase + string.digits, 10
        for __ in range(10):
            token = Tokenable._generate_token(patterns, length)
            with self.subTest(token=token):
                self.assertTokenGeneration(token, patterns, length)

    def test_already_assigned(self):
        """Test the ``_already_assigned`` method."""

        ObjectWithTokenFactory(token='12345')

        other_object_with_token = ObjectWithToken(token='12345')
        self.assertTrue(other_object_with_token._already_assigned())

        other_object_with_token = ObjectWithToken(token='12346')
        self.assertFalse(other_object_with_token._already_assigned())

    @run_tests_concurrently(20)
    def test_assign_token(self):
        """Test the ``_assign_token`` method."""

        object_with_token = ObjectWithTokenFactory()

        self.assertIsNone(object_with_token.token)

        object_with_token._assign_token()

        self.assertIsNotNone(object_with_token.token)
        self.assertTokenGeneration(
            object_with_token.token, settings.DEFAULT_TOKEN_PATTERNS, TOKEN_LENGTH
        )

    def test_token_availability(self):
        """Test the token availability during multiple token assignation."""

        with patch.object(ObjectWithToken, '_generate_token', return_value='12345'):
            token = ObjectWithTokenFactory()._assign_token()
            self.assertEqual(token, '12345')

            # There are no longer any available token
            with self.assertRaises(ValidationError):
                ObjectWithTokenFactory()._assign_token()

    def test_token_deletion(self):
        """Test the ``delete_token`` method."""

        # We must assign a token before delete it
        self.object_with_token._assign_token()
        self.assertIsNotNone(self.object_with_token.token)

        self.object_with_token.delete_token()
        self.assertIsNone(self.object_with_token.token)

    def test_get_or_assign_token(self):
        """Test the ``get_or_assign_token`` method."""

        new_object_with_token = ObjectWithTokenFactory()

        # New object has no token
        self.assertIsNone(new_object_with_token.token)

        # If no token exists, ``get_or_assign_token`` create it
        token = new_object_with_token.get_or_assign_token()
        self.assertIsNotNone(token)
        self.assertTokenGeneration(token, settings.DEFAULT_TOKEN_PATTERNS, TOKEN_LENGTH)

        # Now a token is assigned for this object
        self.assertIsNotNone(new_object_with_token.token)

        # If a token exists, ``get_or_assign_token`` return the existing token
        self.assertEqual(token, new_object_with_token.get_or_assign_token())
