from io import BytesIO
import logging
import os
import uuid

from django.core.files.base import ContentFile
from django.core.files.storage import FileSystemStorage
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import TestCase
from django.test.client import encode_multipart, RequestFactory
from django.urls import reverse
from rest_framework import status
from rest_framework.response import Response
from six import ensure_text

from django_drf_filepond import drf_filepond_settings
import django_drf_filepond
import django_drf_filepond.views as views
from tests.utils import remove_file_upload_dir_if_required


# Python 2/3 support
try:
    from unittest.mock import patch, MagicMock, ANY
except ImportError:
    from mock import patch, MagicMock, ANY

LOG = logging.getLogger(__name__)
#
# New tests for checking file storage outside of BASE_DIR (see #18)
#
# test_store_upload_with_storage_outside_BASE_DIR_without_enable: Set a
#    FileSystemStorage store location that is outside of BASE_DIR and check
#    that the upload fails.
#
# test_store_upload_with_storage_outside_BASE_DIR_with_enable: Set a
#    FileSystemStorage store location outside of BASE_DIR and also set the
#    ALLOW_EXTERNAL_UPLOAD_DIR setting to True and check the upload succeeds
#
# test_relative_UPLOAD_TMP_outside_base_dir_not_allowed: Check that when a
#    a relative path is provided that gets around the requirement for
#    UPLOAD_TMP to be under BASE_DIR, that this is detected and a 500 thrown
#
# test_new_chunked_upload_request: Check that a new chunked upload request
#    results in handle_upload being called on the chunked uploader class.
#


