from __future__ import annotations

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

from core.commands.base import CommandContext
from core.commands.engine import _matches_trigger, process_inbound_message
from core.messaging.reply_sync import extract_reply_ref, resolve_reply_target
from core.models import (
    ChatSession,
    ChatTaskSource,
    CommandAction,
    CommandChannelBinding,
    CommandProfile,
    CommandVariantPolicy,
    Message,
    Person,
    PersonIdentifier,
    User,
)
from core.views.compose import _command_options_for_channel


class Phase1ReplyResolutionTests(TestCase):
    def setUp(self):
        self.user = User.objects.create_user(
            username="phase1-reply-user",
            email="phase1-reply@example.com",
            password="x",
        )
        self.person = Person.objects.create(user=self.user, name="Reply Person")
        self.identifier = PersonIdentifier.objects.create(
            user=self.user,
            person=self.person,
            service="signal",
            identifier="+15550000001",
        )
        self.session = ChatSession.objects.create(
            user=self.user,
            identifier=self.identifier,
        )

    def test_resolve_reply_target_by_source_message_id(self):
        anchor = Message.objects.create(
            user=self.user,
            session=self.session,
            sender_uuid="+15550000001",
            text="anchor",
            ts=1000,
            source_service="signal",
            source_message_id="signal-msg-1",
        )
        resolved = async_to_sync(resolve_reply_target)(
            self.user,
            self.session,
            {
                "reply_source_service": "signal",
                "reply_source_message_id": "signal-msg-1",
            },
        )
        self.assertEqual(anchor.id, resolved.id if resolved else None)

    def test_resolve_reply_target_with_bridge_ref_fallback(self):
        anchor = Message.objects.create(
            user=self.user,
            session=self.session,
            sender_uuid="+15550000001",
            text="anchor",
            ts=2000,
            receipt_payload={
                "bridge_refs": {
                    "signal": [
                        {
                            "xmpp_message_id": "xmpp-bridge-1",
                            "upstream_message_id": "signal-upstream-1",
                            "upstream_author": "+15550000001",
                            "upstream_ts": 2000,
                            "updated_at": 2000,
                        }
                    ]
                }
            },
        )
        resolved = async_to_sync(resolve_reply_target)(
            self.user,
            self.session,
            {
                "reply_source_service": "signal",
                "reply_source_message_id": "signal-upstream-1",
            },
        )
        self.assertEqual(anchor.id, resolved.id if resolved else None)

    def test_resolve_reply_target_miss(self):
        resolved = async_to_sync(resolve_reply_target)(
            self.user,
            self.session,
            {
                "reply_source_service": "signal",
                "reply_source_message_id": "does-not-exist",
            },
        )
        self.assertIsNone(resolved)

    def test_extract_reply_ref_xmpp(self):
        result = extract_reply_ref(
            "xmpp",
            {
                "reply_source_message_id": "xmpp-msg-1",
                "reply_source_chat_id": "alice@example.test",
            },
        )
        self.assertEqual("xmpp-msg-1", result.get("reply_source_message_id"))
        self.assertEqual("xmpp", result.get("reply_source_service"))

    def test_extract_reply_ref_signal(self):
        result = extract_reply_ref(
            "signal",
            {
                "envelope": {
                    "dataMessage": {
                        "quote": {"id": "signal-msg-quoted"},
                    }
                }
            },
        )
        self.assertEqual("signal-msg-quoted", result.get("reply_source_message_id"))
        self.assertEqual("signal", result.get("reply_source_service"))

    def test_extract_reply_ref_signal_target_sent_timestamp_variant(self):
        result = extract_reply_ref(
            "signal",
            {
                "envelope": {
                    "dataMessage": {
                        "quote": {
                            "targetSentTimestamp": 1772545268786,
                            "authorNumber": "+15550000001",
                        }
                    }
                }
            },
        )
        self.assertEqual(
            "1772545268786",
            result.get("reply_source_message_id"),
        )
        self.assertEqual("signal", result.get("reply_source_service"))
        self.assertEqual("+15550000001", result.get("reply_source_chat_id"))

    def test_extract_reply_ref_whatsapp(self):
        result = extract_reply_ref(
            "whatsapp",
            {
                "extendedTextMessage": {
                    "contextInfo": {
                        "stanzaId": "wa-msg-quoted",
                        "participant": "12345@s.whatsapp.net",
                    }
                }
            },
        )
        self.assertEqual("wa-msg-quoted", result.get("reply_source_message_id"))
        self.assertEqual("whatsapp", result.get("reply_source_service"))

    def test_extract_reply_ref_whatsapp_stanza_id_variant(self):
        result = extract_reply_ref(
            "whatsapp",
            {
                "extendedTextMessage": {
                    "contextInfo": {
                        "stanzaID": "wa-msg-quoted-2",
                    }
                }
            },
        )
        self.assertEqual("wa-msg-quoted-2", result.get("reply_source_message_id"))
        self.assertEqual("whatsapp", result.get("reply_source_service"))


