"""Tests for persistent email-style inter-agent messaging."""

from __future__ import annotations

import pytest

from loom.graph import store
from loom.graph.project import get_project_status
from loom.graph.task import Message, ProjectStatus
from loom.ids import message_id as gen_message_id


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


async def _send(pool, project, from_agent="alice", to_agent="bob",
                body="hello", thread_id=None):
    return await store.create_message(
        pool, project, from_agent, to_agent, body, thread_id,
    )


# ===========================================================================
# Cycle 1: Domain model (unit tests, no DB)
# ===========================================================================


class TestDomainModel:
    """Pure unit tests — no database needed."""

    def test_message_id_prefix(self):
        mid = gen_message_id()
        assert mid.startswith("msg-")
        assert len(mid) == 12  # msg- + 8 hex chars

    def test_message_model_fields(self):
        m = Message(
            id="msg-test1234",
            project_id="some-uuid",
            from_agent="alice",
            to_agent="bob",
            thread_id="msg-test1234",
            body="hello",
        )
        assert m.from_agent == "alice"
        assert m.to_agent == "bob"
        assert m.body == "hello"
        assert m.is_read is False
        assert m.archived_at is None

    def test_project_status_has_unread_messages(self):
        ps = ProjectStatus(project_id="x", project_name="x")
        assert ps.unread_messages == 0


# ===========================================================================
# Cycle 2: Store CRUD (integration tests, needs DB fixtures)
# ===========================================================================


@pytest.mark.asyncio
async def test_create_message(pool, project):
    """Create a message and verify fields."""
    msg = await _send(pool, project)
    assert msg.from_agent == "alice"
    assert msg.to_agent == "bob"
    assert msg.body == "hello"
    assert msg.id.startswith("msg-")
    assert msg.project_id == project


@pytest.mark.asyncio
async def test_create_message_auto_thread(pool, project):
    """Thread ID auto-generated as the message ID when not provided."""
    msg = await _send(pool, project)
    assert msg.thread_id == msg.id


@pytest.mark.asyncio
async def test_create_message_with_thread(pool, project):
    """Explicit thread_id is preserved."""
    msg = await _send(pool, project, thread_id="custom-thread")
    assert msg.thread_id == "custom-thread"


@pytest.mark.asyncio
async def test_create_broadcast(pool, project):
    """Broadcast message has to_agent='broadcast'."""
    msg = await _send(pool, project, to_agent="broadcast", body="hey all")
    assert msg.to_agent == "broadcast"
    assert msg.body == "hey all"


# ===========================================================================
# Cycle 3: Inbox retrieval
# ===========================================================================


@pytest.mark.asyncio
async def test_get_inbox_direct(pool, project):
    """Direct messages appear in recipient's inbox."""
    await _send(pool, project, from_agent="alice", to_agent="bob", body="m1")
    await _send(pool, project, from_agent="alice", to_agent="charlie", body="m2")

    inbox = await store.get_inbox(pool, project, "bob")
    assert len(inbox) == 1
    assert inbox[0].body == "m1"


@pytest.mark.asyncio
async def test_get_inbox_broadcast(pool, project):
    """Broadcast messages appear in every agent's inbox."""
    await _send(pool, project, from_agent="alice", to_agent="broadcast", body="announce")

    inbox_bob = await store.get_inbox(pool, project, "bob")
    inbox_charlie = await store.get_inbox(pool, project, "charlie")
    assert len(inbox_bob) == 1
    assert len(inbox_charlie) == 1
    assert inbox_bob[0].body == "announce"


@pytest.mark.asyncio
async def test_get_inbox_unread_ordering(pool, project):
    """Unread messages appear before read ones."""
    m1 = await _send(pool, project, to_agent="bob", body="old")
    await store.mark_messages_read(pool, [m1.id], "bob")
    m2 = await _send(pool, project, to_agent="bob", body="new")

    inbox = await store.get_inbox(pool, project, "bob")
    assert len(inbox) == 2
    assert inbox[0].body == "new"  # unread first
    assert inbox[0].is_read is False
    assert inbox[1].is_read is True


@pytest.mark.asyncio
async def test_get_inbox_unread_only(pool, project):
    """unread_only=True filters out read messages."""
    m1 = await _send(pool, project, to_agent="bob", body="read-me")
    await store.mark_messages_read(pool, [m1.id], "bob")
    await _send(pool, project, to_agent="bob", body="unread")

    inbox = await store.get_inbox(pool, project, "bob", unread_only=True)
    assert len(inbox) == 1
    assert inbox[0].body == "unread"


@pytest.mark.asyncio
async def test_get_inbox_thread_filter(pool, project):
    """Filter inbox by thread_id."""
    await _send(pool, project, to_agent="bob", body="t1-m1", thread_id="thread-1")
    await _send(pool, project, to_agent="bob", body="t2-m1", thread_id="thread-2")
    await _send(pool, project, to_agent="bob", body="t1-m2", thread_id="thread-1")

    inbox = await store.get_inbox(pool, project, "bob", thread_id="thread-1")
    assert len(inbox) == 2
    bodies = {m.body for m in inbox}
    assert bodies == {"t1-m1", "t1-m2"}


