from types import SimpleNamespace
from unittest import TestCase
from unittest.mock import patch

import requests

from django_workflows.services import email as email_utils


class EmailUtilsTest(TestCase):
    def test_get_delivery_provider_uses_configured_provider_for_true_string_and_bool(self):
        with patch.object(email_utils, "EMAIL_DELIVERY_PROVIDER", "smtp"):
            self.assertEqual(email_utils._get_delivery_provider(enabled="true"), "smtp")
            self.assertEqual(email_utils._get_delivery_provider(enabled=True), "smtp")

    def test_get_delivery_provider_handles_false_bool_invalid_type_and_dev_mode(self):
        with patch.object(email_utils, "EMAIL_DELIVERY_PROVIDER", ""), patch.object(
            email_utils, "ENABLE_EMAIL_DELIVERY", False
        ), patch.object(email_utils, "DEVELOPMENT_MODE", True), patch.object(
            email_utils.logger, "error"
        ) as mock_error:
            self.assertEqual(email_utils._get_delivery_provider(enabled=False), "disabled")
            self.assertIsNone(email_utils._get_delivery_provider(enabled=123))
            self.assertEqual(email_utils._get_delivery_provider(), "smtp")

        mock_error.assert_called_once()

    def test_get_from_email_falls_back_to_default_from_email(self):
        with patch.object(email_utils, "EMAIL_FROM", ""), patch.object(
            email_utils, "DEFAULT_FROM_EMAIL", "fallback@example.com"
        ):
            self.assertEqual(email_utils._get_from_email(), "fallback@example.com")

    def test_build_reply_to_uses_configured_and_passed_addresses(self):
        with patch.object(email_utils, "EMAIL_REPLY_TO", "ops@example.com"), patch.object(
            email_utils.logger, "warning"
        ):
            result = email_utils._build_reply_to(["support@example.com", "not-an-email"])

        self.assertEqual(result, ["ops@example.com", "support@example.com"])

    def test_recipients_to_list_supports_none_iterable_and_csv(self):
        self.assertEqual(email_utils._recipients_to_list(None), [])
        self.assertEqual(
            email_utils._recipients_to_list([" one@example.com ", "", "two@example.com"]),
            ["one@example.com", "two@example.com"],
        )
        self.assertEqual(
            email_utils._recipients_to_list("a@example.com, b@example.com"),
            ["a@example.com", "b@example.com"],
        )

    def test_filter_valid_emails_skips_blank_candidates(self):
        result = email_utils._filter_valid_emails(
            ["", "   ", "valid@example.com"],
            field_name="to",
        )

        self.assertEqual(result, ["valid@example.com"])

    def test_send_email_returns_none_when_disabled(self):
        with patch.object(email_utils, "ENABLE_EMAIL_DELIVERY", False):
            response = email_utils.send_email(
                to="user@example.com",
                subject="Test",
                html="<p>Hello</p>",
            )

        self.assertIsNone(response)

    def test_send_email_disabled_does_not_log_html_body(self):
        with patch.object(email_utils, "ENABLE_EMAIL_DELIVERY", False), patch.object(
            email_utils.logger, "info"
        ) as mock_info:
            response = email_utils.send_email(
                to="user@example.com",
                subject="Test",
                html="<p>Secret code 123456</p>",
            )

        self.assertIsNone(response)
        logged_text = "\n".join(" ".join(map(str, call.args)) for call in mock_info.call_args_list)
        self.assertNotIn("Secret code", logged_text)

    def test_send_email_skips_when_no_valid_to_recipient(self):
        with patch.object(email_utils, "ENABLE_EMAIL_DELIVERY", True), patch.object(
            email_utils, "EMAIL_DOMAIN", "mg.example.com"
        ), patch.object(email_utils, "EMAIL_API_KEY", "key-123"), patch.object(
            email_utils, "EMAIL_FROM", "Company <no-reply@example.com>"
        ), patch("django_workflows.services.email.requests.post") as mock_post:
            response = email_utils.send_email(
                to="not-an-email",
                subject="Test",
                html="<p>Hello</p>",
            )

        self.assertIsNone(response)
        mock_post.assert_not_called()

    def test_send_email_skips_when_required_config_missing(self):
        with patch.object(email_utils, "ENABLE_EMAIL_DELIVERY", True), patch.object(
            email_utils, "EMAIL_DOMAIN", ""
        ), patch.object(email_utils, "EMAIL_API_KEY", ""), patch.object(
            email_utils, "EMAIL_FROM", ""
        ), patch("django_workflows.services.email.requests.post") as mock_post:
            response = email_utils.send_email(
                to="to@example.com",
                subject="Test",
                html="<p>Hello</p>",
            )

        self.assertIsNone(response)
        mock_post.assert_not_called()

    @patch("django_workflows.services.email.requests.post")
    def test_send_email_explicit_enabled_overrides_env_flag(self, mock_post):
        fake_response = SimpleNamespace(status_code=200, text="ok")
        mock_post.return_value = fake_response

        with patch.object(email_utils, "ENABLE_EMAIL_DELIVERY", False), patch.object(
            email_utils, "EMAIL_DOMAIN", "mg.example.com"
        ), patch.object(email_utils, "EMAIL_API_KEY", "key-123"), patch.object(
            email_utils, "EMAIL_FROM", "Company <no-reply@example.com>"
        ):
            response = email_utils.send_email(
                to="to@example.com",
                subject="Subject",
                html="<p>Hi</p>",
                enabled=True,
            )

        self.assertIs(response, fake_response)
        mock_post.assert_called_once()

    @patch("django_workflows.services.email.requests.post")
    def test_send_email_enabled_string_false_disables_send(self, mock_post):
        with patch.object(email_utils, "ENABLE_EMAIL_DELIVERY", True):
            response = email_utils.send_email(
                to="to@example.com",
                subject="Subject",
                html="<p>Hi</p>",
                enabled="false",
            )

        self.assertIsNone(response)
        mock_post.assert_not_called()

    @patch("django_workflows.services.email.requests.post")
    def test_send_email_enabled_string_true_enables_send(self, mock_post):
        fake_response = SimpleNamespace(status_code=200, text="ok")
        mock_post.return_value = fake_response

        with patch.object(email_utils, "ENABLE_EMAIL_DELIVERY", False), patch.object(
            email_utils, "EMAIL_DOMAIN", "mg.example.com"
        ), patch.object(email_utils, "EMAIL_API_KEY", "key-123"), patch.object(
            email_utils, "EMAIL_FROM", "Company <no-reply@example.com>"
        ):
            response = email_utils.send_email(
                to="to@example.com",
                subject="Subject",
                html="<p>Hi</p>",
                enabled="true",
            )

        self.assertIs(response, fake_response)
        mock_post.assert_called_once()

    @patch("django_workflows.services.email.requests.post")
    def test_send_email_invalid_enabled_override_returns_none(self, mock_post):
        with patch.object(email_utils, "ENABLE_EMAIL_DELIVERY", True), patch.object(
            email_utils.logger, "error"
        ) as mock_error:
            response = email_utils.send_email(
                to="to@example.com",
                subject="Subject",
                html="<p>Hi</p>",
                enabled="definitely-maybe",
            )

        self.assertIsNone(response)
        mock_post.assert_not_called()
        mock_error.assert_called_once()

    @patch("django_workflows.services.email.requests.post")
    def test_send_email_builds_payload_and_dedupes_bcc(self, mock_post):
        fake_response = SimpleNamespace(status_code=200, text="ok")
        mock_post.return_value = fake_response

        with patch.object(email_utils, "ENABLE_EMAIL_DELIVERY", True), patch.object(
            email_utils, "EMAIL_DOMAIN", "mg.example.com"
        ), patch.object(email_utils, "EMAIL_API_KEY", "key-123"), patch.object(
            email_utils, "EMAIL_FROM", "Company <no-reply@example.com>"
        ), patch.object(email_utils, "BCC_RECIPIENTS", ["audit@example.com"]), patch.object(
            email_utils, "EMAIL_REPLY_TO", "dispatch@example.com"
        ):
            response = email_utils.send_email(
                to="to1@example.com, to2@example.com",
                subject="Subject",
                html="<p>Hi</p>",
                attachments=[("report.pdf", b"PDF", "application/pdf")],
                cc=["cc1@example.com", "bad-cc"],
                bcc=["audit@example.com", "extra@example.com"],
                reply_to=["office@example.com"],
            )

        self.assertIs(response, fake_response)
        self.assertEqual(mock_post.call_count, 1)

        kwargs = mock_post.call_args.kwargs
        self.assertEqual(kwargs["auth"], ("api", "key-123"))
        self.assertEqual(kwargs["data"]["to"], "to1@example.com, to2@example.com")
        self.assertEqual(kwargs["data"]["cc"], ["cc1@example.com"])
        self.assertEqual(kwargs["data"]["bcc"], ["audit@example.com", "extra@example.com"])
        self.assertEqual(kwargs["data"]["h:Reply-To"], "dispatch@example.com, office@example.com")
        self.assertEqual(kwargs["timeout"], 10)
        self.assertEqual(kwargs["files"], [("attachment", ("report.pdf", b"PDF", "application/pdf"))])

    @patch("django_workflows.services.email.EmailMultiAlternatives")
    def test_send_email_smtp_sets_reply_to(self, mock_message_class):
        mock_message = mock_message_class.return_value
        mock_message.send.return_value = 1

        with patch.object(email_utils, "EMAIL_DELIVERY_PROVIDER", "smtp"), patch.object(
            email_utils, "EMAIL_FROM", "Company <no-reply@example.com>"
        ), patch.object(email_utils, "EMAIL_REPLY_TO", "dispatch@example.com"):
            response = email_utils.send_email(
                to="to@example.com",
                subject="Subject",
                html="<p>Hi</p>",
                reply_to="office@example.com",
            )

        self.assertIsNotNone(response)
        kwargs = mock_message_class.call_args.kwargs
        self.assertEqual(kwargs["reply_to"], ["dispatch@example.com", "office@example.com"])

    @patch("django_workflows.services.email.EmailMultiAlternatives")
    def test_send_email_smtp_attaches_files(self, mock_message_class):
        mock_message = mock_message_class.return_value
        mock_message.send.return_value = 1

        with patch.object(email_utils, "EMAIL_DELIVERY_PROVIDER", "smtp"), patch.object(
            email_utils, "EMAIL_FROM", "Company <no-reply@example.com>"
        ):
            response = email_utils.send_email(
                to="to@example.com",
                subject="Subject",
                html="<p>Hi</p>",
                attachments=[("report.pdf", b"PDF", "application/pdf")],
            )

        self.assertIsNotNone(response)
        mock_message.attach.assert_called_once_with("report.pdf", b"PDF", "application/pdf")

    @patch("django_workflows.services.email.EmailMultiAlternatives")
    def test_send_email_smtp_returns_none_when_no_valid_recipients(self, mock_message_class):
        with patch.object(email_utils, "EMAIL_DELIVERY_PROVIDER", "smtp"), patch.object(
            email_utils.logger, "warning"
        ) as mock_warning:
            response = email_utils.send_email(
                to="not-an-email",
                subject="Subject",
                html="<p>Hi</p>",
            )

        self.assertIsNone(response)
        mock_message_class.assert_not_called()
        mock_warning.assert_any_call("SMTP email not sent: no valid 'to' recipients")

    @patch("django_workflows.services.email.EmailMultiAlternatives")
    def test_send_email_smtp_returns_none_on_send_exception(self, mock_message_class):
        mock_message = mock_message_class.return_value
        mock_message.send.side_effect = RuntimeError("boom")

        with patch.object(email_utils, "EMAIL_DELIVERY_PROVIDER", "smtp"), patch.object(
            email_utils, "EMAIL_FROM", "Company <no-reply@example.com>"
        ), patch.object(email_utils.logger, "exception") as mock_exception:
            response = email_utils.send_email(
                to="to@example.com",
                subject="Subject",
                html="<p>Hi</p>",
            )

        self.assertIsNone(response)
        mock_exception.assert_called_once_with("SMTP email send failed")

    @patch("django_workflows.services.email.requests.post")
    def test_send_email_info_log_excludes_response_text(self, mock_post):
        fake_response = SimpleNamespace(status_code=200, text="sensitive-response-body")
        mock_post.return_value = fake_response

        with patch.object(email_utils, "ENABLE_EMAIL_DELIVERY", True), patch.object(
            email_utils, "EMAIL_DOMAIN", "mg.example.com"
        ), patch.object(email_utils, "EMAIL_API_KEY", "key-123"), patch.object(
            email_utils, "EMAIL_FROM", "Company <no-reply@example.com>"
        ), patch.object(email_utils.logger, "info") as mock_info:
            response = email_utils.send_email(
                to="to@example.com",
                subject="Subject",
                html="<p>Hi</p>",
            )

        self.assertIs(response, fake_response)
        info_text = "\n".join(" ".join(map(str, call.args)) for call in mock_info.call_args_list)
        self.assertNotIn("sensitive-response-body", info_text)

    @patch("django_workflows.services.email.requests.post")
    def test_send_email_debug_response_text_requires_opt_in(self, mock_post):
        fake_response = SimpleNamespace(status_code=200, text="sensitive-response-body")
        mock_post.return_value = fake_response

        with patch.object(email_utils, "ENABLE_EMAIL_DELIVERY", True), patch.object(
            email_utils, "LOG_EMAIL_RESPONSE_TEXT", False
        ), patch.object(email_utils, "EMAIL_DOMAIN", "mg.example.com"), patch.object(
            email_utils, "EMAIL_API_KEY", "key-123"
        ), patch.object(email_utils, "EMAIL_FROM", "Company <no-reply@example.com>"), patch.object(
            email_utils.logger, "isEnabledFor", return_value=True
        ), patch.object(email_utils.logger, "debug") as mock_debug:
            response = email_utils.send_email(
                to="to@example.com",
                subject="Subject",
                html="<p>Hi</p>",
            )

        self.assertIs(response, fake_response)
        mock_debug.assert_not_called()

    @patch("django_workflows.services.email.requests.post")
    def test_send_email_debug_response_text_logs_when_opted_in(self, mock_post):
        fake_response = SimpleNamespace(status_code=200, text="sensitive-response-body")
        mock_post.return_value = fake_response

        with patch.object(email_utils, "ENABLE_EMAIL_DELIVERY", True), patch.object(
            email_utils, "LOG_EMAIL_RESPONSE_TEXT", True
        ), patch.object(email_utils, "EMAIL_DOMAIN", "mg.example.com"), patch.object(
            email_utils, "EMAIL_API_KEY", "key-123"
        ), patch.object(email_utils, "EMAIL_FROM", "Company <no-reply@example.com>"), patch.object(
            email_utils.logger, "isEnabledFor", return_value=True
        ), patch.object(email_utils.logger, "debug") as mock_debug:
            response = email_utils.send_email(
                to="to@example.com",
                subject="Subject",
                html="<p>Hi</p>",
            )

        self.assertIs(response, fake_response)
        mock_debug.assert_called_once()

    @patch(
        "django_workflows.services.email.requests.post",
        side_effect=requests.RequestException("boom"),
    )
    def test_send_email_returns_none_on_request_exception(self, _mock_post):
        with patch.object(email_utils, "ENABLE_EMAIL_DELIVERY", True), patch.object(
            email_utils, "EMAIL_DOMAIN", "mg.example.com"
        ), patch.object(email_utils, "EMAIL_API_KEY", "key-123"), patch.object(
            email_utils, "EMAIL_FROM", "Company <no-reply@example.com>"
        ), patch.object(email_utils.logger, "exception") as mock_exception:
            response = email_utils.send_email(
                to="to@example.com",
                subject="Subject",
                html="<p>Hi</p>",
            )

        self.assertIsNone(response)
        mock_exception.assert_called_once()