"""
Tests for Phase 37: Private Agent-to-Agent Direct Messaging.
"""

import json
import time
import pytest


@pytest.fixture(autouse=True)
def _isolate(tmp_path, monkeypatch):
    """Isolate all DM tests to a temporary directory."""
    dms = tmp_path / "dms"
    cursors = dms / "cursors"
    blocks = dms / "blocks"
    for d in (dms, cursors, blocks):
        d.mkdir(parents=True, exist_ok=True)

    import brynq.server as srv
    monkeypatch.setattr(srv, "BROKER_DIR", tmp_path)

    import brynq.direct_messaging as dm
    monkeypatch.setattr(dm, "DMS_DIR", dms)
    monkeypatch.setattr(dm, "DM_CURSORS_DIR", cursors)
    monkeypatch.setattr(dm, "DM_BLOCKS_DIR", blocks)


# ── Send & Read DMs ────────────────────────────────────────────────────────


class TestSendDM:
    def test_send_dm_basic(self):
        from brynq.direct_messaging import send_dm
        result = send_dm("agent-B", "Hello!", sender_id="agent-A", sender_name="A")
        assert result["id"]
        assert result["message"] == "Hello!"
        assert result["from_session_id"] == "agent-A"
        assert result["to_session_id"] == "agent-B"
        assert result["msg_type"] == "info"

    def test_send_dm_custom_type(self):
        from brynq.direct_messaging import send_dm
        result = send_dm("agent-B", "Need help", msg_type="request",
                         sender_id="agent-A")
        assert result["msg_type"] == "request"

    def test_send_dm_invalid_type(self):
        from brynq.direct_messaging import send_dm
        result = send_dm("agent-B", "Hello", msg_type="telepathy",
                         sender_id="agent-A")
        assert "error" in result

    def test_send_dm_empty_message(self):
        from brynq.direct_messaging import send_dm
        result = send_dm("agent-B", "", sender_id="agent-A")
        assert "error" in result

    def test_send_dm_empty_whitespace(self):
        from brynq.direct_messaging import send_dm
        result = send_dm("agent-B", "   ", sender_id="agent-A")
        assert "error" in result

    def test_send_dm_empty_recipient(self):
        from brynq.direct_messaging import send_dm
        result = send_dm("", "Hello", sender_id="agent-A")
        assert "error" in result

    def test_self_dm_prevention(self):
        from brynq.direct_messaging import send_dm
        result = send_dm("agent-A", "Talking to myself", sender_id="agent-A")
        assert "error" in result
        assert "yourself" in result["error"]


class TestReadDMs:
    def test_read_specific_conversation(self):
        from brynq.direct_messaging import send_dm, read_dms
        send_dm("agent-B", "msg1", sender_id="agent-A")
        send_dm("agent-B", "msg2", sender_id="agent-A")

        result = read_dms(from_session_id="agent-A", reader_id="agent-B")
        assert result["count"] == 2
        assert result["messages"][0]["message"] == "msg1"
        assert result["messages"][1]["message"] == "msg2"

    def test_read_all_conversations(self):
        from brynq.direct_messaging import send_dm, read_dms
        send_dm("agent-B", "Hi B", sender_id="agent-A")
        send_dm("agent-C", "Hi C", sender_id="agent-A")

        result = read_dms(reader_id="agent-A", unread_only=False)
        assert result["count"] == 2

    def test_read_empty_inbox(self):
        from brynq.direct_messaging import read_dms
        result = read_dms(reader_id="agent-Z")
        assert result["count"] == 0
        assert result["messages"] == []


# ── Unread Tracking with Cursors ───────────────────────────────────────────


