from django.test import override_settings
from django.urls import reverse

from core.models import ObjectType
from users.models import Group, ObjectPermission, Token, User
from utilities.data import deepmerge
from utilities.testing import APIViewTestCases, APITestCase, create_test_user


class AppTest(APITestCase):

    def test_root(self):

        url = reverse('users-api:api-root')
        response = self.client.get(f'{url}?format=api', **self.header)
        self.assertEqual(response.status_code, 200)


class UserTest(APIViewTestCases.APIViewTestCase):
    model = User
    brief_fields = ['display', 'id', 'url', 'username']
    validation_excluded_fields = ['password']
    bulk_update_data = {
        'email': 'test@example.com',
    }

    @classmethod
    def setUpTestData(cls):

        permissions = (
            ObjectPermission(name='Permission 1', actions=['view']),
            ObjectPermission(name='Permission 2', actions=['view']),
            ObjectPermission(name='Permission 3', actions=['view']),
        )
        ObjectPermission.objects.bulk_create(permissions)
        permissions[0].object_types.add(ObjectType.objects.get_by_natural_key('dcim', 'site'))
        permissions[1].object_types.add(ObjectType.objects.get_by_natural_key('dcim', 'location'))
        permissions[2].object_types.add(ObjectType.objects.get_by_natural_key('dcim', 'rack'))

        users = (
            User(username='User1', password='FooBarFooBar1'),
            User(username='User2', password='FooBarFooBar2'),
            User(username='User3', password='FooBarFooBar3'),
        )
        User.objects.bulk_create(users)

        cls.create_data = [
            {
                'username': 'User4',
                'password': 'FooBarFooBar4',
                'permissions': [permissions[0].pk],
            },
            {
                'username': 'User5',
                'password': 'FooBarFooBar5',
                'permissions': [permissions[1].pk],
            },
            {
                'username': 'User6',
                'password': 'FooBarFooBar6',
                'permissions': [permissions[2].pk],
            },
        ]

    def test_that_password_is_changed(self):
        """
        Test that password is changed
        """

        obj_perm = ObjectPermission(
            name='Test permission',
            actions=['change']
        )
        obj_perm.save()
        obj_perm.users.add(self.user)
        obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))

        user_credentials = {
            'username': 'newuser',
            'password': 'abc123FOO',
        }
        user = User.objects.create_user(**user_credentials)

        data = {
            'password': 'FooBarFooBar1'
        }
        url = reverse('users-api:user-detail', kwargs={'pk': user.id})
        response = self.client.patch(url, data, format='json', **self.header)
        self.assertEqual(response.status_code, 200)
        user.refresh_from_db()
        self.assertTrue(user.check_password(data['password']))

    @override_settings(AUTH_PASSWORD_VALIDATORS=[{
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
        'OPTIONS': {'min_length': 8}
    }])
    def test_password_validation_enforced(self):
        """
        Test that any configured password validation rules (AUTH_PASSWORD_VALIDATORS) are enforced.
        """
        self.add_permissions('users.add_user')

        data = {
            'username': 'new_user',
            'password': 'f1A',
        }
        url = reverse('users-api:user-list')

        # Password too short
        response = self.client.post(url, data, format='json', **self.header)
        self.assertEqual(response.status_code, 400)

        # Password long enough
        data['password'] = 'FooBar123'
        response = self.client.post(url, data, format='json', **self.header)
        self.assertEqual(response.status_code, 201)

        # Password no number
        data['password'] = 'foobarFoo'
        response = self.client.post(url, data, format='json', **self.header)
        self.assertEqual(response.status_code, 400)

        # Password no letter
        data['password'] = '123456789012'
        response = self.client.post(url, data, format='json', **self.header)
        self.assertEqual(response.status_code, 400)

        # Password no uppercase
        data['password'] = 'foobarfoo1'
        response = self.client.post(url, data, format='json', **self.header)
        self.assertEqual(response.status_code, 400)

        # Password no lowercase
        data['password'] = 'FOOBARFOO1'
        response = self.client.post(url, data, format='json', **self.header)
        self.assertEqual(response.status_code, 400)


