from unittest.mock import patch
from datetime import datetime
import pytest
from szurubooru import db, model, errors
from szurubooru.func import auth, users, files, util


EMPTY_PIXEL = (
    b'\x47\x49\x46\x38\x39\x61\x01\x00\x01\x00\x80\x01\x00\x00\x00\x00'
    b'\xff\xff\xff\x21\xf9\x04\x01\x00\x00\x01\x00\x2c\x00\x00\x00\x00'
    b'\x01\x00\x01\x00\x00\x02\x02\x4c\x01\x00\x3b')


@pytest.mark.parametrize('user_name', ['test', 'TEST'])
def test_get_avatar_path(user_name):
    assert users.get_avatar_path(user_name) == 'avatars/test.png'


@pytest.mark.parametrize('user_name,user_email,avatar_style,expected_url', [
    (
        'user',
        None,
        model.User.AVATAR_GRAVATAR,
        ('https://gravatar.com/avatar/' +
            'ee11cbb19052e40b07aac0ca060c23ee?d=retro&s=100'),
    ),
    (
        None,
        'user@example.com',
        model.User.AVATAR_GRAVATAR,
        ('https://gravatar.com/avatar/' +
            'b58996c504c5638798eb6b511e6f49af?d=retro&s=100'),
    ),
    (
        'user',
        'user@example.com',
        model.User.AVATAR_GRAVATAR,
        ('https://gravatar.com/avatar/' +
            'b58996c504c5638798eb6b511e6f49af?d=retro&s=100'),
    ),
    (
        'user',
        None,
        model.User.AVATAR_MANUAL,
        'http://example.com/avatars/user.png',
    ),
])
def test_get_avatar_url(
        user_name, user_email, avatar_style, expected_url, config_injector):
    config_injector({
        'data_url': 'http://example.com/',
        'thumbnails': {'avatar_width': 100},
    })
    user = model.User()
    user.name = user_name
    user.email = user_email
    user.avatar_style = avatar_style
    assert users.get_avatar_url(user) == expected_url


@pytest.mark.parametrize(
    'same_user,can_edit_any_email,force_show,expected_email',
    [
        (False, False, False, False),
        (True, False, False, 'test@example.com'),
        (False, True, False, 'test@example.com'),
        (False, False, True, 'test@example.com'),
    ])
def test_get_email(
        same_user,
        can_edit_any_email,
        force_show,
        expected_email,
        user_factory):
    with patch('szurubooru.func.auth.has_privilege'):
        auth.has_privilege = lambda user, name: can_edit_any_email
        user = user_factory()
        user.email = 'test@example.com'
        auth_user = user if same_user else user_factory()
        db.session.add_all([user, auth_user])
        db.session.flush()
        assert users.get_email(user, auth_user, force_show) == expected_email


@pytest.mark.parametrize(
    'same_user,score,expected_liked_post_count,expected_disliked_post_count',
    [
        (False, 1, False, False),
        (False, -1, False, False),
        (True, 1, 1, 0),
        (True, -1, 0, 1),
    ])
def test_get_liked_post_count(
        same_user,
        score,
        expected_liked_post_count,
        expected_disliked_post_count,
        user_factory,
        post_factory):
    user = user_factory()
    post = post_factory()
    auth_user = user if same_user else user_factory()
    score = model.PostScore(
        post=post, user=user, score=score, time=datetime.now())
    db.session.add_all([post, user, score])
    db.session.flush()
    actual_liked_post_count = users.get_liked_post_count(user, auth_user)
    actual_disliked_post_count = users.get_disliked_post_count(user, auth_user)
    assert actual_liked_post_count == expected_liked_post_count
    assert actual_disliked_post_count == expected_disliked_post_count


def test_serialize_user_when_empty():
    assert users.serialize_user(None, None) is None


