"""Tests for MemoryClient CRUD operations (get, get_all, delete, delete_all)."""

from __future__ import annotations

import uuid
from contextlib import asynccontextmanager
from datetime import UTC, datetime
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from arandu.client import EntityDetail, FactDetail, MemoryClient

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

AGENT_ID = "test-agent"
FACT_ID = uuid.uuid4()
NOW = datetime.now(tz=UTC)


def _make_orm_fact(
    fact_id: uuid.UUID = FACT_ID,
    agent_id: str = AGENT_ID,
    **overrides: object,
) -> MagicMock:
    """Build a MagicMock that looks like a MemoryFact ORM row."""
    defaults = {
        "id": fact_id,
        "agent_id": agent_id,
        "entity_name": "Rafael",
        "entity_key": "person::rafael",
        "entity_type": "person",
        "attribute_key": "location",
        "fact_text": "Rafael lives in São Paulo",
        "category": "personal",
        "confidence": 0.95,
        "importance": 0.5,
        "valid_from": NOW,
        "created_at": NOW,
        "source_context": None,
    }
    defaults.update(overrides)
    mock = MagicMock()
    for k, v in defaults.items():
        setattr(mock, k, v)
    return mock


def _make_client() -> MemoryClient:
    """Create a MemoryClient with mocked providers (no real DB)."""
    with patch("arandu.client.create_engine"), patch("arandu.client.create_session_factory"):
        return MemoryClient(
            database_url="postgresql+psycopg://fake:fake@localhost/fake",
            llm=MagicMock(),
            embeddings=MagicMock(),
        )


def _mock_session_factory(session: AsyncMock) -> MagicMock:
    """Wrap an AsyncMock session in an async context manager factory."""

    @asynccontextmanager
    async def _factory():  # type: ignore[no-untyped-def]
        yield session

    return _factory


# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------


class TestGet:
    """Tests for MemoryClient.get()."""

    @pytest.mark.asyncio
    async def test_returns_fact_detail_when_found(self) -> None:
        orm_fact = _make_orm_fact()

        session = AsyncMock()
        mock_result = MagicMock()
        mock_result.scalar_one_or_none.return_value = orm_fact
        session.execute.return_value = mock_result

        client = _make_client()
        client._session_factory = _mock_session_factory(session)

        detail = await client.get(AGENT_ID, str(FACT_ID))

        assert detail is not None
        assert isinstance(detail, FactDetail)
        assert detail.fact_id == str(FACT_ID)
        assert detail.entity_name == "Rafael"
        assert detail.entity_key == "person::rafael"
        assert detail.entity_type == "person"
        assert detail.fact_text == "Rafael lives in São Paulo"
        assert detail.confidence == 0.95

    @pytest.mark.asyncio
    async def test_returns_none_when_not_found(self) -> None:
        session = AsyncMock()
        mock_result = MagicMock()
        mock_result.scalar_one_or_none.return_value = None
        session.execute.return_value = mock_result

        client = _make_client()
        client._session_factory = _mock_session_factory(session)

        detail = await client.get(AGENT_ID, str(uuid.uuid4()))

        assert detail is None

    @pytest.mark.asyncio
    async def test_returns_none_for_invalid_uuid(self) -> None:
        client = _make_client()
        client._session_factory = _mock_session_factory(AsyncMock())

        detail = await client.get(AGENT_ID, "not-a-uuid")

        assert detail is None


class TestGetAll:
    """Tests for MemoryClient.get_all()."""

    @pytest.mark.asyncio
    async def test_returns_list_of_fact_details(self) -> None:
        facts = [_make_orm_fact(fact_id=uuid.uuid4()) for _ in range(3)]

        session = AsyncMock()
        mock_result = MagicMock()
        mock_result.scalars.return_value.all.return_value = facts
        session.execute.return_value = mock_result

        client = _make_client()
        client._session_factory = _mock_session_factory(session)

        details = await client.get_all(AGENT_ID)

        assert len(details) == 3
        assert all(isinstance(d, FactDetail) for d in details)

    @pytest.mark.asyncio
    async def test_returns_empty_list_when_no_facts(self) -> None:
        session = AsyncMock()
        mock_result = MagicMock()
        mock_result.scalars.return_value.all.return_value = []
        session.execute.return_value = mock_result

        client = _make_client()
        client._session_factory = _mock_session_factory(session)

        details = await client.get_all(AGENT_ID)

        assert details == []

    @pytest.mark.asyncio
    async def test_passes_limit_and_offset(self) -> None:
        session = AsyncMock()
        mock_result = MagicMock()
        mock_result.scalars.return_value.all.return_value = []
        session.execute.return_value = mock_result

        client = _make_client()
        client._session_factory = _mock_session_factory(session)

        await client.get_all(AGENT_ID, limit=10, offset=5)

        # Verify execute was called (the query itself carries limit/offset)
        session.execute.assert_called_once()