class TestUnreadTracking:
    def test_cursor_advances_on_read(self):
        from brynq.direct_messaging import send_dm, read_dms
        send_dm("agent-B", "msg1", sender_id="agent-A")
        send_dm("agent-B", "msg2", sender_id="agent-A")

        # First read — gets both
        r1 = read_dms(from_session_id="agent-A", reader_id="agent-B")
        assert r1["count"] == 2

        # Second read — nothing new
        r2 = read_dms(from_session_id="agent-A", reader_id="agent-B")
        assert r2["count"] == 0

    def test_new_message_after_read(self):
        from brynq.direct_messaging import send_dm, read_dms
        send_dm("agent-B", "first", sender_id="agent-A")

        read_dms(from_session_id="agent-A", reader_id="agent-B")

        send_dm("agent-B", "second", sender_id="agent-A")
        r = read_dms(from_session_id="agent-A", reader_id="agent-B")
        assert r["count"] == 1
        assert r["messages"][0]["message"] == "second"

    def test_unread_only_false_returns_all(self):
        from brynq.direct_messaging import send_dm, read_dms
        send_dm("agent-B", "msg1", sender_id="agent-A")
        read_dms(from_session_id="agent-A", reader_id="agent-B")

        send_dm("agent-B", "msg2", sender_id="agent-A")
        r = read_dms(from_session_id="agent-A", reader_id="agent-B",
                     unread_only=False)
        assert r["count"] == 2


# ── Conversation List ──────────────────────────────────────────────────────


class TestConversationList:
    def test_list_conversations_basic(self):
        from brynq.direct_messaging import send_dm, list_conversations
        send_dm("agent-B", "Hi B", sender_id="agent-A")
        send_dm("agent-C", "Hi C", sender_id="agent-A")

        result = list_conversations(session_id="agent-A")
        assert result["total_conversations"] == 2

    def test_list_conversations_unread_count(self):
        from brynq.direct_messaging import send_dm, read_dms, list_conversations
        send_dm("agent-A", "one", sender_id="agent-B")
        send_dm("agent-A", "two", sender_id="agent-B")
        send_dm("agent-A", "three", sender_id="agent-B")

        convs = list_conversations(session_id="agent-A")
        assert convs["conversations"][0]["unread_count"] == 3

        # Read them
        read_dms(from_session_id="agent-B", reader_id="agent-A")

        convs2 = list_conversations(session_id="agent-A")
        assert convs2["conversations"][0]["unread_count"] == 0

    def test_list_conversations_last_message_preview(self):
        from brynq.direct_messaging import send_dm, list_conversations
        send_dm("agent-B", "first", sender_id="agent-A")
        send_dm("agent-B", "latest message here", sender_id="agent-A")

        convs = list_conversations(session_id="agent-A")
        assert "latest message here" in convs["conversations"][0]["last_message"]

    def test_empty_conversation_list(self):
        from brynq.direct_messaging import list_conversations
        result = list_conversations(session_id="agent-nobody")
        assert result["total_conversations"] == 0
        assert result["conversations"] == []


# ── Conversation History with Pagination ───────────────────────────────────


class TestConversationHistory:
    def test_get_conversation_basic(self):
        from brynq.direct_messaging import send_dm, get_conversation
        for i in range(5):
            send_dm("agent-B", f"msg-{i}", sender_id="agent-A")

        result = get_conversation("agent-B", session_id="agent-A")
        assert result["count"] == 5
        assert result["total"] == 5
        assert result["has_more"] is False

    def test_get_conversation_with_limit(self):
        from brynq.direct_messaging import send_dm, get_conversation
        for i in range(10):
            send_dm("agent-B", f"msg-{i}", sender_id="agent-A")

        result = get_conversation("agent-B", limit=3, session_id="agent-A")
        assert result["count"] == 3
        assert result["has_more"] is True
        # Should return the LAST 3 messages
        assert result["messages"][0]["message"] == "msg-7"
        assert result["messages"][2]["message"] == "msg-9"

    def test_get_conversation_with_before(self):
        from brynq.direct_messaging import send_dm, get_conversation
        send_dm("agent-B", "early", sender_id="agent-A")
        # Small sleep to ensure distinct timestamps
        time.sleep(0.01)
        m2 = send_dm("agent-B", "middle", sender_id="agent-A")
        time.sleep(0.01)
        send_dm("agent-B", "late", sender_id="agent-A")

        ts = m2["timestamp"]
        result = get_conversation("agent-B", before=ts, session_id="agent-A")
        # Only 'early' is before the 'middle' timestamp
        assert result["count"] == 1
        assert result["messages"][0]["message"] == "early"

    def test_get_empty_conversation(self):
        from brynq.direct_messaging import get_conversation
        result = get_conversation("agent-nobody", session_id="agent-A")
        assert result["count"] == 0
        assert result["messages"] == []


