from __future__ import annotations

import unittest
from unittest.mock import Mock, patch
from datetime import datetime
from types import SimpleNamespace

import apps.messaging_service as app
from apps.messaging_service.drivers.android_local import AndroidLocalDriver
from apps.messaging_service.drivers.base import ProviderConfigurationError
from apps.messaging_service.drivers.registry import DriverRegistry
from apps.messaging_service.drivers.smtp import SMTPDriver
from apps.messaging_service.service import MessagingError, mask_recipient, normalize_email, normalize_phone
from apps.messaging_service.service import MessagingService
from apps.messaging_service.templates import BUILTIN_TEMPLATES, TemplateError, render_template


class ContractChecks(unittest.TestCase):
    def test_manifest_is_valid_and_send_requires_idempotency(self):
        self.assertEqual([], app.APP_MANIFEST.validate())
        contract = app.APP_MANIFEST.actions["send_template_v1"]
        self.assertTrue(contract.side_effects)
        self.assertTrue(contract.idempotency_key_required)
        self.assertFalse(contract.idempotent)

    def test_health_is_local(self):
        payload = app.handle_request({"request_id": "r1"}, "health_v1")
        self.assertEqual("ok", payload["status"])
        self.assertFalse(payload["data"]["provider_checked"])

    def test_wordpress_administrator_is_an_admin_principal(self):
        identity = SimpleNamespace(roles=("administrator",))
        self.assertTrue(app._is_admin({"identity": identity}))


class TemplateChecks(unittest.TestCase):
    def test_strict_render_and_html_escape(self):
        definition = BUILTIN_TEMPLATES[("security_login_code_email", "email")]
        rendered = render_template(definition, {"code": "<428&>", "expiry_minutes": 5})
        self.assertIn("<428&>", rendered["text"])
        self.assertIn("&lt;428&amp;&gt;", rendered["html"])

    def test_missing_and_unexpected_variables_are_rejected(self):
        definition = BUILTIN_TEMPLATES[("security_login_code_sms", "sms")]
        with self.assertRaises(TemplateError):
            render_template(definition, {"code": "1"})
        with self.assertRaises(TemplateError):
            render_template(definition, {"code": "1", "expiry_minutes": 5, "body": "free text"})


class RecipientChecks(unittest.TestCase):
    def test_australian_phone_normalizes_to_e164(self):
        self.assertEqual("+61404493988", normalize_phone("0404 493 988"))
        self.assertEqual("+6140******88", mask_recipient("+61404493988", "sms"))

    def test_invalid_recipients_are_rejected(self):
        with self.assertRaises(MessagingError): normalize_phone("localhost")
        with self.assertRaises(MessagingError): normalize_email("not-an-email")
        self.assertEqual("c***@example.com", mask_recipient("carlos@example.com", "email"))


class RegistryChecks(unittest.TestCase):
    def test_registry_has_single_strategy_per_name(self):
        registry = DriverRegistry()
        driver = Mock()
        registry.register("test", driver)
        self.assertIs(driver, registry.get("test"))
        with self.assertRaises(ValueError): registry.register("test", driver)
        with self.assertRaises(LookupError): registry.get("missing")


class AndroidDriverChecks(unittest.TestCase):
    def setUp(self):
        self.session = Mock()
        self.driver = AndroidLocalDriver(session=self.session)
        self.config = {"base_url": "http://10.8.0.2:8082", "endpoint": "/", "connect_timeout": 2, "read_timeout": 4}

    def test_request_uses_direct_authorization_and_exact_json(self):
        self.session.post.return_value = Mock(status_code=200)
        result = self.driver.send(self.config, "top-secret", {"recipient": "+61404493988", "text": "test"})
        self.assertTrue(result.accepted)
        kwargs = self.session.post.call_args.kwargs
        self.assertEqual("top-secret", kwargs["headers"]["Authorization"])
        self.assertNotIn("Bearer", kwargs["headers"]["Authorization"])
        self.assertEqual({"to": "+61404493988", "message": "test"}, kwargs["json"])
        self.assertFalse(kwargs["allow_redirects"])
        self.assertEqual((2.0, 4.0), kwargs["timeout"])

    def test_ssrf_redirect_and_credentials_are_blocked(self):
        for url in ("http://127.0.0.1:8082", "http://169.254.169.254", "http://user:pass@10.8.0.2:8082"):
            with self.assertRaises(ProviderConfigurationError):
                self.driver.validate_configuration({"base_url": url, "endpoint": "/"})
        self.session.post.return_value = Mock(status_code=302)
        result = self.driver.send(self.config, "secret", {"recipient": "+61404493988", "text": "test"})
        self.assertFalse(result.accepted)
        self.assertFalse(result.retryable)

    def test_auth_and_server_errors_are_normalized(self):
        for status, category, retryable in ((401, "credential_error", False), (403, "credential_error", False), (500, "provider_unavailable", True)):
            self.session.post.return_value = Mock(status_code=status)
            result = self.driver.send(self.config, "secret", {"recipient": "+61404493988", "text": "test"})
            self.assertEqual(category, result.error_category)
            self.assertEqual(retryable, result.retryable)


