"""Tests for sibyl-core graph/entities.py EntityManager."""

import json
from datetime import UTC, datetime
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from sibyl_core.backends.surreal import SurrealDriver
from sibyl_core.errors import EntityCreationError, EntityNotFoundError, SearchError
from sibyl_core.graph.entities import EntityManager, EntityNode, EpisodicNode, sanitize_search_query
from sibyl_core.models.entities import Entity, EntityType
from sibyl_core.models.tasks import (
    Epic,
    EpicStatus,
    Note,
    Project,
    ProjectStatus,
    Task,
    TaskPriority,
    TaskStatus,
)

# =============================================================================
# Fixtures
# =============================================================================


@pytest.fixture
def mock_driver() -> MagicMock:
    """Create a mock FalkorDB driver."""
    driver = MagicMock()
    driver.execute_query = AsyncMock(return_value=([], None, None))
    return driver


@pytest.fixture
def mock_graphiti_client(mock_driver: MagicMock) -> MagicMock:
    """Create a mock Graphiti client."""
    client = MagicMock()
    client.driver = mock_driver
    client.driver.clone = MagicMock(return_value=mock_driver)
    client.add_episode = AsyncMock()
    client.embedder = MagicMock()
    client.embedder.create = AsyncMock(return_value=[0.1] * 1536)
    client.search_ = AsyncMock()
    return client


@pytest.fixture
def mock_graph_client(mock_graphiti_client: MagicMock) -> MagicMock:
    """Create a mock GraphClient wrapper."""
    graph_client = MagicMock()
    graph_client.client = mock_graphiti_client
    graph_client.driver = mock_graphiti_client.driver
    graph_client.get_org_driver = MagicMock(return_value=mock_graphiti_client.driver)
    graph_client.normalize_result = MagicMock(side_effect=lambda x: x[0] if x else [])
    return graph_client


@pytest.fixture
def entity_manager(mock_graph_client: MagicMock) -> EntityManager:
    """Create EntityManager with mocked dependencies."""
    return EntityManager(mock_graph_client, group_id="test-org-123")


@pytest.fixture
def surreal_entity_manager() -> EntityManager:
    """Create EntityManager backed by a Surreal driver clone."""
    driver = SurrealDriver("memory://")
    org_driver = driver.clone("test-org-123")
    client = MagicMock()
    client.driver = driver
    client.add_episode = AsyncMock()
    client.embedder = MagicMock()
    client.embedder.create = AsyncMock(return_value=[0.1] * 1536)

    graph_client = MagicMock()
    graph_client.client = client
    graph_client.driver = driver
    graph_client.get_org_driver = MagicMock(return_value=org_driver)
    graph_client.normalize_result = MagicMock(side_effect=lambda x: x[0] if x else [])
    return EntityManager(graph_client, group_id="test-org-123")


@pytest.fixture
def sample_task() -> Task:
    """Create a sample task for testing."""
    return Task(
        id="task-001",
        name="Implement auth flow",
        title="Implement auth flow",
        description="Add OAuth2 authentication",
        status=TaskStatus.TODO,
        priority=TaskPriority.HIGH,
        project_id="project-001",
        feature="authentication",
        tags=["backend", "security"],
        technologies=["python", "oauth2"],
    )


@pytest.fixture
def sample_project() -> Project:
    """Create a sample project for testing."""
    return Project(
        id="project-001",
        name="Sibyl API",
        title="Sibyl API",
        description="Knowledge graph API server",
        status=ProjectStatus.ACTIVE,
        tech_stack=["python", "fastapi", "graphiti"],
        features=["task-management", "knowledge-graph"],
    )


@pytest.fixture
def sample_epic() -> Epic:
    """Create a sample epic for testing."""
    return Epic(
        id="epic-001",
        name="Authentication System",
        title="Authentication System",
        description="Complete auth implementation",
        status=EpicStatus.IN_PROGRESS,
        priority=TaskPriority.HIGH,
        project_id="project-001",
    )


@pytest.fixture
def sample_entity() -> Entity:
    """Create a generic entity for testing."""
    return Entity(
        id="entity-001",
        entity_type=EntityType.PATTERN,
        name="Repository Pattern",
        description="Data access abstraction layer",
        content="Use repositories for data access...",
        metadata={"category": "architecture"},
    )


@pytest.fixture
def sample_entity_node() -> EntityNode:
    """Create a mock EntityNode from Graphiti."""
    return EntityNode(
        uuid="entity-001",
        name="Test Entity",
        group_id="test-org-123",
        labels=["Entity", "pattern"],
        created_at=datetime.now(UTC),
        summary="A test entity summary",
        attributes={
            "entity_type": "pattern",
            "description": "Test description",
            "content": "Test content",
            "metadata": json.dumps({"category": "testing"}),
        },
    )


@pytest.fixture
def sample_episodic_node() -> EpisodicNode:
    """Create a mock EpisodicNode from Graphiti."""
    node = MagicMock(spec=EpisodicNode)
    node.uuid = "episode-001"
    node.name = "pattern:Test Episode"
    node.group_id = "test-org-123"
    node.content = "Episode content"
    node.source_description = "MCP Entity: pattern"
    node.created_at = datetime.now(UTC)
    return node


# =============================================================================
# EntityManager Initialization Tests
# =============================================================================


class TestEntityManagerInit:
    """Test EntityManager initialization and configuration."""

    def test_init_with_valid_group_id(self, mock_graph_client: MagicMock) -> None:
        """EntityManager initializes with valid group_id."""
        manager = EntityManager(mock_graph_client, group_id="org-123")
        assert manager._group_id == "org-123"
        assert manager._client == mock_graph_client

    def test_init_requires_group_id(self, mock_graph_client: MagicMock) -> None:
        """EntityManager requires non-empty group_id."""
        with pytest.raises(ValueError, match="group_id is required"):
            EntityManager(mock_graph_client, group_id="")

    def test_init_gets_org_driver_for_org(self, mock_graph_client: MagicMock) -> None:
        """EntityManager scopes itself through the graph runtime helper."""
        EntityManager(mock_graph_client, group_id="my-org")
        mock_graph_client.get_org_driver.assert_called_once_with("my-org")

    def test_surreal_like_driver_exposes_declared_node_ops(self) -> None:
        entity_ops = object()
        episode_ops = object()
        driver = SimpleNamespace(entity_node_ops=entity_ops, episode_node_ops=episode_ops)
        graph_client = MagicMock()
        graph_client.get_org_driver.return_value = driver
        manager = EntityManager(graph_client, group_id="org-123")

        assert manager._surreal_entity_node_ops() is entity_ops
        assert manager._surreal_episode_node_ops() is episode_ops


# =============================================================================
# Entity Creation Tests
# =============================================================================


class TestEntityCreate:
    """Test entity creation via add_episode."""

    @pytest.mark.asyncio
    async def test_create_uses_surreal_direct_path(
        self,
        surreal_entity_manager: EntityManager,
        sample_entity: Entity,
    ) -> None:
        with patch.object(
            surreal_entity_manager,
            "create_direct",
            new_callable=AsyncMock,
            return_value=sample_entity.id,
        ) as create_direct:
            result = await surreal_entity_manager.create(sample_entity)

        assert result == sample_entity.id
        create_direct.assert_awaited_once_with(sample_entity)
        surreal_entity_manager._client.client.add_episode.assert_not_called()

    @pytest.mark.asyncio
    async def test_create_surreal_episode_uses_episode_ops(
        self,
        surreal_entity_manager: EntityManager,
    ) -> None:
        episode = Entity(
            id="episode-001",
            entity_type=EntityType.EPISODE,
            name="Surreal lesson",
            description="Captured from a session",
            content="Graphiti should write the raw episode.",
            metadata={"project_id": "project-123"},
        )
        episode_ops = surreal_entity_manager._driver.episode_node_ops
        episode_ops.save = AsyncMock()
        surreal_entity_manager._driver.execute_query = AsyncMock()

        with patch.object(
            surreal_entity_manager,
            "create_direct",
            new_callable=AsyncMock,
        ) as create_direct:
            result = await surreal_entity_manager.create(episode)

        assert result == "episode-001"
        create_direct.assert_not_awaited()
        surreal_entity_manager._client.client.add_episode.assert_not_awaited()
        episode_ops.save.assert_awaited_once()
        saved_episode = episode_ops.save.await_args.args[1]
        assert saved_episode.uuid == "episode-001"
        assert saved_episode.name == "episode:Surreal lesson"
        assert saved_episode.content == "Graphiti should write the raw episode."
        assert saved_episode.source.value == "text"
        surreal_entity_manager._driver.execute_query.assert_awaited_once()

    @pytest.mark.asyncio
    async def test_create_refuses_surreal_legacy_fallback(
        self,
        surreal_entity_manager: EntityManager,
        sample_entity: Entity,
    ) -> None:
        surreal_entity_manager._driver.execute_query = AsyncMock()

        with (
            patch.object(surreal_entity_manager, "_surreal_entity_node_ops", return_value=None),
            patch.object(surreal_entity_manager, "_surreal_episode_node_ops", return_value=None),
            pytest.raises(RuntimeError, match="native node operations"),
        ):
            await surreal_entity_manager.create(sample_entity)

        surreal_entity_manager._client.client.add_episode.assert_not_awaited()
        surreal_entity_manager._driver.execute_query.assert_not_awaited()

    @pytest.mark.asyncio
    async def test_create_entity_success(
        self,
        entity_manager: EntityManager,
        sample_entity: Entity,
        mock_graph_client: MagicMock,
    ) -> None:
        """create() stores entity via add_episode and returns ID."""
        # Setup mock episode result
        mock_episode = MagicMock()
        mock_episode.uuid = "generated-uuid"
        mock_result = MagicMock()
        mock_result.episode = mock_episode
        mock_graph_client.client.add_episode.return_value = mock_result

        result = await entity_manager.create(sample_entity)

        # Should use provided entity ID
        assert result == sample_entity.id
        mock_graph_client.client.add_episode.assert_called_once()

    @pytest.mark.asyncio
    async def test_create_entity_sanitizes_name(
        self,
        entity_manager: EntityManager,
        mock_graph_client: MagicMock,
    ) -> None:
        """create() sanitizes special characters in entity name."""
        entity = Entity(
            id="entity-special",
            entity_type=EntityType.PATTERN,
            name="**Bold** _italic_ [link](url)",
            description="Test",
        )

        mock_episode = MagicMock()
        mock_episode.uuid = "uuid-123"
        mock_result = MagicMock()
        mock_result.episode = mock_episode
        mock_graph_client.client.add_episode.return_value = mock_result

        await entity_manager.create(entity)

        # Verify add_episode was called with sanitized name
        call_args = mock_graph_client.client.add_episode.call_args
        assert call_args is not None
        # Name should not contain markdown special chars
        name = call_args.kwargs.get("name", "")
        assert "**" not in name
        assert "_" not in name
        assert "[" not in name

    @pytest.mark.asyncio
    async def test_create_task_entity(
        self,
        entity_manager: EntityManager,
        sample_task: Task,
        mock_graph_client: MagicMock,
    ) -> None:
        """create() handles Task entities with all fields."""
        mock_episode = MagicMock()
        mock_episode.uuid = "task-uuid"
        mock_result = MagicMock()
        mock_result.episode = mock_episode
        mock_graph_client.client.add_episode.return_value = mock_result

        result = await entity_manager.create(sample_task)

        assert result == sample_task.id
        # Episode body should contain task-specific fields
        call_args = mock_graph_client.client.add_episode.call_args
        episode_body = call_args.kwargs.get("episode_body", "")
        assert "Status:" in episode_body
        assert "Priority:" in episode_body