def test_serialize_user(user_factory):
    with patch('szurubooru.func.users.get_email'), \
            patch('szurubooru.func.users.get_avatar_url'), \
            patch('szurubooru.func.users.get_liked_post_count'), \
            patch('szurubooru.func.users.get_disliked_post_count'):
        users.get_email.return_value = 'test@example.com'
        users.get_avatar_url.return_value = 'https://example.com/avatar.png'
        users.get_liked_post_count.return_value = 66
        users.get_disliked_post_count.return_value = 33
        auth_user = user_factory()
        user = user_factory(name='dummy user')
        user.creation_time = datetime(1997, 1, 1)
        user.last_edit_time = datetime(1998, 1, 1)
        user.avatar_style = model.User.AVATAR_MANUAL
        user.rank = model.User.RANK_ADMINISTRATOR
        db.session.add(user)
        db.session.flush()
        assert users.serialize_user(user, auth_user) == {
            'version': 1,
            'name': 'dummy user',
            'email': 'test@example.com',
            'rank': 'administrator',
            'creationTime': datetime(1997, 1, 1, 0, 0),
            'lastLoginTime': None,
            'avatarStyle': 'manual',
            'avatarUrl': 'https://example.com/avatar.png',
            'likedPostCount': 66,
            'dislikedPostCount': 33,
            'commentCount': 0,
            'favoritePostCount': 0,
            'uploadedPostCount': 0,
        }


def test_serialize_micro_user(user_factory):
    with patch('szurubooru.func.users.get_avatar_url'):
        users.get_avatar_url.return_value = 'https://example.com/avatar.png'
        auth_user = user_factory()
        user = user_factory(name='dummy user')
        db.session.add(user)
        db.session.flush()
        assert users.serialize_micro_user(user, auth_user) == {
            'name': 'dummy user',
            'avatarUrl': 'https://example.com/avatar.png',
        }


@pytest.mark.parametrize('count', [0, 1, 2])
def test_get_user_count(count, user_factory):
    for _ in range(count):
        db.session.add(user_factory())
    db.session.flush()
    assert users.get_user_count() == count


def test_try_get_user_by_name(user_factory):
    user = user_factory(name='name', email='email')
    db.session.add(user)
    db.session.flush()
    assert users.try_get_user_by_name('non-existing') is None
    assert users.try_get_user_by_name('email') is None
    assert users.try_get_user_by_name('name') is user
    assert users.try_get_user_by_name('NAME') is user


def test_get_user_by_name(user_factory):
    user = user_factory(name='name', email='email')
    db.session.add(user)
    db.session.flush()
    with pytest.raises(users.UserNotFoundError):
        assert users.get_user_by_name('non-existing')
    with pytest.raises(users.UserNotFoundError):
        assert users.get_user_by_name('email')
    assert users.get_user_by_name('name') is user
    assert users.get_user_by_name('NAME') is user


def test_try_get_user_by_name_or_email(user_factory):
    user = user_factory(name='name', email='email')
    db.session.add(user)
    db.session.flush()
    assert users.try_get_user_by_name_or_email('non-existing') is None
    assert users.try_get_user_by_name_or_email('email') is user
    assert users.try_get_user_by_name_or_email('EMAIL') is user
    assert users.try_get_user_by_name_or_email('name') is user
    assert users.try_get_user_by_name_or_email('NAME') is user


def test_get_user_by_name_or_email(user_factory):
    user = user_factory(name='name', email='email')
    db.session.add(user)
    db.session.flush()
    with pytest.raises(users.UserNotFoundError):
        assert users.get_user_by_name_or_email('non-existing')
    assert users.get_user_by_name_or_email('email') is user
    assert users.get_user_by_name_or_email('EMAIL') is user
    assert users.get_user_by_name_or_email('name') is user
    assert users.get_user_by_name_or_email('NAME') is user


def test_create_user_for_first_user(fake_datetime):
    with patch('szurubooru.func.users.update_user_name'), \
            patch('szurubooru.func.users.update_user_password'), \
            patch('szurubooru.func.users.update_user_email'), \
            fake_datetime('1997-01-01'):
        user = users.create_user('name', 'password', 'email')
        assert user.creation_time == datetime(1997, 1, 1)
        assert user.last_login_time is None
        assert user.rank == model.User.RANK_ADMINISTRATOR
        users.update_user_name.assert_called_once_with(user, 'name')
        users.update_user_password.assert_called_once_with(user, 'password')
        users.update_user_email.assert_called_once_with(user, 'email')