class SMTPDriverChecks(unittest.TestCase):
    CONFIG = {"host": "smtp.example.com", "port": 587, "encryption": "starttls", "username": "mailer",
              "from_email": "noreply@example.com", "from_name": "USG", "timeout": 5}

    @patch("apps.messaging_service.drivers.smtp.smtplib.SMTP")
    def test_starttls_login_and_message(self, smtp):
        client = smtp.return_value
        client.__enter__.return_value = client
        client.send_message.return_value = {}
        result = SMTPDriver().send(self.CONFIG, "password", {"recipient": "c@example.com", "subject": "Code", "text": "body", "html": None})
        self.assertTrue(result.accepted)
        client.starttls.assert_called_once()
        client.login.assert_called_once_with("mailer", "password")
        sent = client.send_message.call_args.args[0]
        self.assertEqual("Code", sent["Subject"])
        self.assertNotIn("password", sent.as_string())

    def test_invalid_encryption_is_rejected(self):
        with self.assertRaises(ProviderConfigurationError):
            SMTPDriver().validate_configuration({**self.CONFIG, "encryption": "magic"})


class WordPressPackChecks(unittest.TestCase):
    def test_pack_uses_bridge_and_contains_no_gateway_details(self):
        from pathlib import Path
        php = (Path(app.__file__).parent / "wordpress-pack/messaging-service-wp/messaging-service-wp.php").read_text()
        self.assertIn("wp_flask_bridge_invoke", php)
        self.assertNotIn("Authorization", php)
        self.assertIn('type="password" name="gateway_token"', php)
        self.assertNotIn("update_option('ams_messaging_gateway_token'", php)


class DeliveryPolicyChecks(unittest.TestCase):
    class Repo:
        def __init__(self, allowed=True):
            self.allowed = allowed
            self.saved = None
            self.attempts = 0

        def permission(self, tenant, caller):
            if not self.allowed: return None
            return SimpleNamespace(channels_json=["sms"], templates_json=["security_login_code_sms"],
                                   purposes_json=["qr_login"], allow_fallback=False)

        def template(self, tenant, key, channel): return BUILTIN_TEMPLATES.get((key, channel))
        def existing(self, tenant, caller, operation, key): return self.saved
        def count_since(self, **kwargs): return 0
        def providers(self, tenant, channel):
            return [SimpleNamespace(provider_id="p1", driver="fake", config_json={}, secret_ref=None,
                                    max_retries=2, daily_limit=None, per_minute_limit=None)]

        def create_delivery(self, **values):
            self.saved = SimpleNamespace(delivery_id="d1", provider_id=None, status="pending", created_at=datetime.utcnow(), **values)
            return self.saved, True

        def complete_attempt(self, delivery_id, provider_id, attempt, result):
            self.attempts += 1
            self.saved.provider_id, self.saved.status = provider_id, result.status
            return self.saved

    def payload(self):
        return {"channel": "sms", "recipient": {"phone": "0404493988"},
                "template_key": "security_login_code_sms", "template_data": {"code": "428193", "expiry_minutes": 5},
                "purpose": "qr_login", "idempotency_key": "login-attempt-001", "metadata": {}}

    @patch("apps.messaging_service.service.AuditLogger")
    @patch("apps.messaging_service.service.consume")
    def test_idempotent_replay_does_not_call_provider_twice(self, consume, audit):
        from apps.messaging_service.drivers import default_registry
        repo = self.Repo()
        service = MessagingService(repository=repo, registry=default_registry(include_fake=True), secrets=Mock())
        context = {"tenant": {"tenant_id": "t1"}, "requesting_app": "qr_service_app", "request_id": "r1"}
        first = service.send_template(context, self.payload())
        second = service.send_template(context, self.payload())
        self.assertEqual(first["delivery_id"], second["delivery_id"])
        self.assertTrue(second["idempotent_replay"])
        self.assertEqual(1, repo.attempts)
        consume.assert_called_once()

    def test_untrusted_body_cannot_grant_caller_permission(self):
        service = MessagingService(repository=self.Repo(allowed=False), registry=Mock(), secrets=Mock())
        payload = self.payload(); payload["metadata"] = {"requested_by_app": "allowed_app"}
        with self.assertRaises(MessagingError) as caught:
            service.send_template({"tenant": {"tenant_id": "t1"}, "requesting_app": "blocked_app"}, payload)
        self.assertEqual("permission_denied", caught.exception.code)


if __name__ == "__main__":
    unittest.main()