# ── Mark Read ──────────────────────────────────────────────────────────────


class TestMarkRead:
    def test_mark_read_clears_unread(self):
        from brynq.direct_messaging import send_dm, mark_read, get_unread_count
        send_dm("agent-A", "hi", sender_id="agent-B")
        send_dm("agent-A", "there", sender_id="agent-B")

        unread_before = get_unread_count(session_id="agent-A")
        assert unread_before["total_unread"] == 2

        mark_read("agent-B", session_id="agent-A")

        unread_after = get_unread_count(session_id="agent-A")
        assert unread_after["total_unread"] == 0

    def test_mark_read_returns_cursor(self):
        from brynq.direct_messaging import send_dm, mark_read
        send_dm("agent-A", "msg", sender_id="agent-B")
        result = mark_read("agent-B", session_id="agent-A")
        assert result["status"] == "marked_read"
        assert result["cursor"] == 1


# ── Unread Count ───────────────────────────────────────────────────────────


class TestUnreadCount:
    def test_unread_count_multiple_conversations(self):
        from brynq.direct_messaging import send_dm, get_unread_count
        send_dm("agent-A", "msg1", sender_id="agent-B")
        send_dm("agent-A", "msg2", sender_id="agent-C")
        send_dm("agent-A", "msg3", sender_id="agent-C")

        result = get_unread_count(session_id="agent-A")
        assert result["total_unread"] == 3
        assert len(result["breakdown"]) == 2

    def test_unread_count_zero(self):
        from brynq.direct_messaging import get_unread_count
        result = get_unread_count(session_id="agent-fresh")
        assert result["total_unread"] == 0
        assert result["breakdown"] == []


# ── Block / Unblock ────────────────────────────────────────────────────────


class TestBlockUnblock:
    def test_block_prevents_sending(self):
        from brynq.direct_messaging import block_agent, send_dm
        block_agent("agent-A", reason="spam", blocker_id="agent-B")
        result = send_dm("agent-B", "you can't stop me", sender_id="agent-A")
        assert "error" in result
        assert "blocked" in result["error"]

    def test_unblock_restores_sending(self):
        from brynq.direct_messaging import block_agent, unblock_agent, send_dm
        block_agent("agent-A", reason="test", blocker_id="agent-B")
        unblock_agent("agent-A", blocker_id="agent-B")
        result = send_dm("agent-B", "I'm back", sender_id="agent-A")
        assert "error" not in result
        assert result["message"] == "I'm back"

    def test_is_blocked(self):
        from brynq.direct_messaging import block_agent, is_blocked
        block_agent("agent-X", blocker_id="agent-Y")
        assert is_blocked("agent-X", blocker_id="agent-Y") is True
        assert is_blocked("agent-Z", blocker_id="agent-Y") is False

    def test_list_blocked(self):
        from brynq.direct_messaging import block_agent, list_blocked
        block_agent("agent-X", reason="rude", blocker_id="agent-Y")
        block_agent("agent-Z", reason="spam", blocker_id="agent-Y")

        result = list_blocked(session_id="agent-Y")
        assert result["count"] == 2
        ids = [a["session_id"] for a in result["blocked_agents"]]
        assert "agent-X" in ids
        assert "agent-Z" in ids

    def test_unblock_not_blocked(self):
        from brynq.direct_messaging import unblock_agent
        result = unblock_agent("agent-nobody", blocker_id="agent-A")
        assert "error" in result

    def test_block_self_prevention(self):
        from brynq.direct_messaging import block_agent
        result = block_agent("agent-A", blocker_id="agent-A")
        assert "error" in result
        assert "yourself" in result["error"]


