import datetime
import json
import logging
from datetime import date, timedelta
from enum import Enum

import pytest
from django.contrib.auth.models import User
from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ObjectDoesNotExist
from django.core.exceptions import ValidationError
from django.urls import reverse
from rest_framework import status

from console.tasks import expire_users
from core.utils import dtnow
from perms.models import Perm
from users.models import UserInfo
from users.services.userinfo import UserStatusService, InUserInfoStrategy

_log = logging.getLogger(__name__)


class MethodEnum(Enum):
    GET = 0
    HEAD = 1
    POST = 3
    PUT = 4
    DELETE = 5
    OPTIONS = 6
    TRACE = 7
    PATCH = 8


@pytest.fixture
def receive_url(client):
    def _get(url, method=MethodEnum.GET, **kwargs):
        if method == MethodEnum.GET:
            return client.get(url, **kwargs)
        elif method == MethodEnum.HEAD:
            return client.head(url, **kwargs)
        elif method == MethodEnum.POST:
            return client.post(url, **kwargs, content_type="application/json")
        elif method == MethodEnum.PUT:
            return client.put(url, **kwargs, content_type="application/json")
        elif method == MethodEnum.DELETE:
            return client.delete(url, **kwargs)
        elif method == MethodEnum.OPTIONS:
            return client.options(url, **kwargs)
        elif method == MethodEnum.TRACE:
            return client.trace(url, **kwargs)
        elif method == MethodEnum.PATCH:
            return client.patch(url, **kwargs, content_type="application/json")

        raise NotImplemented(f'Method {method} not implemented')

    return _get


@pytest.fixture()
def create_user_info():
    """Return new user with userinfo"""
    user = User.objects.create(username="Max", first_name="Pane", is_active=True)
    UserInfo.objects.create(user=user, comment="New user Max Pane")


@pytest.fixture()
def create_users():
    users_list = []
    for item in range(5):
        if item % 2 == 0:
            username = f'deleted_{item}'
            user = User.objects.create_user(username=username, is_active=False)
        else:
            username = f'user_{item}'
            user = User.objects.create_user(username=username, is_active=False)
        users_list.append(user)

    for user in users_list:
        UserInfo.objects.create(user=user, comment=f'{user.username}')


@pytest.mark.django_db
@pytest.fixture
def can_open_url(client, receive_url, add_user_with_permissions):
    def _open(url: str, permissions: list, method=MethodEnum.GET, **kwargs):
        username = 'user'
        password = 'test'
        user = add_user_with_permissions(username=username, password=password)
        client.force_login(user)

        if permissions:
            response = receive_url(url=url, method=method, **kwargs)
            assert response.status_code == status.HTTP_403_FORBIDDEN
        user_1 = add_user_with_permissions(username=username + '1', password=password, permissions=permissions)
        client.force_login(user_1)
        response = receive_url(url=url, method=method, **kwargs)
        assert response.status_code != status.HTTP_403_FORBIDDEN

    return _open


test_args = [

    ('active-users-list', [MethodEnum.GET], [Perm.can_view_user_list]),
    ('active-users-list', [MethodEnum.POST], [Perm.can_add_user]),
    ('active-users-detail', [MethodEnum.GET], [Perm.can_view_user]),
    ('active-users-detail', [MethodEnum.PUT], [Perm.can_edit_user]),
    ('active-users-detail', [MethodEnum.PATCH], [Perm.can_edit_user]),
]