class TestDelete:
    """Tests for MemoryClient.delete()."""

    @pytest.mark.asyncio
    async def test_returns_true_when_deleted(self) -> None:
        session = AsyncMock()
        mock_result = MagicMock()
        mock_result.rowcount = 1
        session.execute.return_value = mock_result

        client = _make_client()
        client._session_factory = _mock_session_factory(session)

        deleted = await client.delete(AGENT_ID, str(FACT_ID))

        assert deleted is True
        session.commit.assert_called_once()

    @pytest.mark.asyncio
    async def test_returns_false_when_not_found(self) -> None:
        session = AsyncMock()
        mock_result = MagicMock()
        mock_result.rowcount = 0
        session.execute.return_value = mock_result

        client = _make_client()
        client._session_factory = _mock_session_factory(session)

        deleted = await client.delete(AGENT_ID, str(uuid.uuid4()))

        assert deleted is False
        session.commit.assert_called_once()

    @pytest.mark.asyncio
    async def test_returns_false_for_invalid_uuid(self) -> None:
        session = AsyncMock()

        client = _make_client()
        client._session_factory = _mock_session_factory(session)

        deleted = await client.delete(AGENT_ID, "not-a-uuid")

        assert deleted is False


class TestDeleteAll:
    """Tests for MemoryClient.delete_all()."""

    @pytest.mark.asyncio
    async def test_returns_count_when_deleted(self) -> None:
        session = AsyncMock()
        mock_result = MagicMock()
        mock_result.rowcount = 5
        session.execute.return_value = mock_result

        client = _make_client()
        client._session_factory = _mock_session_factory(session)

        count = await client.delete_all(AGENT_ID)

        assert count == 5
        session.commit.assert_called_once()

    @pytest.mark.asyncio
    async def test_returns_zero_when_no_facts(self) -> None:
        session = AsyncMock()
        mock_result = MagicMock()
        mock_result.rowcount = 0
        session.execute.return_value = mock_result

        client = _make_client()
        client._session_factory = _mock_session_factory(session)

        count = await client.delete_all(AGENT_ID)

        assert count == 0
        session.commit.assert_called_once()


# ---------------------------------------------------------------------------
# Entity helpers
# ---------------------------------------------------------------------------


def _make_orm_entity(
    entity_id: uuid.UUID | None = None,
    agent_id: str = AGENT_ID,
    **overrides: object,
) -> MagicMock:
    """Build a MagicMock that looks like a MemoryEntity ORM row."""
    defaults = {
        "id": entity_id or uuid.uuid4(),
        "agent_id": agent_id,
        "canonical_key": "person::rafael",
        "display_name": "Rafael",
        "entity_type": "person",
        "summary_text": "Software engineer in São Paulo",
        "fact_count": 5,
        "importance_score": 0.8,
        "first_seen_at": NOW,
        "last_seen_at": NOW,
        "profile_text": None,
        "is_active": True,
    }
    defaults.update(overrides)
    mock = MagicMock()
    for k, v in defaults.items():
        setattr(mock, k, v)
    return mock


class TestEntities:
    """Tests for MemoryClient.entities()."""

    @pytest.mark.asyncio
    async def test_returns_list_of_entity_details(self) -> None:
        entities = [_make_orm_entity(entity_id=uuid.uuid4()) for _ in range(3)]

        session = AsyncMock()
        mock_result = MagicMock()
        mock_result.scalars.return_value.all.return_value = entities
        session.execute.return_value = mock_result

        client = _make_client()
        client._session_factory = _mock_session_factory(session)

        details = await client.entities(AGENT_ID)

        assert len(details) == 3
        assert all(isinstance(d, EntityDetail) for d in details)
        assert details[0].canonical_key == "person::rafael"
        assert details[0].display_name == "Rafael"
        assert details[0].entity_type == "person"

    @pytest.mark.asyncio
    async def test_returns_empty_list_when_no_entities(self) -> None:
        session = AsyncMock()
        mock_result = MagicMock()
        mock_result.scalars.return_value.all.return_value = []
        session.execute.return_value = mock_result

        client = _make_client()
        client._session_factory = _mock_session_factory(session)

        details = await client.entities(AGENT_ID)

        assert details == []

    @pytest.mark.asyncio
    async def test_passes_limit_and_offset(self) -> None:
        session = AsyncMock()
        mock_result = MagicMock()
        mock_result.scalars.return_value.all.return_value = []
        session.execute.return_value = mock_result

        client = _make_client()
        client._session_factory = _mock_session_factory(session)

        await client.entities(AGENT_ID, limit=10, offset=5)

        session.execute.assert_called_once()
