"""Production adapter contracts; all transports are mocked or in memory."""
import smtplib
from unittest.mock import patch
from django.core import mail
from django.core.exceptions import ImproperlyConfigured
from django.test import SimpleTestCase, override_settings
from . import providers as p
from .checks import communication_configuration
from .test_runner import NonDeliveringRunner


PRODUCTION = dict(COMMUNICATIONS_PRODUCTION=True, COMMUNICATIONS_ALLOW_EXTERNAL=True,
    COMMUNICATION_PROVIDERS={"EMAIL": "apps.communications.providers.DjangoEmailProvider"},
    EMAIL_BACKEND=p.SMTP_BACKEND, EMAIL_HOST="smtp.example.invalid", EMAIL_PORT=587,
    EMAIL_HOST_USER="synthetic-user", EMAIL_HOST_PASSWORD="SECRET-SYNTHETIC",
    EMAIL_USE_TLS=True, EMAIL_USE_SSL=False, EMAIL_TIMEOUT=10,
    DEFAULT_FROM_EMAIL="service@example.invalid")


class FutureSms(p.SmsProvider):
    """Contract fixture, not a vendor or a network transport."""
    identifier = "contract-fixture"
    production_ready = True

    def send(self, **kwargs):
        return p.DeliveryResult("ACCEPTED", "opaque-reference")


