import io
import itertools
import os
import sys
import tempfile
from unittest import skipIf

from django.core.files.base import ContentFile
from django.http import FileResponse
from django.test import SimpleTestCase


class UnseekableBytesIO(io.BytesIO):
    def seekable(self):
        return False


class FileResponseTests(SimpleTestCase):
    def test_content_length_file(self):
        response = FileResponse(open(__file__, "rb"))
        response.close()
        self.assertEqual(
            response.headers["Content-Length"], str(os.path.getsize(__file__))
        )

    def test_content_length_buffer(self):
        response = FileResponse(io.BytesIO(b"binary content"))
        self.assertEqual(response.headers["Content-Length"], "14")

    def test_content_length_nonzero_starting_position_file(self):
        file = open(__file__, "rb")
        file.seek(10)
        response = FileResponse(file)
        response.close()
        self.assertEqual(
            response.headers["Content-Length"], str(os.path.getsize(__file__) - 10)
        )

    def test_content_length_nonzero_starting_position_buffer(self):
        test_tuples = (
            ("BytesIO", io.BytesIO),
            ("UnseekableBytesIO", UnseekableBytesIO),
        )
        for buffer_class_name, BufferClass in test_tuples:
            with self.subTest(buffer_class_name=buffer_class_name):
                buffer = BufferClass(b"binary content")
                buffer.seek(10)
                response = FileResponse(buffer)
                self.assertEqual(response.headers["Content-Length"], "4")

    def test_content_length_nonzero_starting_position_file_seekable_no_tell(self):
        class TestFile:
            def __init__(self, path, *args, **kwargs):
                self._file = open(path, *args, **kwargs)

            def read(self, n_bytes=-1):
                return self._file.read(n_bytes)

            def seek(self, offset, whence=io.SEEK_SET):
                return self._file.seek(offset, whence)

            def seekable(self):
                return True

            @property
            def name(self):
                return self._file.name

            def close(self):
                if self._file:
                    self._file.close()
                    self._file = None

            def __enter__(self):
                return self

            def __exit__(self, e_type, e_val, e_tb):
                self.close()

        file = TestFile(__file__, "rb")
        file.seek(10)
        response = FileResponse(file)
        response.close()
        self.assertEqual(
            response.headers["Content-Length"], str(os.path.getsize(__file__) - 10)
        )

    def test_content_type_file(self):
        response = FileResponse(open(__file__, "rb"))
        response.close()
        self.assertIn(response.headers["Content-Type"], ["text/x-python", "text/plain"])

    def test_content_type_buffer(self):
        response = FileResponse(io.BytesIO(b"binary content"))
        self.assertEqual(response.headers["Content-Type"], "application/octet-stream")

    def test_content_type_buffer_explicit(self):
        response = FileResponse(
            io.BytesIO(b"binary content"), content_type="video/webm"
        )
        self.assertEqual(response.headers["Content-Type"], "video/webm")

    def test_content_type_buffer_explicit_default(self):
        response = FileResponse(
            io.BytesIO(b"binary content"), content_type="text/html; charset=utf-8"
        )
        self.assertEqual(response.headers["Content-Type"], "text/html; charset=utf-8")

    def test_content_type_buffer_named(self):
        test_tuples = (
            (__file__, ["text/x-python", "text/plain"]),
            (__file__ + "nosuchfile", ["application/octet-stream"]),
            ("test_fileresponse.py", ["text/x-python", "text/plain"]),
            ("test_fileresponse.pynosuchfile", ["application/octet-stream"]),
        )
        for filename, content_types in test_tuples:
            with self.subTest(filename=filename):
                buffer = io.BytesIO(b"binary content")
                buffer.name = filename
                response = FileResponse(buffer)
                self.assertIn(response.headers["Content-Type"], content_types)

    def test_content_disposition_file(self):
        filenames = (
            ("", "test_fileresponse.py"),
            ("custom_name.py", "custom_name.py"),
        )
        dispositions = (
            (False, "inline"),
            (True, "attachment"),
        )
        for (filename, header_filename), (
            as_attachment,
            header_disposition,
        ) in itertools.product(filenames, dispositions):
            with self.subTest(filename=filename, disposition=header_disposition):
                response = FileResponse(
                    open(__file__, "rb"), filename=filename, as_attachment=as_attachment
                )
                response.close()
                self.assertEqual(
                    response.headers["Content-Disposition"],
                    '%s; filename="%s"' % (header_disposition, header_filename),
                )

    def test_content_disposition_escaping(self):
        # fmt: off
        tests = [
            (
                'multi-part-one";\" dummy".txt',
                r"multi-part-one\";\" dummy\".txt"
            ),
        ]
        # fmt: on
        # Non-escape sequence backslashes are path segments on Windows, and are
        # eliminated by an os.path.basename() check in FileResponse.
        if sys.platform != "win32":
            # fmt: off
            tests += [
                (
                    'multi-part-one\\";\" dummy".txt',
                    r"multi-part-one\\\";\" dummy\".txt"
                ),
                (
                    'multi-part-one\\";\\\" dummy".txt',
                    r"multi-part-one\\\";\\\" dummy\".txt"
                )
            ]
            # fmt: on
        for filename, escaped in tests:
            with self.subTest(filename=filename, escaped=escaped):
                response = FileResponse(
                    io.BytesIO(b"binary content"), filename=filename, as_attachment=True
                )
                response.close()
                self.assertEqual(
                    response.headers["Content-Disposition"],
                    f'attachment; filename="{escaped}"',
                )

    def test_content_disposition_buffer(self):
        response = FileResponse(io.BytesIO(b"binary content"))
        self.assertFalse(response.has_header("Content-Disposition"))

    def test_content_disposition_buffer_attachment(self):
        response = FileResponse(io.BytesIO(b"binary content"), as_attachment=True)
        self.assertEqual(response.headers["Content-Disposition"], "attachment")

    def test_content_disposition_buffer_explicit_filename(self):
        dispositions = (
            (False, "inline"),
            (True, "attachment"),
        )
        for as_attachment, header_disposition in dispositions:
            response = FileResponse(
                io.BytesIO(b"binary content"),
                as_attachment=as_attachment,
                filename="custom_name.py",
            )
            self.assertEqual(
                response.headers["Content-Disposition"],
                '%s; filename="custom_name.py"' % header_disposition,
            )

    def test_response_buffer(self):
        response = FileResponse(io.BytesIO(b"binary content"))
        self.assertEqual(list(response), [b"binary content"])

    def test_response_nonzero_starting_position(self):
        test_tuples = (
            ("BytesIO", io.BytesIO),
            ("UnseekableBytesIO", UnseekableBytesIO),
        )
        for buffer_class_name, BufferClass in test_tuples:
            with self.subTest(buffer_class_name=buffer_class_name):
                buffer = BufferClass(b"binary content")
                buffer.seek(10)
                response = FileResponse(buffer)
                self.assertEqual(list(response), [b"tent"])

    def test_buffer_explicit_absolute_filename(self):
        """
        Headers are set correctly with a buffer when an absolute filename is
        provided.
        """
        response = FileResponse(io.BytesIO(b"binary content"), filename=__file__)
        self.assertEqual(response.headers["Content-Length"], "14")
        self.assertEqual(
            response.headers["Content-Disposition"],
            'inline; filename="test_fileresponse.py"',
        )

    @skipIf(sys.platform == "win32", "Named pipes are Unix-only.")
    def test_file_from_named_pipe_response(self):
        with tempfile.TemporaryDirectory() as temp_dir:
            pipe_file = os.path.join(temp_dir, "named_pipe")
            os.mkfifo(pipe_file)
            pipe_for_read = os.open(pipe_file, os.O_RDONLY | os.O_NONBLOCK)
            with open(pipe_file, "wb") as pipe_for_write:
                pipe_for_write.write(b"binary content")

            response = FileResponse(os.fdopen(pipe_for_read, mode="rb"))
            response_content = list(response)
            response.close()
            self.assertEqual(response_content, [b"binary content"])
            self.assertFalse(response.has_header("Content-Length"))

    def test_compressed_response(self):
        """
        If compressed responses are served with the uncompressed Content-Type
        and a compression Content-Encoding, browsers might automatically
        uncompress the file, which is most probably not wanted.
        """
        test_tuples = (
            (".tar.gz", "application/gzip"),
            (".tar.bz2", "application/x-bzip"),
            (".tar.xz", "application/x-xz"),
        )
        for extension, mimetype in test_tuples:
            with self.subTest(ext=extension):
                with tempfile.NamedTemporaryFile(suffix=extension) as tmp:
                    response = FileResponse(tmp)
                self.assertEqual(response.headers["Content-Type"], mimetype)
                self.assertFalse(response.has_header("Content-Encoding"))

    def test_unicode_attachment(self):
        response = FileResponse(
            ContentFile(b"binary content", name="祝您平安.odt"),
            as_attachment=True,
            content_type="application/vnd.oasis.opendocument.text",
        )
        self.assertEqual(
            response.headers["Content-Type"],
            "application/vnd.oasis.opendocument.text",
        )
        self.assertEqual(
            response.headers["Content-Disposition"],
            "attachment; filename*=utf-8''%E7%A5%9D%E6%82%A8%E5%B9%B3%E5%AE%89.odt",
        )

    def test_repr(self):
        response = FileResponse(io.BytesIO(b"binary content"))
        self.assertEqual(
            repr(response),
            '<FileResponse status_code=200, "application/octet-stream">',
        )