# ── Broadcast DM ──────────────────────────────────────────────────────────


class TestBroadcastDM:
    def test_broadcast_to_multiple(self):
        from brynq.direct_messaging import broadcast_dm, read_dms
        result = broadcast_dm(
            ["agent-B", "agent-C", "agent-D"],
            "Announcement!",
            sender_id="agent-A",
        )
        assert result["sent"] == 3
        assert result["failed"] == 0
        assert result["total"] == 3

        # Each recipient should have 1 unread
        for rid in ["agent-B", "agent-C", "agent-D"]:
            msgs = read_dms(from_session_id="agent-A", reader_id=rid)
            assert msgs["count"] == 1
            assert msgs["messages"][0]["message"] == "Announcement!"

    def test_broadcast_empty_list(self):
        from brynq.direct_messaging import broadcast_dm
        result = broadcast_dm([], "Hello", sender_id="agent-A")
        assert "error" in result

    def test_broadcast_empty_message(self):
        from brynq.direct_messaging import broadcast_dm
        result = broadcast_dm(["agent-B"], "", sender_id="agent-A")
        assert "error" in result

    def test_broadcast_with_blocked(self):
        from brynq.direct_messaging import block_agent, broadcast_dm
        block_agent("agent-A", blocker_id="agent-C")

        result = broadcast_dm(
            ["agent-B", "agent-C"],
            "Try to reach all",
            sender_id="agent-A",
        )
        assert result["sent"] == 1
        assert result["failed"] == 1

    def test_broadcast_skips_self(self):
        from brynq.direct_messaging import broadcast_dm
        result = broadcast_dm(
            ["agent-A", "agent-B"],
            "To all including me",
            sender_id="agent-A",
        )
        assert result["sent"] == 1
        assert result["failed"] == 1


# ── Message Ordering ──────────────────────────────────────────────────────


class TestMessageOrdering:
    def test_messages_chronological(self):
        from brynq.direct_messaging import send_dm, get_conversation
        for i in range(5):
            send_dm("agent-B", f"msg-{i}", sender_id="agent-A")

        result = get_conversation("agent-B", session_id="agent-A")
        timestamps = [m["timestamp"] for m in result["messages"]]
        assert timestamps == sorted(timestamps)

    def test_bidirectional_conversation(self):
        from brynq.direct_messaging import send_dm, get_conversation
        send_dm("agent-B", "hey B", sender_id="agent-A")
        send_dm("agent-A", "hey A", sender_id="agent-B")
        send_dm("agent-B", "how are you?", sender_id="agent-A")

        # Both sides see the same conversation
        r_a = get_conversation("agent-B", session_id="agent-A")
        r_b = get_conversation("agent-A", session_id="agent-B")
        assert r_a["count"] == 3
        assert r_b["count"] == 3
        assert r_a["conversation_key"] == r_b["conversation_key"]


# ── Conversation Key Determinism ──────────────────────────────────────────


class TestConversationKey:
    def test_key_is_symmetric(self):
        from brynq.direct_messaging import _conversation_key
        k1 = _conversation_key("agent-A", "agent-B")
        k2 = _conversation_key("agent-B", "agent-A")
        assert k1 == k2

    def test_key_is_unique_per_pair(self):
        from brynq.direct_messaging import _conversation_key
        k1 = _conversation_key("agent-A", "agent-B")
        k2 = _conversation_key("agent-A", "agent-C")
        assert k1 != k2
