import json
from io import BytesIO
from textwrap import dedent
from unittest.mock import ANY

from django.test import ignore_warnings, tag

from anymail.exceptions import AnymailNotSupportedWarning
from anymail.inbound import AnymailInboundMessage
from anymail.signals import AnymailInboundEvent
from anymail.webhooks.sendgrid import SendGridInboundWebhookView

from .utils import (
    dedent_bytes,
    encode_multipart,
    make_fileobj,
    sample_email_content,
    sample_image_content,
)
from .webhook_cases import WebhookTestCase


@tag("sendgrid")
@ignore_warnings(category=AnymailNotSupportedWarning)
class SendgridInboundTestCase(WebhookTestCase):
    def test_not_supported_warning(self):
        with self.assertWarns(
            AnymailNotSupportedWarning,
            msg="django-anymail has dropped official support for SendGrid.",
        ):
            self.client.post("/anymail/sendgrid/inbound/", data={"email": ""})

    def test_inbound_basics(self):
        raw_event = {
            "headers": dedent(
                """\
                Received: from mail.example.org by mx987654321.sendgrid.net ...
                Received: by mail.example.org for <test@inbound.example.com> ...
                DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.org; ...
                MIME-Version: 1.0
                Received: by 10.10.1.71 with HTTP; Wed, 11 Oct 2017 18:31:04 -0700 (PDT)
                From: "Displayed From" <from+test@example.org>
                Date: Wed, 11 Oct 2017 18:31:04 -0700
                Message-ID: <CAEPk3R+4Zr@mail.example.org>
                Subject: Test subject
                To: "Test Inbound" <test@inbound.example.com>, other@example.com
                Cc: cc@example.com
                Content-Type: multipart/mixed; boundary="94eb2c115edcf35387055b61f849"
                """
            ),
            "from": "Displayed From <from+test@example.org>",
            "to": "Test Inbound <test@inbound.example.com>, other@example.com",
            "subject": "Test subject",
            "text": "Test body plain",
            "html": "<div>Test body html</div>",
            "attachments": "0",
            "charsets": '{"to":"UTF-8","html":"UTF-8",'
            '"subject":"UTF-8","from":"UTF-8","text":"UTF-8"}',
            "envelope": '{"to":["test@inbound.example.com"],'
            '"from":"envelope-from@example.org"}',
            "sender_ip": "10.10.1.71",
            # yep, SendGrid uses not-exactly-json for this field:
            "dkim": "{@example.org : pass}",
            "SPF": "pass",
            "spam_score": "1.7",
            "spam_report": "Spam detection software, running on the system"
            ' "mx987654321.sendgrid.net", '
            "has identified this incoming email as possible spam...",
        }
        response = self.client.post("/anymail/sendgrid/inbound/", data=raw_event)
        self.assertEqual(response.status_code, 200)
        kwargs = self.assert_handler_called_once_with(
            self.inbound_handler,
            sender=SendGridInboundWebhookView,
            event=ANY,
            esp_name="SendGrid",
        )
        # AnymailInboundEvent
        event = kwargs["event"]
        self.assertIsInstance(event, AnymailInboundEvent)
        self.assertEqual(event.event_type, "inbound")
        self.assertIsNone(event.timestamp)
        self.assertIsNone(event.event_id)
        self.assertIsInstance(event.message, AnymailInboundMessage)
        # esp_event is a Django HttpRequest:
        self.assertEqual(event.esp_event.POST.dict(), raw_event)

        # AnymailInboundMessage - convenience properties
        message = event.message

        self.assertEqual(message.from_email.display_name, "Displayed From")
        self.assertEqual(message.from_email.addr_spec, "from+test@example.org")
        self.assertEqual(
            [str(e) for e in message.to],
            ["Test Inbound <test@inbound.example.com>", "other@example.com"],
        )
        self.assertEqual([str(e) for e in message.cc], ["cc@example.com"])
        self.assertEqual(message.subject, "Test subject")
        self.assertEqual(message.date.isoformat(" "), "2017-10-11 18:31:04-07:00")
        self.assertEqual(message.text, "Test body plain")
        self.assertEqual(message.html, "<div>Test body html</div>")

        self.assertEqual(message.envelope_sender, "envelope-from@example.org")
        self.assertEqual(message.envelope_recipient, "test@inbound.example.com")
        self.assertIsNone(message.stripped_text)
        self.assertIsNone(message.stripped_html)
        # SendGrid doesn't give a simple spam yes/no; check the score yourself:
        self.assertIsNone(message.spam_detected)
        self.assertEqual(message.spam_score, 1.7)

        # AnymailInboundMessage - other headers
        self.assertEqual(message["Message-ID"], "<CAEPk3R+4Zr@mail.example.org>")
        self.assertEqual(
            message.get_all("Received"),
            [
                "from mail.example.org by mx987654321.sendgrid.net ...",
                "by mail.example.org for <test@inbound.example.com> ...",
                "by 10.10.1.71 with HTTP; Wed, 11 Oct 2017 18:31:04 -0700 (PDT)",
            ],
        )

    def test_attachments(self):
        att1 = BytesIO("test attachment".encode("utf-8"))
        att1.name = "test.txt"
        image_content = sample_image_content()
        att2 = BytesIO(image_content)
        att2.name = "image.png"
        email_content = sample_email_content()
        att3 = BytesIO(email_content)
        att3.name = "\\share\\mail\\forwarded.msg"
        att3.content_type = 'message/rfc822; charset="us-ascii"'
        raw_event = {
            "headers": "",
            "attachments": "3",
            "attachment-info": json.dumps(
                {
                    "attachment3": {
                        "filename": "\\share\\mail\\forwarded.msg",
                        "charset": "US-ASCII",
                        "type": "message/rfc822",
                    },
                    "attachment2": {
                        "filename": "image.png",
                        "type": "image/png",
                        "content-id": "abc123",
                    },
                    "attachment1": {
                        "filename": "test.txt",
                        "charset": "UTF-8",
                        "type": "text/plain",
                    },
                }
            ),
            "content-ids": '{"abc123": "attachment2"}',
            "attachment1": att1,
            "attachment2": att2,  # inline
            "attachment3": att3,
        }

        response = self.client.post("/anymail/sendgrid/inbound/", data=raw_event)
        self.assertEqual(response.status_code, 200)
        kwargs = self.assert_handler_called_once_with(
            self.inbound_handler,
            sender=SendGridInboundWebhookView,
            event=ANY,
            esp_name="SendGrid",
        )
        event = kwargs["event"]
        message = event.message
        attachments = message.attachments  # AnymailInboundMessage convenience accessor
        self.assertEqual(len(attachments), 2)
        self.assertEqual(attachments[0].get_filename(), "test.txt")
        self.assertEqual(attachments[0].get_content_type(), "text/plain")
        self.assertEqual(attachments[0].get_content_text(), "test attachment")
        # Django strips path:
        self.assertEqual(attachments[1].get_filename(), "forwarded.msg")
        self.assertEqual(attachments[1].get_content_type(), "message/rfc822")
        self.assertEqualIgnoringHeaderFolding(
            attachments[1].get_content_bytes(), email_content
        )

        inlines = message.content_id_map
        self.assertEqual(len(inlines), 1)
        inline = inlines["abc123"]
        self.assertEqual(inline.get_filename(), "image.png")
        self.assertEqual(inline.get_content_type(), "image/png")
        self.assertEqual(inline.get_content_bytes(), image_content)

    def test_filtered_attachment_filenames(self):
        # Make sure the inbound webhook can deal with missing fields caused by
        # Django's multipart/form-data filename filtering. (The attachments are lost,
        # but shouldn't cause errors in the inbound webhook.)
        filenames = [
            "",
            "path\\",
            "path/" ".",
            "path\\.",
            "path/.",
            "..",
            "path\\..",
            "path/..",
        ]
        num_attachments = len(filenames)
        payload = {
            "attachment%d"
            % (i + 1): make_fileobj(
                "content", filename=filenames[i], content_type="text/pdf"
            )
            for i in range(num_attachments)
        }
        attachment_info = {
            key: {"filename": value.name, "type": "text/pdf"}
            for key, value in payload.items()
        }
        payload.update(
            {
                "headers": "",
                "attachments": str(num_attachments),
                "attachment-info": json.dumps(attachment_info),
            }
        )

        # Must do our own form-data encoding to properly test empty attachment
        # filenames. Must do our own multipart/form-data encoding for empty filenames:
        response = self.client.post(
            "/anymail/sendgrid/inbound/",
            data=encode_multipart("BoUnDaRy", payload),
            content_type="multipart/form-data; boundary=BoUnDaRy",
        )
        self.assertEqual(response.status_code, 200)
        kwargs = self.assert_handler_called_once_with(
            self.inbound_handler,
            sender=SendGridInboundWebhookView,
            event=ANY,
            esp_name="SendGrid",
        )

        # Different Django releases strip different filename patterns.
        # Just verify that at least some attachments got dropped (so the test is valid)
        # without causing an error in the inbound webhook:
        attachments = kwargs["event"].message.attachments
        self.assertLess(len(attachments), num_attachments)

    def test_inbound_mime(self):
        # SendGrid has an option to send the full, raw MIME message
        raw_event = {
            "email": dedent(
                """\
                From: A tester <test@example.org>
                Date: Thu, 12 Oct 2017 18:03:30 -0700
                Message-ID: <CAEPk3RKEx@mail.example.org>
                Subject: Raw MIME test
                To: test@inbound.example.com
                MIME-Version: 1.0
                Content-Type: multipart/alternative; boundary="94eb2c05e174adb140055b6339c5"

                --94eb2c05e174adb140055b6339c5
                Content-Type: text/plain; charset="UTF-8"
                Content-Transfer-Encoding: quoted-printable

                It's a body=E2=80=A6

                --94eb2c05e174adb140055b6339c5
                Content-Type: text/html; charset="UTF-8"
                Content-Transfer-Encoding: quoted-printable

                <div dir=3D"ltr">It's a body=E2=80=A6</div>

                --94eb2c05e174adb140055b6339c5--
                """  # NOQA: E501
            ),
            "from": "A tester <test@example.org>",
            "to": "test@inbound.example.com",
            "subject": "Raw MIME test",
            "charsets": '{"to":"UTF-8","subject":"UTF-8","from":"UTF-8"}',
            "envelope": '{"to":["test@inbound.example.com"],'
            '"from":"envelope-from@example.org"}',
            "sender_ip": "10.10.1.71",
            # yep, SendGrid uses not-exactly-json for this field:
            "dkim": "{@example.org : pass}",
            "SPF": "pass",
            "spam_score": "1.7",
            "spam_report": "Spam detection software, running on the system"
            ' "mx987654321.sendgrid.net", '
            "has identified this incoming email as possible spam...",
        }

        response = self.client.post("/anymail/sendgrid/inbound/", data=raw_event)
        self.assertEqual(response.status_code, 200)
        kwargs = self.assert_handler_called_once_with(
            self.inbound_handler,
            sender=SendGridInboundWebhookView,
            event=ANY,
            esp_name="SendGrid",
        )
        event = kwargs["event"]
        message = event.message
        self.assertEqual(message.envelope_sender, "envelope-from@example.org")
        self.assertEqual(message.envelope_recipient, "test@inbound.example.com")
        self.assertEqual(message.subject, "Raw MIME test")
        self.assertEqual(message.text, "It's a body\N{HORIZONTAL ELLIPSIS}\n")
        self.assertEqual(
            message.html,
            """<div dir="ltr">It's a body\N{HORIZONTAL ELLIPSIS}</div>\n""",
        )

    def test_inbound_charsets(self):
        # Captured (sanitized) from actual SendGrid inbound webhook payload 7/2020,
        # using a test message constructed with a variety of charsets:
        raw_post = dedent_bytes(
            b"""\
            --xYzZY
            Content-Disposition: form-data; name="headers"

            Date: Fri, 24 Jul 2020 16:43:46 UTC
            To: =?utf-8?q?R=C3=A9cipiendaire_pr=C3=A9cieux?= <inbound@sg.example.com>
            From: =?utf-8?q?Op=C3=A9rateur?= de test <sender@example.com>
            Subject: =?cp850?q?Como_usted_pidi=A2?=

            --xYzZY
            Content-Disposition: form-data; name="subject"

            Como usted pidi\xa2
            --xYzZY
            Content-Disposition: form-data; name="to"

            R\xc3\xa9cipiendaire pr\xc3\xa9cieux <inbound@sg.example.com>
            --xYzZY
            Content-Disposition: form-data; name="html"

            <p>\xbfEsto se ve como esperabas?</p>
            --xYzZY
            Content-Disposition: form-data; name="from"

            Op\xc3\xa9rateur de test <sender@example.com>
            --xYzZY
            Content-Disposition: form-data; name="text"

            Test the ESP\x92s inbound charset handling\x85
            --xYzZY
            Content-Disposition: form-data; name="charsets"

            {"to":"UTF-8","cc":"UTF-8","html":"iso-8859-1","subject":"cp850","from":"UTF-8","text":"windows-1252"}
            --xYzZY--
            """
        ).replace(b"\n", b"\r\n")

        response = self.client.post(
            "/anymail/sendgrid/inbound/",
            data=raw_post,
            content_type="multipart/form-data; boundary=xYzZY",
        )
        self.assertEqual(response.status_code, 200)
        kwargs = self.assert_handler_called_once_with(
            self.inbound_handler,
            sender=SendGridInboundWebhookView,
            event=ANY,
            esp_name="SendGrid",
        )
        event = kwargs["event"]
        message = event.message

        self.assertEqual(message.from_email.display_name, "Opérateur de test")
        self.assertEqual(message.from_email.addr_spec, "sender@example.com")
        self.assertEqual(len(message.to), 1)
        self.assertEqual(message.to[0].display_name, "Récipiendaire précieux")
        self.assertEqual(message.to[0].addr_spec, "inbound@sg.example.com")
        self.assertEqual(message.subject, "Como usted pidió")
        self.assertEqual(message.text, "Test the ESP’s inbound charset handling…")
        self.assertEqual(message.html, "<p>¿Esto se ve como esperabas?</p>")
