import time
from io import StringIO
from unittest.mock import patch

from django.core.management import call_command
from django.test import TestCase, override_settings

from core.events.ledger import append_event_sync
from core.events.projection import shadow_compare_session
from core.models import ChatSession, Message, Person, PersonIdentifier, User


@override_settings(EVENT_LEDGER_DUAL_WRITE=True)
class EventProjectionShadowTests(TestCase):
    def setUp(self):
        self.user = User.objects.create_user(
            username="projection-user",
            email="projection@example.com",
            password="x",
        )
        self.person = Person.objects.create(user=self.user, name="Projection Person")
        self.identifier = PersonIdentifier.objects.create(
            user=self.user,
            person=self.person,
            service="signal",
            identifier="+15555550333",
        )
        self.session = ChatSession.objects.create(
            user=self.user, identifier=self.identifier
        )

    def test_shadow_compare_has_zero_mismatch_when_projection_matches(self):
        message = Message.objects.create(
            user=self.user,
            session=self.session,
            ts=1700000000000,
            sender_uuid="+15555550333",
            text="hello",
            delivered_ts=1700000000000,
            read_ts=1700000000500,
            receipt_payload={
                "reactions": [
                    {
                        "source_service": "signal",
                        "actor": "user:1:signal",
                        "emoji": "👍",
                        "removed": False,
                    }
                ]
            },
        )
        append_event_sync(
            user=self.user,
            session=self.session,
            ts=1700000000000,
            event_type="message_created",
            direction="in",
            origin_transport="signal",
            origin_message_id=str(message.id),
            payload={"message_id": str(message.id), "text": "hello"},
        )
        append_event_sync(
            user=self.user,
            session=self.session,
            ts=1700000000500,
            event_type="read_receipt",
            direction="system",
            origin_transport="signal",
            origin_message_id=str(message.id),
            payload={"message_id": str(message.id), "read_ts": 1700000000500},
        )
        append_event_sync(
            user=self.user,
            session=self.session,
            ts=1700000000600,
            event_type="reaction_added",
            direction="system",
            actor_identifier="user:1:signal",
            origin_transport="signal",
            origin_message_id=str(message.id),
            payload={
                "message_id": str(message.id),
                "emoji": "👍",
                "source_service": "signal",
                "actor": "user:1:signal",
            },
        )

        compared = shadow_compare_session(self.session, detail_limit=10)
        self.assertEqual(0, compared["mismatch_total"])

    def test_shadow_compare_detects_missing_projection_row(self):
        Message.objects.create(
            user=self.user,
            session=self.session,
            ts=1700000000000,
            sender_uuid="+15555550333",
            text="no-event",
        )
        compared = shadow_compare_session(self.session, detail_limit=10)
        self.assertGreater(compared["counters"]["missing_in_projection"], 0)
        self.assertIn("cause_samples", compared)
        self.assertIn("missing_event_write", compared["cause_samples"])
        self.assertGreaterEqual(
            len(compared["cause_samples"]["missing_event_write"]),
            1,
        )

    @patch("core.events.projection.get_session_event_rows")
    def test_shadow_compare_can_project_from_manticore_rows(self, mocked_rows):
        message = Message.objects.create(
            user=self.user,
            session=self.session,
            ts=1700000000000,
            sender_uuid="+15555550333",
            text="hello",
            delivered_ts=1700000000000,
            read_ts=1700000000500,
        )
        mocked_rows.return_value = [
            {
                "ts": 1700000000000,
                "event_type": "message_created",
                "origin_transport": "signal",
                "origin_message_id": str(message.id),
                "payload": {"message_id": str(message.id), "text": "hello"},
            },
            {
                "ts": 1700000000500,
                "event_type": "read_receipt",
                "origin_transport": "signal",
                "origin_message_id": str(message.id),
                "payload": {"message_id": str(message.id), "read_ts": 1700000000500},
            },
        ]

        compared = shadow_compare_session(self.session, detail_limit=10)

        self.assertEqual(0, compared["counters"]["missing_in_projection"])

    def test_management_command_emits_summary(self):
        out = StringIO()
        call_command(
            "event_projection_shadow",
            user_id=str(self.user.id),
            limit_sessions=5,
            stdout=out,
        )
        rendered = out.getvalue()
        self.assertIn("shadow compare:", rendered)
        self.assertIn("cause_counts=", rendered)

    def test_management_command_supports_service_and_recent_filters(self):
        Message.objects.create(
            user=self.user,
            session=self.session,
            ts=int(time.time() * 1000),
            sender_uuid="+15550000000",
            text="recent",
            source_service="signal",
            source_message_id="recent-1",
        )
        out = StringIO()
        call_command(
            "event_projection_shadow",
            user_id=str(self.user.id),
            service="signal",
            recent_minutes=120,
            limit_sessions=5,
            stdout=out,
        )
        rendered = out.getvalue()
        self.assertIn("shadow compare:", rendered)

    def test_management_command_supports_recent_only_switch(self):
        Message.objects.create(
            user=self.user,
            session=self.session,
            ts=int(time.time() * 1000),
            sender_uuid="+15550000001",
            text="recent-only",
            source_service="signal",
            source_message_id="recent-only-1",
        )
        out = StringIO()
        call_command(
            "event_projection_shadow",
            user_id=str(self.user.id),
            recent_only=True,
            limit_sessions=5,
            stdout=out,
        )
        rendered = out.getvalue()
        self.assertIn("shadow compare:", rendered)