class TestEntityCreateDirect:
    """Test direct entity creation bypassing LLM."""

    @pytest.mark.asyncio
    async def test_create_direct_uses_surreal_entity_ops(
        self,
        surreal_entity_manager: EntityManager,
        sample_entity: Entity,
    ) -> None:
        ops = surreal_entity_manager._driver.entity_node_ops
        ops.save = AsyncMock()

        with (
            patch.object(
                surreal_entity_manager,
                "_persist_entity_attributes",
                new_callable=AsyncMock,
            ) as persist_attrs,
            patch.object(EntityNode, "save", new_callable=AsyncMock) as legacy_save,
        ):
            result = await surreal_entity_manager.create_direct(
                sample_entity,
                generate_embedding=False,
            )

        assert result == sample_entity.id
        ops.save.assert_awaited_once()
        persist_attrs.assert_not_awaited()
        legacy_save.assert_not_called()

    @pytest.mark.asyncio
    async def test_create_direct_stores_embedding_via_surreal_entity_ops(
        self,
        surreal_entity_manager: EntityManager,
        sample_entity: Entity,
    ) -> None:
        ops = surreal_entity_manager._driver.entity_node_ops
        ops.save = AsyncMock()

        await surreal_entity_manager.create_direct(sample_entity, generate_embedding=True)

        assert ops.save.await_count == 2
        second_call_node = ops.save.await_args_list[1].args[1]
        assert second_call_node.name_embedding is not None

    @pytest.mark.asyncio
    async def test_create_direct_routes_surreal_episode_to_episode_ops(
        self,
        surreal_entity_manager: EntityManager,
    ) -> None:
        entity_ops = surreal_entity_manager._driver.entity_node_ops
        episode_ops = surreal_entity_manager._driver.episode_node_ops
        entity_ops.save = AsyncMock()
        episode_ops.save = AsyncMock()
        episode = Entity(
            id="episode-001",
            entity_type=EntityType.EPISODE,
            name="Task learning",
            description="Task completion knowledge",
            content="Direct episode writes must land in the episode table.",
        )

        result = await surreal_entity_manager.create_direct(episode)

        assert result == "episode-001"
        episode_ops.save.assert_awaited_once()
        entity_ops.save.assert_not_awaited()
        saved_node = episode_ops.save.await_args.args[1]
        assert isinstance(saved_node, EpisodicNode)
        assert saved_node.uuid == "episode-001"
        assert saved_node.content == "Direct episode writes must land in the episode table."

    @pytest.mark.asyncio
    async def test_create_direct_preserves_provided_embedding(
        self,
        surreal_entity_manager: EntityManager,
        sample_entity: Entity,
        mock_graph_client: MagicMock,
    ) -> None:
        ops = surreal_entity_manager._driver.entity_node_ops
        ops.save = AsyncMock()
        sample_entity.embedding = [0.1, 0.2, 0.3]

        await surreal_entity_manager.create_direct(sample_entity, generate_embedding=False)

        assert ops.save.await_count == 1
        saved_node = ops.save.await_args.args[1]
        assert saved_node.name_embedding == [0.1, 0.2, 0.3]
        mock_graph_client.client.embedder.create.assert_not_called()

    @pytest.mark.asyncio
    async def test_create_direct_persists_surreal_filter_fields(
        self,
        surreal_entity_manager: EntityManager,
        sample_task: Task,
    ) -> None:
        ops = surreal_entity_manager._driver.entity_node_ops
        ops.save = AsyncMock()

        await surreal_entity_manager.create_direct(sample_task, generate_embedding=False)

        saved_node = ops.save.await_args.args[1]
        assert saved_node.attributes["project_id"] == "project-001"
        assert saved_node.attributes["status"] == "todo"
        assert saved_node.attributes["priority"] == "high"
        assert saved_node.attributes["feature"] == "authentication"
        assert saved_node.attributes["tags"] == ["backend", "security"]

    @pytest.mark.asyncio
    async def test_create_direct_persists_note_task_id_for_surreal_filters(
        self,
        surreal_entity_manager: EntityManager,
    ) -> None:
        ops = surreal_entity_manager._driver.entity_node_ops
        ops.save = AsyncMock()
        note = Note(
            id="note-001",
            name="Progress note",
            content="Captured the Surreal filter path.",
            task_id="task-001",
            author_name="Nova",
        )

        await surreal_entity_manager.create_direct(note, generate_embedding=False)

        saved_node = ops.save.await_args.args[1]
        assert saved_node.attributes["task_id"] == "task-001"
        assert saved_node.attributes["author_type"] == "user"
        assert saved_node.attributes["author_name"] == "Nova"

    @pytest.mark.asyncio
    async def test_create_direct_success(
        self,
        entity_manager: EntityManager,
        sample_entity: Entity,
    ) -> None:
        """create_direct() creates entity via EntityNode.save()."""
        with patch.object(EntityNode, "save", new_callable=AsyncMock) as mock_save:
            result = await entity_manager.create_direct(sample_entity)

            assert result == sample_entity.id
            mock_save.assert_called_once()

    @pytest.mark.asyncio
    async def test_create_direct_generates_embedding(
        self,
        entity_manager: EntityManager,
        sample_entity: Entity,
        mock_graph_client: MagicMock,
    ) -> None:
        """create_direct() generates embeddings by default."""
        with patch.object(EntityNode, "save", new_callable=AsyncMock):
            await entity_manager.create_direct(sample_entity, generate_embedding=True)

            mock_graph_client.client.embedder.create.assert_called_once()

    @pytest.mark.asyncio
    async def test_create_direct_skip_embedding(
        self,
        entity_manager: EntityManager,
        sample_entity: Entity,
        mock_graph_client: MagicMock,
    ) -> None:
        """create_direct() can skip embedding generation."""
        with patch.object(EntityNode, "save", new_callable=AsyncMock):
            await entity_manager.create_direct(sample_entity, generate_embedding=False)

            mock_graph_client.client.embedder.create.assert_not_called()

    @pytest.mark.asyncio
    async def test_create_direct_failure_raises_error(
        self,
        entity_manager: EntityManager,
        sample_entity: Entity,
        mock_graph_client: MagicMock,
    ) -> None:
        """create_direct() raises EntityCreationError on failure."""
        # Mock EntityNode.save to raise an exception
        with (
            patch.object(
                EntityNode,
                "save",
                new_callable=AsyncMock,
                side_effect=Exception("DB error"),
            ),
            pytest.raises(EntityCreationError, match="Failed to create entity"),
        ):
            await entity_manager.create_direct(sample_entity)


# =============================================================================
# Entity Retrieval Tests
# =============================================================================


class TestEntityGet:
    """Test entity retrieval by ID."""

    @pytest.mark.asyncio
    async def test_get_entity_node_uses_surreal_entity_ops(
        self,
        surreal_entity_manager: EntityManager,
        sample_entity_node: EntityNode,
    ) -> None:
        ops = surreal_entity_manager._driver.entity_node_ops
        ops.get_by_uuid = AsyncMock(return_value=sample_entity_node)

        with patch.object(EntityNode, "get_by_uuid", new_callable=AsyncMock) as legacy_get:
            result = await surreal_entity_manager.get("entity-001")

        assert result.id == "entity-001"
        ops.get_by_uuid.assert_awaited_once_with(surreal_entity_manager._driver, "entity-001")
        legacy_get.assert_not_called()

    @pytest.mark.asyncio
    async def test_get_episodic_node_uses_surreal_episode_ops(
        self,
        surreal_entity_manager: EntityManager,
        sample_episodic_node: EpisodicNode,
    ) -> None:
        entity_ops = surreal_entity_manager._driver.entity_node_ops
        episode_ops = surreal_entity_manager._driver.episode_node_ops
        entity_ops.get_by_uuid = AsyncMock(side_effect=Exception("Not found"))
        episode_ops.get_by_uuid = AsyncMock(return_value=sample_episodic_node)

        with patch.object(
            surreal_entity_manager,
            "_get_node_entity_type",
            new_callable=AsyncMock,
        ) as get_entity_type:
            result = await surreal_entity_manager.get("episode-001")

        assert result.id == "episode-001"
        episode_ops.get_by_uuid.assert_awaited_once_with(
            surreal_entity_manager._driver,
            "episode-001",
        )
        get_entity_type.assert_not_awaited()

    @pytest.mark.asyncio
    async def test_get_skips_surreal_episode_lookup_for_typed_graph_ids(
        self,
        surreal_entity_manager: EntityManager,
    ) -> None:
        entity_ops = surreal_entity_manager._driver.entity_node_ops
        episode_ops = surreal_entity_manager._driver.episode_node_ops
        entity_ops.get_by_uuid = AsyncMock(side_effect=Exception("Not found"))
        episode_ops.get_by_uuid = AsyncMock()

        with pytest.raises(EntityNotFoundError):
            await surreal_entity_manager.get("project_abc123")

        entity_ops.get_by_uuid.assert_awaited_once_with(
            surreal_entity_manager._driver,
            "project_abc123",
        )
        episode_ops.get_by_uuid.assert_not_awaited()

    @pytest.mark.asyncio
    async def test_get_entity_node_success(
        self,
        entity_manager: EntityManager,
        sample_entity_node: EntityNode,
    ) -> None:
        """get() retrieves entity from EntityNode."""
        with patch.object(
            EntityNode,
            "get_by_uuid",
            new_callable=AsyncMock,
            return_value=sample_entity_node,
        ):
            result = await entity_manager.get("entity-001")

            assert result.id == "entity-001"
            assert result.name == "Test Entity"
            assert result.entity_type == EntityType.PATTERN

    @pytest.mark.asyncio
    async def test_get_episodic_node_fallback(
        self,
        entity_manager: EntityManager,
        sample_episodic_node: EpisodicNode,
    ) -> None:
        """get() falls back to EpisodicNode if EntityNode not found."""
        with (
            patch.object(
                EntityNode,
                "get_by_uuid",
                new_callable=AsyncMock,
                side_effect=Exception("Not found"),
            ),
            patch.object(
                EpisodicNode,
                "get_by_uuid",
                new_callable=AsyncMock,
                return_value=sample_episodic_node,
            ),
        ):
            result = await entity_manager.get("episode-001")

            assert result.id == "episode-001"
            assert result.entity_type == EntityType.PATTERN

    @pytest.mark.asyncio
    async def test_get_not_found_raises_error(
        self,
        entity_manager: EntityManager,
    ) -> None:
        """get() raises EntityNotFoundError when entity doesn't exist."""
        with (
            patch.object(
                EntityNode,
                "get_by_uuid",
                new_callable=AsyncMock,
                side_effect=Exception("Not found"),
            ),
            patch.object(
                EpisodicNode,
                "get_by_uuid",
                new_callable=AsyncMock,
                side_effect=Exception("Not found"),
            ),
            pytest.raises(EntityNotFoundError, match="Entity not found"),
        ):
            await entity_manager.get("nonexistent-id")

    @pytest.mark.asyncio
    async def test_get_filters_by_group_id(
        self,
        entity_manager: EntityManager,
    ) -> None:
        """get() only returns entities from the correct group."""
        # Create node with different group_id
        wrong_group_node = EntityNode(
            uuid="entity-001",
            name="Test",
            group_id="other-org",  # Different org
            labels=["Entity"],
            created_at=datetime.now(UTC),
            summary="Test",
            attributes={},
        )

        with (
            patch.object(
                EntityNode,
                "get_by_uuid",
                new_callable=AsyncMock,
                return_value=wrong_group_node,
            ),
            patch.object(
                EpisodicNode,
                "get_by_uuid",
                new_callable=AsyncMock,
                side_effect=Exception("Not found"),
            ),
            pytest.raises(EntityNotFoundError),
        ):
            await entity_manager.get("entity-001")


# =============================================================================
# Typed Hydration Tests
# =============================================================================


class TestTypedHydration:
    """Test typed hydration for EntityManager get/list_by_type."""

    @pytest.mark.asyncio
    async def test_get_hydrates_task_record(
        self,
        entity_manager: EntityManager,
    ) -> None:
        """get() returns Task when entity_type is task."""
        task_node = EntityNode(
            uuid="task-typed-001",
            name="Typed Task",
            group_id="test-org-123",
            labels=["Entity", "task"],
            created_at=datetime.now(UTC),
            summary="",
            attributes={
                "entity_type": "task",
                "description": "Task description",
                "content": "Task description",
                "metadata": json.dumps(
                    {
                        "status": "doing",
                        "priority": "high",
                        "project_id": "project-typed-001",
                    }
                ),
            },
        )

        with patch.object(
            EntityNode,
            "get_by_uuid",
            new_callable=AsyncMock,
            return_value=task_node,
        ):
            result = await entity_manager.get("task-typed-001")

        assert isinstance(result, Task)
        assert result.title == "Typed Task"
        assert result.status == TaskStatus.DOING
        assert result.priority == TaskPriority.HIGH
        assert result.project_id == "project-typed-001"

    def test_record_to_episode_entity_preserves_metadata_dict(
        self,
        entity_manager: EntityManager,
    ) -> None:
        entity = entity_manager._record_to_episode_entity(
            {
                "uuid": "episode-typed-001",
                "name": "episode:Scoped episode",
                "group_id": "test-org-123",
                "metadata": {"project_id": "project-secret", "category": "notes"},
                "content": "secret content",
                "source_description": "MCP Entity: episode",
            }
        )

        assert entity.metadata.get("project_id") == "project-secret"
        assert entity.metadata.get("category") == "notes"


# =============================================================================
# Entity Update Tests
# =============================================================================