class ProviderTests(SimpleTestCase):
    def send(self):
        return p.DjangoEmailProvider().send(destination="synthetic@example.test", subject="Test",
            body="Private synthetic content", idempotency_key="stable-key")

    def test_default_is_non_delivering(self):
        self.assertEqual(type(p.get_provider("SMS")), p.Provider)
        self.assertEqual(self.send().outcome, "ACCEPTED")
        self.assertEqual(len(mail.outbox), 1)

    @override_settings(COMMUNICATION_PROVIDERS={"SMS": "apps.communications.providers.FakeSmsProvider"})
    def test_development_fake_sms_is_deterministic(self):
        adapter = p.get_provider("SMS")
        args = dict(destination="+8801700000000", subject="", body="Test", idempotency_key="same")
        self.assertEqual(adapter.send(**args), adapter.send(**args))
        self.assertTrue(adapter.send(**args).reference.startswith("fake-"))

    @override_settings(**PRODUCTION)
    def test_missing_production_sms_fails_closed(self):
        with self.assertRaises(ImproperlyConfigured):
            p.get_provider("SMS")

    @override_settings(**PRODUCTION)
    def test_fake_or_base_sms_rejected_in_production(self):
        for name in ("FakeSmsProvider", "SmsProvider", "Provider"):
            with self.settings(COMMUNICATION_PROVIDERS={"SMS": "apps.communications.providers."+name}):
                self.assertTrue(communication_configuration(None))

    @override_settings(**PRODUCTION)
    def test_future_adapter_contract_and_reference(self):
        with self.settings(COMMUNICATION_PROVIDERS={"SMS": "apps.communications.test_providers.FutureSms"}):
            result = p.get_provider("SMS").send(destination="+8801700000000", subject="", body="Test", idempotency_key="key")
            self.assertEqual(result, p.DeliveryResult("ACCEPTED", "opaque-reference"))
            self.assertEqual(communication_configuration(None), [])

    def test_external_sms_refused_in_development(self):
        with self.settings(COMMUNICATION_PROVIDERS={"SMS": "apps.communications.test_providers.FutureSms"}):
            with self.assertRaises(ImproperlyConfigured):
                p.get_provider("SMS")

    @override_settings(**PRODUCTION)
    def test_smtp_acceptance_without_network(self):
        with patch.object(p, "EmailMessage") as message:
            message.return_value.send.return_value = 1
            self.assertEqual(self.send().outcome, "ACCEPTED")
            message.return_value.send.assert_called_once_with(fail_silently=False)

    @override_settings(**PRODUCTION)
    def test_configuration_failures_never_instantiate_transport(self):
        for change in (dict(EMAIL_HOST=""), dict(EMAIL_PORT="bad"), dict(EMAIL_TIMEOUT=None),
                dict(EMAIL_TIMEOUT=float("inf")), dict(EMAIL_USE_SSL=True), dict(EMAIL_USE_TLS=False),
                dict(EMAIL_HOST_PASSWORD=""), dict(DEFAULT_FROM_EMAIL="bad"),
                dict(COMMUNICATIONS_ALLOW_EXTERNAL=False), dict(EMAIL_BACKEND=p.MEMORY_BACKEND)):
            with self.subTest(change=list(change)), self.settings(**change), patch.object(p, "EmailMessage") as message:
                self.assertEqual(self.send().outcome, "REJECTED")
                self.assertTrue(communication_configuration(None))
                message.assert_not_called()

    @override_settings(**PRODUCTION)
    def test_definitive_smtp_rejections(self):
        for error in (smtplib.SMTPAuthenticationError(535, b"SECRET"),
                smtplib.SMTPRecipientsRefused({"secret@example.test": (550, "SECRET")}),
                smtplib.SMTPSenderRefused(550, "SECRET", "secret@example.test")):
            with patch.object(p, "EmailMessage") as message:
                message.return_value.send.side_effect = error
                self.assertEqual(self.send().outcome, "REJECTED")

    @override_settings(**PRODUCTION)
    def test_uncertain_failures_and_malformed_backend_results(self):
        for error in (TimeoutError("SECRET"), ConnectionError("SECRET"), smtplib.SMTPServerDisconnected("SECRET"),
                RuntimeError("SECRET"), smtplib.SMTPDataError(450, b"SECRET")):
            with patch.object(p, "EmailMessage") as message:
                message.return_value.send.side_effect = error
                self.assertEqual(self.send().outcome, "UNKNOWN")
        for value in (0, None, 2, True, "accepted"):
            with patch.object(p, "EmailMessage") as message:
                message.return_value.send.return_value = value
                self.assertEqual(self.send().outcome, "UNKNOWN")

    def test_header_or_destination_rejection(self):
        with patch.object(p, "EmailMessage") as message:
            for destination, subject in (("bad", "Test"), ("valid@example.test", "Test\nBcc: bad")):
                self.assertEqual(p.DjangoEmailProvider().send(destination=destination, subject=subject, body="Test", idempotency_key="x").outcome, "REJECTED")
            message.assert_not_called()

    @override_settings(**PRODUCTION)
    def test_structured_logs_never_include_payload_or_exception(self):
        with patch.object(p, "EmailMessage") as message, self.assertLogs(p.logger, level="INFO") as logs:
            message.return_value.send.side_effect = RuntimeError("SECRET-SYNTHETIC")
            self.send()
        self.assertEqual(logs.records[0].delivery_outcome, "UNKNOWN")
        self.assertNotIn("SECRET", str(logs.records[0].__dict__))
        self.assertNotIn("Private synthetic", str(logs.records[0].__dict__))

    @override_settings(**PRODUCTION)
    def test_runner_overrides_live_settings_before_tests(self):
        from django.conf import settings
        runner = NonDeliveringRunner()
        with patch("django.test.runner.DiscoverRunner.setup_test_environment"), patch("django.test.runner.DiscoverRunner.teardown_test_environment"):
            runner.setup_test_environment()
            try:
                self.assertFalse(settings.COMMUNICATIONS_ALLOW_EXTERNAL)
                self.assertFalse(settings.COMMUNICATIONS_PRODUCTION)
                self.assertEqual(settings.EMAIL_BACKEND, p.MEMORY_BACKEND)
                self.assertEqual(settings.COMMUNICATION_PROVIDERS, {})
            finally:
                runner.teardown_test_environment()
        self.assertTrue(settings.COMMUNICATIONS_ALLOW_EXTERNAL)

    def test_wrong_channel_adapter_rejected(self):
        with self.settings(COMMUNICATION_PROVIDERS={"SMS": "apps.communications.providers.DjangoEmailProvider"}):
            self.assertTrue(communication_configuration(None))

    def test_logging_outage_does_not_change_acceptance(self):
        with patch.object(p.logger, "info", side_effect=RuntimeError("logging unavailable")):
            self.assertEqual(self.send().outcome, "ACCEPTED")
