"""
This file demonstrates writing tests using the unittest module. These will pass
when you run "manage.py test".

Replace this with more appropriate tests for your application.
"""

import re
import tempfile
from unittest import mock

import pytest
from django.contrib.auth.models import User
from django.contrib.auth.tokens import default_token_generator
from django.core import mail, management
from django.core.files.base import ContentFile
from django.test import Client, TestCase
from django.test.utils import override_settings
from django.utils.encoding import force_bytes, force_str
from django.utils.http import urlsafe_base64_encode

from docbow_project.docbow import app_settings, notification
from docbow_project.docbow.models import (
    AttachedFile,
    AutomaticForwarding,
    Delegation,
    DocbowProfile,
    Document,
    FileType,
    FileTypeAttachedFileKind,
    Mailbox,
    MailingList,
    Notification,
    NotificationPreference,
    all_emails,
    generate_filename,
    is_guest,
)
from docbow_project.docbow.notification import MailNotifier
from docbow_project.docbow.upload_views import file_response
from docbow_project.docbow.utils import file_match_mime_types

MEDIA_ROOT = tempfile.mkdtemp()


def create_users(num_user):
    """Creates 'num_user' User objects
    with a DocbowProfile for each User
    Return the User list
    """
    result = []
    for i in range(num_user):
        result.append(User.objects.create(username='user-%s' % i, email='user-%s@example.com' % i))
        result[-1].set_password('password')
        result[-1].save()
        DocbowProfile.objects.create(user=result[-1], personal_email='personal-email-user-%s@example.com' % i)
    return result


@pytest.fixture
def users_fixture():
    return create_users(10)


@pytest.fixture
def admin():
    admin = User.objects.create(username='admin', email='admin@localhost', is_superuser=True, is_staff=True)
    admin.set_password('admin')
    admin.save()
    return admin


def create_filetypes(num_filetype):
    """Creates 'num_filetype' FileType objects
    Return the list of creqted FileType objects
    """
    result = []
    for i in range(num_filetype):
        result.append(FileType.objects.create(name='filetype-%s' % i))
    return result


@pytest.fixture
def filetype_fixtures():
    return create_filetypes(10)


class MailingListTreeTestCase(TestCase):
    '''Test mailing lists member resolution in a classical tree setup.'''

    def setUp(self):
        self.users = []
        self.mls = []
        for i in range(20):
            self.users.append(User.objects.create(username='%s' % i))
        sublist = []
        for i in range(19, -1, -1):
            self.mls.insert(0, MailingList.objects.create(name='%s' % i))
            self.mls[0].members.set([self.users[i]])
            self.mls[0].mailing_list_members.set(sublist)
            sublist = [self.mls[0]]

    def test_mailing_list_recursive(self):
        """
        Tests that 1 + 1 always equals 2.
        """
        for i in range(19, -1, -1):
            members = self.mls[i].recursive_members()
            self.assertEqual(len(members), 20 - i)

    def test_mailing_list_recursive_with_origin(self):
        """
        Tests that 1 + 1 always equals 2.
        """
        for i in range(19, -1, -1):
            mwo = self.mls[i].recursive_members_with_origin()
            self.assertEqual(len(mwo), 20 - i)
            for j in range(i, 20):
                self.assertEqual(mwo[self.users[j]], set(self.mls[i : j + 1]))


