"""End-to-end tests for the packet processing pipeline.

These tests verify the full flow from raw packet arrival through to
WebSocket broadcast, using real packet data and a real database.

The fixtures in fixtures/websocket_events.json define the contract
between backend and frontend - both sides test against the same data.
"""

import json
from pathlib import Path
from unittest.mock import patch, MagicMock

import pytest

from app.database import Database
from app.repository import ChannelRepository, MessageRepository, ContactRepository, RawPacketRepository


# Load shared fixtures
FIXTURES_PATH = Path(__file__).parent / "fixtures" / "websocket_events.json"
with open(FIXTURES_PATH) as f:
    FIXTURES = json.load(f)


@pytest.fixture
async def test_db():
    """Create an in-memory test database.

    We need to patch the db module-level variable before any repository
    methods are called, so they use our test database.
    """
    import app.repository as repo_module

    db = Database(":memory:")
    await db.connect()

    # Store original and patch the module attribute directly
    original_db = repo_module.db
    repo_module.db = db

    try:
        yield db
    finally:
        repo_module.db = original_db
        await db.disconnect()


@pytest.fixture
def captured_broadcasts():
    """Capture WebSocket broadcasts for verification."""
    broadcasts = []

    def mock_broadcast(event_type: str, data: dict):
        """Synchronous mock that captures broadcasts."""
        broadcasts.append({"type": event_type, "data": data})

    return broadcasts, mock_broadcast


class TestChannelMessagePipeline:
    """Test channel message flow: packet → decrypt → store → broadcast."""

    @pytest.mark.asyncio
    async def test_channel_message_creates_message_and_broadcasts(self, test_db, captured_broadcasts):
        """A decryptable channel packet creates a message and broadcasts it."""
        from app.packet_processor import process_raw_packet

        fixture = FIXTURES["channel_message"]
        packet_bytes = bytes.fromhex(fixture["raw_packet_hex"])

        # Create the channel in DB first using upsert
        await ChannelRepository.upsert(
            key=fixture["channel_key_hex"].upper(),
            name=fixture["channel_name"],
            is_hashtag=True
        )

        broadcasts, mock_broadcast = captured_broadcasts

        with patch("app.packet_processor.broadcast_event", mock_broadcast):
            result = await process_raw_packet(packet_bytes, timestamp=1700000000)

        # Verify packet was processed successfully
        assert result is not None
        assert result.get("decrypted") is True

        # Verify message was stored in database
        messages = await MessageRepository.get_all(
            msg_type="CHAN",
            conversation_key=fixture["channel_key_hex"].upper(),
            limit=10
        )
        assert len(messages) == 1
        msg = messages[0]
        assert "Flightless🥝:" in msg.text
        assert "hashtag room is essentially public" in msg.text

        # Verify WebSocket broadcast format matches fixture
        message_broadcasts = [b for b in broadcasts if b["type"] == "message"]
        assert len(message_broadcasts) == 1

        broadcast = message_broadcasts[0]
        expected = fixture["expected_ws_event"]["data"]
        assert broadcast["data"]["type"] == expected["type"]
        assert broadcast["data"]["conversation_key"] == expected["conversation_key"]
        assert broadcast["data"]["outgoing"] == expected["outgoing"]
        assert expected["text"][:30] in broadcast["data"]["text"]  # Check text contains expected content

    @pytest.mark.asyncio
    async def test_duplicate_packet_not_broadcast_twice(self, test_db, captured_broadcasts):
        """Same packet arriving twice only creates one message and one broadcast."""
        from app.packet_processor import process_raw_packet

        fixture = FIXTURES["duplicate_channel_message"]
        packet_bytes = bytes.fromhex(fixture["raw_packet_hex"])
        channel_key_hex = "7ABA109EDCF304A84433CB71D0F3AB73"

        # Create the channel in DB first
        await ChannelRepository.upsert(
            key=channel_key_hex,
            name=fixture["channel_name"],
            is_hashtag=True
        )

        broadcasts, mock_broadcast = captured_broadcasts

        with patch("app.packet_processor.broadcast_event", mock_broadcast):
            # Process same packet twice
            result1 = await process_raw_packet(packet_bytes, timestamp=1700000000)
            result2 = await process_raw_packet(packet_bytes, timestamp=1700000001)

        # First should succeed, second should be detected as duplicate
        assert result1 is not None
        assert result1.get("decrypted") is True

        # Second packet still processes but message is deduplicated
        assert result2 is not None

        # Only ONE message should exist in database
        messages = await MessageRepository.get_all(
            msg_type="CHAN",
            conversation_key=channel_key_hex,
            limit=10
        )
        assert len(messages) == 1

        # Only ONE message broadcast should have been sent
        message_broadcasts = [b for b in broadcasts if b["type"] == "message"]
        assert len(message_broadcasts) == 1

    @pytest.mark.asyncio
    async def test_unknown_channel_stores_raw_packet_only(self, test_db, captured_broadcasts):
        """Packet for unknown channel is stored but not decrypted."""
        from app.packet_processor import process_raw_packet

        fixture = FIXTURES["channel_message"]
        packet_bytes = bytes.fromhex(fixture["raw_packet_hex"])

        # DON'T create the channel - simulate unknown channel

        broadcasts, mock_broadcast = captured_broadcasts

        with patch("app.packet_processor.broadcast_event", mock_broadcast):
            result = await process_raw_packet(packet_bytes, timestamp=1700000000)

        # Packet should be stored but not decrypted
        assert result is not None

        # Raw packet should be stored
        raw_packets = await RawPacketRepository.get_undecrypted(limit=10)
        assert len(raw_packets) >= 1

        # No message broadcast (only raw_packet broadcast)
        message_broadcasts = [b for b in broadcasts if b["type"] == "message"]
        assert len(message_broadcasts) == 0