@pytest.mark.django_db
class TestUsers:
    url_name_and_url_kwargs_map = {}
    url_data_map = {}  # data for POST, PATCH, PUT

    @pytest.fixture(autouse=True)
    def setup_test(self, add_user_with_permissions):
        self.user = add_user_with_permissions(username='test_username1', password='testpwd', is_superuser=True)
        self.url_name_and_url_kwargs_map['active-users-detail'] = {'pk': 1}

    @pytest.mark.unit
    def test_api_list(self, client, add_user_with_permissions, create_users):
        user_count = User.objects.count()
        user = add_user_with_permissions(username='user0', password='passwd12', is_superuser=True)
        url = reverse('active-users-list')
        client.force_login(user)
        response = client.get(url)
        data_json = response.json()
        assert response.status_code == 200
        assert len(data_json) == 4
        assert len(data_json) != user_count

    # Test skipped, because we have no permissions
    @pytest.mark.skip
    @pytest.mark.unit
    @pytest.mark.parametrize("url, methods, permissions", test_args)
    def test_permissions(self, url: str, methods: list, permissions: list, client, receive_url, get_url,
                         add_user_with_permissions, can_open_url):

        url_kwargs = self.url_name_and_url_kwargs_map.get(url)
        if url_kwargs:
            _url = get_url(url, kwargs=url_kwargs)
        else:
            _url = get_url(url)
        data = self.url_data_map.get(url, {})
        for method in methods:
            can_open_url(url=_url, method=method, permissions=permissions, **data)

    @pytest.mark.unit
    def test_user_details(self, client, add_user_with_permissions, create_user_info):

        user = add_user_with_permissions(username='user0', password='passwd12', is_superuser=True)

        url = reverse('active-users-detail', kwargs={"pk": User.objects.get(username='Max').pk})
        client.force_login(user)
        response = client.get(url)
        data_json = response.json()
        assert response.status_code == 200
        assert data_json["user"]["username"] == "Max"
        assert data_json["user"]["is_active"] == True
        assert data_json["user"]["first_name"] == "Pane"

    @pytest.mark.unit
    def test_add_user(self, client, add_user_with_permissions, create_user_info):
        url = reverse('active-users-list')
        client.force_login(self.user)
        users_before = User.objects.count()
        data = {
            "user": {
                "username": "swaggeruserYYYYMMDD",
                "first_name": "swagger_name",
                "is_active": True,
                "email": "swagger@mail.ru",
                "password": "IamTheKingOfTheWorld1"
            },
            "comment": "asdasd",
            "timezone": "Europe/Moscow",
            "expire_date": "2030-10-10"

        }
        response = client.post(url, data=json.dumps(data), content_type="application/json")
        assert response.status_code == 201
        users_after = User.objects.count()
        assert users_before != users_after
        new_user = User.objects.get(username="swaggeruserYYYYMMDD")
        info = UserInfo.objects.get(user=new_user)
        assert new_user.first_name == 'swagger_name'
        assert new_user.email == "swagger@mail.ru"
        assert info.comment == "asdasd"
        assert info.timezone == "Europe/Moscow"
        assert new_user.is_superuser
        assert new_user.is_staff
        assert info.expire_date == datetime.date(2030, 10, 10)
        client.logout()
        response_ = client.post(reverse('api_login'), {"username": "swaggeruserYYYYMMDD",
                                                       "password": "IamTheKingOfTheWorld1"})
        assert response_.wsgi_request.user.is_authenticated
        url = reverse('active-users-detail', kwargs={"pk": User.objects.get(username='Max').pk})
        response = client.get(url)
        assert response.status_code == 200

    @pytest.mark.unit
    def test_add_user_bad_password(self, client, add_user_with_permissions, create_user_info):
        url = reverse('active-users-list')
        client.force_login(self.user)
        users_before = User.objects.count()
        data = {
            "user": {
                "username": "swagger_user_YYYY-MM-DD",
                "first_name": "swagger_name",
                "is_active": True,
                "email": "swagger@mail.ru",
                "password": "password"
            },
            "comment": "asdasd",
            "timezone": "Europe/Moscow",
            "expire_date": "2030-10-10"

        }
        response = client.post(url, data=json.dumps(data), content_type="application/json")
        assert response.status_code == 400
        assert users_before == User.objects.count()

    @pytest.mark.unit
    def test_add_user_bad_expire_date(self, client, add_user_with_permissions, create_user_info):

        url = reverse('active-users-list')
        client.force_login(self.user)
        data = {
            "user": {
                "username": "userBadExpDate",
                "first_name": "swagger_name",
                "is_active": True,
                "email": "swagger@mail.ru",
                "password": "IamTheKingOfTheWorld1"
            },
            "comment": "asdasd",
            "timezone": "Europe/Moscow",
            "expire_date": "2000-10-10"

        }
        before = User.objects.count()
        response = client.post(url, data=json.dumps(data), content_type="application/json")
        assert response.status_code == 400
        assert before == User.objects.count()

    @pytest.mark.unit
    def test_add_user_empty_first_name(self, api_client):

        url = reverse('active-users-list')
        api_client.force_authenticate(self.user)
        data = {
            "user": {
                "username": "userBadExpDate",
                "first_name": "",
                "is_active": True,
                "email": "swagger@mail.ru",
                "password": "IamTheKingOfTheWorld1"
            },
            "comment": "asdasd",
            "timezone": "Europe/Moscow",
            "expire_date": "2000-10-10"

        }
        before = User.objects.count()
        response = api_client.post(url, data=data, format='json')
        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert 'first_name' in response.json()['user']
        assert response.json()['user']['first_name'] == ['This field may not be blank.']
        assert before == User.objects.count()

    @pytest.mark.unit
    def test_add_user_with_too_long_username(self, api_client, add_user_with_permissions):
        url = reverse('active-users-list')
        api_client.force_authenticate(self.user)
        long_username = 'a' * 150
        data = {
            "user": {
                "username": long_username,
                "first_name": "swagger_name",
                "is_active": True,
                "email": "swagger@mail.ru",
                "password": "IamTheKingOfTheWorld1"
            },
            "comment": "asdasd",
            "timezone": "Europe/Moscow",
        }
        response = api_client.post(url, data=data, format="json")
        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert response.json()['user']['username'] == ["Ensure this field has no more than 131 characters."]

        normal_username = 'a' * 131
        data['user']['username'] = normal_username
        response = api_client.post(url, data=data, format="json")
        print(response.json())
        assert response.status_code == status.HTTP_201_CREATED

    @pytest.mark.unit
    def test_edit_user_patch(self, client, add_user_with_permissions, create_user_info):

        customer = User.objects.get(username='Max')
        url = reverse('active-users-detail', kwargs={"pk": customer.pk})
        client.force_login(self.user)
        data = {"user": {"first_name": "user"}, "timezone": "Europe/Moscow"}
        response = client.patch(url, data=data,
                                content_type="application/json", format="json")
        edited_customer = User.objects.get(pk=customer.pk)
        info = UserInfo.objects.get(user=edited_customer)
        assert response.status_code == 200
        assert edited_customer.username == 'Max'
        assert edited_customer.first_name == "user"
        assert edited_customer.is_active

    @pytest.mark.unit
    def test_edit_user_patch_good_expire_date(self, client, add_user_with_permissions, create_user_info):

        customer = User.objects.get(username='Max')
        url = reverse('active-users-detail', kwargs={"pk": customer.pk})
        client.force_login(self.user)
        data = {"user": {"first_name": "user"}, "expire_date": date.today() + timedelta(days=1),
                "timezone": "Europe/Moscow"}
        response = client.patch(url, data=data,
                                content_type="application/json", format="json")
        edited_customer = User.objects.get(pk=customer.pk)
        info = UserInfo.objects.get(user=edited_customer)
        assert response.status_code == 200
        assert edited_customer.username == 'Max'
        assert edited_customer.first_name == "user"
        assert edited_customer.is_active

    @pytest.mark.unit
    def test_edit_user_patch_bad_expire_date(self, client, add_user_with_permissions, create_user_info):

        customer = User.objects.get(username='Max')
        url = reverse('active-users-detail', kwargs={"pk": customer.pk})
        client.force_login(self.user)
        data = {"user": {"first_name": "user"}, "expire_date": date.today() - timedelta(days=1)}
        response = client.patch(url, data=data,
                                content_type="application/json", format="json")
        assert response.status_code == 400

    @pytest.mark.unit
    def test_edit_user_patch_today_expire_date(self, client, add_user_with_permissions, create_user_info):

        customer = User.objects.get(username='Max')
        url = reverse('active-users-detail', kwargs={"pk": customer.pk})
        client.force_login(self.user)
        data = {"user": {"first_name": "user"}, "expire_date": date.today()}
        response = client.patch(url, data=data,
                                content_type="application/json", format="json")
        assert response.status_code == 400

    @pytest.mark.unit
    def test_edit_user_without_user(self, client, add_user_with_permissions, create_user_info):

        customer = User.objects.get(username='Max')
        url = reverse('active-users-detail', kwargs={"pk": customer.pk})
        client.force_login(self.user)
        data = {"comment": "asdasdasd"}
        response = client.patch(url, data=data,
                                content_type="application/json", format="json")
        edited_customer = User.objects.get(pk=customer.pk)
        info = UserInfo.objects.get(user=edited_customer)
        assert response.status_code == 400

    @pytest.mark.unit
    def test_edit_user_patch_with_password(self, client, add_user_with_permissions, create_user_info):
        customer = User.objects.get(username='Max')
        url = reverse('active-users-detail', kwargs={"pk": customer.pk})
        client.force_login(self.user)
        data = {
            "user": {"username": "Max", "first_name": "Ivan", "is_active": False, "password": "IamAlien2"},
            "comment": "comment for new user", "timezone": "Europe/Moscow"}
        response = client.patch(url, data=data,
                                content_type="application/json")
        edited_customer = User.objects.get(pk=customer.pk)
        response_ = client.post(reverse('api_login'), {"username": "Max", "password": "IamAlien2"})
        assert response_.wsgi_request.user.is_authenticated == True
        assert edited_customer.username == 'Max'
        assert response.status_code == 200
        assert edited_customer.first_name == "Ivan"
        assert not edited_customer.is_active
        client.logout()

    @pytest.mark.unit
    def test_edit_user_patch_with_bad_password(self, client, add_user_with_permissions, create_user_info):
        customer = User.objects.get(username='Max')
        url = reverse('active-users-detail', kwargs={"pk": customer.pk})
        client.force_login(self.user)
        data = {
            "user": {"username": "Max", "first_name": "Ivan", "is_active": False, "password": "12345675"},
            "comment": "comment for new user", "timezone": "Europe/Moscow"}
        response = client.patch(url, data=data,
                                content_type="application/json", format="json")
        assert response.status_code == 400

    @pytest.mark.unit
    def test_edit_user_patch_with_short_password(self, client, add_user_with_permissions, create_user_info):
        customer = User.objects.get(username='Max')
        url = reverse('active-users-detail', kwargs={"pk": customer.pk})
        client.force_login(self.user)
        data = {
            "user": {"username": "Max", "first_name": "Ivan", "is_active": False, "password": "Thepss1"},
            "comment": "comment for new user", "timezone": "Europe/Moscow"}
        response = client.patch(url, data=data,
                                content_type="application/json", format="json")
        assert response.status_code == 400

    @pytest.mark.unit
    def test_edit_user_patch_with_previous_password(self, client, add_user_with_permissions):
        user = add_user_with_permissions(username='user0', password='User1user!')
        url = reverse('active-users-detail', kwargs={"pk": user.pk})
        client.force_login(self.user)
        data = {"user": {"password": "User1user!"}}
        response = client.patch(url, data=data,
                                content_type="application/json", format="json")
        assert response.status_code == 400

    @pytest.mark.unit
    def test_edit_user_with_too_long_username(self, api_client, add_user_with_permissions):
        user = add_user_with_permissions(username='user0', password='User1user!')
        url = reverse('active-users-detail', kwargs={"pk": user.pk})
        api_client.force_authenticate(self.user)
        long_username = 'a' * 150
        data = {"user": {"username": long_username}}
        response = api_client.patch(url, data=data, format="json")
        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert response.json()['user']['username'] == ["Ensure this field has no more than 131 characters."]

        normal_username = 'a' * 131
        data = {"user": {"username": normal_username}, 'timezone': 'Africa/Bangui'}
        response = api_client.patch(url, data=data, format="json")
        print(response.json())
        assert response.status_code == status.HTTP_200_OK

    @pytest.mark.unit
    def test_delete_user(self, client, add_user_with_permissions, create_user_info):
        operator = User.objects.get(username="Max")
        client.force_login(self.user)
        url = reverse('active-users-detail', kwargs={"pk": operator.pk})
        response = client.delete(url)
        assert response.status_code == 204
        with pytest.raises(ObjectDoesNotExist):
            User.objects.get(username="Max")
            assert False

    @pytest.mark.unit
    def test_user_cannot_delete_himself(self, api_client):
        api_client.force_authenticate(self.user)
        url = reverse('active-users-detail', kwargs={"pk": self.user.pk})
        response = api_client.delete(url)
        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert response.json() == {'details': 'You cannot delete your account'}

    @pytest.mark.unit
    def test_admin_disable_himself(self, client, add_user_with_permissions, create_user_info):
        admin = add_user_with_permissions(username='user007', password='passwd12', is_superuser=True)
        url = reverse('active-users-detail', kwargs={"pk": admin.userinfo.pk})
        client.force_login(admin)
        data = {"user": {"is_active": False}}
        response = client.patch(url, data=data,
                                content_type="application/json", format="json")
        edited_admin = User.objects.get(pk=admin.pk)
        assert response.status_code == 400
        assert edited_admin.is_active

    @pytest.mark.unit
    def test_admin_disable_other(self, client, add_user_with_permissions, create_user_info):
        admin = add_user_with_permissions(username='user007', password='passwd12', is_superuser=True)
        url = reverse('active-users-detail', kwargs={"pk": admin.userinfo.pk})
        client.force_login(self.user)
        data = {"user": {"is_active": False}, "timezone": "Europe/Moscow"}
        response = client.patch(url, data=data,
                                content_type="application/json", format="json")
        edited_admin = User.objects.get(pk=admin.pk)
        assert response.status_code == 200
        assert not edited_admin.is_active

    @pytest.mark.unit
    def test_user_whoami(self, client, add_user_with_permissions, create_user_info):
        url = reverse('active-users-whoami')
        client.force_login(self.user)
        response = client.get(url)
        data_json = response.json()
        assert response.status_code == 200
        assert data_json["username"] == "test_username1"

    def test_validate_username_create_user(self, client):
        """Test create user with bad username(with space and special symbol)."""
        url = reverse('active-users-list')
        client.force_login(self.user)
        users_before = User.objects.count()
        data = {
            "user": {
                "username": "username with space and +-#",
                "first_name": "swagger_name",
                "is_active": True,
                "email": "swagger@mail.ru",
                "password": "Pas123wordZ"
            },
            "comment": "asdasd",
            "timezone": "Europe/Moscow",
            "expire_date": "2030-10-10"

        }
        response = client.post(url, data=json.dumps(data), content_type="application/json")
        assert response.status_code == 400
        assert response.json() == {'user': ['The username must be contain only latin alphabet and digit, 0-9']}

    @pytest.mark.unit
    def test_edit_user_password(self, api_client, add_user_with_permissions, create_user_info):
        api_client.force_authenticate(self.user)

        customer = User.objects.get(username='Max')
        url = reverse('active-users-detail', kwargs={"pk": customer.pk})

        data = {
            "user": {"username": "Max", "first_name": "Ivan", "is_active": False, "password": "IamAlien2"},
            "comment": "comment for new user", "timezone": "Europe/Moscow"}
        response = api_client.patch(url, data=data, format='json')
        assert response.status_code == status.HTTP_200_OK

    @pytest.mark.unit
    def test_change_password_for_myself(self, api_client, add_user_with_permissions, create_user_info):
        api_client.force_authenticate(self.user)
        url = reverse('active-users-detail', kwargs={"pk": self.user.pk})

        data = {
            "user": {"username": "TESTTEST", "first_name": "Ivan", "password": "IamAlien2"},
            "comment": "comment for new user", "timezone": "Europe/Moscow"}
        response = api_client.patch(url, data=data, format='json')
        assert response.status_code == status.HTTP_400_BAD_REQUEST  # because old_password is required

        data['user']['old_password'] = 'testpwd'

        response = api_client.patch(url, data=data, format='json')
        assert response.status_code == status.HTTP_200_OK


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

    @pytest.mark.unit
    def test_expiry(self):
        """ Tests that user becomes disabled if their expire date has happened """
        user_before_count = User.objects.filter(is_active=True).count()
        user = User.objects.create_user(username='expired_active', is_active=True)
        UserInfo(user=user, expire_date=dtnow(days=-3).date()).save()
        user = User.objects.create_user(username='expired_not_active', is_active=False)
        UserInfo(user=user, expire_date=dtnow(days=-1).date()).save()
        user = User.objects.create_user(username='not_expired_active', is_active=True)
        UserInfo(user=user, expire_date=dtnow(days=1).date()).save()
        user = User.objects.create_user(username='not_expired_not_active', is_active=False)
        UserInfo(user=user, expire_date=dtnow(days=3).date()).save()
        user = User.objects.create_user(username='no_expire_active', is_active=True)
        UserInfo(user=user).save()
        user = User.objects.create_user(username='no_expire_not_active', is_active=False)
        UserInfo(user=user).save()

        assert User.objects.filter(is_active=True).count() == 3 + user_before_count
        expire_users()
        assert User.objects.filter(is_active=True).count() == 2 + user_before_count