class TestEntityUpdate:
    """Test entity update operations."""

    @pytest.mark.asyncio
    async def test_update_uses_surreal_entity_ops(
        self,
        surreal_entity_manager: EntityManager,
        sample_entity_node: EntityNode,
    ) -> None:
        ops = surreal_entity_manager._driver.entity_node_ops
        ops.get_by_uuid = AsyncMock(return_value=sample_entity_node)
        ops.save = AsyncMock()

        with patch.object(
            surreal_entity_manager,
            "_persist_entity_attributes",
            new_callable=AsyncMock,
        ) as persist_attrs:
            result = await surreal_entity_manager.update(
                "entity-001",
                {
                    "description": "Updated description",
                    "metadata": {"new_key": "new_value"},
                    "embedding": [0.2] * 1536,
                },
            )

        assert result is not None
        assert result.description == "Updated description"
        assert result.metadata["category"] == "testing"
        assert result.metadata["new_key"] == "new_value"
        ops.get_by_uuid.assert_awaited_once_with(surreal_entity_manager._driver, "entity-001")
        ops.save.assert_awaited_once()
        saved_node = ops.save.await_args.args[1]
        assert saved_node.name_embedding is not None
        persist_attrs.assert_not_awaited()

    @pytest.mark.asyncio
    async def test_update_partial(
        self,
        entity_manager: EntityManager,
        sample_entity_node: EntityNode,
        mock_driver: MagicMock,
    ) -> None:
        """update() applies partial updates preserving other fields."""
        with patch.object(
            EntityNode,
            "get_by_uuid",
            new_callable=AsyncMock,
            return_value=sample_entity_node,
        ):
            result = await entity_manager.update(
                "entity-001",
                {"description": "Updated description"},
            )

            assert result is not None
            assert result.description == "Updated description"
            # Name should be preserved
            assert result.name == "Test Entity"

    @pytest.mark.asyncio
    async def test_update_metadata_merge(
        self,
        entity_manager: EntityManager,
        sample_entity_node: EntityNode,
    ) -> None:
        """update() merges metadata rather than replacing."""
        with patch.object(
            EntityNode,
            "get_by_uuid",
            new_callable=AsyncMock,
            return_value=sample_entity_node,
        ):
            result = await entity_manager.update(
                "entity-001",
                {"metadata": {"new_key": "new_value"}},
            )

            assert result is not None
            # Original metadata should be preserved
            assert "category" in result.metadata
            # New metadata should be added
            assert result.metadata.get("new_key") == "new_value"

    @pytest.mark.asyncio
    async def test_update_not_found_raises_error(
        self,
        entity_manager: EntityManager,
    ) -> None:
        """update() raises EntityNotFoundError if entity doesn't exist."""
        with (
            patch.object(
                EntityNode,
                "get_by_uuid",
                new_callable=AsyncMock,
                side_effect=Exception("Not found"),
            ),
            patch.object(
                EpisodicNode,
                "get_by_uuid",
                new_callable=AsyncMock,
                side_effect=Exception("Not found"),
            ),
            pytest.raises(EntityNotFoundError),
        ):
            await entity_manager.update("nonexistent", {"name": "New Name"})

    @pytest.mark.asyncio
    async def test_update_embedding(
        self,
        entity_manager: EntityManager,
        sample_entity_node: EntityNode,
        mock_driver: MagicMock,
    ) -> None:
        """update() can store new embedding on node."""
        with patch.object(
            EntityNode,
            "get_by_uuid",
            new_callable=AsyncMock,
            return_value=sample_entity_node,
        ):
            embedding = [0.1] * 1536
            result = await entity_manager.update(
                "entity-001",
                {"embedding": embedding},
            )

            assert result is not None
            # Verify execute_query was called to set embedding
            mock_driver.execute_query.assert_called()


# =============================================================================
# Entity Delete Tests
# =============================================================================


class TestEntityDelete:
    """Test entity deletion operations."""

    @pytest.mark.asyncio
    async def test_delete_entity_node_uses_surreal_entity_ops(
        self,
        surreal_entity_manager: EntityManager,
    ) -> None:
        node = MagicMock(spec=EntityNode)
        node.uuid = "entity-001"
        node.group_id = "test-org-123"

        ops = surreal_entity_manager._driver.entity_node_ops
        ops.get_by_uuid = AsyncMock(return_value=node)
        ops.delete = AsyncMock()

        with patch.object(EntityNode, "get_by_uuid", new_callable=AsyncMock) as legacy_get:
            result = await surreal_entity_manager.delete("entity-001")

        assert result is True
        ops.get_by_uuid.assert_awaited_once_with(surreal_entity_manager._driver, "entity-001")
        ops.delete.assert_awaited_once_with(surreal_entity_manager._driver, node)
        legacy_get.assert_not_called()

    @pytest.mark.asyncio
    async def test_delete_episodic_node_uses_surreal_episode_ops(
        self,
        surreal_entity_manager: EntityManager,
    ) -> None:
        episodic = MagicMock(spec=EpisodicNode)
        episodic.uuid = "episode-001"
        episodic.group_id = "test-org-123"

        entity_ops = surreal_entity_manager._driver.entity_node_ops
        episode_ops = surreal_entity_manager._driver.episode_node_ops
        entity_ops.get_by_uuid = AsyncMock(side_effect=Exception("Not found"))
        episode_ops.get_by_uuid = AsyncMock(return_value=episodic)
        episode_ops.delete = AsyncMock()

        with patch.object(EpisodicNode, "get_by_uuid", new_callable=AsyncMock) as legacy_get:
            result = await surreal_entity_manager.delete("episode-001")

        assert result is True
        episode_ops.get_by_uuid.assert_awaited_once_with(
            surreal_entity_manager._driver,
            "episode-001",
        )
        episode_ops.delete.assert_awaited_once_with(surreal_entity_manager._driver, episodic)
        legacy_get.assert_not_called()

    @pytest.mark.asyncio
    async def test_delete_entity_node(
        self,
        entity_manager: EntityManager,
    ) -> None:
        """delete() removes entity via EntityNode.delete()."""
        # Create a mock entity node with the delete method as a MagicMock
        mock_entity = MagicMock(spec=EntityNode)
        mock_entity.uuid = "entity-001"
        mock_entity.group_id = "test-org-123"
        mock_entity.delete = AsyncMock()

        with patch.object(
            EntityNode,
            "get_by_uuid",
            new_callable=AsyncMock,
            return_value=mock_entity,
        ):
            result = await entity_manager.delete("entity-001")

            assert result is True
            mock_entity.delete.assert_called_once()

    @pytest.mark.asyncio
    async def test_delete_episodic_node_fallback(
        self,
        entity_manager: EntityManager,
    ) -> None:
        """delete() falls back to EpisodicNode if EntityNode not found."""
        # Create a mock episodic node with the delete method as a MagicMock
        mock_episodic = MagicMock(spec=EpisodicNode)
        mock_episodic.uuid = "episode-001"
        mock_episodic.group_id = "test-org-123"
        mock_episodic.delete = AsyncMock()

        with (
            patch.object(
                EntityNode,
                "get_by_uuid",
                new_callable=AsyncMock,
                side_effect=Exception("Not found"),
            ),
            patch.object(
                EpisodicNode,
                "get_by_uuid",
                new_callable=AsyncMock,
                return_value=mock_episodic,
            ),
        ):
            result = await entity_manager.delete("episode-001")

            assert result is True
            mock_episodic.delete.assert_called_once()

    @pytest.mark.asyncio
    async def test_delete_not_found_raises_error(
        self,
        entity_manager: EntityManager,
    ) -> None:
        """delete() raises EntityNotFoundError if entity doesn't exist."""
        with (
            patch.object(
                EntityNode,
                "get_by_uuid",
                new_callable=AsyncMock,
                side_effect=Exception("Not found"),
            ),
            patch.object(
                EpisodicNode,
                "get_by_uuid",
                new_callable=AsyncMock,
                side_effect=Exception("Not found"),
            ),
            pytest.raises(EntityNotFoundError),
        ):
            await entity_manager.delete("nonexistent")

    @pytest.mark.asyncio
    async def test_delete_refuses_surreal_legacy_fallback(
        self,
        surreal_entity_manager: EntityManager,
    ) -> None:
        surreal_entity_manager._driver.execute_query = AsyncMock()

        with (
            patch.object(surreal_entity_manager, "_surreal_entity_node_ops", return_value=None),
            patch.object(surreal_entity_manager, "_surreal_episode_node_ops", return_value=None),
            pytest.raises(RuntimeError, match="native node operations"),
        ):
            await surreal_entity_manager.delete("entity-001")

        surreal_entity_manager._driver.execute_query.assert_not_awaited()


# =============================================================================
# Entity List/Query Tests
# =============================================================================


