from __future__ import annotations

import json

from django.test import TestCase
from django.urls import reverse
from unittest.mock import patch

from core.clients import transport
from core.models import (
    ChatSession,
    ConversationEvent,
    Message,
    Person,
    PersonIdentifier,
    PlatformChatLink,
    User,
)
from core.presence import latest_state_for_people
from core.presence.inference import now_ms
from core.views.compose import (
    COMPOSE_WORKSPACE_CONTACTS_PAGE_SIZE,
    COMPOSE_WORKSPACE_HISTORY_PAGE_SIZE,
    _compose_availability_payload,
    _context_base,
    _filter_manual_contact_rows,
    _manual_contact_rows,
    _recent_manual_contacts,
)


class PresenceQueryAndComposeContextTests(TestCase):
    def setUp(self):
        self.user = User.objects.create_user("ctx-user", "ctx@example.com", "x")
        self.person = Person.objects.create(user=self.user, name="Ctx Person")
        self.identifier = PersonIdentifier.objects.create(
            user=self.user,
            person=self.person,
            service="signal",
            identifier="15551234567",
        )

    @patch("core.presence.query.get_behavioral_latest_states")
    def test_latest_state_map_uses_string_person_keys(self, mocked_states):
        mocked_states.return_value = [
            {
                "person_id": str(self.person.id),
                "transport": "signal",
                "kind": "presence_available",
                "ts": now_ms(),
            }
        ]
        state_map = latest_state_for_people(
            user=self.user,
            person_ids=[str(self.person.id)],
            service="signal",
        )
        self.assertIn(str(self.person.id), state_map)

    @patch("core.presence.query.get_behavioral_latest_states")
    def test_latest_state_map_falls_back_to_conversation_event_shadow(
        self, mocked_states
    ):
        session = ChatSession.objects.create(user=self.user, identifier=self.identifier)
        ConversationEvent.objects.create(
            user=self.user,
            session=session,
            ts=now_ms(),
            event_type="presence_available",
            direction="system",
            origin_transport="signal",
        )
        mocked_states.return_value = []

        state_map = latest_state_for_people(
            user=self.user,
            person_ids=[str(self.person.id)],
            service="signal",
        )

        self.assertEqual("available", str(state_map[str(self.person.id)]["state"]))

    def test_context_base_matches_signal_identifier_variants(self):
        base = _context_base(
            user=self.user,
            service="signal",
            identifier="+1 (555) 123-4567",
            person=self.person,
        )
        self.assertIsNotNone(base["person_identifier"])
        self.assertEqual(str(self.person.id), str(base["person"].id))

    def test_filter_manual_contact_rows_matches_person_identifier_and_service(self):
        rows = [
            {
                "person_name": "Alice Example",
                "linked_person_name": "Alice Example",
                "detected_name": "",
                "service": "whatsapp",
                "identifier": "447700000001",
            },
            {
                "person_name": "Bob Example",
                "linked_person_name": "Bob Example",
                "detected_name": "",
                "service": "signal",
                "identifier": "+15551230000",
            },
        ]

        filtered = _filter_manual_contact_rows(rows, "alice whatsapp 000001")

        self.assertEqual(1, len(filtered))
        self.assertEqual("Alice Example", filtered[0]["person_name"])

    @patch("core.views.compose._manual_contact_rows")
    def test_recent_manual_contacts_keeps_current_person_name_for_signal_alias(
        self, mocked_manual_contact_rows
    ):
        current_identifier = "d3198cce-f003-4290-a016-eaaa1a11c392"
        PersonIdentifier.objects.create(
            user=self.user,
            person=self.person,
            service="signal",
            identifier=current_identifier,
        )
        mocked_manual_contact_rows.return_value = [
            {
                "person_name": self.person.name,
                "linked_person_name": self.person.name,
                "person_id": str(self.person.id),
                "detected_name": "",
                "service": "signal",
                "service_icon_class": "fa-solid fa-signal",
                "identifier": "+15551234567",
                "compose_url": "/compose/page/?service=signal&identifier=%2B15551234567",
                "compose_widget_url": "/compose/widget/?service=signal&identifier=%2B15551234567",
                "linked_person": True,
                "source": "linked",
            }
        ]

        rows = _recent_manual_contacts(
            self.user,
            current_service="signal",
            current_identifier=current_identifier,
            current_person=self.person,
            limit=12,
        )

        self.assertGreaterEqual(len(rows), 1)
        self.assertEqual(self.person.name, rows[0]["person_name"])
        self.assertEqual(str(self.person.id), rows[0]["person_id"])
        self.assertEqual(current_identifier, rows[0]["identifier"])
        self.assertEqual(
            {"signal": current_identifier},
            rows[0]["service_identifiers"],
        )
        self.assertEqual(
            {"signal": current_identifier},
            json.loads(rows[0]["service_identifiers_json"]),
        )
        self.assertTrue(bool(rows[0]["is_active"]))

    @patch("core.presence.query.get_behavioral_latest_states")
    @patch("core.presence.query.get_behavioral_events_for_range")
    def test_compose_availability_payload_falls_back_to_cross_service(
        self, mocked_events, mocked_states
    ):
        ts_value = now_ms()
        mocked_events.return_value = [
            {
                "person_id": str(self.person.id),
                "transport": "whatsapp",
                "kind": "presence_available",
                "ts": ts_value,
                "payload": {},
            }
        ]
        mocked_states.return_value = [
            {
                "person_id": str(self.person.id),
                "transport": "whatsapp",
                "kind": "presence_available",
                "ts": ts_value,
            }
        ]
        enabled, slices, summary = _compose_availability_payload(
            user=self.user,
            person=self.person,
            service="signal",
            range_start=ts_value - 60000,
            range_end=ts_value + 60000,
        )
        self.assertTrue(enabled)
        self.assertGreaterEqual(len(slices), 1)
        self.assertEqual("whatsapp", str(slices[0].get("service")))
        self.assertEqual("available", str(summary.get("state")))
        self.assertTrue(bool(summary.get("is_cross_service")))

    @patch("core.presence.query.get_behavioral_latest_states")
    @patch("core.presence.query.get_behavioral_events_for_range")
    def test_compose_availability_payload_can_fallback_to_manticore_behavioral(
        self,
        mocked_events,
        mocked_states,
    ):
        ts_value = now_ms()
        mocked_events.return_value = [
            {
                "person_id": str(self.person.id),
                "transport": "signal",
                "kind": "presence_available",
                "ts": ts_value - 30000,
                "payload": {},
            },
            {
                "person_id": str(self.person.id),
                "transport": "signal",
                "kind": "composing_abandoned",
                "ts": ts_value - 5000,
                "payload": {},
            },
        ]
        mocked_states.return_value = [
            {
                "person_id": str(self.person.id),
                "transport": "signal",
                "kind": "composing_abandoned",
                "ts": ts_value - 5000,
            }
        ]

        enabled, slices, summary = _compose_availability_payload(
            user=self.user,
            person=self.person,
            service="signal",
            range_start=ts_value - 60000,
            range_end=ts_value + 60000,
        )

        self.assertTrue(enabled)
        self.assertGreaterEqual(len(slices), 1)
        self.assertEqual("signal", str(slices[0].get("service")))
        self.assertEqual("fading", str(summary.get("state")))
        self.assertEqual(
            "behavioral:composing_abandoned",
            str(summary.get("source_kind")),
        )

    @patch("core.presence.query.get_behavioral_latest_states")
    @patch("core.presence.query.get_behavioral_events_for_range")
    def test_compose_availability_payload_falls_back_to_conversation_event_shadow(
        self,
        mocked_events,
        mocked_states,
    ):
        ts_value = now_ms()
        session = ChatSession.objects.create(user=self.user, identifier=self.identifier)
        ConversationEvent.objects.create(
            user=self.user,
            session=session,
            ts=ts_value - 10000,
            event_type="presence_available",
            direction="system",
            origin_transport="signal",
        )
        mocked_events.return_value = []
        mocked_states.return_value = []

        enabled, slices, summary = _compose_availability_payload(
            user=self.user,
            person=self.person,
            service="signal",
            range_start=ts_value - 60000,
            range_end=ts_value + 60000,
        )

        self.assertTrue(enabled)
        self.assertGreaterEqual(len(slices), 1)
        self.assertEqual("signal", str(slices[0].get("service")))
        self.assertEqual("available", str(summary.get("state")))

    @patch("core.presence.query.get_behavioral_latest_states")
    @patch("core.presence.query.get_behavioral_events_for_range")
    def test_compose_availability_payload_prefers_manticore_over_django(
        self,
        mocked_events,
        mocked_states,
    ):
        ts_value = now_ms()
        mocked_events.return_value = [
            {
                "person_id": str(self.person.id),
                "transport": "signal",
                "kind": "composing_abandoned",
                "ts": ts_value - 5000,
                "payload": {},
            }
        ]
        mocked_states.return_value = [
            {
                "person_id": str(self.person.id),
                "transport": "signal",
                "kind": "composing_abandoned",
                "ts": ts_value - 5000,
            }
        ]

        enabled, slices, summary = _compose_availability_payload(
            user=self.user,
            person=self.person,
            service="signal",
            range_start=ts_value - 60000,
            range_end=ts_value + 60000,
        )

        self.assertTrue(enabled)
        self.assertGreaterEqual(len(slices), 1)
        self.assertEqual("fading", str(slices[0].get("state")))
        self.assertEqual("fading", str(summary.get("state")))
        self.assertEqual(
            "behavioral:composing_abandoned",
            str(summary.get("source_kind")),
        )

    def test_context_base_preserves_native_signal_group_identifier(self):
        PlatformChatLink.objects.create(
            user=self.user,
            service="signal",
            chat_identifier="signal-group-123",
            chat_name="Signal Group",
            is_group=True,
        )

        base = _context_base(
            user=self.user,
            service="signal",
            identifier="signal-group-123",
            person=None,
        )

        self.assertTrue(bool(base["is_group"]))
        self.assertEqual("signal-group-123", str(base["identifier"]))

    def test_context_base_prefers_explicit_signal_group_over_xmpp_identifier_match(self):
        PlatformChatLink.objects.create(
            user=self.user,
            service="signal",
            chat_identifier="signal-group-123",
            chat_name="Signal Group",
            is_group=True,
        )
        xmpp_person = Person.objects.create(user=self.user, name="Bridge Alias")
        PersonIdentifier.objects.create(
            user=self.user,
            person=xmpp_person,
            service="xmpp",
            identifier="signal-group-123",
        )

        base = _context_base(
            user=self.user,
            service="signal",
            identifier="signal-group-123",
            person=None,
        )

        self.assertEqual("signal", str(base["service"]))
        self.assertTrue(bool(base["is_group"]))
        self.assertIsNone(base["person_identifier"])

    def test_manual_contact_rows_include_signal_groups(self):
        PlatformChatLink.objects.create(
            user=self.user,
            service="signal",
            chat_identifier="signal-group-123",
            chat_name="Misinformation Club",
            is_group=True,
        )

        rows = _manual_contact_rows(self.user)
        match = next(
            (
                row
                for row in rows
                if str(row.get("service")) == "signal"
                and str(row.get("identifier")) == "signal-group-123"
            ),
            None,
        )

        self.assertIsNotNone(match)
        self.assertEqual("Misinformation Club", str(match.get("detected_name") or ""))

    def test_manual_contact_rows_collapse_signal_group_aliases(self):
        PlatformChatLink.objects.create(
            user=self.user,
            service="signal",
            chat_identifier="group.signal-club",
            chat_name="Misinformation Club",
            is_group=True,
        )
        transport.update_runtime_state(
            "signal",
            accounts=["+447700900001"],
            groups=[
                {
                    "identifier": "group.signal-club",
                    "identifiers": [
                        "group.signal-club",
                        "sEGA9F0HQ/eyLgmvKx23hha9Vp7mDRhpq23/roVSZbI=",
                    ],
                    "name": "Misinformation Club",
                    "id": "group.signal-club",
                    "internal_id": "sEGA9F0HQ/eyLgmvKx23hha9Vp7mDRhpq23/roVSZbI=",
                }
            ],
        )

        rows = [
            row
            for row in _manual_contact_rows(self.user)
            if str(row.get("service")) == "signal"
            and str(row.get("detected_name")) == "Misinformation Club"
        ]

        self.assertEqual(1, len(rows))
        self.assertEqual("group.signal-club", str(rows[0].get("identifier") or ""))
        self.assertEqual(
            ["sEGA9F0HQ/eyLgmvKx23hha9Vp7mDRhpq23/roVSZbI="],
            rows[0].get("identifier_aliases"),
        )

    def test_manual_contact_rows_fill_whatsapp_detected_name_for_linked_jid(self):
        transport.update_runtime_state("whatsapp", accounts=[], contacts=[])
        whatsapp_person = Person.objects.create(
            user=self.user,
            name="Mapped Contact",
        )
        PersonIdentifier.objects.create(
            user=self.user,
            person=whatsapp_person,
            service="whatsapp",
            identifier="962799299627@s.whatsapp.net",
        )
        transport.update_runtime_state(
            "whatsapp",
            accounts=["155500000000@s.whatsapp.net"],
            contacts=[
                {
                    "identifier": "962799299627",
                    "jid": "962799299627@s.whatsapp.net",
                    "name": "Detected Contact",
                    "chat": "",
                }
            ],
        )

        rows = [
            row
            for row in _manual_contact_rows(self.user)
            if str(row.get("service")) == "whatsapp"
            and str(row.get("identifier")) == "962799299627@s.whatsapp.net"
        ]

        self.assertEqual(1, len(rows))
        self.assertTrue(bool(rows[0].get("linked_person")))
        self.assertEqual("Mapped Contact", str(rows[0].get("linked_person_name") or ""))
        self.assertEqual("Detected Contact", str(rows[0].get("detected_name") or ""))