class TestAdvertisementPipeline:
    """Test advertisement flow: packet → parse → upsert contact → broadcast."""

    @pytest.mark.asyncio
    async def test_advertisement_creates_contact_with_gps(self, test_db, captured_broadcasts):
        """Advertisement packet creates/updates contact with GPS coordinates."""
        from app.packet_processor import process_raw_packet

        fixture = FIXTURES["advertisement_with_gps"]
        packet_bytes = bytes.fromhex(fixture["raw_packet_hex"])

        broadcasts, mock_broadcast = captured_broadcasts

        with patch("app.packet_processor.broadcast_event", mock_broadcast):
            # Process the advertisement packet through the normal pipeline
            result = await process_raw_packet(packet_bytes, timestamp=1700000000)

        # Verify contact was created in database
        expected = fixture["expected_ws_event"]["data"]
        contact = await ContactRepository.get_by_key_prefix(expected["public_key"][:12])

        assert contact is not None
        assert contact.name == expected["name"]
        assert contact.type == expected["type"]
        assert contact.lat is not None
        assert contact.lon is not None
        assert abs(contact.lat - expected["lat"]) < 0.001
        assert abs(contact.lon - expected["lon"]) < 0.001

        # Verify WebSocket broadcast
        contact_broadcasts = [b for b in broadcasts if b["type"] == "contact"]
        assert len(contact_broadcasts) == 1

        broadcast = contact_broadcasts[0]
        assert broadcast["data"]["public_key"] == expected["public_key"]
        assert broadcast["data"]["name"] == expected["name"]
        assert broadcast["data"]["type"] == expected["type"]

    @pytest.mark.asyncio
    async def test_advertisement_updates_existing_contact(self, test_db, captured_broadcasts):
        """Advertisement for existing contact updates their info."""
        from app.packet_processor import process_raw_packet

        fixture = FIXTURES["advertisement_chat_node"]
        packet_bytes = bytes.fromhex(fixture["raw_packet_hex"])
        expected = fixture["expected_ws_event"]["data"]

        # Create existing contact with different/missing data
        await ContactRepository.upsert({
            "public_key": expected["public_key"],
            "name": "OldName",
            "type": 0,
            "lat": None,
            "lon": None
        })

        broadcasts, mock_broadcast = captured_broadcasts

        with patch("app.packet_processor.broadcast_event", mock_broadcast):
            await process_raw_packet(packet_bytes, timestamp=1700000000)

        # Verify contact was updated
        contact = await ContactRepository.get_by_key_prefix(expected["public_key"][:12])

        assert contact.name == expected["name"]  # Name updated
        assert contact.type == expected["type"]  # Type updated
        assert contact.lat is not None  # GPS added
        assert contact.lon is not None


