from __future__ import annotations

from unittest.mock import AsyncMock, patch

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

from core.assist.repeat_answer import find_repeat_answer, learn_from_message
from core.models import (
    AnswerSuggestionEvent,
    Chat,
    ChatSession,
    ChatTaskSource,
    DerivedTask,
    DerivedTaskEvent,
    Message,
    Person,
    PersonIdentifier,
    TaskCompletionPattern,
    TaskEpic,
    TaskProject,
    User,
)
from core.tasks.engine import process_inbound_task_intelligence


class RepeatAnswerTests(TestCase):
    def setUp(self):
        self.user = User.objects.create_user("repeat-user", "repeat@example.com", "x")
        self.person = Person.objects.create(user=self.user, name="Repeat Person")
        self.identifier = PersonIdentifier.objects.create(
            user=self.user,
            person=self.person,
            service="whatsapp",
            identifier="120363402761690215@g.us",
        )
        self.session = ChatSession.objects.create(
            user=self.user, identifier=self.identifier
        )

    def test_suggest_only_for_repeated_group_question(self):
        q1 = Message.objects.create(
            user=self.user,
            session=self.session,
            sender_uuid="peer",
            text="What is the deploy command?",
            ts=1000,
            source_service="whatsapp",
            source_chat_id="120363402761690215@g.us",
        )
        a1 = Message.objects.create(
            user=self.user,
            session=self.session,
            sender_uuid="me",
            text="Use make deploy-prod.",
            ts=1200,
            source_service="whatsapp",
            source_chat_id="120363402761690215@g.us",
            reply_to=q1,
        )
        async_to_sync(learn_from_message)(a1)

        q2 = Message.objects.create(
            user=self.user,
            session=self.session,
            sender_uuid="peer",
            text="What is the deploy command?",
            ts=2000,
            source_service="whatsapp",
            source_chat_id="120363402761690215@g.us",
        )
        suggestion = async_to_sync(find_repeat_answer)(self.user, q2)
        self.assertIsNotNone(suggestion)
        self.assertIn("deploy", suggestion.answer_text.lower())
        self.assertTrue(
            AnswerSuggestionEvent.objects.filter(
                message=q2, status="suggested"
            ).exists()
        )