class GroupTest(APIViewTestCases.APIViewTestCase):
    model = Group
    brief_fields = ['description', 'display', 'id', 'name', 'url']

    @classmethod
    def setUpTestData(cls):

        permissions = (
            ObjectPermission(name='Permission 1', actions=['view']),
            ObjectPermission(name='Permission 2', actions=['view']),
            ObjectPermission(name='Permission 3', actions=['view']),
        )
        ObjectPermission.objects.bulk_create(permissions)
        permissions[0].object_types.add(ObjectType.objects.get_by_natural_key('dcim', 'site'))
        permissions[1].object_types.add(ObjectType.objects.get_by_natural_key('dcim', 'location'))
        permissions[2].object_types.add(ObjectType.objects.get_by_natural_key('dcim', 'rack'))

        groups = (
            Group(name='Group 1'),
            Group(name='Group 2'),
            Group(name='Group 3'),
        )
        Group.objects.bulk_create(groups)

        cls.create_data = [
            {
                'name': 'Group 4',
                'permissions': [permissions[0].pk],
            },
            {
                'name': 'Group 5',
                'permissions': [permissions[1].pk],
            },
            {
                'name': 'Group 6',
                'permissions': [permissions[2].pk],
            },
        ]

    def model_to_dict(self, instance, *args, **kwargs):
        # Overwrite permissions attr to work around the serializer field having a different name
        data = super().model_to_dict(instance, *args, **kwargs)
        data['permissions'] = list(instance.object_permissions.values_list('id', flat=True))
        return data

    def test_bulk_update_objects(self):
        """
        Disabled test. There's no attribute we can set in bulk for Groups.
        """
        return


class TokenTest(
    # No GraphQL support for Token
    APIViewTestCases.GetObjectViewTestCase,
    APIViewTestCases.ListObjectsViewTestCase,
    APIViewTestCases.CreateObjectViewTestCase,
    APIViewTestCases.UpdateObjectViewTestCase,
    APIViewTestCases.DeleteObjectViewTestCase
):
    model = Token
    brief_fields = ['description', 'display', 'id', 'key', 'url', 'write_enabled']
    bulk_update_data = {
        'description': 'New description',
    }

    def setUp(self):
        super().setUp()

        # Apply grant_token permission to enable the creation of Tokens for other Users
        self.add_permissions('users.grant_token')

    @classmethod
    def setUpTestData(cls):
        users = (
            create_test_user('User1'),
            create_test_user('User2'),
            create_test_user('User3'),
        )

        tokens = (
            Token(user=users[0]),
            Token(user=users[1]),
            Token(user=users[2]),
        )
        # Use save() instead of bulk_create() to ensure keys get automatically generated
        for token in tokens:
            token.save()

        cls.create_data = [
            {
                'user': users[0].pk,
            },
            {
                'user': users[1].pk,
            },
            {
                'user': users[2].pk,
            },
        ]

    def test_provision_token_valid(self):
        """
        Test the provisioning of a new REST API token given a valid username and password.
        """
        user_credentials = {
            'username': 'user1',
            'password': 'abc123',
        }
        user = User.objects.create_user(**user_credentials)

        data = {
            **user_credentials,
            'description': 'My API token',
            'expires': '2099-12-31T23:59:59Z',
        }
        url = reverse('users-api:token_provision')

        response = self.client.post(url, data, format='json', **self.header)
        self.assertEqual(response.status_code, 201)
        self.assertIn('key', response.data)
        self.assertEqual(len(response.data['key']), 40)
        self.assertEqual(response.data['description'], data['description'])
        self.assertEqual(response.data['expires'], data['expires'])
        token = Token.objects.get(user=user)
        self.assertEqual(token.key, response.data['key'])

    def test_provision_token_invalid(self):
        """
        Test the behavior of the token provisioning view when invalid credentials are supplied.
        """
        data = {
            'username': 'nonexistentuser',
            'password': 'abc123',
        }
        url = reverse('users-api:token_provision')

        response = self.client.post(url, data, format='json', **self.header)
        self.assertEqual(response.status_code, 403)

    def test_provision_token_other_user(self):
        """
        Test provisioning a Token for a different User with & without the grant_token permission.
        """
        # Clear grant_token permission assigned by setUpTestData
        ObjectPermission.objects.filter(users=self.user).delete()

        self.add_permissions('users.add_token')
        user2 = User.objects.create_user(username='testuser2')
        data = {
            'user': user2.id,
        }
        url = reverse('users-api:token-list')

        # Attempt to create a new Token for User2 *without* the grant_token permission
        response = self.client.post(url, data, format='json', **self.header)
        self.assertEqual(response.status_code, 403)

        # Assign grant_token permission and successfully create a new Token for User2
        self.add_permissions('users.grant_token')
        response = self.client.post(url, data, format='json', **self.header)
        self.assertEqual(response.status_code, 201)