def test_create_user_for_subsequent_users(user_factory, config_injector):
    config_injector({'default_rank': 'regular'})
    db.session.add(user_factory())
    db.session.flush()
    with patch('szurubooru.func.users.update_user_name'), \
            patch('szurubooru.func.users.update_user_email'), \
            patch('szurubooru.func.users.update_user_password'):
        user = users.create_user('name', 'password', 'email')
        assert user.rank == model.User.RANK_REGULAR


def test_update_user_name_with_empty_string(user_factory):
    user = user_factory()
    with pytest.raises(users.InvalidUserNameError):
        users.update_user_name(user, None)


def test_update_user_name_with_too_long_string(user_factory):
    user = user_factory()
    with pytest.raises(users.InvalidUserNameError):
        users.update_user_name(user, 'a' * 300)


def test_update_user_name_with_invalid_name(user_factory, config_injector):
    config_injector({'user_name_regex': '^[a-z]+$'})
    user = user_factory()
    with pytest.raises(users.InvalidUserNameError):
        users.update_user_name(user, '0')


def test_update_user_name_with_duplicate_name(user_factory, config_injector):
    config_injector({'user_name_regex': '^[a-z]+$'})
    user = user_factory()
    existing_user = user_factory(name='dummy')
    db.session.add(existing_user)
    db.session.flush()
    with pytest.raises(users.UserAlreadyExistsError):
        users.update_user_name(user, 'dummy')


def test_update_user_name_reusing_own_name(user_factory, config_injector):
    config_injector({'user_name_regex': '^[a-z]+$'})
    user = user_factory(name='dummy')
    db.session.add(user)
    db.session.flush()
    with patch('szurubooru.func.files.has'):
        files.has.return_value = False
        users.update_user_name(user, 'dummy')
        db.session.flush()
        assert users.try_get_user_by_name('dummy') is user


def test_update_user_name_for_new_user(user_factory, config_injector):
    config_injector({'user_name_regex': '^[a-z]+$'})
    user = user_factory()
    with patch('szurubooru.func.files.has'):
        files.has.return_value = False
        users.update_user_name(user, 'dummy')
        assert user.name == 'dummy'


def test_update_user_name_moves_avatar(user_factory, config_injector):
    config_injector({'user_name_regex': '^[a-z]+$'})
    user = user_factory(name='old')
    with patch('szurubooru.func.files.has'), \
            patch('szurubooru.func.files.move'):
        files.has.return_value = True
        users.update_user_name(user, 'new')
        files.move.assert_called_once_with(
            'avatars/old.png', 'avatars/new.png')


def test_update_user_password_with_empty_string(user_factory):
    user = user_factory()
    with pytest.raises(users.InvalidPasswordError):
        users.update_user_password(user, None)


def test_update_user_password_with_invalid_string(
        user_factory, config_injector):
    config_injector({'password_regex': '^[a-z]+$'})
    user = user_factory()
    with pytest.raises(users.InvalidPasswordError):
        users.update_user_password(user, '0')


def test_update_user_password(user_factory, config_injector):
    config_injector({'password_regex': '^[a-z]+$'})
    user = user_factory()
    with patch('szurubooru.func.auth.create_password'), \
            patch('szurubooru.func.auth.get_password_hash'):
        auth.create_password.return_value = 'salt'
        auth.get_password_hash.return_value = ('hash', 3)
        users.update_user_password(user, 'a')
        assert user.password_salt == 'salt'
        assert user.password_hash == 'hash'
        assert user.password_revision == 3


def test_update_user_email_with_too_long_string(user_factory):
    user = user_factory()
    with pytest.raises(users.InvalidEmailError):
        users.update_user_email(user, 'a' * 300)


def test_update_user_email_with_invalid_email(user_factory):
    user = user_factory()
    with patch('szurubooru.func.util.is_valid_email'):
        util.is_valid_email.return_value = False
        with pytest.raises(users.InvalidEmailError):
            users.update_user_email(user, 'a')


