"""
Cross-platform messaging tests: replies, reactions, and messages across
Signal, WhatsApp, and XMPP.

Signal coverage is in test_signal_reply_send.py.  This file fills the gaps
for WhatsApp and XMPP, and verifies the shared reply_sync infrastructure
works correctly for both services.
"""

from __future__ import annotations

import xml.etree.ElementTree as ET
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch

from asgiref.sync import async_to_sync
from django.test import SimpleTestCase, TestCase

from core.clients import transport
from core.clients.xmpp import (
    _extract_xmpp_reaction,
    _extract_xmpp_reply_target_id,
    _parse_greentext_reaction,
)
from core.messaging import history, reply_sync
from core.models import ChatSession, Message, Person, PersonIdentifier, User
from core.presence.inference import now_ms

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _fake_stanza(xml_text: str) -> SimpleNamespace:
    """Minimal stanza-like object with an .xml attribute."""
    return SimpleNamespace(xml=ET.fromstring(xml_text))


# ---------------------------------------------------------------------------
# WhatsApp — reply extraction (pure, no DB)
# ---------------------------------------------------------------------------


class WhatsAppReplyExtractionTests(SimpleTestCase):
    def test_extract_reply_ref_from_contextinfo_stanza_id(self):
        payload = {
            "contextInfo": {
                "stanzaId": "wa-anchor-001",
                "participant": "447700900001@s.whatsapp.net",
            }
        }
        ref = reply_sync.extract_reply_ref("whatsapp", payload)
        self.assertEqual("wa-anchor-001", ref.get("reply_source_message_id"))
        self.assertEqual("whatsapp", ref.get("reply_source_service"))
        self.assertEqual("447700900001@s.whatsapp.net", ref.get("reply_source_chat_id"))

    def test_extract_reply_ref_from_extended_text_message(self):
        payload = {
            "extendedTextMessage": {
                "text": "quoting you",
                "contextInfo": {
                    "stanzaId": "wa-anchor-002",
                    "participant": "447700900002@s.whatsapp.net",
                },
            }
        }
        ref = reply_sync.extract_reply_ref("whatsapp", payload)
        self.assertEqual("wa-anchor-002", ref.get("reply_source_message_id"))

    def test_extract_reply_ref_returns_empty_when_no_context(self):
        ref = reply_sync.extract_reply_ref("whatsapp", {"conversation": "plain text"})
        self.assertEqual({}, ref)

    def test_extract_reply_ref_from_image_message_contextinfo(self):
        payload = {
            "imageMessage": {
                "caption": "look at this",
                "contextInfo": {
                    "stanzaId": "wa-anchor-003",
                    "participant": "447700900003@s.whatsapp.net",
                },
            }
        }
        ref = reply_sync.extract_reply_ref("whatsapp", payload)
        self.assertEqual("wa-anchor-003", ref.get("reply_source_message_id"))


# ---------------------------------------------------------------------------
# WhatsApp — reply resolution (requires DB)
# ---------------------------------------------------------------------------


class WhatsAppReplyResolutionTests(TestCase):
    def setUp(self):
        self.user = User.objects.create_user(
            "wa-resolve-user", "wa-resolve@example.com", "x"
        )
        self.person = Person.objects.create(user=self.user, name="WA Resolve")
        self.identifier = PersonIdentifier.objects.create(
            user=self.user,
            person=self.person,
            service="whatsapp",
            identifier="447700900001@s.whatsapp.net",
        )
        self.session = ChatSession.objects.create(
            user=self.user, identifier=self.identifier
        )
        self.anchor = Message.objects.create(
            user=self.user,
            session=self.session,
            ts=now_ms(),
            text="anchor message",
            source_service="whatsapp",
            source_message_id="wa-anchor-001",
            source_chat_id="447700900001@s.whatsapp.net",
            sender_uuid="447700900001@s.whatsapp.net",
        )

    def test_resolve_reply_target_by_source_message_id(self):
        ref = {
            "reply_source_message_id": "wa-anchor-001",
            "reply_source_service": "whatsapp",
            "reply_source_chat_id": "447700900001@s.whatsapp.net",
        }
        target = async_to_sync(reply_sync.resolve_reply_target)(
            self.user, self.session, ref
        )
        self.assertIsNotNone(target)
        self.assertEqual(self.anchor.id, target.id)

    def test_resolve_returns_none_for_unknown_id(self):
        ref = {
            "reply_source_message_id": "wa-nonexistent-999",
            "reply_source_service": "whatsapp",
            "reply_source_chat_id": "447700900001@s.whatsapp.net",
        }
        target = async_to_sync(reply_sync.resolve_reply_target)(
            self.user, self.session, ref
        )
        self.assertIsNone(target)

    def test_reaction_applied_to_whatsapp_anchor(self):
        async_to_sync(history.apply_reaction)(
            self.user,
            self.identifier,
            target_message_id="wa-anchor-001",
            target_ts=int(self.anchor.ts),
            emoji="👍",
            source_service="whatsapp",
            actor="447700900001@s.whatsapp.net",
            remove=False,
            payload={"event": "reaction"},
        )
        self.anchor.refresh_from_db()
        reactions = list((self.anchor.receipt_payload or {}).get("reactions") or [])
        self.assertEqual(1, len(reactions))
        self.assertEqual("👍", reactions[0].get("emoji"))

    def test_reaction_removal_clears_flag(self):
        async_to_sync(history.apply_reaction)(
            self.user,
            self.identifier,
            target_message_id="wa-anchor-001",
            target_ts=int(self.anchor.ts),
            emoji="👍",
            source_service="whatsapp",
            actor="447700900001@s.whatsapp.net",
            remove=False,
            payload={},
        )
        async_to_sync(history.apply_reaction)(
            self.user,
            self.identifier,
            target_message_id="wa-anchor-001",
            target_ts=int(self.anchor.ts),
            emoji="👍",
            source_service="whatsapp",
            actor="447700900001@s.whatsapp.net",
            remove=True,
            payload={},
        )
        self.anchor.refresh_from_db()
        reactions = list((self.anchor.receipt_payload or {}).get("reactions") or [])
        removed = [
            r for r in reactions if r.get("emoji") == "👍" and not r.get("removed")
        ]
        self.assertEqual(0, len(removed))