class TestAckPipeline:
    """Test ACK flow: outgoing message → ACK received → broadcast update."""

    @pytest.mark.asyncio
    async def test_ack_updates_message_and_broadcasts(self, test_db, captured_broadcasts):
        """ACK receipt updates message ack count and broadcasts."""
        from app.event_handlers import on_ack, track_pending_ack
        from app.repository import MessageRepository

        # Create a message that's waiting for ACK (acked defaults to 0)
        msg_id = await MessageRepository.create(
            msg_type="PRIV",
            text="Hello",
            conversation_key="abc123def456789012345678901234567890123456789012345678901234",
            sender_timestamp=1700000000,
            received_at=1700000000,
            outgoing=True
        )

        # Track pending ACK
        ack_code = "test_ack_123"
        track_pending_ack(ack_code, message_id=msg_id, timeout_ms=30000)

        broadcasts, mock_broadcast = captured_broadcasts

        # Create a mock Event with the ACK code
        # on_ack expects event.payload.get("code")
        mock_event = MagicMock()
        mock_event.payload = {"code": ack_code}

        # Patch broadcast_event in the event_handlers module
        with patch("app.event_handlers.broadcast_event", mock_broadcast):
            await on_ack(mock_event)

        # Verify message was updated in database
        messages = await MessageRepository.get_all(
            msg_type="PRIV",
            conversation_key="abc123def456789012345678901234567890123456789012345678901234",
            limit=10
        )
        assert len(messages) == 1
        assert messages[0].acked == 1

        # Verify broadcast format matches fixture
        ack_broadcasts = [b for b in broadcasts if b["type"] == "message_acked"]
        assert len(ack_broadcasts) == 1

        expected = FIXTURES["message_acked"]["expected_ws_event"]["data"]
        broadcast = ack_broadcasts[0]
        assert "message_id" in broadcast["data"]
        assert "ack_count" in broadcast["data"]
        assert broadcast["data"]["ack_count"] == 1


class TestCreateMessageFromDecrypted:
    """Test the shared message creation function used by both real-time and historical decryption."""

    @pytest.mark.asyncio
    async def test_creates_message_and_broadcasts(self, test_db, captured_broadcasts):
        """create_message_from_decrypted creates message and broadcasts correctly."""
        from app.packet_processor import create_message_from_decrypted

        # Create a raw packet first (required for the function)
        packet_id = await RawPacketRepository.create(b"test_packet_data", 1700000000)

        broadcasts, mock_broadcast = captured_broadcasts

        with patch("app.packet_processor.broadcast_event", mock_broadcast):
            msg_id = await create_message_from_decrypted(
                packet_id=packet_id,
                channel_key="ABC123DEF456",
                sender="TestSender",
                message_text="Hello world",
                timestamp=1700000000,
                received_at=1700000001,
            )

        # Should return a message ID
        assert msg_id is not None
        assert isinstance(msg_id, int)

        # Verify message was stored in database
        messages = await MessageRepository.get_all(
            msg_type="CHAN",
            conversation_key="ABC123DEF456",
            limit=10
        )
        assert len(messages) == 1
        assert messages[0].text == "TestSender: Hello world"
        assert messages[0].sender_timestamp == 1700000000

        # Verify broadcast was sent with correct structure
        message_broadcasts = [b for b in broadcasts if b["type"] == "message"]
        assert len(message_broadcasts) == 1

        broadcast = message_broadcasts[0]["data"]
        assert broadcast["id"] == msg_id
        assert broadcast["type"] == "CHAN"
        assert broadcast["conversation_key"] == "ABC123DEF456"
        assert broadcast["text"] == "TestSender: Hello world"
        assert broadcast["sender_timestamp"] == 1700000000
        assert broadcast["received_at"] == 1700000001
        assert broadcast["path_len"] is None  # Historical decryption has no path info
        assert broadcast["outgoing"] is False
        assert broadcast["acked"] == 0

    @pytest.mark.asyncio
    async def test_handles_message_without_sender(self, test_db, captured_broadcasts):
        """create_message_from_decrypted handles messages without sender prefix."""
        from app.packet_processor import create_message_from_decrypted

        packet_id = await RawPacketRepository.create(b"test_packet_data_2", 1700000000)

        broadcasts, mock_broadcast = captured_broadcasts

        with patch("app.packet_processor.broadcast_event", mock_broadcast):
            msg_id = await create_message_from_decrypted(
                packet_id=packet_id,
                channel_key="ABC123DEF456",
                sender=None,  # No sender
                message_text="System message",
                timestamp=1700000000,
                received_at=1700000001,
            )

        assert msg_id is not None

        # Verify text is stored without sender prefix
        messages = await MessageRepository.get_all(
            msg_type="CHAN",
            conversation_key="ABC123DEF456",
            limit=10
        )
        assert len(messages) == 1
        assert messages[0].text == "System message"  # No "None: " prefix

    @pytest.mark.asyncio
    async def test_returns_none_for_duplicate(self, test_db, captured_broadcasts):
        """create_message_from_decrypted returns None for duplicate message."""
        from app.packet_processor import create_message_from_decrypted

        packet_id_1 = await RawPacketRepository.create(b"packet_1", 1700000000)
        packet_id_2 = await RawPacketRepository.create(b"packet_2", 1700000001)

        broadcasts, mock_broadcast = captured_broadcasts

        with patch("app.packet_processor.broadcast_event", mock_broadcast):
            # First call creates the message
            msg_id_1 = await create_message_from_decrypted(
                packet_id=packet_id_1,
                channel_key="ABC123DEF456",
                sender="Sender",
                message_text="Duplicate test",
                timestamp=1700000000,
                received_at=1700000001,
            )

            # Second call with same content (different packet) returns None
            msg_id_2 = await create_message_from_decrypted(
                packet_id=packet_id_2,
                channel_key="ABC123DEF456",
                sender="Sender",
                message_text="Duplicate test",
                timestamp=1700000000,  # Same sender_timestamp
                received_at=1700000002,
            )

        assert msg_id_1 is not None
        assert msg_id_2 is None  # Duplicate detected

        # Only one message broadcast
        message_broadcasts = [b for b in broadcasts if b["type"] == "message"]
        assert len(message_broadcasts) == 1

    @pytest.mark.asyncio
    async def test_links_raw_packet_to_message(self, test_db, captured_broadcasts):
        """create_message_from_decrypted links raw packet to created message."""
        from app.packet_processor import create_message_from_decrypted

        packet_id = await RawPacketRepository.create(b"test_packet", 1700000000)

        broadcasts, mock_broadcast = captured_broadcasts

        with patch("app.packet_processor.broadcast_event", mock_broadcast):
            msg_id = await create_message_from_decrypted(
                packet_id=packet_id,
                channel_key="ABC123DEF456",
                sender="Sender",
                message_text="Link test",
                timestamp=1700000000,
                received_at=1700000001,
            )

        # Verify packet is marked decrypted
        undecrypted = await RawPacketRepository.get_undecrypted(limit=100)
        packet_ids = [p[0] for p in undecrypted]
        assert packet_id not in packet_ids  # Should be marked as decrypted