class TestEntityListByType:
    """Test listing entities by type with filters."""

    @pytest.mark.asyncio
    async def test_list_by_type_uses_surreal_direct_query_path(
        self,
        surreal_entity_manager: EntityManager,
    ) -> None:
        surreal_entity_manager._driver.execute_query = AsyncMock(
            return_value=[
                {
                    "uuid": "task-001",
                    "name": "Doing Task",
                    "entity_type": "task",
                    "group_id": "test-org-123",
                    "description": "Doing",
                    "metadata": json.dumps({"status": "doing", "project_id": "project-001"}),
                    "created_at": datetime.now(UTC),
                },
                {
                    "uuid": "task-002",
                    "name": "Todo Task",
                    "entity_type": "task",
                    "group_id": "test-org-123",
                    "description": "Todo",
                    "metadata": json.dumps({"status": "todo", "project_id": "project-001"}),
                    "created_at": datetime.now(UTC),
                },
            ]
        )

        results = await surreal_entity_manager.list_by_type(
            EntityType.TASK,
            project_id="project-001",
            status="doing",
        )

        assert len(results) == 1
        assert results[0].id == "task-001"
        query = surreal_entity_manager._driver.execute_query.await_args.args[0]
        assert "FROM entity" in query
        assert "entity_type = $entity_type" in query
        assert "project_id = $project_id" in query
        assert "string::lowercase(status ?? '') IN $status_values" in query
        assert (
            surreal_entity_manager._driver.execute_query.await_args.kwargs["entity_type"] == "task"
        )
        assert (
            surreal_entity_manager._driver.execute_query.await_args.kwargs["project_id"]
            == "project-001"
        )
        assert surreal_entity_manager._driver.execute_query.await_args.kwargs["status_values"] == [
            "doing"
        ]

    @pytest.mark.asyncio
    async def test_list_by_type_falls_back_to_legacy_surreal_metadata_rows(
        self,
        surreal_entity_manager: EntityManager,
    ) -> None:
        surreal_entity_manager._driver.execute_query = AsyncMock(
            side_effect=[
                [],
                [
                    {
                        "uuid": "task-legacy",
                        "name": "Legacy Doing Task",
                        "entity_type": "task",
                        "group_id": "test-org-123",
                        "description": "Doing",
                        "metadata": json.dumps({"status": "doing", "project_id": "project-001"}),
                        "created_at": datetime.now(UTC),
                    }
                ],
            ]
        )

        results = await surreal_entity_manager.list_by_type(
            EntityType.TASK,
            project_id="project-001",
            status="doing",
        )

        assert [entity.id for entity in results] == ["task-legacy"]
        first_query = surreal_entity_manager._driver.execute_query.await_args_list[0].args[0]
        second_query = surreal_entity_manager._driver.execute_query.await_args_list[1].args[0]
        assert "project_id = $project_id" in first_query
        assert "project_id = $project_id" not in second_query

    @pytest.mark.asyncio
    async def test_list_by_type_refuses_surreal_legacy_fallback(
        self,
        surreal_entity_manager: EntityManager,
    ) -> None:
        surreal_entity_manager._driver.execute_query = AsyncMock()

        with (
            patch.object(surreal_entity_manager, "_surreal_entity_node_ops", return_value=None),
            pytest.raises(RuntimeError, match="native node operations"),
        ):
            await surreal_entity_manager.list_by_type(EntityType.TASK)

        surreal_entity_manager._driver.execute_query.assert_not_awaited()

    @pytest.mark.asyncio
    async def test_list_by_type_basic(
        self,
        entity_manager: EntityManager,
        mock_driver: MagicMock,
    ) -> None:
        """list_by_type() returns entities of specified type."""
        mock_driver.execute_query.return_value = (
            [
                {
                    "uuid": "task-001",
                    "name": "Task 1",
                    "entity_type": "task",
                    "group_id": "test-org-123",
                    "metadata": json.dumps({"status": "todo"}),
                }
            ],
            None,
            None,
        )

        results = await entity_manager.list_by_type(EntityType.TASK)

        assert len(results) == 1
        assert results[0].id == "task-001"
        assert results[0].entity_type == EntityType.TASK

    @pytest.mark.asyncio
    async def test_list_by_type_with_status_filter(
        self,
        entity_manager: EntityManager,
        mock_driver: MagicMock,
    ) -> None:
        """list_by_type() filters by status from metadata."""
        mock_driver.execute_query.return_value = (
            [
                {
                    "uuid": "task-001",
                    "name": "Task 1",
                    "entity_type": "task",
                    "group_id": "test-org-123",
                    "metadata": json.dumps({"status": "doing"}),
                },
                {
                    "uuid": "task-002",
                    "name": "Task 2",
                    "entity_type": "task",
                    "group_id": "test-org-123",
                    "metadata": json.dumps({"status": "todo"}),
                },
            ],
            None,
            None,
        )

        results = await entity_manager.list_by_type(EntityType.TASK, status="doing")

        assert len(results) == 1
        assert results[0].id == "task-001"
        query = mock_driver.execute_query.await_args.args[0]
        assert "toLower(n.status) IN $status_values" in query
        assert "legacy_status_0_compact" in query
        assert "n.status IS NULL OR n.status = ''" in query
        assert mock_driver.execute_query.await_args.kwargs["status_values"] == ["doing"]
        assert mock_driver.execute_query.await_args.kwargs["legacy_status_0_compact"] == (
            '"status":"doing"'
        )
        assert mock_driver.execute_query.await_args.kwargs["legacy_status_0_spaced"] == (
            '"status": "doing"'
        )

    @pytest.mark.asyncio
    async def test_list_by_type_multiple_statuses(
        self,
        entity_manager: EntityManager,
        mock_driver: MagicMock,
    ) -> None:
        """list_by_type() supports comma-separated status values."""
        mock_driver.execute_query.return_value = (
            [
                {
                    "uuid": "task-001",
                    "name": "Task 1",
                    "entity_type": "task",
                    "group_id": "test-org-123",
                    "metadata": json.dumps({"status": "doing"}),
                },
                {
                    "uuid": "task-002",
                    "name": "Task 2",
                    "entity_type": "task",
                    "group_id": "test-org-123",
                    "metadata": json.dumps({"status": "blocked"}),
                },
                {
                    "uuid": "task-003",
                    "name": "Task 3",
                    "entity_type": "task",
                    "group_id": "test-org-123",
                    "metadata": json.dumps({"status": "done"}),
                },
            ],
            None,
            None,
        )

        results = await entity_manager.list_by_type(EntityType.TASK, status="doing,blocked")

        assert len(results) == 2
        ids = {r.id for r in results}
        assert ids == {"task-001", "task-002"}

    @pytest.mark.asyncio
    async def test_list_by_type_caps_status_filter_value_expansion(
        self,
        entity_manager: EntityManager,
        mock_driver: MagicMock,
    ) -> None:
        """list_by_type() caps comma-separated filter expansion to prevent query blowups."""
        mock_driver.execute_query.return_value = ([], None, None)
        status_filter = ",".join(f"value{i}" for i in range(60))

        await entity_manager.list_by_type(EntityType.TASK, status=status_filter)

        kwargs = mock_driver.execute_query.await_args.kwargs
        assert len(kwargs["status_values"]) == 25
        assert kwargs["status_values"][0] == "value0"
        assert kwargs["status_values"][-1] == "value24"
        legacy_keys = [k for k in kwargs if k.startswith("legacy_status_")]
        assert len(legacy_keys) == 50

    @pytest.mark.asyncio
    async def test_list_by_type_with_priority_filter(
        self,
        entity_manager: EntityManager,
        mock_driver: MagicMock,
    ) -> None:
        """list_by_type() filters by priority from metadata."""
        mock_driver.execute_query.return_value = (
            [
                {
                    "uuid": "task-001",
                    "name": "Critical Task",
                    "entity_type": "task",
                    "group_id": "test-org-123",
                    "metadata": json.dumps({"status": "todo", "priority": "critical"}),
                },
                {
                    "uuid": "task-002",
                    "name": "Low Task",
                    "entity_type": "task",
                    "group_id": "test-org-123",
                    "metadata": json.dumps({"status": "todo", "priority": "low"}),
                },
            ],
            None,
            None,
        )

        results = await entity_manager.list_by_type(EntityType.TASK, priority="critical")

        assert len(results) == 1
        assert results[0].id == "task-001"

    @pytest.mark.asyncio
    async def test_list_by_type_with_project_filter(
        self,
        entity_manager: EntityManager,
        mock_driver: MagicMock,
    ) -> None:
        """list_by_type() filters by project_id."""
        mock_driver.execute_query.return_value = (
            [
                {
                    "uuid": "task-001",
                    "name": "Task 1",
                    "entity_type": "task",
                    "group_id": "test-org-123",
                    "metadata": json.dumps({"project_id": "project-001", "status": "todo"}),
                },
                {
                    "uuid": "task-002",
                    "name": "Task 2",
                    "entity_type": "task",
                    "group_id": "test-org-123",
                    "metadata": json.dumps({"project_id": "project-002", "status": "todo"}),
                },
            ],
            None,
            None,
        )

        results = await entity_manager.list_by_type(EntityType.TASK, project_id="project-001")

        assert len(results) == 1
        assert results[0].id == "task-001"
        query = mock_driver.execute_query.await_args.args[0]
        assert "legacy_project_compact" in query
        assert "n.project_id IS NULL OR n.project_id = ''" in query
        assert mock_driver.execute_query.await_args.kwargs["legacy_project_compact"] == (
            '"project_id":"project-001"'
        )
        assert mock_driver.execute_query.await_args.kwargs["legacy_project_spaced"] == (
            '"project_id": "project-001"'
        )

    @pytest.mark.asyncio
    async def test_list_by_type_with_tags_filter(
        self,
        entity_manager: EntityManager,
        mock_driver: MagicMock,
    ) -> None:
        """list_by_type() filters by tags (any match)."""
        mock_driver.execute_query.return_value = (
            [
                {
                    "uuid": "task-001",
                    "name": "Task 1",
                    "entity_type": "task",
                    "group_id": "test-org-123",
                    "metadata": json.dumps({"tags": ["backend", "api"], "status": "todo"}),
                },
                {
                    "uuid": "task-002",
                    "name": "Task 2",
                    "entity_type": "task",
                    "group_id": "test-org-123",
                    "metadata": json.dumps({"tags": ["frontend"], "status": "todo"}),
                },
            ],
            None,
            None,
        )

        results = await entity_manager.list_by_type(EntityType.TASK, tags=["backend"])

        assert len(results) == 1
        assert results[0].id == "task-001"

    @pytest.mark.asyncio
    async def test_list_by_type_excludes_archived_by_default(
        self,
        entity_manager: EntityManager,
        mock_driver: MagicMock,
    ) -> None:
        """list_by_type() excludes archived entities by default."""
        mock_driver.execute_query.return_value = (
            [
                {
                    "uuid": "task-001",
                    "name": "Active Task",
                    "entity_type": "task",
                    "group_id": "test-org-123",
                    "metadata": json.dumps({"status": "todo"}),
                },
                {
                    "uuid": "task-002",
                    "name": "Archived Task",
                    "entity_type": "task",
                    "group_id": "test-org-123",
                    "metadata": json.dumps({"status": "archived"}),
                },
            ],
            None,
            None,
        )

        results = await entity_manager.list_by_type(EntityType.TASK)

        assert len(results) == 1
        assert results[0].id == "task-001"

    @pytest.mark.asyncio
    async def test_list_by_type_include_archived(
        self,
        entity_manager: EntityManager,
        mock_driver: MagicMock,
    ) -> None:
        """list_by_type() can include archived entities."""
        mock_driver.execute_query.return_value = (
            [
                {
                    "uuid": "task-001",
                    "name": "Active Task",
                    "entity_type": "task",
                    "group_id": "test-org-123",
                    "metadata": json.dumps({"status": "todo"}),
                },
                {
                    "uuid": "task-002",
                    "name": "Archived Task",
                    "entity_type": "task",
                    "group_id": "test-org-123",
                    "metadata": json.dumps({"status": "archived"}),
                },
            ],
            None,
            None,
        )

        results = await entity_manager.list_by_type(EntityType.TASK, include_archived=True)

        assert len(results) == 2

    @pytest.mark.asyncio
    async def test_list_by_type_pagination(
        self,
        entity_manager: EntityManager,
        mock_driver: MagicMock,
    ) -> None:
        """list_by_type() respects limit and offset."""
        mock_driver.execute_query.return_value = (
            [
                {
                    "uuid": f"task-{i:03d}",
                    "name": f"Task {i}",
                    "entity_type": "task",
                    "group_id": "test-org-123",
                    "metadata": json.dumps({"status": "todo"}),
                }
                for i in range(5)
            ],
            None,
            None,
        )

        results = await entity_manager.list_by_type(EntityType.TASK, limit=3, offset=2)

        assert len(results) == 3
        assert results[0].id == "task-002"
        assert results[1].id == "task-003"
        assert results[2].id == "task-004"
        query = mock_driver.execute_query.await_args.args[0]
        assert "SKIP $query_offset" in query
        assert "LIMIT $query_limit" in query
        assert mock_driver.execute_query.await_args.kwargs["query_offset"] == 0
        assert mock_driver.execute_query.await_args.kwargs["query_limit"] == 5

    @pytest.mark.asyncio
    async def test_list_by_type_pushes_exact_pagination_to_cypher(
        self,
        entity_manager: EntityManager,
        mock_driver: MagicMock,
    ) -> None:
        """Exact pagination uses DB offset when legacy metadata rechecks are not needed."""
        mock_driver.execute_query.return_value = (
            [
                {
                    "uuid": "task-003",
                    "name": "Task 3",
                    "entity_type": "task",
                    "group_id": "test-org-123",
                    "metadata": json.dumps({"status": "todo"}),
                },
                {
                    "uuid": "task-004",
                    "name": "Task 4",
                    "entity_type": "task",
                    "group_id": "test-org-123",
                    "metadata": json.dumps({"status": "todo"}),
                },
                {
                    "uuid": "task-005",
                    "name": "Task 5",
                    "entity_type": "task",
                    "group_id": "test-org-123",
                    "metadata": json.dumps({"status": "todo"}),
                },
            ],
            None,
            None,
        )

        results = await entity_manager.list_by_type(
            EntityType.TASK,
            limit=3,
            offset=2,
            include_archived=True,
        )

        assert [result.id for result in results] == ["task-003", "task-004", "task-005"]
        assert mock_driver.execute_query.await_args.kwargs["query_offset"] == 2
        assert mock_driver.execute_query.await_args.kwargs["query_limit"] == 3

    @pytest.mark.asyncio
    async def test_list_by_type_pagination_survives_legacy_metadata_rechecks(
        self,
        entity_manager: EntityManager,
        mock_driver: MagicMock,
    ) -> None:
        """Legacy metadata rows should not collapse a page after DB-side filtering."""
        mock_driver.execute_query.side_effect = [
            (
                [
                    {
                        "uuid": "task-001",
                        "name": "Legacy mismatch",
                        "entity_type": "task",
                        "group_id": "test-org-123",
                        "metadata": json.dumps({"status": "todo"}),
                    },
                    {
                        "uuid": "task-002",
                        "name": "Task 2",
                        "entity_type": "task",
                        "group_id": "test-org-123",
                        "metadata": json.dumps({"status": "doing"}),
                    },
                ],
                None,
                None,
            ),
            (
                [
                    {
                        "uuid": "task-003",
                        "name": "Task 3",
                        "entity_type": "task",
                        "group_id": "test-org-123",
                        "metadata": json.dumps({"status": "doing"}),
                    }
                ],
                None,
                None,
            ),
        ]

        results = await entity_manager.list_by_type(EntityType.TASK, status="doing", limit=2)

        assert [result.id for result in results] == ["task-002", "task-003"]
        assert mock_driver.execute_query.await_count == 2

    @pytest.mark.asyncio
    async def test_list_by_type_caps_large_legacy_scan_batches(
        self,
        entity_manager: EntityManager,
        mock_driver: MagicMock,
    ) -> None:
        """Large filtered scans page incrementally instead of requesting the full window."""
        mock_driver.execute_query.side_effect = [
            (
                [
                    {
                        "uuid": f"task-{i:04d}",
                        "name": f"Task {i}",
                        "entity_type": "task",
                        "group_id": "test-org-123",
                        "metadata": json.dumps({"status": "doing"}),
                    }
                    for i in range(1000)
                ],
                None,
                None,
            ),
            ([], None, None),
        ]

        await entity_manager.list_by_type(EntityType.TASK, status="doing", limit=1500, offset=600)

        first_call = mock_driver.execute_query.await_args_list[0]
        assert first_call.kwargs["query_offset"] == 0
        assert first_call.kwargs["query_limit"] == 1000

    @pytest.mark.asyncio
    async def test_list_by_type_stops_on_repeated_page(
        self,
        entity_manager: EntityManager,
        mock_driver: MagicMock,
    ) -> None:
        """Repeated pages should not duplicate results or loop indefinitely."""
        repeated_page = (
            [
                {
                    "uuid": "task-001",
                    "name": "Task 1",
                    "entity_type": "task",
                    "group_id": "test-org-123",
                    "metadata": json.dumps({"status": "doing"}),
                },
                {
                    "uuid": "task-002",
                    "name": "Task 2",
                    "entity_type": "task",
                    "group_id": "test-org-123",
                    "metadata": json.dumps({"status": "todo"}),
                },
            ],
            None,
            None,
        )
        mock_driver.execute_query.side_effect = [repeated_page, repeated_page]

        results = await entity_manager.list_by_type(EntityType.TASK, status="doing", limit=2)

        assert [result.id for result in results] == ["task-001"]
        assert mock_driver.execute_query.await_count == 2

    @pytest.mark.asyncio
    async def test_list_by_type_skips_duplicate_hydration(
        self,
        entity_manager: EntityManager,
        mock_driver: MagicMock,
    ) -> None:
        """Repeated UUIDs are skipped before entity hydration work."""
        duplicate_record = {
            "uuid": "task-001",
            "name": "Task 1",
            "entity_type": "task",
            "group_id": "test-org-123",
            "metadata": json.dumps({"status": "todo"}),
        }
        mock_driver.execute_query.return_value = ([duplicate_record, duplicate_record], None, None)

        with patch.object(
            entity_manager,
            "_record_to_entity",
            wraps=entity_manager._record_to_entity,
        ) as record_to_entity:
            results = await entity_manager.list_by_type(EntityType.TASK, include_archived=True)

        assert [result.id for result in results] == ["task-001"]
        assert record_to_entity.call_count == 1

    @pytest.mark.asyncio
    async def test_list_by_type_empty_results(
        self,
        entity_manager: EntityManager,
        mock_driver: MagicMock,
    ) -> None:
        """list_by_type() returns empty list when no matches."""
        mock_driver.execute_query.return_value = ([], None, None)

        results = await entity_manager.list_by_type(EntityType.TASK)

        assert results == []

    @pytest.mark.asyncio
    async def test_list_by_type_with_epic_id(
        self,
        entity_manager: EntityManager,
        mock_driver: MagicMock,
    ) -> None:
        """list_by_type() filters by epic_id using BELONGS_TO relationship."""
        mock_driver.execute_query.return_value = (
            [
                {
                    "uuid": "task-001",
                    "name": "Epic Task",
                    "entity_type": "task",
                    "group_id": "test-org-123",
                    "metadata": json.dumps({"status": "todo", "epic_id": "epic-001"}),
                }
            ],
            None,
            None,
        )

        await entity_manager.list_by_type(EntityType.TASK, epic_id="epic-001")

        # Verify query uses BELONGS_TO pattern
        call_args = mock_driver.execute_query.call_args
        query = call_args[0][0] if call_args[0] else ""
        assert "BELONGS_TO" in query

    @pytest.mark.asyncio
    async def test_list_by_type_no_epic(
        self,
        entity_manager: EntityManager,
        mock_driver: MagicMock,
    ) -> None:
        """list_by_type() can filter for entities without an epic."""
        mock_driver.execute_query.return_value = (
            [
                {
                    "uuid": "task-001",
                    "name": "Task with Epic",
                    "entity_type": "task",
                    "group_id": "test-org-123",
                    "metadata": json.dumps({"status": "todo", "epic_id": "epic-001"}),
                },
                {
                    "uuid": "task-002",
                    "name": "Task without Epic",
                    "entity_type": "task",
                    "group_id": "test-org-123",
                    "metadata": json.dumps({"status": "todo"}),
                },
            ],
            None,
            None,
        )

        results = await entity_manager.list_by_type(EntityType.TASK, no_epic=True)

        assert len(results) == 1
        assert results[0].id == "task-002"