class ProcessTestCase(TestCase):

    def setUp(self):
        # Create some random data to test upload.
        data = BytesIO()
        data.write(os.urandom(16384))
        self.test_data = data

    def test_process_data(self):
        self._process_data()

    def test_UPLOAD_TMP_not_set(self):
        upload_tmp = drf_filepond_settings.UPLOAD_TMP
        delattr(drf_filepond_settings, 'UPLOAD_TMP')

        # Set up and run request
        (encoded_form, content_type) = self._get_encoded_form('testfile.dat')

        rf = RequestFactory()
        req = rf.post(reverse('process'),
                      data=encoded_form, content_type=content_type)
        pv = views.ProcessView.as_view()
        response = pv(req)

        self.assertContains(response, 'The file upload path settings are '
                            'not configured correctly.', status_code=500)

        setattr(drf_filepond_settings, 'UPLOAD_TMP', upload_tmp)

    def test_process_invalid_storage_location(self):
        old_storage = views.storage
        views.storage = FileSystemStorage(location='/django_test')
        (encoded_form, content_type) = self._get_encoded_form('testfile.dat')

        rf = RequestFactory()
        req = rf.post(reverse('process'),
                      data=encoded_form, content_type=content_type)
        pv = views.ProcessView.as_view()
        response = pv(req)
        views.storage = old_storage
        self.assertEqual(response.status_code, 500, 'Expecting 500 error due'
                         ' to invalid storage location.')
        self.assertEqual(
            response.data,
            'The file upload path settings are not configured correctly.',
            ('Expecting error showing path settings are configured '
             'incorrectly.'))

    def test_process_invalid_data(self):
        upload_form = {'somekey':
                       SimpleUploadedFile('test.txt', self.test_data.read())}
        enc_form = encode_multipart('abc', upload_form)
        rf = RequestFactory()
        req = rf.post(reverse('process'), data=enc_form,
                      content_type='multipart/form-data; boundary=abc')
        pv = views.ProcessView.as_view()
        response = pv(req)
        self.assertEqual(response.status_code, 400, 'Expecting 400 error due'
                         ' to invalid data being provided.')
        self.assertTrue('detail' in response.data,
                        'Error detail missing in response.')
        self.assertIn(response.data['detail'], ('Invalid request data has '
                                                'been provided.'))

    def test_upload_non_file_data(self):
        cf = ContentFile(self.test_data.read(), name='test.txt')
        upload_form = {'filepond': cf}
        enc_form = encode_multipart('abc', upload_form)
        rf = RequestFactory()
        req = rf.post(reverse('process'), data=enc_form,
                      content_type='multipart/form-data; boundary=abc')
        req.FILES['filepond'] = cf
        pv = views.ProcessView.as_view()
        response = pv(req)
        self.assertEqual(response.status_code, 400, 'Expecting 400 error due'
                         ' to non-file data being provided.')
        self.assertTrue('detail' in response.data,
                        'Error detail missing in response.')
        self.assertIn(response.data['detail'], ('Invalid data type has been '
                                                'parsed.'))

    # Based on the modification for issue #4, test that we can successfully
    # handle an upload that provides its data under a field name that is not
    # the default "filepond" and that an error is thrown when an invalid
    # name is expected
    def test_process_data_different_field_name(self):
        self._process_data('somefield')

    def test_process_data_invalid_different_field_name(self):
        upload_form = {'somekey': SimpleUploadedFile(
            'test.txt', self.test_data.read()), 'fp_upload_field': 'somekey2'
        }
        boundary = str(uuid.uuid4()).replace('-', '')
        enc_form = encode_multipart(boundary, upload_form)

        rf = RequestFactory()
        req = rf.post(reverse('process'), data=enc_form,
                      content_type='multipart/form-data; boundary=%s'
                      % boundary)
        pv = views.ProcessView.as_view()
        response = pv(req)
        self.assertEqual(response.status_code, 400, 'Expecting 400 error due'
                         ' to invalid data being provided.')
        self.assertTrue('detail' in response.data,
                        'Error detail missing in response.')
        self.assertIn(response.data['detail'], ('Invalid request data has '
                                                'been provided.'))

    def test_store_upload_with_storage_outside_BASE_DIR_without_enable(self):
        old_storage = views.storage
        views.storage = FileSystemStorage(location='/tmp/uploads')
        (encoded_form, content_type) = self._get_encoded_form('testfile.dat')

        rf = RequestFactory()
        req = rf.post(reverse('process'),
                      data=encoded_form, content_type=content_type)
        pv = views.ProcessView.as_view()
        response = pv(req)
        views.storage = old_storage
        self.assertEqual(response.status_code, 500, 'Expecting 500 error due'
                         ' to invalid storage location.')
        self.assertEqual(
            response.data,
            'The file upload path settings are not configured correctly.',
            ('Expecting error showing path settings are configured '
             'incorrectly.'))

    def test_store_upload_with_storage_outside_BASE_DIR_with_enable(self):
        old_storage = views.storage
        old_UPLOAD_TMP = drf_filepond_settings.UPLOAD_TMP

        drf_filepond_settings.ALLOW_EXTERNAL_UPLOAD_DIR = True

        views.storage = FileSystemStorage(location='/tmp/uploads')
        drf_filepond_settings.UPLOAD_TMP = '/tmp/uploads'

        (encoded_form, content_type) = self._get_encoded_form('testfile.dat')

        rf = RequestFactory()
        req = rf.post(reverse('process'),
                      data=encoded_form, content_type=content_type)
        pv = views.ProcessView.as_view()
        response = pv(req)
        views.storage = old_storage
        drf_filepond_settings.UPLOAD_TMP = old_UPLOAD_TMP
        drf_filepond_settings.ALLOW_EXTERNAL_UPLOAD_DIR = False
        self.assertEqual(response.status_code, 200, 'Expecting upload to be '
                         'successful.')

    def test_relative_UPLOAD_TMP_outside_base_dir_not_allowed(self):
        upload_tmp = drf_filepond_settings.UPLOAD_TMP
        drf_filepond_settings.UPLOAD_TMP = os.path.join(
            drf_filepond_settings.BASE_DIR, '..', '..', 'some_dir')

        # Set up and run request
        (encoded_form, content_type) = self._get_encoded_form('testfile.dat')

        rf = RequestFactory()
        req = rf.post(reverse('process'),
                      data=encoded_form, content_type=content_type)
        pv = views.ProcessView.as_view()
        response = pv(req)

        self.assertContains(response, 'An invalid storage location has been '
                            'specified.', status_code=500)

        drf_filepond_settings.UPLOAD_TMP = upload_tmp

    @patch('django_drf_filepond.uploaders.FilepondChunkedFileUploader.'
           '_handle_new_chunk_upload')
    def test_new_chunked_upload_request(self, mock_chunked_ul):
        # Tried to patch the mocked _get_file_id function but the views
        # module seems to have been pre-initialised within the Django init
        # phase and it has already imported the unmocked object.
        # For testing we manually assign the mocked object here and then
        # revert to the original after the view call.
        upload_id = ensure_text('ababababababababababab')
        file_id = ensure_text('xyxyxyxyxyxyxyxyxyxyxy')
        mock_gfid = MagicMock(spec='django_drf_filepond.utils._get_file_id')
        mock_gfid.side_effect = [upload_id, file_id, upload_id, file_id]

        original_gfid = django_drf_filepond.views._get_file_id
        django_drf_filepond.views._get_file_id = mock_gfid

        mock_chunked_ul.return_value = Response(upload_id,
                                                status=status.HTTP_200_OK,
                                                content_type='text/plain')
        (encoded_form, content_type) = self._get_encoded_form('testfile.dat',
                                                              file_spec='{}')
        rf = RequestFactory(HTTP_UPLOAD_LENGTH=1048576)
        req = rf.post(reverse('process'),
                      data=encoded_form, content_type=content_type)
        pv = views.ProcessView.as_view()
        pv(req)
        django_drf_filepond.views._get_file_id = original_gfid

        mock_chunked_ul.assert_called_once_with(ANY, upload_id, file_id)

    def _process_data(self, upload_field_name=None):
        tmp_upload_dir = drf_filepond_settings.UPLOAD_TMP
        self.uploaddir_exists_pre_test = os.path.exists(tmp_upload_dir)
        (encoded_form, content_type) = self._get_encoded_form(
            'testfile.dat', upload_field_name)

        response = self.client.post(
            reverse('process'),
            data=encoded_form, content_type=content_type)

        # Attempt created file/directory removal before assert statements
        # so that we can clean up as far as possible at this stage.
        if hasattr(response, 'data'):
            dir_path = os.path.join(tmp_upload_dir, response.data)
            if len(response.data) == 22 and os.path.exists(dir_path):
                dir_list = os.listdir(dir_path)
                if len(dir_list) == 1 and len(dir_list[0]) == 22:
                    file_path = os.path.join(dir_path, dir_list[0])
                    LOG.debug('Removing generated file <%s>' % file_path)
                    os.remove(file_path)
                    LOG.debug('Removing temporary directory <%s>'
                              % dir_path)
                    os.rmdir(dir_path)
                else:
                    LOG.warning('Name of uploaded file in the temp '
                                'directory doesn\'t have 22 chars, '
                                'not deleting the file')
            else:
                LOG.error('Couldn\'t proceed with file deleting since the '
                          'response received was not the right length (22)')

        remove_file_upload_dir_if_required(self.uploaddir_exists_pre_test,
                                           tmp_upload_dir)

        self.assertEqual(response.status_code, 200,
                         'Response received status code <%s> instead of 200.'
                         % (response.status_code))

        self.assertTrue(hasattr(response, 'data'),
                        ('The response does not contain a data attribute.'))

        self.assertEqual(len(response.data), 22,
                         'Response data is not of the correct length.')

    def _get_encoded_form(self, filename, upload_field_name=None,
                          file_spec=None):
        if not file_spec:
            file_spec = SimpleUploadedFile(filename, self.test_data.read())
        if upload_field_name:
            upload_form = {upload_field_name: file_spec,
                           'fp_upload_field': upload_field_name}
        else:
            upload_form = {'filepond': file_spec}

        boundary = str(uuid.uuid4()).replace('-', '')

        encoded_form = encode_multipart(boundary, upload_form)

        content_type = ('multipart/form-data; boundary=%s' % (boundary))

        return (encoded_form, content_type)