class ObjectPermissionTest(
    # No GraphQL support for ObjectPermission
    APIViewTestCases.GetObjectViewTestCase,
    APIViewTestCases.ListObjectsViewTestCase,
    APIViewTestCases.CreateObjectViewTestCase,
    APIViewTestCases.UpdateObjectViewTestCase,
    APIViewTestCases.DeleteObjectViewTestCase
):
    model = ObjectPermission
    brief_fields = ['actions', 'description', 'display', 'enabled', 'id', 'name', 'object_types', 'url']

    @classmethod
    def setUpTestData(cls):

        groups = (
            Group(name='Group 1'),
            Group(name='Group 2'),
            Group(name='Group 3'),
        )
        Group.objects.bulk_create(groups)

        users = (
            User(username='User1', is_active=True),
            User(username='User2', is_active=True),
            User(username='User3', is_active=True),
        )
        User.objects.bulk_create(users)

        object_type = ObjectType.objects.get(app_label='dcim', model='device')

        for i in range(3):
            objectpermission = ObjectPermission(
                name=f'Permission {i + 1}',
                actions=['view', 'add', 'change', 'delete'],
                constraints={'name': f'TEST{i + 1}'}
            )
            objectpermission.save()
            objectpermission.object_types.add(object_type)
            objectpermission.groups.add(groups[i])
            objectpermission.users.add(users[i])

        cls.create_data = [
            {
                'name': 'Permission 4',
                'object_types': ['dcim.site'],
                'groups': [groups[0].pk],
                'users': [users[0].pk],
                'actions': ['view', 'add', 'change', 'delete'],
                'constraints': {'name': 'TEST4'},
            },
            {
                'name': 'Permission 5',
                'object_types': ['dcim.site'],
                'groups': [groups[1].pk],
                'users': [users[1].pk],
                'actions': ['view', 'add', 'change', 'delete'],
                'constraints': {'name': 'TEST5'},
            },
            {
                'name': 'Permission 6',
                'object_types': ['dcim.site'],
                'groups': [groups[2].pk],
                'users': [users[2].pk],
                'actions': ['view', 'add', 'change', 'delete'],
                'constraints': {'name': 'TEST6'},
            },
        ]

        cls.bulk_update_data = {
            'description': 'New description',
        }


class UserConfigTest(APITestCase):

    def test_get(self):
        """
        Retrieve user configuration via GET request.
        """
        userconfig = self.user.config
        url = reverse('users-api:userconfig-list')

        response = self.client.get(url, **self.header)
        self.assertEqual(response.data, {})

        data = {
            "a": 123,
            "b": 456,
            "c": 789,
        }
        userconfig.data = data
        userconfig.save()
        response = self.client.get(url, **self.header)
        self.assertEqual(response.data, data)

    def test_patch(self):
        """
        Set user config via PATCH requests.
        """
        userconfig = self.user.config
        url = reverse('users-api:userconfig-list')

        data = {
            "a": {
                "a1": "X",
                "a2": "Y",
            },
            "b": {
                "b1": "Z",
            }
        }
        response = self.client.patch(url, data=data, format='json', **self.header)
        self.assertDictEqual(response.data, data)
        userconfig.refresh_from_db()
        self.assertDictEqual(userconfig.data, data)

        update_data = {
            "c": 123
        }
        response = self.client.patch(url, data=update_data, format='json', **self.header)
        new_data = deepmerge(data, update_data)
        self.assertDictEqual(response.data, new_data)
        userconfig.refresh_from_db()
        self.assertDictEqual(userconfig.data, new_data)