# ===========================================================================
# Cycle 4: Mark read
# ===========================================================================


@pytest.mark.asyncio
async def test_mark_messages_read(pool, project):
    """Mark messages as read, verify is_read=True in inbox."""
    m1 = await _send(pool, project, to_agent="bob", body="msg")
    assert m1.is_read is False

    await store.mark_messages_read(pool, [m1.id], "bob")

    inbox = await store.get_inbox(pool, project, "bob")
    assert inbox[0].is_read is True


@pytest.mark.asyncio
async def test_mark_messages_read_idempotent(pool, project):
    """Marking the same message read twice doesn't error."""
    m1 = await _send(pool, project, to_agent="bob", body="msg")
    await store.mark_messages_read(pool, [m1.id], "bob")
    await store.mark_messages_read(pool, [m1.id], "bob")  # no error

    inbox = await store.get_inbox(pool, project, "bob")
    assert inbox[0].is_read is True


@pytest.mark.asyncio
async def test_mark_empty_list(pool, project):
    """Marking an empty list returns 0."""
    count = await store.mark_messages_read(pool, [], "bob")
    assert count == 0


# ===========================================================================
# Cycle 5: Unread count
# ===========================================================================


@pytest.mark.asyncio
async def test_get_unread_count(pool, project):
    """Count unread messages for an agent."""
    await _send(pool, project, to_agent="bob", body="m1")
    await _send(pool, project, to_agent="bob", body="m2")
    await _send(pool, project, to_agent="charlie", body="m3")

    assert await store.get_unread_count(pool, project, "bob") == 2
    assert await store.get_unread_count(pool, project, "charlie") == 1


@pytest.mark.asyncio
async def test_get_unread_count_with_broadcast(pool, project):
    """Broadcasts count toward each agent's unread count."""
    await _send(pool, project, to_agent="broadcast", body="hey")
    await _send(pool, project, to_agent="bob", body="direct")

    assert await store.get_unread_count(pool, project, "bob") == 2
    assert await store.get_unread_count(pool, project, "charlie") == 1


@pytest.mark.asyncio
async def test_unread_count_decreases_after_read(pool, project):
    """Marking read decreases unread count."""
    m1 = await _send(pool, project, to_agent="bob", body="m1")
    await _send(pool, project, to_agent="bob", body="m2")
    assert await store.get_unread_count(pool, project, "bob") == 2

    await store.mark_messages_read(pool, [m1.id], "bob")
    assert await store.get_unread_count(pool, project, "bob") == 1


# ===========================================================================
# Cycle 6: Archive (soft delete)
# ===========================================================================


@pytest.mark.asyncio
async def test_archive_message(pool, project):
    """Archived messages have archived_at set."""
    msg = await _send(pool, project, to_agent="bob")
    archived = await store.archive_message(pool, msg.id)
    assert archived.archived_at is not None


@pytest.mark.asyncio
async def test_archived_excluded_from_inbox(pool, project):
    """Archived messages don't appear in inbox."""
    msg = await _send(pool, project, to_agent="bob", body="gone")
    await _send(pool, project, to_agent="bob", body="still here")
    await store.archive_message(pool, msg.id)

    inbox = await store.get_inbox(pool, project, "bob")
    assert len(inbox) == 1
    assert inbox[0].body == "still here"


@pytest.mark.asyncio
async def test_archived_excluded_from_unread_count(pool, project):
    """Archived messages don't count as unread."""
    msg = await _send(pool, project, to_agent="bob")
    await store.archive_message(pool, msg.id)
    assert await store.get_unread_count(pool, project, "bob") == 0


@pytest.mark.asyncio
async def test_archive_nonexistent_raises(pool, project):
    """Archiving a missing message raises LookupError."""
    with pytest.raises(LookupError, match="not found"):
        await store.archive_message(pool, "msg-nonexist")


# ===========================================================================
# Cycle 7: Project status unread count
# ===========================================================================


@pytest.mark.asyncio
async def test_project_status_unread_count(pool, project):
    """Unread message count appears in project status."""
    await _send(pool, project, to_agent="bob", body="m1")
    await _send(pool, project, to_agent="charlie", body="m2")

    ps = await get_project_status(pool, project)
    assert ps.unread_messages == 2


@pytest.mark.asyncio
async def test_project_status_read_not_counted(pool, project):
    """Read messages don't count in project status."""
    m1 = await _send(pool, project, to_agent="bob", body="m1")
    await store.mark_messages_read(pool, [m1.id], "bob")

    ps = await get_project_status(pool, project)
    assert ps.unread_messages == 0