# ---------------------------------------------------------------------------
# WhatsApp — outbound reply metadata
# ---------------------------------------------------------------------------


class WhatsAppOutboundReplyTests(TestCase):
    def test_transport_passes_reply_metadata_to_whatsapp_api(self):
        mock_client = MagicMock()
        mock_client.send_message_raw = AsyncMock(return_value="wa-sent-001")
        with patch(
            "core.clients.transport.get_runtime_client",
            return_value=mock_client,
        ), patch(
            "core.clients.transport.prepare_outbound_attachments",
            new=AsyncMock(return_value=[]),
        ), patch(
            "core.clients.transport._capability_checks_enabled",
            return_value=False,
        ):
            result = async_to_sync(transport.send_message_raw)(
                "whatsapp",
                "447700900001@s.whatsapp.net",
                text="reply text",
                attachments=[],
                metadata={
                    "quote_id": "wa-anchor-001",
                    "quote_author": "447700900001@s.whatsapp.net",
                    "quote_text": "anchor message",
                },
            )
        self.assertEqual("wa-sent-001", result)
        mock_client.send_message_raw.assert_awaited_once()
        _, call_kwargs = mock_client.send_message_raw.call_args
        meta = call_kwargs.get("metadata") or {}
        self.assertEqual("wa-anchor-001", meta.get("quote_id"))


# ---------------------------------------------------------------------------
# XMPP — reaction extraction (pure, no DB)
# ---------------------------------------------------------------------------


class XMPPReactionExtractionTests(SimpleTestCase):
    def test_extract_xep_0444_reaction(self):
        stanza = _fake_stanza(
            "<message>"
            "<reactions xmlns='urn:xmpp:reactions:0' id='xmpp-anchor-001'>"
            "<reaction>👍</reaction>"
            "</reactions>"
            "</message>"
        )
        result = _extract_xmpp_reaction(stanza)
        self.assertIsNotNone(result)
        self.assertEqual("xmpp-anchor-001", result.get("target_id"))
        self.assertEqual("👍", result.get("emoji"))
        self.assertFalse(result.get("remove"))

    def test_extract_xep_0444_reaction_removal(self):
        stanza = _fake_stanza(
            "<message>"
            "<reactions xmlns='urn:xmpp:reactions:0' id='xmpp-anchor-002'>"
            "</reactions>"
            "</message>"
        )
        result = _extract_xmpp_reaction(stanza)
        self.assertIsNotNone(result)
        self.assertEqual("xmpp-anchor-002", result.get("target_id"))
        self.assertTrue(result.get("remove"))

    def test_extract_returns_none_for_plain_message(self):
        stanza = _fake_stanza("<message><body>hello</body></message>")
        self.assertIsNone(_extract_xmpp_reaction(stanza))

    def test_parse_greentext_reaction_valid(self):
        result = _parse_greentext_reaction(">anchor message\n😊")
        self.assertIsNotNone(result)
        self.assertEqual("anchor message", result.get("quoted_text"))
        self.assertEqual("😊", result.get("emoji"))

    def test_parse_greentext_reaction_rejects_non_emoji_second_line(self):
        result = _parse_greentext_reaction(">anchor message\nnot an emoji")
        self.assertIsNone(result)

    def test_parse_greentext_reaction_rejects_single_line(self):
        result = _parse_greentext_reaction(">anchor message")
        self.assertIsNone(result)

    def test_parse_greentext_reaction_rejects_no_quote_prefix(self):
        result = _parse_greentext_reaction("anchor message\n😊")
        self.assertIsNone(result)