def test_update_user_email_with_empty_string(user_factory):
    user = user_factory()
    with patch('szurubooru.func.util.is_valid_email'):
        util.is_valid_email.return_value = True
        users.update_user_email(user, '')
        assert user.email is None


def test_update_user_email(user_factory):
    user = user_factory()
    with patch('szurubooru.func.util.is_valid_email'):
        util.is_valid_email.return_value = True
        users.update_user_email(user, 'a')
        assert user.email == 'a'


def test_update_user_rank_with_empty_string(user_factory):
    user = user_factory()
    auth_user = user_factory()
    with pytest.raises(users.InvalidRankError):
        users.update_user_rank(user, '', auth_user)


def test_update_user_rank_with_invalid_string(user_factory):
    user = user_factory()
    auth_user = user_factory()
    with pytest.raises(users.InvalidRankError):
        users.update_user_rank(user, 'invalid', auth_user)
    with pytest.raises(users.InvalidRankError):
        users.update_user_rank(user, 'anonymous', auth_user)
    with pytest.raises(users.InvalidRankError):
        users.update_user_rank(user, 'nobody', auth_user)


def test_update_user_rank_with_higher_rank_than_possible(user_factory):
    db.session.add(user_factory())
    db.session.flush()
    user = user_factory()
    auth_user = user_factory()
    auth_user.rank = model.User.RANK_ANONYMOUS
    with pytest.raises(errors.AuthError):
        users.update_user_rank(user, 'regular', auth_user)
    with pytest.raises(errors.AuthError):
        users.update_user_rank(auth_user, 'regular', auth_user)


def test_update_user_rank(user_factory):
    db.session.add(user_factory())
    db.session.flush()
    user = user_factory()
    auth_user = user_factory()
    auth_user.rank = model.User.RANK_ADMINISTRATOR
    users.update_user_rank(user, 'regular', auth_user)
    users.update_user_rank(auth_user, 'regular', auth_user)
    assert user.rank == model.User.RANK_REGULAR
    assert auth_user.rank == model.User.RANK_REGULAR


def test_update_user_avatar_with_invalid_style(user_factory):
    user = user_factory()
    with pytest.raises(users.InvalidAvatarError):
        users.update_user_avatar(user, 'invalid', b'')


def test_update_user_avatar_to_gravatar(user_factory):
    user = user_factory()
    users.update_user_avatar(user, 'gravatar')
    assert user.avatar_style == model.User.AVATAR_GRAVATAR


def test_update_user_avatar_to_empty_manual(user_factory):
    user = user_factory()
    with patch('szurubooru.func.files.has'), \
            pytest.raises(users.InvalidAvatarError):
        files.has.return_value = False
        users.update_user_avatar(user, 'manual', b'')


def test_update_user_avatar_to_previous_manual(user_factory):
    user = user_factory()
    with patch('szurubooru.func.files.has'):
        files.has.return_value = True
        users.update_user_avatar(user, 'manual', b'')


def test_update_user_avatar_to_new_manual(user_factory, config_injector):
    config_injector(
        {'thumbnails': {'avatar_width': 500, 'avatar_height': 500}})
    user = user_factory()
    with patch('szurubooru.func.files.save'):
        users.update_user_avatar(user, 'manual', EMPTY_PIXEL)
        assert user.avatar_style == model.User.AVATAR_MANUAL
        assert files.save.called


def test_bump_user_login_time(user_factory, fake_datetime):
    user = user_factory()
    with fake_datetime('1997-01-01'):
        users.bump_user_login_time(user)
        assert user.last_login_time == datetime(1997, 1, 1)


def test_reset_user_password(user_factory):
    with patch('szurubooru.func.auth.create_password'), \
            patch('szurubooru.func.auth.get_password_hash'):
        user = user_factory()
        auth.create_password.return_value = 'salt'
        auth.get_password_hash.return_value = ('hash', 3)
        users.reset_user_password(user)
        assert user.password_salt == 'salt'
        assert user.password_hash == 'hash'
        assert user.password_revision == 3