@pytest.mark.django_db
def test_admin_mailing_list(admin, client):
    users = []
    mls = []
    for i in range(20):
        users.append(User.objects.create(username='user%s' % i))
    sublist = []
    for i in range(19, -1, -1):
        mls.insert(0, MailingList.objects.create(name='ml%s' % i))
        mls[0].members.set([users[i]])
        mls[0].mailing_list_members.set(sublist)
        sublist = [mls[0]]

    client.login(username='admin', password='admin')
    from django.db import connection as conn
    from django.test.utils import CaptureQueriesContext

    with CaptureQueriesContext(conn) as context:
        resp = client.get('/admin/docbow/mailinglist/?q=' + ('user19 ml18 a%20' * 10))
    assert context[-1]['sql'].count('UPPER') == 8

    from bs4 import BeautifulSoup

    resp = client.get('/admin/docbow/mailinglist/?q=user19')
    soup = BeautifulSoup(resp.content)
    tags = soup.find_all('th', {'class': 'field-name'})
    assert {tag.text for tag in tags} == {'ml19'}

    resp = client.get('/admin/docbow/mailinglist/?q=ml18')
    soup = BeautifulSoup(resp.content)
    tags = soup.find_all('th', {'class': 'field-name'})
    assert {tag.text for tag in tags} == {'ml18'}


class MailingListCycle(TestCase):
    '''Test mailing lists member resolution in a cyclic setup.'''

    def setUp(self):
        self.users = []
        self.mls = []
        for i in range(20):
            self.users.append(User.objects.create(username='%s' % i))
        sublist = []
        for i in range(19, -1, -1):
            self.mls.insert(0, MailingList.objects.create(name='%s' % i))
            self.mls[0].members.set([self.users[i]])
            self.mls[0].mailing_list_members.set(sublist)
            sublist = [self.mls[0]]
        self.mls[19].mailing_list_members.set([self.mls[0]])

    def test_mailing_list_recursive(self):
        """
        Tests that 1 + 1 always equals 2.
        """
        for i in range(19, -1, -1):
            members = self.mls[i].recursive_members()
            self.assertEqual(len(members), 20)

    def test_mailing_list_recursive_with_origin(self):
        """
        Tests that 1 + 1 always equals 2.
        """
        for i in range(19, -1, -1):
            mwo = self.mls[i].recursive_members_with_origin()
            self.assertEqual(len(mwo), 20)
            for j in range(i, 20):
                self.assertEqual(mwo[self.users[j]], set(self.mls))


@override_settings(MEDIA_ROOT=MEDIA_ROOT)
class BaseTestCase(TestCase):
    COUNT = 10

    def setUp(self):
        self.setUpUsers()
        self.setUpDocuments()

    def setUpUsers(self):
        self.users = create_users(self.COUNT)
        self.filetypes = create_filetypes(self.COUNT)

    def setUpDocuments(self):
        self.documents = []
        for i in range(self.COUNT):
            self.documents.append(
                Document.objects.create(sender=self.users[(i + 2) % self.COUNT], filetype=self.filetypes[i])
            )
            self.documents[-1].to_user.set([self.users[i % self.COUNT], self.users[(i + 1) % self.COUNT]])
            for j in range(2):
                attached_file = AttachedFile(name='file%s' % j, document=self.documents[-1], kind=None)
                attached_file.content.save('file%s.pdf' % j, ContentFile('coucou'))
                attached_file.save()
            self.documents[-1].post()