# ---------------------------------------------------------------------------
# XMPP — reply extraction (pure, no DB)
# ---------------------------------------------------------------------------


class XMPPReplyExtractionTests(SimpleTestCase):
    def test_extract_reply_target_id_from_xep_0461_stanza(self):
        stanza = _fake_stanza(
            "<message>"
            "<reply xmlns='urn:xmpp:reply:0' id='xmpp-anchor-001'/>"
            "<body>quoted reply</body>"
            "</message>"
        )
        target_id = _extract_xmpp_reply_target_id(stanza)
        self.assertEqual("xmpp-anchor-001", target_id)

    def test_extract_reply_target_id_returns_empty_for_plain(self):
        stanza = _fake_stanza("<message><body>hello</body></message>")
        self.assertEqual("", _extract_xmpp_reply_target_id(stanza))

    def test_extract_reply_ref_for_xmpp_service(self):
        ref = reply_sync.extract_reply_ref(
            "xmpp",
            {
                "reply_source_message_id": "xmpp-anchor-001",
                "reply_source_chat_id": "user@example.test/mobile",
            },
        )
        self.assertEqual("xmpp-anchor-001", ref.get("reply_source_message_id"))
        self.assertEqual("xmpp", ref.get("reply_source_service"))
        self.assertEqual("user@example.test/mobile", ref.get("reply_source_chat_id"))

    def test_extract_reply_ref_returns_empty_for_missing_id(self):
        ref = reply_sync.extract_reply_ref(
            "xmpp", {"reply_source_chat_id": "user@example.test"}
        )
        self.assertEqual({}, ref)


# ---------------------------------------------------------------------------
# XMPP — reply resolution (requires DB)
# ---------------------------------------------------------------------------


class XMPPReplyResolutionTests(TestCase):
    def setUp(self):
        self.user = User.objects.create_user(
            "xmpp-resolve-user", "xmpp-resolve@example.com", "x"
        )
        self.person = Person.objects.create(user=self.user, name="XMPP Resolve")
        self.identifier = PersonIdentifier.objects.create(
            user=self.user,
            person=self.person,
            service="xmpp",
            identifier="contact@example.test",
        )
        self.session = ChatSession.objects.create(
            user=self.user, identifier=self.identifier
        )
        self.anchor = Message.objects.create(
            user=self.user,
            session=self.session,
            ts=now_ms(),
            text="xmpp anchor",
            source_service="xmpp",
            source_message_id="xmpp-anchor-001",
            source_chat_id="contact@example.test/mobile",
            sender_uuid="contact@example.test",
        )

    def test_resolve_reply_target_by_source_message_id(self):
        ref = reply_sync.extract_reply_ref(
            "xmpp",
            {
                "reply_source_message_id": "xmpp-anchor-001",
                "reply_source_chat_id": "contact@example.test/mobile",
            },
        )
        target = async_to_sync(reply_sync.resolve_reply_target)(
            self.user, self.session, ref
        )
        self.assertIsNotNone(target)
        self.assertEqual(self.anchor.id, target.id)

    def test_xmpp_reaction_applied_to_anchor_via_history(self):
        async_to_sync(history.apply_reaction)(
            self.user,
            self.identifier,
            target_message_id="xmpp-anchor-001",
            target_ts=int(self.anchor.ts),
            emoji="🔥",
            source_service="xmpp",
            actor="contact@example.test",
            remove=False,
            payload={"target_xmpp_id": "xmpp-anchor-001"},
        )
        self.anchor.refresh_from_db()
        reactions = list((self.anchor.receipt_payload or {}).get("reactions") or [])
        self.assertTrue(
            any(r.get("emoji") == "🔥" for r in reactions),
            "Expected 🔥 reaction to be stored on the anchor.",
        )

    def test_xmpp_reply_ref_resolved_to_none_for_unknown_id(self):
        ref = reply_sync.extract_reply_ref(
            "xmpp",
            {"reply_source_message_id": "xmpp-nonexistent-999"},
        )
        target = async_to_sync(reply_sync.resolve_reply_target)(
            self.user, self.session, ref
        )
        self.assertIsNone(target)
