from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch

from asgiref.sync import async_to_sync
from django.test import TestCase, override_settings
from slixmpp.plugins.xep_0356.permissions import MessagePermission

from core.clients.xmpp import XMPPComponent
from core.models import Person, PersonIdentifier, User


@override_settings(
    XMPP_JID="component.example.test",
    XMPP_USER_DOMAIN="example.test",
)
class XMPPCarbonTests(TestCase):
    def _component(self):
        component = XMPPComponent(
            ur=MagicMock(),
            jid="component.example.test",
            secret="secret",
            server="localhost",
            port=5347,
        )
        component.log = MagicMock()
        return component

    def test_build_privileged_outbound_message_targets_contact(self):
        component = self._component()

        msg = component._build_privileged_outbound_message(
            user_jid="user@example.test",
            contact_jid="contact|signal@component.example.test",
            body_text="hello from signal",
            attachment_url="https://files.example.test/demo.png",
        )

        self.assertEqual("user@example.test", str(msg["from"]))
        self.assertEqual("contact|signal@component.example.test", str(msg["to"]))
        body = next(
            (child for child in msg.xml if str(child.tag).endswith("body")),
            None,
        )
        self.assertIsNotNone(body)
        self.assertEqual("hello from signal", body.text)
        oob = msg.xml.find(".//{jabber:x:oob}url")
        self.assertIsNotNone(oob)
        self.assertEqual("https://files.example.test/demo.png", oob.text)

    def test_send_sent_carbon_copy_requires_outgoing_privilege(self):
        component = self._component()
        plugin = SimpleNamespace(
            granted_privileges={"example.test": SimpleNamespace(message="none")},
            send_privileged_message=MagicMock(),
            _make_privileged_message=MagicMock(),
        )
        with (
            patch.object(component.plugin, "get", return_value=plugin),
            patch.object(component, "_user_xmpp_domain", return_value="other.example.test"),
        ):
            sent = async_to_sync(component.send_sent_carbon_copy)(
                user_jid="user@example.test",
                contact_jid="contact|signal@component.example.test",
                body_text="hello",
            )

        self.assertFalse(sent)
        plugin.send_privileged_message.assert_not_called()
        plugin._make_privileged_message.assert_not_called()

    def test_send_sent_carbon_copy_sends_privileged_message_when_allowed(self):
        component = self._component()
        plugin = SimpleNamespace(
            granted_privileges={
                "example.test": SimpleNamespace(message=MessagePermission.OUTGOING)
            },
            send_privileged_message=MagicMock(),
        )
        with patch.object(component.plugin, "get", return_value=plugin):
            sent = async_to_sync(component.send_sent_carbon_copy)(
                user_jid="user@example.test",
                contact_jid="contact|signal@component.example.test",
                body_text="hello",
            )

        self.assertTrue(sent)
        plugin.send_privileged_message.assert_called_once()
        sent_message = plugin.send_privileged_message.call_args.args[0]
        self.assertEqual("contact|signal@component.example.test", str(sent_message["to"]))
        self.assertEqual("user@example.test", str(sent_message["from"]))
        self.assertIsNotNone(
            next(
                (child for child in sent_message.xml if str(child.tag).endswith("body")),
                None,
            )
        )

    def test_send_sent_carbon_copy_uses_configured_domain_without_advertisement(self):
        component = self._component()
        wrapped = MagicMock()
        plugin = SimpleNamespace(
            granted_privileges={},
            send_privileged_message=MagicMock(),
            _make_privileged_message=MagicMock(return_value=wrapped),
        )
        with patch.object(component.plugin, "get", return_value=plugin):
            sent = async_to_sync(component.send_sent_carbon_copy)(
                user_jid="user@example.test",
                contact_jid="contact|signal@component.example.test",
                body_text="hello",
            )

        self.assertTrue(sent)
        plugin.send_privileged_message.assert_not_called()
        plugin._make_privileged_message.assert_called_once()
        wrapped.send.assert_called_once()

    def test_outgoing_relay_keeps_you_prefix_while_attempting_carbon_copy(self):
        component = self._component()
        user = User.objects.create_user(username="user", password="pw")
        person = Person.objects.create(user=user, name="Contact")
        identifier = PersonIdentifier.objects.create(
            user=user,
            person=person,
            service="signal",
            identifier="+15550000000",
        )
        call_order = []

        async def send_xmpp_message(*args, **kwargs):
            call_order.append("fallback")
            return "xmpp-message-id"

        async def send_sent_carbon_copy(*args, **kwargs):
            call_order.append("carbon")
            return True

        component.send_sent_carbon_copy = AsyncMock(side_effect=send_sent_carbon_copy)
        component.send_xmpp_message = AsyncMock(side_effect=send_xmpp_message)

        with (
            patch("core.clients.xmpp.transport.record_bridge_mapping"),
            patch("core.clients.xmpp.history.save_bridge_ref", new=AsyncMock()),
        ):
            async_to_sync(component.send_from_external)(
                user,
                identifier,
                "hello",
                True,
                attachments=[],
                source_ref={},
            )

        component.send_sent_carbon_copy.assert_awaited_once_with(
            user_jid="user@example.test",
            contact_jid="contact|signal@component.example.test",
            body_text="hello",
        )
        component.send_xmpp_message.assert_awaited_once_with(
            "user@example.test",
            "contact|signal@component.example.test",
            "YOU: hello",
            use_omemo_encryption=True,
        )
        self.assertEqual(["fallback", "carbon"], call_order)