class ComposeWorkspaceContactsWidgetTests(TestCase):
    def setUp(self):
        self.user = User.objects.create_user(
            "workspace-user",
            "workspace@example.com",
            "x",
        )
        self.client.force_login(self.user)
        for index in range(65):
            person = Person.objects.create(
                user=self.user,
                name=f"Person {index:03d}",
            )
            PersonIdentifier.objects.create(
                user=self.user,
                person=person,
                service="whatsapp",
                identifier=f"4477000{index:03d}",
            )

    def test_workspace_contact_widget_limits_initial_render(self):
        response = self.client.get(reverse("compose_workspace_contacts_widget"))

        self.assertEqual(200, response.status_code)
        content = response.content.decode()
        self.assertIn("Person 000", content)
        self.assertIn(
            f"Showing {COMPOSE_WORKSPACE_CONTACTS_PAGE_SIZE} contacts.",
            content,
        )
        self.assertNotIn("Window", content)
        self.assertNotIn("Match</span>", content)
        self.assertNotIn(f"Person {COMPOSE_WORKSPACE_CONTACTS_PAGE_SIZE:03d}", content)
        self.assertIn(
            f"Show {COMPOSE_WORKSPACE_CONTACTS_PAGE_SIZE} more",
            content,
        )

    def test_workspace_contact_widget_results_fragment_filters_rows(self):
        response = self.client.get(
            reverse("compose_workspace_contacts_widget"),
            {"fragment": "results", "q": "Person 064"},
        )

        self.assertEqual(200, response.status_code)
        content = response.content.decode()
        self.assertIn("Person 064", content)
        self.assertNotIn("Person 000", content)
        self.assertNotIn('id="widget-', content)

    def test_compose_workspace_page_uses_shared_workspace_shell_asset(self):
        response = self.client.get(reverse("compose_workspace"))

        self.assertEqual(200, response.status_code)
        content = response.content.decode()
        self.assertIn("js/workspace-shell.js", content)
        self.assertNotIn("window.giaPrepareWidgetTarget = function", content)