@pytest.mark.django_db
class TestPasswordValidator:

    @pytest.mark.uint
    def test_password_validator(self):
        passwords = [{'name': 'Short password', 'value': 'asdW567', 'expect': 'error', 'result': 'Not test'},
                     {'name': 'Only digit', 'value': '12345678', 'expect': 'error', 'result': 'Not test'},
                     {'name': 'No upper-case letter', 'value': '123qwert', 'expect': 'error', 'result': 'Not test'},
                     {'name': 'No lower-case letter', 'value': '123QWERT', 'expect': 'error', 'result': 'Not test'},
                     {'name': 'Good password', 'value': '123qweWW', 'expect': 'success', 'result': 'Not test'}]

        test_result = True

        for p in passwords:
            try:
                validate_password(p['value'], user=None)
                if p['expect'] == 'error':
                    test_result = False
                    p['result'] = 'Bad'
                else:
                    p['result'] = 'Ok'

            except ValidationError:
                if p['expect'] == 'success':
                    test_result = False
                    p['result'] = 'Bad'
                else:
                    p['result'] = 'Ok'
            _log.info(f"Test {p['name']}, expect {p['expect']}, result - {p['result']}")

        assert test_result == True


def get_params(step):
    params = (
        (dtnow() - datetime.timedelta(minutes=2), 'offline'),
        (dtnow(), 'online'),
        (None, 'offline'),
    )

    return params[step]


@pytest.mark.django_db
@pytest.mark.unit
class TestUserStatusService:

    @pytest.mark.parametrize('step', range(3))
    def test_user_get_status_online_offline(self, step, add_user_with_permissions):
        user = add_user_with_permissions(username='test_username_status', password='Testpwd!123*', is_superuser=True)
        user.userinfo.last_seen = get_params(step)[0]
        user.userinfo.save()

        service = UserStatusService(user, strategy=InUserInfoStrategy)
        assert service.get_status() == get_params(step)[1]