class TestMessageBroadcastStructure:
    """Test that message broadcasts have the correct structure for frontend."""

    @pytest.mark.asyncio
    async def test_realtime_broadcast_includes_path_len(self, test_db, captured_broadcasts):
        """Real-time packet processing includes path_len in broadcast."""
        from app.packet_processor import process_raw_packet

        fixture = FIXTURES["channel_message"]
        packet_bytes = bytes.fromhex(fixture["raw_packet_hex"])
        channel_key_hex = fixture["channel_key_hex"].upper()

        await ChannelRepository.upsert(
            key=channel_key_hex,
            name=fixture["channel_name"],
            is_hashtag=True
        )

        broadcasts, mock_broadcast = captured_broadcasts

        with patch("app.packet_processor.broadcast_event", mock_broadcast):
            await process_raw_packet(packet_bytes, timestamp=1700000000)

        message_broadcasts = [b for b in broadcasts if b["type"] == "message"]
        assert len(message_broadcasts) == 1

        broadcast = message_broadcasts[0]["data"]
        # Real-time processing extracts path_len from packet (flood packets have path_len=0)
        assert "path_len" in broadcast
        # The test packet is a flood packet, so path_len should be 0 or None depending on packet structure


class TestRawPacketStorage:
    """Test raw packet storage for later decryption."""

    @pytest.mark.asyncio
    async def test_raw_packet_stored_with_decryption_status(self, test_db, captured_broadcasts):
        """Raw packets are stored with correct decryption status."""
        from app.packet_processor import process_raw_packet

        fixture = FIXTURES["channel_message"]
        packet_bytes = bytes.fromhex(fixture["raw_packet_hex"])
        channel_key_hex = fixture["channel_key_hex"].upper()

        # Create channel so packet can be decrypted
        await ChannelRepository.upsert(
            key=channel_key_hex,
            name=fixture["channel_name"],
            is_hashtag=True
        )

        broadcasts, mock_broadcast = captured_broadcasts

        with patch("app.packet_processor.broadcast_event", mock_broadcast):
            result = await process_raw_packet(packet_bytes, timestamp=1700000000)

        # Verify raw_packet broadcast was sent
        raw_broadcasts = [b for b in broadcasts if b["type"] == "raw_packet"]
        assert len(raw_broadcasts) == 1

        # Verify broadcast includes decryption info
        raw_broadcast = raw_broadcasts[0]["data"]
        assert raw_broadcast["decrypted"] is True
        assert "decrypted_info" in raw_broadcast
        assert raw_broadcast["decrypted_info"]["channel_name"] == fixture["channel_name"]