class TestEntityListAll:
    """Test listing all entities."""

    @pytest.mark.asyncio
    async def test_list_all_basic(
        self,
        entity_manager: EntityManager,
    ) -> None:
        """list_all() returns entities of all types."""

        async def _list_by_type(
            entity_type: EntityType,
            limit: int = 50,
            offset: int = 0,
            include_archived: bool = False,
            **_: object,
        ) -> list[Entity]:
            if offset > 0 or include_archived:
                return []
            if entity_type == EntityType.TASK:
                return [
                    Entity(
                        id="task-001",
                        name="Task 1",
                        entity_type=EntityType.TASK,
                        created_at=datetime(2025, 1, 2, tzinfo=UTC),
                    )
                ]
            if entity_type == EntityType.PATTERN:
                return [
                    Entity(
                        id="pattern-001",
                        name="Pattern 1",
                        entity_type=EntityType.PATTERN,
                        created_at=datetime(2025, 1, 1, tzinfo=UTC),
                    )
                ]
            return []

        with patch.object(entity_manager, "list_by_type", new=AsyncMock(side_effect=_list_by_type)):
            results = await entity_manager.list_all()

        assert len(results) == 2
        types = {r.entity_type for r in results}
        assert EntityType.TASK in types
        assert EntityType.PATTERN in types

    @pytest.mark.asyncio
    async def test_list_all_uses_surreal_direct_query_path(
        self,
        surreal_entity_manager: EntityManager,
    ) -> None:
        surreal_entity_manager._driver.execute_query = AsyncMock(
            return_value=[
                {
                    "uuid": "task-001",
                    "name": "Task 1",
                    "entity_type": "task",
                    "group_id": "test-org-123",
                    "metadata": json.dumps({"status": "todo"}),
                    "created_at": datetime(2025, 1, 2, tzinfo=UTC),
                },
                {
                    "uuid": "pattern-001",
                    "name": "Pattern 1",
                    "entity_type": "pattern",
                    "group_id": "test-org-123",
                    "metadata": json.dumps({"status": "todo"}),
                    "created_at": datetime(2025, 1, 1, tzinfo=UTC),
                },
            ]
        )

        results = await surreal_entity_manager.list_all(limit=2, include_archived=True)

        assert [result.id for result in results] == ["task-001", "pattern-001"]
        query = surreal_entity_manager._driver.execute_query.await_args.args[0]
        assert "FROM entity" in query
        assert "entity_type = $entity_type" not in query
        assert surreal_entity_manager._driver.execute_query.await_args.kwargs["query_limit"] == 2
        assert surreal_entity_manager._driver.execute_query.await_args.kwargs["query_offset"] == 0

    @pytest.mark.asyncio
    async def test_list_all_refuses_surreal_legacy_fallback(
        self,
        surreal_entity_manager: EntityManager,
    ) -> None:
        surreal_entity_manager._driver.execute_query = AsyncMock()

        with (
            patch.object(surreal_entity_manager, "_surreal_entity_node_ops", return_value=None),
            pytest.raises(RuntimeError, match="native node operations"),
        ):
            await surreal_entity_manager.list_all()

        surreal_entity_manager._driver.execute_query.assert_not_awaited()


# =============================================================================
# Search Tests
# =============================================================================