class ComposeWorkspaceHistoryWidgetTests(TestCase):
    def setUp(self):
        self.user = User.objects.create_user(
            "history-user",
            "history@example.com",
            "x",
        )
        self.client.force_login(self.user)
        self.person_signal = Person.objects.create(user=self.user, name="Ada Signal")
        self.person_whatsapp = Person.objects.create(
            user=self.user,
            name="Willa WhatsApp",
        )
        self.signal_identifier = PersonIdentifier.objects.create(
            user=self.user,
            person=self.person_signal,
            service="signal",
            identifier="15551230001",
        )
        self.whatsapp_identifier = PersonIdentifier.objects.create(
            user=self.user,
            person=self.person_whatsapp,
            service="whatsapp",
            identifier="447700000001",
        )
        self.signal_session = ChatSession.objects.create(
            user=self.user,
            identifier=self.signal_identifier,
        )
        self.whatsapp_session = ChatSession.objects.create(
            user=self.user,
            identifier=self.whatsapp_identifier,
        )
        base_ts = now_ms()
        for index in range(32):
            Message.objects.create(
                user=self.user,
                session=self.signal_session,
                ts=base_ts + index,
                text=f"Ada message {index:02d}",
                custom_author="USER" if index % 2 else "",
            )
        Message.objects.create(
            user=self.user,
            session=self.whatsapp_session,
            ts=base_ts - 1000,
            text="WhatsApp follow up",
            custom_author="",
        )

    def test_history_widget_limits_initial_render(self):
        response = self.client.get(reverse("compose_workspace_history_widget"))

        self.assertEqual(200, response.status_code)
        content = response.content.decode()
        self.assertIn("Ada message 31", content)
        lower_bound = 32 - COMPOSE_WORKSPACE_HISTORY_PAGE_SIZE
        self.assertIn(f"Ada message {lower_bound:02d}", content)
        self.assertNotIn(f"Ada message {lower_bound - 1:02d}", content)
        self.assertIn("Show more history", content)

    def test_history_widget_filters_by_service_and_fragment_mode(self):
        response = self.client.get(
            reverse("compose_workspace_history_widget"),
            {"fragment": "results", "service": "whatsapp", "q": "follow up"},
        )

        self.assertEqual(200, response.status_code)
        content = response.content.decode()
        self.assertIn("WhatsApp follow up", content)
        self.assertNotIn("Ada message 32", content)
        self.assertNotIn('id="widget-', content)

    def test_history_widget_respects_person_scope(self):
        response = self.client.get(
            reverse("compose_workspace_history_widget"),
            {"person": str(self.person_signal.id)},
        )

        self.assertEqual(200, response.status_code)
        content = response.content.decode()
        self.assertIn("Scoped to <strong>Ada Signal</strong>.", content)
        self.assertNotIn("WhatsApp follow up", content)
