"""Tests for worker-to-worker messaging via Engram.

Issue #52: Workers can't communicate directly — all coordination goes through orchestrator.
Solution: Ephemeral "inbox" in Engram with short TTL (30 min).
"""

from __future__ import annotations

from datetime import UTC, datetime, timedelta
from pathlib import Path
from unittest.mock import patch

import pytest

from claude_engram.categories import (
    CATEGORY_TTL,
    MemoryCategory,
    TTLTier,
    get_ttl_seconds,
    validate_category,
)
from claude_engram.daemon import MemoryDaemon
from claude_engram.storage import MemoryStorage


class TestWorkerMsgCategory:
    """Test the worker_msg category configuration."""

    def test_worker_msg_category_exists(self) -> None:
        """worker_msg should be a valid MemoryCategory."""
        assert hasattr(MemoryCategory, "WORKER_MSG")
        assert MemoryCategory.WORKER_MSG.value == "worker_msg"

    def test_worker_msg_validates(self) -> None:
        """worker_msg should pass category validation."""
        result = validate_category("worker_msg")
        assert result == MemoryCategory.WORKER_MSG

    def test_worker_msg_ttl_is_30_minutes(self) -> None:
        """worker_msg category should have 30-minute TTL."""
        ttl = get_ttl_seconds(MemoryCategory.WORKER_MSG)
        assert ttl == 1800  # 30 minutes in seconds

    def test_short_tier_exists(self) -> None:
        """SHORT TTL tier should exist for 30-minute TTL."""
        assert hasattr(TTLTier, "SHORT")
        assert TTLTier.SHORT.value == "short"

    def test_worker_msg_uses_short_tier(self) -> None:
        """worker_msg should map to SHORT TTL tier."""
        tier = CATEGORY_TTL[MemoryCategory.WORKER_MSG]
        assert tier == TTLTier.SHORT


class TestWorkerMsgStorage:
    """Test storing and retrieving worker messages."""

    @pytest.fixture
    def storage(self, tmp_path: Path) -> MemoryStorage:
        """Create a temporary storage instance."""
        return MemoryStorage(db_path=tmp_path / "test.db")

    def test_store_worker_message(self, storage: MemoryStorage) -> None:
        """Should store worker_msg with proper TTL."""
        content = "TO:W2 FROM:W1 — Test message"
        mem_id = storage.store(content, category="worker_msg")
        assert mem_id > 0

        # Verify it's stored with correct category
        conn = storage._get_conn()
        row = conn.execute(
            "SELECT category, ttl_tier, expires_at FROM memories WHERE id = ?",
            (mem_id,),
        ).fetchone()
        assert row["category"] == "worker_msg"
        assert row["ttl_tier"] == "short"
        assert row["expires_at"] is not None

        # Verify expires_at is ~30 min from now
        expires = datetime.fromisoformat(row["expires_at"])
        now = datetime.now(tz=UTC)
        delta = expires - now
        # Should be close to 30 min (allow 1 min tolerance)
        assert 29 * 60 <= delta.total_seconds() <= 31 * 60

    def test_worker_message_expires(self, storage: MemoryStorage) -> None:
        """Worker messages should expire after 30 minutes."""
        content = "TO:W2 FROM:W1 — Expiring message"
        mem_id = storage.store(content, category="worker_msg")

        # Manually set expires_at to the past
        conn = storage._get_conn()
        past = (datetime.now(tz=UTC) - timedelta(minutes=1)).isoformat()
        conn.execute("UPDATE memories SET expires_at = ? WHERE id = ?", (past, mem_id))
        conn.commit()

        # Cleanup should remove it
        count = storage.cleanup_expired()
        assert count == 1

        # Verify it's gone
        result = storage.get_by_id(mem_id)
        assert result is None


class TestInboxAction:
    """Test the inbox daemon action for retrieving worker messages."""

    @pytest.fixture
    def daemon(self) -> MemoryDaemon:
        """Create a daemon instance with mock storage path."""
        with patch.object(MemoryStorage, "__init__", lambda self: None):
            d = MemoryDaemon()
            d.storage = MemoryStorage.__new__(MemoryStorage)
        return d

    @pytest.fixture
    def real_daemon(self, tmp_path: Path) -> MemoryDaemon:
        """Create a real daemon with temporary storage."""
        daemon = MemoryDaemon()
        daemon.storage = MemoryStorage(db_path=tmp_path / "test.db")
        return daemon

    @pytest.mark.asyncio
    async def test_inbox_returns_messages_for_target(self, real_daemon: MemoryDaemon) -> None:
        """inbox action should return messages addressed to target worker."""
        # Store messages for different workers
        real_daemon.storage.store("TO:W2 FROM:W1 — Message for W2", category="worker_msg")
        real_daemon.storage.store("TO:W1 FROM:W3 — Message for W1", category="worker_msg")
        real_daemon.storage.store("TO:W2 FROM:W3 — Another for W2", category="worker_msg")

        # Query inbox for W2
        response = await real_daemon._process_request(
            {
                "action": "inbox",
                "worker_id": "W2",
            }
        )

        assert response["status"] == "ok"
        assert response["count"] == 2
        messages = response["messages"]
        assert len(messages) == 2
        assert all("TO:W2" in m["content"] for m in messages)

    @pytest.mark.asyncio
    async def test_inbox_empty_for_no_messages(self, real_daemon: MemoryDaemon) -> None:
        """inbox should return empty list when no messages exist."""
        response = await real_daemon._process_request(
            {
                "action": "inbox",
                "worker_id": "W99",
            }
        )

        assert response["status"] == "ok"
        assert response["count"] == 0
        assert response["messages"] == []

    @pytest.mark.asyncio
    async def test_inbox_requires_worker_id(self, real_daemon: MemoryDaemon) -> None:
        """inbox action should require worker_id parameter."""
        response = await real_daemon._process_request(
            {
                "action": "inbox",
            }
        )

        assert response["status"] == "error"
        assert "worker_id" in response["error"].lower()

    @pytest.mark.asyncio
    async def test_inbox_excludes_expired(self, real_daemon: MemoryDaemon) -> None:
        """inbox should not return expired messages."""
        # Store a message
        mem_id = real_daemon.storage.store(
            "TO:W2 FROM:W1 — Expired message",
            category="worker_msg",
        )

        # Expire it manually
        conn = real_daemon.storage._get_conn()
        past = (datetime.now(tz=UTC) - timedelta(minutes=1)).isoformat()
        conn.execute("UPDATE memories SET expires_at = ? WHERE id = ?", (past, mem_id))
        conn.commit()

        # Query inbox
        response = await real_daemon._process_request(
            {
                "action": "inbox",
                "worker_id": "W2",
            }
        )

        assert response["status"] == "ok"
        assert response["count"] == 0

    @pytest.mark.asyncio
    async def test_inbox_parses_message_format(self, real_daemon: MemoryDaemon) -> None:
        """inbox should parse FROM and message body from content."""
        real_daemon.storage.store(
            "TO:W2 FROM:W1 — Hello from W1",
            category="worker_msg",
        )

        response = await real_daemon._process_request(
            {
                "action": "inbox",
                "worker_id": "W2",
            }
        )

        assert response["count"] == 1
        msg = response["messages"][0]
        assert msg["from"] == "W1"
        assert msg["body"] == "Hello from W1"
        assert msg["to"] == "W2"