class TestEntitySearch:
    """Test semantic search operations."""

    @pytest.mark.asyncio
    async def test_search_uses_surreal_direct_query_without_graphiti_hybrid_search(
        self,
        surreal_entity_manager: EntityManager,
    ) -> None:
        matching_record = {
            "uuid": "pattern-001",
            "name": "Repository Pattern",
            "group_id": "test-org-123",
            "entity_type": "pattern",
            "created_at": datetime.now(UTC),
            "description": "Repository abstraction",
            "content": "Use repositories for data access",
            "metadata": json.dumps({"category": "architecture"}),
            "search_score": 0.0,
        }

        surreal_entity_manager._client.client.search_ = AsyncMock()
        surreal_entity_manager._driver.entity_node_ops.get_by_group_ids = AsyncMock()
        surreal_entity_manager._driver.execute_query = AsyncMock(return_value=[matching_record])

        results = await surreal_entity_manager.search(
            "repository", entity_types=[EntityType.PATTERN]
        )

        assert len(results) == 1
        assert results[0][0].id == "pattern-001"
        surreal_entity_manager._client.client.search_.assert_not_awaited()
        surreal_entity_manager._driver.entity_node_ops.get_by_group_ids.assert_not_awaited()
        fallback_query = surreal_entity_manager._driver.execute_query.await_args.args[0]
        assert "FROM entity" in fallback_query
        assert "string::lowercase(name ?? '') = $query_lower" in fallback_query
        assert "name @0@ $search_query" in fallback_query
        assert "summary @1@ $search_query" in fallback_query
        assert "description @2@ $search_query" in fallback_query
        assert "content @3@ $search_query" in fallback_query
        assert "attributes.description @2@ $search_query" not in fallback_query
        assert "attributes.content @3@ $search_query" not in fallback_query
        assert "ORDER BY search_score DESC, updated_at DESC" in fallback_query
        assert "attributes.updated_at DESC" not in fallback_query
        assert "string::contains" not in fallback_query

    @pytest.mark.asyncio
    async def test_surreal_fallback_accepts_fulltext_matches_without_substrings(
        self,
        surreal_entity_manager: EntityManager,
    ) -> None:
        matching_record = {
            "uuid": "pattern-001",
            "name": "Data Access Layer",
            "group_id": "test-org-123",
            "entity_type": "pattern",
            "created_at": datetime.now(UTC),
            "description": "Persistence boundary",
            "content": "Use repositories for storage concerns",
            "metadata": json.dumps({"category": "architecture"}),
            "search_score": 0.42,
        }

        surreal_entity_manager._driver.execute_query = AsyncMock(return_value=[matching_record])

        results = await surreal_entity_manager.search(
            "repository", entity_types=[EntityType.PATTERN]
        )

        assert len(results) == 1
        assert results[0][0].id == "pattern-001"
        assert results[0][1] == 0.65

    @pytest.mark.asyncio
    async def test_surreal_fallback_scans_recent_records_for_token_recall(
        self,
        surreal_entity_manager: EntityManager,
    ) -> None:
        matching_record = {
            "uuid": "pattern-001",
            "name": "Graph search recall",
            "group_id": "test-org-123",
            "entity_type": "pattern",
            "created_at": datetime.now(UTC),
            "description": "Sibyl graph queries should find knowledge entities",
            "content": "Graph memory search should not disappear behind documents.",
            "metadata": json.dumps({"category": "search"}),
        }
        unrelated_record = {
            "uuid": "pattern-002",
            "name": "Auth refresh guard",
            "group_id": "test-org-123",
            "entity_type": "pattern",
            "created_at": datetime.now(UTC),
            "description": "Refresh token validation",
            "content": "Malformed claims are rejected.",
            "metadata": json.dumps({"category": "auth"}),
        }

        surreal_entity_manager._driver.execute_query = AsyncMock(
            side_effect=[[], [matching_record, unrelated_record]]
        )

        results = await surreal_entity_manager.search(
            "sibyl search graph",
            entity_types=[EntityType.PATTERN],
        )

        assert len(results) == 1
        assert results[0][0].id == "pattern-001"
        assert results[0][1] > 0.5
        scan_query = surreal_entity_manager._driver.execute_query.await_args_list[1].args[0]
        assert "FROM entity" in scan_query
        assert "@0@ $search_query" not in scan_query

    @pytest.mark.asyncio
    async def test_surreal_fallback_scan_includes_episodes_after_entity_scan_misses(
        self,
        surreal_entity_manager: EntityManager,
    ) -> None:
        unrelated_entity = {
            "uuid": "pattern-002",
            "name": "Auth refresh guard",
            "group_id": "test-org-123",
            "entity_type": "pattern",
            "created_at": datetime.now(UTC),
            "description": "Refresh token validation",
            "content": "Malformed claims are rejected.",
            "metadata": json.dumps({"category": "auth"}),
        }
        matching_episode = {
            "uuid": "episode-001",
            "name": "Graph search diary",
            "group_id": "test-org-123",
            "created_at": datetime.now(UTC),
            "source_description": "Sibyl memory",
            "content": "A graph search diary about finding memories.",
        }

        surreal_entity_manager._driver.execute_query = AsyncMock(
            side_effect=[[], [], [unrelated_entity], [matching_episode]]
        )

        results = await surreal_entity_manager.search("graph search diary")

        assert len(results) == 1
        assert results[0][0].id == "episode-001"
        assert results[0][0].entity_type == EntityType.EPISODE
        episode_scan_query = surreal_entity_manager._driver.execute_query.await_args_list[3].args[0]
        assert "FROM episode" in episode_scan_query

    @pytest.mark.asyncio
    async def test_surreal_token_recall_rejects_single_term_match_for_short_query(
        self,
        surreal_entity_manager: EntityManager,
    ) -> None:
        partial_record = {
            "uuid": "pattern-001",
            "name": "Search tuning",
            "group_id": "test-org-123",
            "entity_type": "pattern",
            "created_at": datetime.now(UTC),
            "description": "Search recall tuning",
            "content": "Only one query term appears here.",
            "metadata": json.dumps({"category": "search"}),
        }

        surreal_entity_manager._driver.execute_query = AsyncMock(side_effect=[[], [partial_record]])

        results = await surreal_entity_manager.search(
            "graph search",
            entity_types=[EntityType.PATTERN],
        )

        assert results == []

    @pytest.mark.asyncio
    async def test_surreal_fallback_sanitizes_fulltext_query(
        self,
        surreal_entity_manager: EntityManager,
    ) -> None:
        matching_record = {
            "uuid": "pattern-001",
            "name": "Repository Pattern",
            "group_id": "test-org-123",
            "entity_type": "pattern",
            "created_at": datetime.now(UTC),
            "description": "Repository abstraction",
            "content": "Use repositories for data access",
            "metadata": json.dumps({"category": "architecture"}),
            "search_score": 0.0,
        }

        surreal_entity_manager._driver.execute_query = AsyncMock(return_value=[matching_record])

        results = await surreal_entity_manager.search(
            'repository "pattern"\x00',
            entity_types=[EntityType.PATTERN],
        )

        assert len(results) == 1
        fallback_params = surreal_entity_manager._driver.execute_query.await_args.kwargs
        assert fallback_params["query_lower"] == 'repository "pattern"\x00'
        assert fallback_params["search_query"] == "repository pattern"

    @pytest.mark.asyncio
    async def test_surreal_fallback_bootstraps_schema_when_fulltext_index_missing(
        self,
        surreal_entity_manager: EntityManager,
    ) -> None:
        matching_record = {
            "uuid": "pattern-001",
            "name": "Repository Pattern",
            "group_id": "test-org-123",
            "entity_type": "pattern",
            "created_at": datetime.now(UTC),
            "description": "Repository abstraction",
            "content": "Use repositories for data access",
            "metadata": json.dumps({"category": "architecture"}),
            "search_score": 0.0,
        }

        surreal_entity_manager._driver.execute_query = AsyncMock(
            side_effect=[
                RuntimeError("There was no suitable index supporting the expression: name @0@"),
                [matching_record],
            ]
        )
        surreal_entity_manager._driver.build_indices_and_constraints = AsyncMock()

        results = await surreal_entity_manager.search(
            "repository",
            entity_types=[EntityType.PATTERN],
        )

        assert len(results) == 1
        assert results[0][0].id == "pattern-001"
        surreal_entity_manager._driver.build_indices_and_constraints.assert_awaited_once()
        assert surreal_entity_manager._driver.execute_query.await_count == 2

    @pytest.mark.asyncio
    async def test_surreal_search_exact_episode_reads_episode_table(
        self,
        surreal_entity_manager: EntityManager,
    ) -> None:
        matching_record = {
            "uuid": "episode-001",
            "name": "episode:Surreal lesson",
            "group_id": "test-org-123",
            "content": "Graphiti wrote this raw memory.",
            "source_description": "MCP Entity: episode",
            "created_at": datetime.now(UTC),
            "valid_at": datetime.now(UTC),
            "search_score": 2.0,
        }

        surreal_entity_manager._driver.execute_query = AsyncMock(return_value=[matching_record])

        results = await surreal_entity_manager.search(
            "Surreal lesson",
            entity_types=[EntityType.EPISODE],
            limit=1,
        )

        assert len(results) == 1
        entity, score = results[0]
        assert entity.id == "episode-001"
        assert entity.name == "Surreal lesson"
        assert entity.entity_type == EntityType.EPISODE
        assert score == 2.0
        query = surreal_entity_manager._driver.execute_query.await_args.args[0]
        params = surreal_entity_manager._driver.execute_query.await_args.kwargs
        assert "FROM episode" in query
        assert "FROM entity" not in query
        assert "$prefixed_query_lower" in query
        assert params["prefixed_query_lower"] == "episode:surreal lesson"

    @pytest.mark.asyncio
    async def test_surreal_episode_search_reapplies_type_filter_after_hydration(
        self,
        surreal_entity_manager: EntityManager,
    ) -> None:
        matching_record = {
            "uuid": "pattern-episode-001",
            "name": "pattern:Legacy pattern episode",
            "group_id": "test-org-123",
            "content": "A legacy episode named like a pattern.",
            "source_description": "MCP Entity: pattern",
            "created_at": datetime.now(UTC),
            "valid_at": datetime.now(UTC),
        }

        surreal_entity_manager._driver.execute_query = AsyncMock(return_value=[matching_record])

        results = await surreal_entity_manager.search(
            "Legacy pattern episode",
            entity_types=[EntityType.EPISODE],
            limit=5,
        )

        assert results == []

    @pytest.mark.asyncio
    async def test_surreal_episode_fallback_reapplies_type_filter_after_hydration(
        self,
        surreal_entity_manager: EntityManager,
    ) -> None:
        matching_record = {
            "uuid": "pattern-episode-001",
            "name": "pattern:Legacy pattern episode",
            "group_id": "test-org-123",
            "content": "A legacy episode named like a pattern.",
            "source_description": "MCP Entity: pattern",
            "created_at": datetime.now(UTC),
            "search_score": 0.33,
        }

        surreal_entity_manager._driver.execute_query = AsyncMock(return_value=[matching_record])

        results = await surreal_entity_manager.search(
            "legacy",
            entity_types=[EntityType.EPISODE],
            limit=5,
        )

        assert results == []

    @pytest.mark.asyncio
    async def test_surreal_search_fallback_episode_content(
        self,
        surreal_entity_manager: EntityManager,
    ) -> None:
        matching_record = {
            "uuid": "episode-001",
            "name": "episode:Surreal lesson",
            "group_id": "test-org-123",
            "content": "Graphiti extraction writes raw memory episodes.",
            "source_description": "MCP Entity: episode",
            "created_at": datetime.now(UTC),
            "search_score": 0.33,
        }

        surreal_entity_manager._driver.execute_query = AsyncMock(return_value=[matching_record])

        results = await surreal_entity_manager.search(
            'raw "memory"\x00',
            entity_types=[EntityType.EPISODE],
            limit=5,
        )

        assert len(results) == 1
        assert results[0][0].id == "episode-001"
        fallback_query = surreal_entity_manager._driver.execute_query.await_args.args[0]
        fallback_params = surreal_entity_manager._driver.execute_query.await_args.kwargs
        assert "FROM episode" in fallback_query
        assert "content @0@ $search_query" in fallback_query
        assert "FROM entity" not in fallback_query
        assert fallback_params["search_query"] == "raw memory"

    @pytest.mark.asyncio
    async def test_search_skips_surreal_fallback_when_exact_results_fill_limit(
        self,
        surreal_entity_manager: EntityManager,
    ) -> None:
        matching_record = {
            "uuid": "pattern-001",
            "name": "Repository Pattern",
            "group_id": "test-org-123",
            "entity_type": "pattern",
            "created_at": datetime.now(UTC),
            "description": "Repository abstraction",
            "content": "Use repositories for data access",
            "metadata": json.dumps({"category": "architecture"}),
        }

        surreal_entity_manager._driver.execute_query = AsyncMock(return_value=[matching_record])

        results = await surreal_entity_manager.search(
            "Repository Pattern",
            entity_types=[EntityType.PATTERN],
            limit=1,
        )

        assert len(results) == 1
        assert results[0][0].id == "pattern-001"
        assert surreal_entity_manager._driver.execute_query.await_count == 1
        exact_query = surreal_entity_manager._driver.execute_query.await_args.args[0]
        assert "FROM entity" in exact_query
        assert (
            "ORDER BY search_score DESC, updated_at DESC, created_at DESC, uuid DESC" in exact_query
        )
        assert "attributes.updated_at DESC" not in exact_query
        assert "string::contains" not in exact_query

    @pytest.mark.asyncio
    async def test_search_basic(
        self,
        entity_manager: EntityManager,
        sample_entity_node: EntityNode,
        mock_graph_client: MagicMock,
    ) -> None:
        """search() returns entities with relevance scores."""
        mock_search_result = MagicMock()
        mock_search_result.nodes = [sample_entity_node]
        mock_search_result.node_reranker_scores = [0.95]
        mock_search_result.episodes = []
        mock_search_result.episode_reranker_scores = []
        mock_graph_client.client.search_.return_value = mock_search_result

        results = await entity_manager.search("test query")

        assert len(results) == 1
        entity, score = results[0]
        assert entity.id == "entity-001"
        assert score == 0.95

    @pytest.mark.asyncio
    async def test_search_filters_by_type(
        self,
        entity_manager: EntityManager,
        mock_graph_client: MagicMock,
    ) -> None:
        """search() filters results by entity type."""
        pattern_node = EntityNode(
            uuid="pattern-001",
            name="Pattern",
            group_id="test-org-123",
            labels=["Entity", "pattern"],
            created_at=datetime.now(UTC),
            summary="A pattern",
            attributes={"entity_type": "pattern"},
        )
        task_node = EntityNode(
            uuid="task-001",
            name="Task",
            group_id="test-org-123",
            labels=["Entity", "task"],
            created_at=datetime.now(UTC),
            summary="A task",
            attributes={"entity_type": "task"},
        )

        mock_search_result = MagicMock()
        mock_search_result.nodes = [pattern_node, task_node]
        mock_search_result.node_reranker_scores = [0.9, 0.8]
        mock_search_result.episodes = []
        mock_search_result.episode_reranker_scores = []
        mock_graph_client.client.search_.return_value = mock_search_result

        results = await entity_manager.search("test", entity_types=[EntityType.PATTERN])

        assert len(results) == 1
        assert results[0][0].entity_type == EntityType.PATTERN

    @pytest.mark.asyncio
    async def test_search_sanitizes_query(
        self,
        entity_manager: EntityManager,
        mock_graph_client: MagicMock,
    ) -> None:
        """search() sanitizes special characters in query."""
        mock_search_result = MagicMock()
        mock_search_result.nodes = []
        mock_search_result.node_reranker_scores = []
        mock_search_result.episodes = []
        mock_search_result.episode_reranker_scores = []
        mock_graph_client.client.search_.return_value = mock_search_result

        # Query with RediSearch special characters
        await entity_manager.search("create/cleanup @user ~fuzzy")

        # Verify search was called (query gets sanitized internally)
        mock_graph_client.client.search_.assert_called_once()

    @pytest.mark.asyncio
    async def test_search_respects_limit(
        self,
        entity_manager: EntityManager,
        mock_graph_client: MagicMock,
    ) -> None:
        """search() limits number of results."""
        nodes = [
            EntityNode(
                uuid=f"entity-{i:03d}",
                name=f"Entity {i}",
                group_id="test-org-123",
                labels=["Entity"],
                created_at=datetime.now(UTC),
                summary=f"Entity {i}",
                attributes={"entity_type": "pattern"},
            )
            for i in range(10)
        ]

        mock_search_result = MagicMock()
        mock_search_result.nodes = nodes
        mock_search_result.node_reranker_scores = [0.9 - i * 0.05 for i in range(10)]
        mock_search_result.episodes = []
        mock_search_result.episode_reranker_scores = []
        mock_graph_client.client.search_.return_value = mock_search_result

        results = await entity_manager.search("test", limit=3)

        assert len(results) == 3

    @pytest.mark.asyncio
    async def test_search_filters_by_group(
        self,
        entity_manager: EntityManager,
        mock_graph_client: MagicMock,
    ) -> None:
        """search() only returns results from correct group."""
        own_group_node = EntityNode(
            uuid="own-entity",
            name="Own Entity",
            group_id="test-org-123",
            labels=["Entity"],
            created_at=datetime.now(UTC),
            summary="Own",
            attributes={"entity_type": "pattern"},
        )
        other_group_node = EntityNode(
            uuid="other-entity",
            name="Other Entity",
            group_id="other-org",
            labels=["Entity"],
            created_at=datetime.now(UTC),
            summary="Other",
            attributes={"entity_type": "pattern"},
        )

        mock_search_result = MagicMock()
        mock_search_result.nodes = [own_group_node, other_group_node]
        mock_search_result.node_reranker_scores = [0.9, 0.85]
        mock_search_result.episodes = []
        mock_search_result.episode_reranker_scores = []
        mock_graph_client.client.search_.return_value = mock_search_result

        results = await entity_manager.search("test")

        assert len(results) == 1
        assert results[0][0].id == "own-entity"

    @pytest.mark.asyncio
    async def test_search_failure_raises_error(
        self,
        entity_manager: EntityManager,
        mock_graph_client: MagicMock,
    ) -> None:
        """search() raises SearchError on failure."""
        mock_graph_client.client.search_.side_effect = Exception("Search failed")

        with pytest.raises(SearchError, match="Search failed"):
            await entity_manager.search("test")


# =============================================================================
# Epic/Project Relationship Tests
# =============================================================================


class TestGetTasksForEpic:
    """Test retrieving tasks for an epic."""

    @pytest.mark.asyncio
    async def test_get_tasks_for_epic(
        self,
        entity_manager: EntityManager,
        mock_driver: MagicMock,
    ) -> None:
        """get_tasks_for_epic() returns tasks via BELONGS_TO relationship."""
        mock_driver.execute_query.return_value = (
            [
                {
                    "uuid": "task-001",
                    "name": "Epic Task 1",
                    "entity_type": "task",
                    "group_id": "test-org-123",
                    "metadata": json.dumps({"status": "doing"}),
                },
                {
                    "uuid": "task-002",
                    "name": "Epic Task 2",
                    "entity_type": "task",
                    "group_id": "test-org-123",
                    "metadata": json.dumps({"status": "todo"}),
                },
            ],
            None,
            None,
        )

        results = await entity_manager.get_tasks_for_epic("epic-001")

        assert len(results) == 2

    @pytest.mark.asyncio
    async def test_get_tasks_for_epic_with_status_filter(
        self,
        entity_manager: EntityManager,
        mock_driver: MagicMock,
    ) -> None:
        """get_tasks_for_epic() filters by status."""
        mock_driver.execute_query.return_value = (
            [
                {
                    "uuid": "task-001",
                    "name": "Task Doing",
                    "entity_type": "task",
                    "group_id": "test-org-123",
                    "metadata": json.dumps({"status": "doing"}),
                },
                {
                    "uuid": "task-002",
                    "name": "Task Todo",
                    "entity_type": "task",
                    "group_id": "test-org-123",
                    "metadata": json.dumps({"status": "todo"}),
                },
            ],
            None,
            None,
        )

        results = await entity_manager.get_tasks_for_epic("epic-001", status="doing")

        assert len(results) == 1
        assert results[0].id == "task-001"