@override_settings(TASK_DERIVATION_USE_AI=False)
class TaskEngineTests(TestCase):
    def setUp(self):
        self.user = User.objects.create_user("task-user", "task@example.com", "x")
        self.person = Person.objects.create(user=self.user, name="Task Person")
        self.identifier = PersonIdentifier.objects.create(
            user=self.user,
            person=self.person,
            service="whatsapp",
            identifier="120363402761690215@g.us",
        )
        self.session = ChatSession.objects.create(
            user=self.user, identifier=self.identifier
        )
        self.project = TaskProject.objects.create(user=self.user, name="Ops")
        ChatTaskSource.objects.create(
            user=self.user,
            service="whatsapp",
            channel_identifier="120363402761690215@g.us",
            project=self.project,
            enabled=True,
        )
        TaskCompletionPattern.objects.create(
            user=self.user, phrase="done", enabled=True
        )

    def test_creates_derived_task_on_task_like_message(self):
        m = Message.objects.create(
            user=self.user,
            session=self.session,
            sender_uuid="peer",
            text="Task: rotate credentials tonight",
            ts=1000,
            source_service="whatsapp",
            source_chat_id="120363402761690215@g.us",
        )
        async_to_sync(process_inbound_task_intelligence)(m)
        task = DerivedTask.objects.get(origin_message=m)
        self.assertEqual("open", task.status_snapshot)
        self.assertTrue(task.reference_code)
        self.assertTrue(
            DerivedTaskEvent.objects.filter(task=task, event_type="created").exists()
        )

    def test_marks_completion_from_regex_marker(self):
        seed = Message.objects.create(
            user=self.user,
            session=self.session,
            sender_uuid="peer",
            text="task: patch kernel",
            ts=1000,
            source_service="whatsapp",
            source_chat_id="120363402761690215@g.us",
        )
        async_to_sync(process_inbound_task_intelligence)(seed)
        task = DerivedTask.objects.get(origin_message=seed)
        marker = Message.objects.create(
            user=self.user,
            session=self.session,
            sender_uuid="peer",
            text=f"done #{task.reference_code}",
            ts=1100,
            source_service="whatsapp",
            source_chat_id="120363402761690215@g.us",
        )
        async_to_sync(process_inbound_task_intelligence)(marker)
        task.refresh_from_db()
        self.assertEqual("completed", task.status_snapshot)
        self.assertTrue(
            DerivedTaskEvent.objects.filter(
                task=task, event_type="completion_marked"
            ).exists()
        )

    def test_matches_whatsapp_private_channel_variants(self):
        ChatTaskSource.objects.create(
            user=self.user,
            service="whatsapp",
            channel_identifier="447700900123@s.whatsapp.net",
            project=self.project,
            enabled=True,
        )
        m = Message.objects.create(
            user=self.user,
            session=self.session,
            sender_uuid="peer",
            text="task: update private chat mapping",
            ts=1200,
            source_service="whatsapp",
            source_chat_id="447700900123",
        )
        async_to_sync(process_inbound_task_intelligence)(m)
        self.assertTrue(
            DerivedTask.objects.filter(origin_message=m).exists(),
            "Expected private WhatsApp bare identifier to match @s.whatsapp.net mapping.",
        )

    def test_matches_signal_uuid_to_number_companion_mapping(self):
        signal_person = Person.objects.create(user=self.user, name="Signal Task Person")
        signal_identifier = PersonIdentifier.objects.create(
            user=self.user,
            person=signal_person,
            service="signal",
            identifier="+447700900555",
        )
        signal_session = ChatSession.objects.create(
            user=self.user, identifier=signal_identifier
        )
        ChatTaskSource.objects.create(
            user=self.user,
            service="signal",
            channel_identifier="+447700900555",
            project=self.project,
            enabled=True,
        )
        Chat.objects.create(
            source_uuid="54cb8dbe-4c5f-4ef9-9f3d-4a9b37fd15d9",
            source_number="+447700900555",
            source_name="Signal Peer",
            account="+447700900000",
        )
        m = Message.objects.create(
            user=self.user,
            session=signal_session,
            sender_uuid="peer",
            text="task: check signal private mapping",
            ts=1300,
            source_service="signal",
            source_chat_id="54cb8dbe-4c5f-4ef9-9f3d-4a9b37fd15d9",
        )
        async_to_sync(process_inbound_task_intelligence)(m)
        self.assertTrue(
            DerivedTask.objects.filter(origin_message=m).exists(),
            "Expected Signal UUID source chat to match source mapping by companion number.",
        )

    def test_lenient_prefix_parsing_allows_hash_todo_in_strict_mode(self):
        source = ChatTaskSource.objects.filter(
            user=self.user,
            service="whatsapp",
            channel_identifier="120363402761690215@g.us",
        ).first()
        source.settings = {
            "derive_enabled": True,
            "match_mode": "strict",
            "require_prefix": True,
            "allowed_prefixes": ["task:", "todo:"],
            "ai_title_enabled": False,
        }
        source.save(update_fields=["settings", "updated_at"])
        m = Message.objects.create(
            user=self.user,
            session=self.session,
            sender_uuid="peer",
            text="#todo: rotate SSL certs",
            ts=1400,
            source_service="whatsapp",
            source_chat_id="120363402761690215@g.us",
        )
        async_to_sync(process_inbound_task_intelligence)(m)
        task = DerivedTask.objects.filter(origin_message=m).first()
        self.assertIsNotNone(task)
        self.assertEqual("rotate SSL certs", str(task.title or ""))

    def test_lenient_prefix_parsing_allows_task_dash_form(self):
        source = ChatTaskSource.objects.filter(
            user=self.user,
            service="whatsapp",
            channel_identifier="120363402761690215@g.us",
        ).first()
        source.settings = {
            "derive_enabled": True,
            "match_mode": "strict",
            "require_prefix": True,
            "allowed_prefixes": ["task:", "todo:"],
            "ai_title_enabled": False,
        }
        source.save(update_fields=["settings", "updated_at"])
        m = Message.objects.create(
            user=self.user,
            session=self.session,
            sender_uuid="peer",
            text="task - setup password sharing",
            ts=1500,
            source_service="whatsapp",
            source_chat_id="120363402761690215@g.us",
        )
        async_to_sync(process_inbound_task_intelligence)(m)
        task = DerivedTask.objects.filter(origin_message=m).first()
        self.assertIsNotNone(task)
        self.assertEqual("setup password sharing", str(task.title or ""))

    def test_lenient_prefix_parsing_allows_double_dot_task_form(self):
        source = ChatTaskSource.objects.filter(
            user=self.user,
            service="whatsapp",
            channel_identifier="120363402761690215@g.us",
        ).first()
        source.settings = {
            "derive_enabled": True,
            "match_mode": "strict",
            "require_prefix": True,
            "allowed_prefixes": ["task:", "todo:"],
            "ai_title_enabled": False,
        }
        source.save(update_fields=["settings", "updated_at"])
        m = Message.objects.create(
            user=self.user,
            session=self.session,
            sender_uuid="peer",
            text="..task setup password sharing",
            ts=1600,
            source_service="whatsapp",
            source_chat_id="120363402761690215@g.us",
        )
        async_to_sync(process_inbound_task_intelligence)(m)
        task = DerivedTask.objects.filter(origin_message=m).first()
        self.assertIsNotNone(task)
        self.assertEqual("setup password sharing", str(task.title or ""))

    @patch("core.tasks.engine.send_message_raw", new_callable=AsyncMock)
    def test_dot_l_lists_tasks_in_scope(self, mocked_send):
        seed = Message.objects.create(
            user=self.user,
            session=self.session,
            sender_uuid="peer",
            text="task: rotate keys",
            ts=1700,
            source_service="whatsapp",
            source_chat_id="120363402761690215@g.us",
        )
        async_to_sync(process_inbound_task_intelligence)(seed)
        cmd = Message.objects.create(
            user=self.user,
            session=self.session,
            sender_uuid="peer",
            text=".l",
            ts=1701,
            source_service="whatsapp",
            source_chat_id="120363402761690215@g.us",
        )
        async_to_sync(process_inbound_task_intelligence)(cmd)
        self.assertTrue(mocked_send.await_count >= 1)
        list_payloads = [
            str(call.kwargs.get("text") or "") for call in mocked_send.await_args_list
        ]
        self.assertTrue(any("open tasks" in row.lower() for row in list_payloads))
        self.assertTrue(any("#1" in row for row in list_payloads))

    @patch("core.tasks.engine.send_message_raw", new_callable=AsyncMock)
    def test_dot_undo_uncreates_latest_task(self, mocked_send):
        m1 = Message.objects.create(
            user=self.user,
            session=self.session,
            sender_uuid="peer",
            text="task: one",
            ts=1800,
            source_service="whatsapp",
            source_chat_id="120363402761690215@g.us",
        )
        m2 = Message.objects.create(
            user=self.user,
            session=self.session,
            sender_uuid="peer",
            text="task: two",
            ts=1801,
            source_service="whatsapp",
            source_chat_id="120363402761690215@g.us",
        )
        async_to_sync(process_inbound_task_intelligence)(m1)
        async_to_sync(process_inbound_task_intelligence)(m2)
        self.assertEqual(
            2, DerivedTask.objects.filter(user=self.user, project=self.project).count()
        )
        cmd = Message.objects.create(
            user=self.user,
            session=self.session,
            sender_uuid="peer",
            text=".undo",
            ts=1802,
            source_service="whatsapp",
            source_chat_id="120363402761690215@g.us",
        )
        async_to_sync(process_inbound_task_intelligence)(cmd)
        remaining = list(
            DerivedTask.objects.filter(user=self.user, project=self.project)
            .order_by("created_at")
            .values_list("title", flat=True)
        )
        self.assertEqual(1, len(remaining))
        self.assertEqual("one", remaining[0])
        payloads = [
            str(call.kwargs.get("text") or "") for call in mocked_send.await_args_list
        ]
        self.assertTrue(any("removed #2" in row.lower() for row in payloads))

    @patch("core.tasks.engine.send_message_raw", new_callable=AsyncMock)
    def test_every_tenth_task_sends_l_and_undo_reminder(self, mocked_send):
        for idx in range(1, 11):
            m = Message.objects.create(
                user=self.user,
                session=self.session,
                sender_uuid="peer",
                text=f"task: item {idx}",
                ts=1900 + idx,
                source_service="whatsapp",
                source_chat_id="120363402761690215@g.us",
            )
            async_to_sync(process_inbound_task_intelligence)(m)
        payloads = [
            str(call.kwargs.get("text") or "") for call in mocked_send.await_args_list
        ]
        self.assertTrue(
            any(
                ".l list tasks" in row.lower() and ".undo" in row.lower()
                for row in payloads
            ),
            "Expected periodic reminder to mention both .l and .undo.",
        )

    @patch("core.tasks.engine.send_message_raw", new_callable=AsyncMock)
    def test_epic_create_command_from_chat(self, mocked_send):
        msg = Message.objects.create(
            user=self.user,
            session=self.session,
            sender_uuid="peer",
            text="epic: Security",
            ts=2001,
            source_service="whatsapp",
            source_chat_id="120363402761690215@g.us",
        )
        async_to_sync(process_inbound_task_intelligence)(msg)
        self.assertTrue(
            TaskEpic.objects.filter(project=self.project, name="Security").exists()
        )
        payloads = [
            str(call.kwargs.get("text") or "") for call in mocked_send.await_args_list
        ]
        self.assertTrue(any("whatsapp usage" in row.lower() for row in payloads))

    def test_task_with_epic_token_assigns_epic(self):
        msg = Message.objects.create(
            user=self.user,
            session=self.session,
            sender_uuid="peer",
            text="task: setup ssl cert rotation [epic:Security]",
            ts=2002,
            source_service="whatsapp",
            source_chat_id="120363402761690215@g.us",
        )
        async_to_sync(process_inbound_task_intelligence)(msg)
        task = DerivedTask.objects.filter(origin_message=msg).first()
        self.assertIsNotNone(task)
        self.assertIsNotNone(task.epic)
        self.assertEqual("Security", str(task.epic.name or ""))