class BasicTestCase(BaseTestCase):
    def test_notification_mail(self):
        management.call_command('notify')
        assert len(mail.outbox) == 20
        outbox = sorted(mail.outbox, key=lambda m: tuple(sorted(m.to)))
        MAIL_LINK_RE = re.compile(r'https?://[^/]+/inbox/\d+/')
        for message, i in zip(outbox, range(20)):
            recipient = self.users[i // 2]
            if app_settings.PERSONAL_EMAIL:
                emails = [recipient.docbowprofile.personal_email, recipient.email]
            else:
                emails = [recipient.email]
            assert set(message.to) == set(emails)
            match = MAIL_LINK_RE.search(message.body)
            self.assertIsNotNone(match)


class UtilsTestCase(BaseTestCase):
    def setUp(self):
        super().setUp()
        self.user1, self.user2 = self.users[:2]
        self.user2.docbowprofile.is_guest = True
        self.user2.docbowprofile.save()

    def test_generate_filename(self):
        assert generate_filename(None, 'xxx.pdf') != generate_filename(None, 'xxx.pdf')

    def test_all_emails(self):
        assert set(all_emails(self.user1)) == {self.user1.email, self.user1.docbowprofile.personal_email}

    def test_document_manager(self):
        with self.assertNumQueries(1):
            # With django 1.8, there is no more
            # eager loading of related AttachedFile objects
            # so only one SQL query here
            list(Document.objects.all())

    def test_document_accessors(self):
        document = self.documents[0]
        filenames = {filename.strip() for filename in document.filenames().split(',')}
        self.assertEqual(filenames, {'file0.pdf', 'file1.pdf'})
        self.assertEqual(set(document.user_human_to()), {'user-0', 'user-1'})
        self.assertEqual(document.group_human_to(), [])
        self.assertEqual(set(document.human_to()), {'user-0', 'user-1'})
        self.assertIsNotNone(document.filename_links())
        assert set(document.to()) == {self.user1, self.user2}
        assert dict(document.to_with_origin()) == {
            self.user1: {'--direct--'},
            self.user2: {'--direct--'},
        }
        self.assertEqual(
            sorted(map(lambda x: x.pk, document.delivered_to())), sorted([self.user1.pk, self.user2.pk])
        )

    def test_is_guest(self):
        self.assertTrue(is_guest(self.user2))


class DelegatesTestCase(BaseTestCase):
    def setUp(self):
        self.setUpUsers()
        self.delegate = delegate = User(username='delegate')
        delegate.set_password('delegate')
        delegate.save()
        Delegation.objects.create(by=self.users[0], to=delegate)
        Delegation.objects.create(by=self.users[1], to=delegate)
        self.guest0 = User(username='user-0-1')
        self.guest0.set_password('guest')
        self.guest0.save()
        DocbowProfile.objects.create(user=self.guest0, is_guest=True)
        Delegation.objects.create(by=self.users[0], to=self.guest0)
        self.guest1 = User(username='user-1-1')
        self.guest1.set_password('guest')
        self.guest1.save()
        DocbowProfile.objects.create(user=self.guest1, is_guest=True)
        Delegation.objects.create(by=self.users[1], to=self.guest1)

    def test_inbox_by_document(self):
        document = Document.objects.create(sender=self.users[0], filetype=self.filetypes[0])
        document.to_user.set([self.users[1]])
        attached_file = AttachedFile(name='file-private-flag.pdf', document=document, kind=None)
        attached_file.content.save('file-private-flag.pdf', ContentFile('coucou'))
        attached_file.save()
        document.post()
        c = Client()
        c.login(username='delegate', password='delegate')
        response = c.get('/inbox/%s/' % document.pk)
        self.assertEqual(response.status_code, 200)

    def test_private_flag(self):
        document = Document.objects.create(sender=self.users[0], filetype=self.filetypes[0], private=True)
        document.to_user.set([self.users[1]])
        attached_file = AttachedFile(name='file-private-flag.pdf', document=document, kind=None)
        attached_file.content.save('file-private-flag.pdf', ContentFile('coucou'))
        attached_file.save()
        document.post()
        self.assertEqual(Mailbox.objects.count(), 2)

        # check user-1 sees document in its outbox
        c = Client()
        c.login(username='user-0', password='password')

        response = c.get('/outbox/')
        self.assertIn('/outbox/%s/' % document.pk, force_str(response.content))
        response = c.get('/outbox/%s/' % document.pk)
        self.assertEqual(response.status_code, 200)

        response = c.get('/inbox/')
        self.assertNotIn('/inbox/%s/' % document.pk, force_str(response.content))
        response = c.get('/inbox/%s/' % document.pk)
        self.assertEqual(response.status_code, 302)

        # check user-1 sees document in its inbox
        c = Client()
        c.login(username='user-1', password='password')

        response = c.get('/inbox/')
        self.assertIn('/inbox/%s/' % document.pk, force_str(response.content))
        response = c.get('/inbox/%s/' % document.pk)
        self.assertEqual(response.status_code, 200)

        response = c.get('/outbox/%s/' % document.pk)
        self.assertEqual(response.status_code, 302)
        response = c.get('/outbox/')
        self.assertNotIn('/outbox/%s/' % document.pk, force_str(response.content))

        # check delegate sees nothing
        c = Client()
        c.login(username='delegate', password='delegate')

        response = c.get('/inbox/')
        self.assertNotIn('/inbox/%s/' % document.pk, force_str(response.content))
        response = c.get('/inbox/%s/' % document.pk)
        self.assertEqual(response.status_code, 302)

        response = c.get('/outbox/')
        self.assertNotIn('/outbox/%s/' % document.pk, force_str(response.content))
        response = c.get('/outbox/%s/' % document.pk)
        self.assertEqual(response.status_code, 302)

        # check notifications
        with self.assertRaises(Notification.DoesNotExist):
            Notification.objects.get(user=self.delegate, document=document)
        Notification.objects.get(user=self.users[1], document=document)


class DummyNotifier(notification.BaseNotifier):
    notifications = []
    description = 'Dummy'
    key = 'dummy'

    def process(self, notification):
        self.notifications.append(notification)

    @classmethod
    def reset(cls):
        cls.notifications = []


class NotificationTestCase(BaseTestCase):
    COUNT = 2

    def test_notification_preferences(self):
        NotificationPreference.objects.create(
            user=self.users[0], kind=DummyNotifier.key, filetype=self.filetypes[0], value=False
        )
        NotificationPreference.objects.create(
            user=self.users[1], kind=DummyNotifier.key, filetype=self.filetypes[1], value=False
        )
        DummyNotifier.notifications = []

        with mock.patch('docbow_project.docbow.notification.get_notifiers') as MockClass:
            MockClass.return_value = [DummyNotifier()]
            notification.process_notifications()

        self.assertEqual(len(DummyNotifier.notifications), 2)
        for notif in DummyNotifier.notifications:
            with self.assertRaises(NotificationPreference.DoesNotExist):
                NotificationPreference.objects.get(
                    user=notif.user, filetype=notif.document.filetype, kind=DummyNotifier.key, value=False
                )


@override_settings(MEDIA_ROOT=MEDIA_ROOT)
class NotificationToDelegatesTestCase(TestCase):
    def send_document(
        self,
        sender,
        user_recipients=[],
        list_recipients=[],
        filetype_name='dummy filetype',
        names_and_contents=(('dummy.pdf', 'dummy content'),),
    ):
        filetype, created = FileType.objects.get_or_create(name=filetype_name)
        document = Document.objects.create(sender=sender, filetype=filetype)
        document.to_user.set(user_recipients)
        document.to_list.set(list_recipients)
        attached_files = []
        for name, content in names_and_contents:
            attached_file = AttachedFile(name=name, document=document, kind=None)
            attached_file.content.save(name, ContentFile(content))
            attached_file.save()
            attached_files.append(attached_file)

        document.post()
        return document, attached_files

    def test_notification_to_delegates(self):
        DummyNotifier.reset()
        sender = User.objects.create(username='sender')
        recipient = User.objects.create(username='recipient')
        delegate = User.objects.create(username='delegate')
        Delegation.objects.create(by=recipient, to=delegate)
        self.send_document(sender, [recipient])

        with mock.patch('docbow_project.docbow.notification.get_notifiers') as MockClass:
            MockClass.return_value = [DummyNotifier()]
            notification.process_notifications()

            self.assertEqual(len(DummyNotifier.notifications), 2)
            self.assertEqual({notif.user for notif in DummyNotifier.notifications}, {recipient, delegate})

            delegate.is_active = False
            delegate.save()
            DummyNotifier.reset()
            self.send_document(sender, [recipient])
            notification.process_notifications()
            self.assertEqual(len(DummyNotifier.notifications), 1)
            self.assertEqual(DummyNotifier.notifications[0].user, recipient)


class AddListTestCase(BaseTestCase):
    def test_add_mailing_list(self):
        management.call_command('add-list', 'some-ml-name')
        ml_list = MailingList.objects.all()
        assert len(ml_list) == 1
        assert ml_list[0].name == 'some-ml-name'


@pytest.mark.django_db
def test_send_file(users_fixture, filetype_fixtures, settings, tmpdir, monkeypatch):
    settings.MEDIA_ROOT = tmpdir.strpath
    monkeypatch.chdir(tmpdir)
    filetosend = tmpdir.join('filetosend.txt')
    filetosend.write('file content')
    management.call_command(
        'sendfile',
        '--sender',
        'user-1',
        '--to-user',
        'user-2',
        '--filetype',
        'filetype-1',
        '--description',
        'yes we can',
        'filetosend.txt',
    )
    docs = Document.objects.all()
    assert len(docs) == 1
    doc = docs[0]
    assert doc.sender.get_username() == 'user-1'
    assert doc.filetype.name == 'filetype-1'
    attf = doc.attached_files.first()
    assert attf.filename() == 'filetosend.txt'
    # FIXME : way more things to assert here


@pytest.mark.django_db
def test_signals(users_fixture, settings, monkeypatch, tmpdir):
    # django_journal is called through django signals (db_post_save).
    # We test here that the docbow_project.docbow.signals.modified_data function
    # which is connected to the db_post_dave signal, does not raised an exception.
    # a MockMiddleware is used to make the function believe that an action
    # of an authenticated user triggered the signal, otherwise the function
    # exit too quickly

    settings.MEDIA_ROOT = tmpdir.strpath

    class MockMiddleware:
        NO_USER = '-'

        def get_extra(self):
            return {'user': 'test-user', 'ip': '0.0.0.0'}

    import docbow_project.docbow.signals

    user0 = users_fixture[0]
    monkeypatch.setattr(docbow_project.docbow.signals, 'middleware', MockMiddleware())
    # We just need no exception raised
    docbow_project.docbow.signals.modified_data(
        sender=User, instance=user0, created=False, raw=False, using='default'
    )


def test_file_response(tmpdir):
    tmp_file = tmpdir.join('somefile')
    data = b'abcdef'
    with tmp_file.open('wb') as f:
        f.write(data)
    response = file_response(tmp_file.open())
    assert response['Content-Type'] == 'application/octet-stream'
    assert response.content == data


def test_mime_buffer_size_default(db):
    assert app_settings.MIME_BUFFER_SIZE == 300000


def test_file_match_mime_types(tmpdir):
    tmp_file = tmpdir.join('aaa.txt')
    with tmp_file.open('w') as f:
        f.write('aaa')
    assert file_match_mime_types(tmp_file.open(), ['text/plain'], 100)


def test_filetype_attached_file_kind(db, tmpdir):
    ft = FileType.objects.create(name='some file type')
    fta = FileTypeAttachedFileKind.objects.create(
        name='text-plain', file_type=ft, mime_types='text/plain', position=1
    )

    text_file = tmpdir.join('aaa.txt')
    with text_file.open('w') as f:
        f.write('aaa')

    with text_file.open() as f:
        assert fta.match_file(f)

    fta.mime_types = 'image/jpeg'
    fta.save()
    with text_file.open() as f:
        assert not fta.match_file(f)


@pytest.mark.django_db
def test_password_reset(users_fixture, settings, monkeypatch, client):
    client.login(username='user-1@example.com', password='password')

    import django.contrib.auth.forms

    email_message = mock.Mock()
    email_message_factory = mock.Mock(return_value=email_message)

    monkeypatch.setattr(django.contrib.auth.forms, 'EmailMultiAlternatives', email_message_factory)

    response = client.post('/accounts/password/reset/', {'identifier': 'user-1@example.com'})
    assert response.status_code == 302
    assert response['Location'].endswith('reset/done/')
    assert email_message_factory.call_args_list[0][0][-1] == ['user-1@example.com']
    email_message.send.assert_called_with()


@pytest.mark.django_db
def test_password_reset_confirm_get(users_fixture, client):
    user = users_fixture[0]
    uid = force_str(urlsafe_base64_encode(force_bytes(user.id)))
    token = default_token_generator.make_token(user)

    response = client.get('/accounts/password/reset/confirm/%s/%s/' % (uid, token))
    assert response.status_code == 302
    redirect_url = '/accounts/password/reset/confirm/%s/set-password/' % uid
    assert response['Location'].endswith(redirect_url)
    response = client.get(redirect_url)
    assert 'new_password' in force_str(response.content)


@pytest.mark.django_db
def test_password_reset_confirm_post(users_fixture, client):
    user = users_fixture[0]
    uid = force_str(urlsafe_base64_encode(force_bytes(user.id)))
    token = default_token_generator.make_token(user)
    response = client.get('/accounts/password/reset/confirm/%s/%s/' % (uid, token))
    assert response.status_code == 302
    redirect_url = '/accounts/password/reset/confirm/%s/set-password/' % uid
    assert response['Location'].endswith(redirect_url)

    response = client.post(redirect_url, {'new_password1': 'newpass', 'new_password2': 'newpass'})
    assert response.status_code == 302
    assert response['Location'].endswith('/accounts/password/reset/complete/')

    response = client.get('/accounts/password/reset/complete/')
    assert response.status_code == 200

    client.login(username='user-1@example.com', password='newpass')


@pytest.mark.django_db
def test_admin_notification(admin, users_fixture, filetype_fixtures, client):
    user = users_fixture[0]
    doc = Document.objects.create(sender=user, filetype=filetype_fixtures[0])
    Notification.objects.create(document=doc)
    client.login(username='admin', password='admin')
    resp = client.get('/admin/docbow/notification/')
    assert resp.status_code == 200


@pytest.mark.django_db
def test_admin_automaticforwarding(admin, users_fixture, filetype_fixtures, client):
    user = users_fixture[0]
    at = AutomaticForwarding.objects.create()
    at.filetypes.add(filetype_fixtures[0])
    at.originaly_to_user.add(admin)
    at.forward_to_user.add(user)
    client.login(username='admin', password='admin')
    resp = client.get('/admin/docbow/automaticforwarding/')
    assert resp.status_code == 200


@pytest.mark.django_db
def test_delegate_login(client):
    recipient = User.objects.create(username='recipient')
    recipient.set_password('password')
    recipient.save()
    delegate = User.objects.create(username='recipient-1')
    delegate.set_password('password')
    delegate.save()
    DocbowProfile.objects.create(user=delegate, is_guest=True)
    Delegation.objects.create(by=recipient, to=delegate)

    response = client.post(
        '/accounts/login/?next=/inbox/', {'username': 'recipient-1', 'password': 'password'}
    )
    assert response.status_code == 302
    assert response['Location'].endswith('inbox/')


@pytest.mark.django_db
def test_delegate_login_hyphen_in_user_name(client):
    recipient = User.objects.create(username='recipient-tricky')
    recipient.set_password('password')
    recipient.save()
    delegate = User.objects.create(username='recipient-tricky-1')
    delegate.set_password('password')
    delegate.save()
    DocbowProfile.objects.create(user=delegate, is_guest=True)
    Delegation.objects.create(by=recipient, to=delegate)
    response = client.post(
        '/accounts/login/?next=/inbox/', {'username': 'recipient-tricky-1', 'password': 'password'}
    )
    assert response.status_code == 302
    assert response['Location'].endswith('inbox/')


@pytest.mark.django_db
def test_mail_notifier_but_nobody_to_notify():
    ctx = {'to': ['']}
    notification = Notification.objects.create(ctx=ctx)
    notifier = MailNotifier()
    notifier.process(notification)
    assert len(mail.outbox) == 0