class TestGetEpicProgress:
    """Test epic progress calculation."""

    @pytest.mark.asyncio
    async def test_get_epic_progress_uses_surreal_task_listing(
        self,
        surreal_entity_manager: EntityManager,
    ) -> None:
        with patch.object(
            surreal_entity_manager,
            "list_by_type",
            new_callable=AsyncMock,
            return_value=[
                Task(
                    id="task-001",
                    name="Done Task",
                    title="Done Task",
                    description="",
                    status=TaskStatus.DONE,
                    priority=TaskPriority.HIGH,
                    project_id="project-001",
                    epic_id="epic-001",
                    metadata={"status": "done"},
                ),
                Task(
                    id="task-002",
                    name="Doing Task",
                    title="Doing Task",
                    description="",
                    status=TaskStatus.DOING,
                    priority=TaskPriority.MEDIUM,
                    project_id="project-001",
                    epic_id="epic-001",
                    metadata={"status": "doing"},
                ),
            ],
        ):
            progress = await surreal_entity_manager.get_epic_progress("epic-001")

        assert progress["total_tasks"] == 2
        assert progress["completed_tasks"] == 1
        assert progress["in_progress_tasks"] == 1
        assert progress["completion_pct"] == 50.0

    @pytest.mark.asyncio
    async def test_get_epic_progress(
        self,
        entity_manager: EntityManager,
        mock_driver: MagicMock,
    ) -> None:
        """get_epic_progress() calculates completion percentage."""
        mock_driver.execute_query.return_value = (
            [
                {"metadata": json.dumps({"status": "done"})},
                {"metadata": json.dumps({"status": "done"})},
                {"metadata": json.dumps({"status": "doing"})},
                {"metadata": json.dumps({"status": "todo"})},
                {"metadata": json.dumps({"status": "blocked"})},
            ],
            None,
            None,
        )

        progress = await entity_manager.get_epic_progress("epic-001")

        assert progress["total_tasks"] == 5
        assert progress["completed_tasks"] == 2
        assert progress["in_progress_tasks"] == 1
        assert progress["blocked_tasks"] == 1
        assert progress["completion_pct"] == 40.0

    @pytest.mark.asyncio
    async def test_get_epic_progress_empty(
        self,
        entity_manager: EntityManager,
        mock_driver: MagicMock,
    ) -> None:
        """get_epic_progress() handles epic with no tasks."""
        mock_driver.execute_query.return_value = ([], None, None)

        progress = await entity_manager.get_epic_progress("epic-001")

        assert progress["total_tasks"] == 0
        assert progress["completed_tasks"] == 0
        assert progress["completion_pct"] == 0.0


class TestListEpicsForProject:
    """Test listing epics for a project."""

    @pytest.mark.asyncio
    async def test_list_epics_for_project_uses_surreal_list_by_type(
        self,
        surreal_entity_manager: EntityManager,
        sample_epic: Epic,
    ) -> None:
        with patch.object(
            surreal_entity_manager,
            "list_by_type",
            new_callable=AsyncMock,
            return_value=[sample_epic],
        ) as list_by_type:
            results = await surreal_entity_manager.list_epics_for_project(
                "project-001",
                status="in_progress",
            )

        assert len(results) == 1
        list_by_type.assert_awaited_once_with(
            EntityType.EPIC,
            project_id="project-001",
            status="in_progress",
            limit=50,
        )

    @pytest.mark.asyncio
    async def test_list_epics_for_project(
        self,
        entity_manager: EntityManager,
        mock_driver: MagicMock,
    ) -> None:
        """list_epics_for_project() returns epics for project."""
        mock_driver.execute_query.return_value = (
            [
                {
                    "uuid": "epic-001",
                    "name": "Epic 1",
                    "entity_type": "epic",
                    "group_id": "test-org-123",
                    "metadata": json.dumps({}),
                    "status": "in_progress",
                    "priority": "high",
                },
                {
                    "uuid": "epic-002",
                    "name": "Epic 2",
                    "entity_type": "epic",
                    "group_id": "test-org-123",
                    "metadata": json.dumps({}),
                    "status": "planning",
                    "priority": "medium",
                },
            ],
            None,
            None,
        )

        results = await entity_manager.list_epics_for_project("project-001")

        assert len(results) == 2

    @pytest.mark.asyncio
    async def test_list_epics_for_project_with_status_filter(
        self,
        entity_manager: EntityManager,
        mock_driver: MagicMock,
    ) -> None:
        """list_epics_for_project() filters by status."""
        mock_driver.execute_query.return_value = (
            [
                {
                    "uuid": "epic-001",
                    "name": "Active Epic",
                    "entity_type": "epic",
                    "group_id": "test-org-123",
                    "metadata": json.dumps({}),
                    "status": "in_progress",
                }
            ],
            None,
            None,
        )

        await entity_manager.list_epics_for_project("project-001", status="in_progress")

        # Verify query includes status filter
        call_args = mock_driver.execute_query.call_args
        assert "status" in call_args.kwargs


# =============================================================================
# Notes Tests
# =============================================================================


class TestGetNotesForTask:
    """Test retrieving notes for a task."""

    @pytest.mark.asyncio
    async def test_get_notes_for_task_uses_surreal_task_id_query(
        self,
        surreal_entity_manager: EntityManager,
    ) -> None:
        surreal_entity_manager._driver.execute_query = AsyncMock(
            return_value=[
                {
                    "uuid": "note-001",
                    "name": "First note",
                    "entity_type": "note",
                    "group_id": "test-org-123",
                    "content": "Note content",
                    "task_id": "task-001",
                    "metadata": json.dumps({"task_id": "task-001"}),
                    "created_at": datetime.now(UTC),
                }
            ]
        )

        results = await surreal_entity_manager.get_notes_for_task("task-001")

        assert len(results) == 1
        assert results[0].id == "note-001"
        query = surreal_entity_manager._driver.execute_query.await_args.args[0]
        assert "entity_type = $entity_type" in query
        assert "task_id = $task_id" in query
        assert surreal_entity_manager._driver.execute_query.await_args.kwargs["entity_type"] == (
            "note"
        )
        assert surreal_entity_manager._driver.execute_query.await_args.kwargs["task_id"] == (
            "task-001"
        )

    @pytest.mark.asyncio
    async def test_get_notes_for_task_falls_back_to_legacy_metadata_rows(
        self,
        surreal_entity_manager: EntityManager,
    ) -> None:
        surreal_entity_manager._driver.execute_query = AsyncMock(
            side_effect=[
                [],
                [
                    {
                        "uuid": "note-legacy",
                        "name": "Legacy note",
                        "entity_type": "note",
                        "group_id": "test-org-123",
                        "content": "Legacy note content",
                        "metadata": json.dumps({"task_id": "task-001"}),
                        "created_at": datetime.now(UTC),
                    },
                    {
                        "uuid": "note-other",
                        "name": "Other note",
                        "entity_type": "note",
                        "group_id": "test-org-123",
                        "content": "Other note content",
                        "metadata": json.dumps({"task_id": "task-999"}),
                        "created_at": datetime.now(UTC),
                    },
                ],
            ]
        )

        results = await surreal_entity_manager.get_notes_for_task("task-001")

        assert [entity.id for entity in results] == ["note-legacy"]
        first_query = surreal_entity_manager._driver.execute_query.await_args_list[0].args[0]
        second_query = surreal_entity_manager._driver.execute_query.await_args_list[1].args[0]
        assert "task_id = $task_id" in first_query
        assert "task_id = $task_id" not in second_query

    @pytest.mark.asyncio
    async def test_get_notes_for_task_refuses_surreal_legacy_fallback(
        self,
        surreal_entity_manager: EntityManager,
    ) -> None:
        surreal_entity_manager._driver.execute_query = AsyncMock()

        with (
            patch.object(surreal_entity_manager, "_surreal_entity_node_ops", return_value=None),
            pytest.raises(RuntimeError, match="native node operations"),
        ):
            await surreal_entity_manager.get_notes_for_task("task-001")

        surreal_entity_manager._driver.execute_query.assert_not_awaited()

    @pytest.mark.asyncio
    async def test_get_notes_for_task(
        self,
        entity_manager: EntityManager,
        mock_driver: MagicMock,
    ) -> None:
        """get_notes_for_task() returns notes via BELONGS_TO relationship."""
        mock_driver.execute_query.return_value = (
            [
                {
                    "uuid": "note-001",
                    "name": "First note",
                    "entity_type": "note",
                    "group_id": "test-org-123",
                    "content": "Note content",
                    "metadata": json.dumps({"task_id": "task-001"}),
                }
            ],
            None,
            None,
        )

        results = await entity_manager.get_notes_for_task("task-001")

        assert len(results) == 1
        assert results[0].id == "note-001"


# =============================================================================
# Bulk Operations Tests
# =============================================================================


class TestBulkCreateDirect:
    """Test bulk entity creation."""

    @pytest.mark.asyncio
    async def test_bulk_create_direct_uses_surreal_bulk_ops(
        self,
        surreal_entity_manager: EntityManager,
    ) -> None:
        entities = [
            Entity(
                id=f"entity-{i:03d}",
                entity_type=EntityType.PATTERN,
                name=f"Pattern {i}",
                description=f"Description {i}",
            )
            for i in range(5)
        ]

        ops = surreal_entity_manager._driver.entity_node_ops
        ops.save_bulk = AsyncMock()

        created, failed = await surreal_entity_manager.bulk_create_direct(entities, batch_size=2)

        assert created == 5
        assert failed == 0
        assert ops.save_bulk.await_count == 3

    @pytest.mark.asyncio
    async def test_bulk_create_direct(
        self,
        entity_manager: EntityManager,
        mock_driver: MagicMock,
    ) -> None:
        """bulk_create_direct() creates multiple entities."""
        entities = [
            Entity(
                id=f"entity-{i:03d}",
                entity_type=EntityType.PATTERN,
                name=f"Pattern {i}",
                description=f"Description {i}",
            )
            for i in range(5)
        ]

        mock_driver.execute_query.return_value = ([{"upserted": 5}], None, None)

        created, failed = await entity_manager.bulk_create_direct(entities)

        assert created == 5
        assert failed == 0
        query = mock_driver.execute_query.await_args.args[0]
        assert "UNWIND $entity_rows AS entity_data" in query

    @pytest.mark.asyncio
    async def test_bulk_create_direct_partial_failure(
        self,
        entity_manager: EntityManager,
        mock_driver: MagicMock,
    ) -> None:
        """bulk_create_direct() tracks failed creations."""
        entities = [
            Entity(
                id=f"entity-{i:03d}",
                entity_type=EntityType.PATTERN,
                name=f"Pattern {i}",
                description=f"Description {i}",
            )
            for i in range(3)
        ]

        call_count = 0

        async def flaky_save(*args: object, **kwargs: object) -> None:
            nonlocal call_count
            call_count += 1
            # Fail on second entity
            if call_count == 2:
                raise Exception("Random failure")
            return None

        mock_driver.execute_query.side_effect = Exception("Batch write failed")
        with patch.object(EntityNode, "save", new_callable=AsyncMock, side_effect=flaky_save):
            created, failed = await entity_manager.bulk_create_direct(entities)

        assert created == 2
        assert failed == 1
        assert mock_driver.execute_query.await_count == 1


# =============================================================================
# Helper Function Tests
# =============================================================================


class TestSanitizeSearchQuery:
    """Test query sanitization helper."""

    def test_sanitize_special_chars(self) -> None:
        """sanitize_search_query() removes RediSearch special characters."""
        query = "create/cleanup @user ~fuzzy | (group) $var -exclude"
        result = sanitize_search_query(query)

        # All special chars should be replaced with spaces
        assert "/" not in result
        assert "@" not in result
        assert "~" not in result
        assert "|" not in result
        assert "(" not in result
        assert ")" not in result
        assert "$" not in result
        assert "-" not in result

    def test_sanitize_preserves_words(self) -> None:
        """sanitize_search_query() preserves normal text."""
        query = "simple search query"
        result = sanitize_search_query(query)

        assert result == "simple search query"


class TestNodeToEntity:
    """Test node conversion helpers."""

    def test_node_to_entity_basic(
        self,
        entity_manager: EntityManager,
        sample_entity_node: EntityNode,
    ) -> None:
        """node_to_entity() converts EntityNode to Entity."""
        result = entity_manager.node_to_entity(sample_entity_node)

        assert result.id == "entity-001"
        assert result.name == "Test Entity"
        assert result.entity_type == EntityType.PATTERN

    def test_node_to_entity_with_labels(
        self,
        entity_manager: EntityManager,
    ) -> None:
        """node_to_entity() extracts type from labels if not in attributes."""
        node = EntityNode(
            uuid="node-001",
            name="Task Node",
            group_id="test-org-123",
            labels=["Entity", "task"],
            created_at=datetime.now(UTC),
            summary="A task",
            attributes={},  # No entity_type attribute
        )

        result = entity_manager.node_to_entity(node)

        assert result.entity_type == EntityType.TASK

    def test_node_to_entity_unknown_type_defaults_to_topic(
        self,
        entity_manager: EntityManager,
    ) -> None:
        """node_to_entity() defaults to TOPIC for unknown types."""
        node = EntityNode(
            uuid="node-001",
            name="Unknown Node",
            group_id="test-org-123",
            labels=["Entity", "unknown_type"],
            created_at=datetime.now(UTC),
            summary="Unknown",
            attributes={"entity_type": "not_a_real_type"},
        )

        result = entity_manager.node_to_entity(node)

        assert result.entity_type == EntityType.TOPIC