class Phase1CommandEngineTests(TestCase):
    def setUp(self):
        self.user = User.objects.create_user(
            username="phase1-command-user",
            email="phase1-command@example.com",
            password="x",
        )
        self.person = Person.objects.create(user=self.user, name="Command Person")
        self.identifier = PersonIdentifier.objects.create(
            user=self.user,
            person=self.person,
            service="signal",
            identifier="+15550000002",
        )
        self.session = ChatSession.objects.create(
            user=self.user,
            identifier=self.identifier,
        )
        self.profile = CommandProfile.objects.create(
            user=self.user,
            slug="bp",
            name="Business Plan",
            enabled=True,
            trigger_token="#bp#",
            reply_required=True,
            exact_match_only=True,
        )
        CommandChannelBinding.objects.create(
            profile=self.profile,
            direction="ingress",
            service="web",
            channel_identifier="web-chan-1",
            enabled=True,
        )

    def test_matches_trigger_exact_only(self):
        self.assertTrue(_matches_trigger(self.profile, "#bp#"))
        self.assertFalse(_matches_trigger(self.profile, " #bp# extra "))

    def test_process_inbound_message_requires_reply(self):
        msg = Message.objects.create(
            user=self.user,
            session=self.session,
            sender_uuid="",
            text="#bp#",
            ts=3000,
            source_service="web",
            source_chat_id="web-chan-1",
            message_meta={},
        )
        results = async_to_sync(process_inbound_message)(
            CommandContext(
                service="web",
                channel_identifier="web-chan-1",
                message_id=str(msg.id),
                user_id=self.user.id,
                message_text="#bp#",
                payload={},
            )
        )
        self.assertEqual(1, len(results))
        self.assertEqual("skipped", results[0].status)
        self.assertEqual("reply_required", results[0].error)

    def test_process_inbound_message_skips_mirrored_origin(self):
        msg = Message.objects.create(
            user=self.user,
            session=self.session,
            sender_uuid="",
            text="#bp#",
            ts=4000,
            source_service="web",
            source_chat_id="web-chan-1",
            message_meta={"origin_tag": "translation:test"},
        )
        results = async_to_sync(process_inbound_message)(
            CommandContext(
                service="web",
                channel_identifier="web-chan-1",
                message_id=str(msg.id),
                user_id=self.user.id,
                message_text="#bp#",
                payload={},
            )
        )
        self.assertEqual([], results)

    def test_eligible_profile_matches_whatsapp_group_identifier_variants(self):
        self.profile.channel_bindings.all().delete()
        CommandChannelBinding.objects.create(
            profile=self.profile,
            direction="ingress",
            service="whatsapp",
            channel_identifier="120363402761690215@g.us",
            enabled=True,
        )
        msg = Message.objects.create(
            user=self.user,
            session=self.session,
            sender_uuid="",
            text="#bp#",
            ts=5000,
            source_service="whatsapp",
            source_chat_id="120363402761690215@g.us",
            message_meta={},
        )
        results = async_to_sync(process_inbound_message)(
            CommandContext(
                service="whatsapp",
                channel_identifier="120363402761690215",
                message_id=str(msg.id),
                user_id=self.user.id,
                message_text="#bp#",
                payload={},
            )
        )
        self.assertEqual(1, len(results))
        self.assertEqual("skipped", results[0].status)
        self.assertEqual("reply_required", results[0].error)

    def test_eligible_profile_matches_signal_group_alias_variants(self):
        self.profile.channel_bindings.all().delete()
        canonical = PersonIdentifier.objects.create(
            user=self.user,
            person=self.person,
            service="signal",
            identifier="group.canonical-signal-group",
        )
        self.session.identifier = canonical
        self.session.save(update_fields=["identifier"])
        CommandChannelBinding.objects.create(
            profile=self.profile,
            direction="ingress",
            service="signal",
            channel_identifier="group.canonical-signal-group",
            enabled=True,
        )
        msg = Message.objects.create(
            user=self.user,
            session=self.session,
            sender_uuid="",
            text="#bp#",
            ts=5500,
            source_service="signal",
            source_chat_id="signal-internal-group-id",
            message_meta={},
        )
        PersonIdentifier.objects.create(
            user=self.user,
            person=self.person,
            service="signal",
            identifier="signal-internal-group-id",
        )
        results = async_to_sync(process_inbound_message)(
            CommandContext(
                service="signal",
                channel_identifier="signal-internal-group-id",
                message_id=str(msg.id),
                user_id=self.user.id,
                message_text="#bp#",
                payload={},
            )
        )
        self.assertEqual(1, len(results))
        self.assertEqual("skipped", results[0].status)
        self.assertEqual("reply_required", results[0].error)

    def test_compose_command_options_show_bp_subcommands(self):
        self.profile.channel_bindings.all().delete()
        CommandChannelBinding.objects.create(
            profile=self.profile,
            direction="ingress",
            service="whatsapp",
            channel_identifier="120363402761690215@g.us",
            enabled=True,
        )
        options = _command_options_for_channel(
            self.user,
            "whatsapp",
            "120363402761690215@g.us",
        )
        names = [str(row.get("name") or "").strip().lower() for row in options]
        self.assertIn("bp", names)
        self.assertIn("bp set", names)
        self.assertIn("bp set range", names)
        self.assertNotIn("announce task ids", names)

    def test_first_user_bp_command_auto_setup_is_idempotent(self):
        CommandProfile.objects.filter(user=self.user, slug="bp").delete()
        msg1 = Message.objects.create(
            user=self.user,
            session=self.session,
            sender_uuid="",
            custom_author="USER",
            text="#bp#",
            ts=7000,
            source_service="web",
            source_chat_id="web-chan-3",
            message_meta={},
        )
        first_results = async_to_sync(process_inbound_message)(
            CommandContext(
                service="web",
                channel_identifier="web-chan-3",
                message_id=str(msg1.id),
                user_id=self.user.id,
                message_text="#bp#",
                payload={},
            )
        )
        self.assertEqual(1, len(first_results))
        self.assertEqual("reply_required", first_results[0].error)
        profile = CommandProfile.objects.filter(user=self.user, slug="bp").first()
        self.assertIsNotNone(profile)
        if profile is None:
            return
        self.assertEqual(3, CommandAction.objects.filter(profile=profile).count())
        self.assertEqual(
            3, CommandVariantPolicy.objects.filter(profile=profile).count()
        )
        self.assertEqual(
            2,
            CommandChannelBinding.objects.filter(
                profile=profile,
                service="signal",
                channel_identifier="+15550000002",
            ).count(),
        )

        msg2 = Message.objects.create(
            user=self.user,
            session=self.session,
            sender_uuid="",
            custom_author="USER",
            text="#bp#",
            ts=8000,
            source_service="web",
            source_chat_id="web-chan-3",
            message_meta={},
        )
        second_results = async_to_sync(process_inbound_message)(
            CommandContext(
                service="web",
                channel_identifier="web-chan-3",
                message_id=str(msg2.id),
                user_id=self.user.id,
                message_text="#bp#",
                payload={},
            )
        )
        self.assertEqual(1, len(second_results))
        self.assertEqual("reply_required", second_results[0].error)
        self.assertEqual(3, CommandAction.objects.filter(profile=profile).count())
        self.assertEqual(
            3, CommandVariantPolicy.objects.filter(profile=profile).count()
        )
        self.assertEqual(
            2,
            CommandChannelBinding.objects.filter(
                profile=profile,
                service="signal",
                channel_identifier="+15550000002",
            ).count(),
        )