# =============================================================================
# Edge Cases and Error Handling
# =============================================================================


class TestEdgeCases:
    """Test edge cases and error handling."""

    @pytest.mark.asyncio
    async def test_list_by_type_handles_malformed_metadata(
        self,
        entity_manager: EntityManager,
        mock_driver: MagicMock,
    ) -> None:
        """list_by_type() handles invalid JSON in metadata."""
        mock_driver.execute_query.return_value = (
            [
                {
                    "uuid": "task-001",
                    "name": "Task with bad metadata",
                    "entity_type": "task",
                    "group_id": "test-org-123",
                    "metadata": "not valid json{{{",
                }
            ],
            None,
            None,
        )

        # Should not raise, but may skip the malformed record
        results = await entity_manager.list_by_type(EntityType.TASK)

        # The record should be skipped or handled gracefully
        assert isinstance(results, list)

    @pytest.mark.asyncio
    async def test_search_handles_empty_results(
        self,
        entity_manager: EntityManager,
        mock_graph_client: MagicMock,
    ) -> None:
        """search() returns empty list when no matches."""
        mock_search_result = MagicMock()
        mock_search_result.nodes = []
        mock_search_result.node_reranker_scores = []
        mock_search_result.episodes = []
        mock_search_result.episode_reranker_scores = []
        mock_graph_client.client.search_.return_value = mock_search_result

        results = await entity_manager.search("nonexistent query xyz")

        assert results == []

    @pytest.mark.asyncio
    async def test_search_falls_back_to_direct_text_scan_when_hybrid_misses(
        self,
        entity_manager: EntityManager,
        mock_graph_client: MagicMock,
        mock_driver: MagicMock,
    ) -> None:
        """search() falls back to direct graph text matching when indexes lag."""
        mock_search_result = MagicMock()
        mock_search_result.nodes = []
        mock_search_result.node_reranker_scores = []
        mock_search_result.episodes = []
        mock_search_result.episode_reranker_scores = []
        mock_graph_client.client.search_.return_value = mock_search_result
        mock_driver.execute_query.return_value = (
            [
                {
                    "uuid": "pattern-001",
                    "name": "Searchable E2E e2e-1234",
                    "entity_type": "pattern",
                    "group_id": "test-org-123",
                    "description": "Unique searchable content",
                    "content": "Unique searchable content e2e-1234 for verification",
                    "metadata": json.dumps({"category": "testing"}),
                    "score": 1.0,
                }
            ],
            None,
            None,
        )

        results = await entity_manager.search(
            "Searchable E2E e2e-1234",
            entity_types=[EntityType.PATTERN],
        )

        assert len(results) == 1
        entity, score = results[0]
        assert entity.id == "pattern-001"
        assert entity.entity_type == EntityType.PATTERN
        assert score == 1.0

    @pytest.mark.asyncio
    async def test_search_prioritizes_exact_name_matches_over_noisy_hybrid_results(
        self,
        entity_manager: EntityManager,
        mock_graph_client: MagicMock,
        mock_driver: MagicMock,
    ) -> None:
        """search() promotes exact title matches when hybrid retrieval is noisy."""
        noisy_node = EntityNode(
            uuid="pattern-noisy",
            name="Searchable E2E old artifact",
            group_id="test-org-123",
            labels=["Entity", "pattern"],
            created_at=datetime.now(UTC),
            summary="A stale partial match",
            attributes={"entity_type": "pattern"},
        )

        mock_search_result = MagicMock()
        mock_search_result.nodes = [noisy_node]
        mock_search_result.node_reranker_scores = [0.95]
        mock_search_result.episodes = []
        mock_search_result.episode_reranker_scores = []
        mock_graph_client.client.search_.return_value = mock_search_result
        mock_driver.execute_query.return_value = (
            [
                {
                    "uuid": "pattern-exact",
                    "name": "Searchable E2E e2e-1234",
                    "entity_type": "pattern",
                    "group_id": "test-org-123",
                    "description": "Exact title match",
                    "content": "Exact title match content",
                    "metadata": json.dumps({"category": "testing"}),
                    "score": 2.0,
                }
            ],
            None,
            None,
        )

        results = await entity_manager.search(
            "Searchable E2E e2e-1234",
            entity_types=[EntityType.PATTERN],
        )

        assert len(results) == 2
        assert results[0][0].id == "pattern-exact"
        assert results[0][1] == 2.0

    @pytest.mark.asyncio
    async def test_list_by_type_handles_db_error(
        self,
        entity_manager: EntityManager,
        mock_driver: MagicMock,
    ) -> None:
        """list_by_type() returns empty list on DB error."""
        mock_driver.execute_query.side_effect = Exception("DB connection lost")

        results = await entity_manager.list_by_type(EntityType.TASK)

        assert results == []

    def test_record_to_entity_handles_missing_fields(
        self,
        entity_manager: EntityManager,
    ) -> None:
        """_record_to_entity() handles records with missing fields."""
        minimal_record = {
            "uuid": "min-001",
            "name": "Minimal",
            "entity_type": "pattern",
        }

        result = entity_manager._record_to_entity(minimal_record)

        assert result.id == "min-001"
        assert result.name == "Minimal"
        assert result.description == ""
        assert result.content == ""

    def test_record_to_entity_handles_unknown_entity_type(
        self,
        entity_manager: EntityManager,
    ) -> None:
        """_record_to_entity() defaults to EPISODE for unknown types."""
        record = {
            "uuid": "unknown-001",
            "name": "Unknown Type Entity",
            "entity_type": "not_a_real_type",
        }

        result = entity_manager._record_to_entity(record)

        assert result.entity_type == EntityType.EPISODE

    @pytest.mark.asyncio
    async def test_update_handles_db_error_during_persist(
        self,
        mock_graph_client: MagicMock,
    ) -> None:
        """update() propagates errors from persistence layer."""
        # Create a fresh manager with proper mock setup
        manager = EntityManager(mock_graph_client, group_id="test-org-123")

        # Create a mock entity node
        mock_entity = MagicMock(spec=EntityNode)
        mock_entity.uuid = "entity-001"
        mock_entity.name = "Test Entity"
        mock_entity.group_id = "test-org-123"
        mock_entity.labels = ["Entity", "pattern"]
        mock_entity.created_at = datetime.now(UTC)
        mock_entity.summary = "Test"
        mock_entity.attributes = {"entity_type": "pattern", "metadata": "{}"}
        mock_entity.name_embedding = None

        with (
            patch.object(
                EntityNode,
                "get_by_uuid",
                new_callable=AsyncMock,
                return_value=mock_entity,
            ),
            patch.object(
                manager,
                "_persist_entity_attributes",
                new_callable=AsyncMock,
                side_effect=Exception("Write failed"),
            ),
            pytest.raises(Exception, match="Write failed"),
        ):
            await manager.update("entity-001", {"name": "New Name"})


# =============================================================================
# Project Summary Tests
# =============================================================================


class TestGetProjectSummary:
    """Test project summary generation."""

    @pytest.mark.asyncio
    async def test_get_project_summary_uses_surreal_listings(
        self,
        surreal_entity_manager: EntityManager,
    ) -> None:
        tasks = [
            Task(
                id="task-001",
                name="CRITICAL Bug",
                title="CRITICAL Bug",
                description="",
                status=TaskStatus.DOING,
                priority=TaskPriority.CRITICAL,
                project_id="project-001",
                epic_id="epic-001",
                metadata={
                    "project_id": "project-001",
                    "status": "doing",
                    "priority": "critical",
                    "epic_id": "epic-001",
                },
            ),
            Task(
                id="task-002",
                name="Blocked task",
                title="Blocked task",
                description="",
                status=TaskStatus.BLOCKED,
                priority=TaskPriority.HIGH,
                project_id="project-001",
                epic_id="epic-001",
                metadata={
                    "project_id": "project-001",
                    "status": "blocked",
                    "priority": "high",
                    "epic_id": "epic-001",
                },
            ),
            Task(
                id="task-003",
                name="Done task",
                title="Done task",
                description="",
                status=TaskStatus.DONE,
                priority=TaskPriority.MEDIUM,
                project_id="project-001",
                epic_id="epic-001",
                metadata={
                    "project_id": "project-001",
                    "status": "done",
                    "priority": "medium",
                    "epic_id": "epic-001",
                },
            ),
        ]
        epics = [
            Epic(
                id="epic-001",
                name="Auth Epic",
                title="Auth Epic",
                description="",
                status=EpicStatus.IN_PROGRESS,
                priority=TaskPriority.HIGH,
                project_id="project-001",
                metadata={"status": "in_progress"},
            )
        ]

        with (
            patch.object(
                surreal_entity_manager,
                "list_by_type",
                new_callable=AsyncMock,
                return_value=tasks,
            ) as list_by_type,
            patch.object(
                surreal_entity_manager,
                "list_epics_for_project",
                new_callable=AsyncMock,
                return_value=epics,
            ),
        ):
            result = await surreal_entity_manager.get_project_summary("project-001")

        assert result["total_tasks"] == 3
        assert result["status_counts"]["doing"] == 1
        assert result["status_counts"]["blocked"] == 1
        assert result["status_counts"]["done"] == 1
        assert result["progress_pct"] == round(1 / 3 * 100, 1)
        assert [task["id"] for task in result["critical_tasks"]] == ["task-001", "task-002"]
        assert result["epics"][0]["progress_pct"] == round(1 / 3 * 100, 1)
        list_by_type.assert_awaited_once_with(
            EntityType.TASK,
            project_id="project-001",
            limit=1000,
            offset=0,
            include_archived=True,
        )

    @pytest.mark.asyncio
    async def test_get_project_summary(
        self,
        entity_manager: EntityManager,
        mock_driver: MagicMock,
    ) -> None:
        """get_project_summary() returns task counts and actionable tasks."""
        mock_driver.execute_query.side_effect = [
            (
                [
                    {
                        "uuid": "task-001",
                        "name": "CRITICAL Bug",
                        "project_id": None,
                        "metadata": json.dumps(
                            {
                                "project_id": "project-001",
                                "status": "doing",
                                "priority": "critical",
                                "epic_id": "epic-001",
                            }
                        ),
                    },
                    {
                        "uuid": "task-002",
                        "name": "Regular task",
                        "project_id": "",
                        "metadata": json.dumps(
                            {
                                "project_id": "project-001",
                                "status": "todo",
                                "priority": "medium",
                                "epic_id": "epic-001",
                            }
                        ),
                    },
                    {
                        "uuid": "task-003",
                        "name": "Blocked task",
                        "project_id": None,
                        "metadata": json.dumps(
                            {
                                "project_id": "project-001",
                                "status": "blocked",
                                "priority": "high",
                                "epic_id": "epic-001",
                            }
                        ),
                    },
                    {
                        "uuid": "task-004",
                        "name": "Done task",
                        "project_id": None,
                        "metadata": json.dumps(
                            {
                                "project_id": "project-001",
                                "status": "done",
                                "priority": "medium",
                                "epic_id": "epic-001",
                            }
                        ),
                    },
                    {
                        "uuid": "task-005",
                        "name": "Other project task",
                        "project_id": None,
                        "metadata": json.dumps(
                            {
                                "project_id": "project-999",
                                "status": "doing",
                                "priority": "high",
                                "epic_id": "epic-001",
                            }
                        ),
                    },
                ],
                None,
                None,
            ),
            (
                [
                    {
                        "uuid": "epic-001",
                        "name": "Auth Epic",
                        "status": "in_progress",
                    }
                ],
                None,
                None,
            ),
        ]

        result = await entity_manager.get_project_summary("project-001")

        assert result["total_tasks"] == 4
        assert result["status_counts"]["doing"] == 1
        assert result["status_counts"]["todo"] == 1
        assert result["status_counts"]["blocked"] == 1
        assert result["status_counts"]["done"] == 1
        assert result["progress_pct"] == 25.0
        # Should have actionable tasks prioritized: doing > blocked
        assert len(result["actionable_tasks"]) > 0
        # Critical task should be in critical_tasks
        assert [task["id"] for task in result["critical_tasks"]] == ["task-001", "task-003"]
        assert result["epics"][0]["progress_pct"] == 25.0
        assert result["epics"][0]["total_tasks"] == 4
        assert mock_driver.execute_query.await_count == 2
        task_query = mock_driver.execute_query.await_args_list[0]
        assert (
            "toLower(toString(n.metadata)) CONTAINS $legacy_project_compact" in task_query.args[0]
        )
        assert task_query.kwargs["legacy_project_compact"] == '"project_id":"project-001"'
        assert task_query.kwargs["legacy_project_spaced"] == '"project_id": "project-001"'
