"""Benchmark tests for lazy spaCy performance improvement (Issue #97).

Measures the performance difference between:
- lazy_spacy=True: regex extraction on ingest, spaCy NER on recall
- lazy_spacy=False: spaCy NER on both ingest and recall

Expected improvement: ~70x faster ingest in real-world usage (LongMemEval).
With mock services, improvement is smaller (~1.5-2.5x) due to fast mock embeddings.
"""

import time
import random
import pytest

from tribalmemory.services.graph_store import GraphStore, SPACY_AVAILABLE
from tribalmemory.services.memory import TribalMemoryService
from tribalmemory.testing.mocks import MockEmbeddingService, MockVectorStore


# Constants
MIN_TIME_EPSILON = 0.0001  # Guard against division by zero in speedup calculations

# Speedup thresholds for assertions (conservative due to mock service variance)
# Small benchmarks: lazy should not be slower (20% variance allowed)
SPEEDUP_THRESHOLD_SMOKE = 1.2
SPEEDUP_THRESHOLD_MEDIUM = 1.3   # 100-memory benchmark: expect measurable improvement with spaCy
SPEEDUP_THRESHOLD_COMPREHENSIVE = 1.2  # 200-memory benchmark: conservative minimum with spaCy


# --- Test Data Generation ---


def generate_test_memories(count: int, seed: int = 42) -> list[str]:
    """Generate test memories with entity-rich content."""
    templates = [
        "{person1} discussed {topic} with {person2} at {place}",
        "The {service} uses {technology} for {function}",
        "{person1} deployed {service} to {environment} using {tool}",
        "Integration between {service1} and {service2} via {protocol}",
        "{person1} fixed {bug_type} bug in {service} {component}",
        "{person1} reviewed code for {service} written in {language}",
        "The {service} connects to {technology} on port {port}",
    ]
    
    names = ["John", "Sarah", "Mary", "Tom", "Alice", "Bob", "Jane", "Mike",
            "Emma", "David", "Lisa", "Chris", "Anna", "Paul", "Rachel", "Steve"]
    places = ["Starbucks", "office", "cafe", "meeting room", "park", "library"]
    topics = ["API design", "database schema", "security", "performance",
              "architecture", "deployment", "monitoring", "testing"]
    services = ["auth-service", "api-gateway", "user-service", "payment-gateway",
                "order-service", "notification-worker", "web-service", "cache-layer",
                "search-service", "analytics-service", "logging-service", "admin-portal"]
    technologies = ["PostgreSQL", "Redis", "MongoDB", "Docker", "Kubernetes",
                    "nginx", "RabbitMQ", "Elasticsearch", "Kafka", "Cassandra"]
    functions = ["authentication", "caching", "logging", "monitoring", "analytics",
                 "search", "storage", "messaging", "routing", "processing"]
    environments = ["production", "staging", "development", "testing"]
    tools = ["Docker", "Kubernetes", "Ansible", "Terraform", "Jenkins"]
    protocols = ["REST API", "gRPC", "WebSocket", "message queue", "HTTP"]
    bug_types = ["memory leak", "race condition", "null pointer", "timeout"]
    components = ["handler", "middleware", "controller", "service layer", "repository"]
    languages = ["Python", "TypeScript", "Go", "Java", "Rust"]
    
    random.seed(seed)
    
    memories = []
    for i in range(count):
        template = random.choice(templates)
        memory = template.format(
            person1=random.choice(names),
            person2=random.choice(names),
            place=random.choice(places),
            topic=random.choice(topics),
            service=random.choice(services),
            service1=random.choice(services),
            service2=random.choice(services),
            technology=random.choice(technologies),
            function=random.choice(functions),
            environment=random.choice(environments),
            tool=random.choice(tools),
            protocol=random.choice(protocols),
            bug_type=random.choice(bug_types),
            component=random.choice(components),
            language=random.choice(languages),
            port=random.randint(3000, 9000),
        )
        memories.append(memory)
    
    return memories


# --- Fixtures ---


@pytest.fixture
def mock_embedding_service() -> MockEmbeddingService:
    return MockEmbeddingService(embedding_dim=64)


@pytest.fixture
def mock_graph_store(tmp_path) -> GraphStore:
    return GraphStore(str(tmp_path / "graph.db"))


# --- Benchmark Tests ---


class TestLazySpacyBenchmark:
    """Benchmark lazy vs eager spaCy modes (Issue #97)."""

    @pytest.mark.asyncio
    async def test_ingest_benchmark_small(
        self, tmp_path, mock_embedding_service
    ):
        """Benchmark ingest with 20 memories (smoke test)."""
        memories = generate_test_memories(20, seed=97)
        
        # Create separate vector stores for isolation
        lazy_vector_store = MockVectorStore(mock_embedding_service)
        eager_vector_store = MockVectorStore(mock_embedding_service)
        
        # Lazy mode
        lazy_graph_store = GraphStore(str(tmp_path / "lazy_graph.db"))
        lazy_service = TribalMemoryService(
            instance_id="lazy",
            embedding_service=mock_embedding_service,
            vector_store=lazy_vector_store,
            graph_store=lazy_graph_store,
            graph_enabled=True,
            lazy_spacy=True,
        )
        
        start = time.perf_counter()
        for content in memories:
            await lazy_service.remember(content)
        lazy_time = time.perf_counter() - start
        
        # Eager mode
        eager_graph_store = GraphStore(str(tmp_path / "eager_graph.db"))
        eager_service = TribalMemoryService(
            instance_id="eager",
            embedding_service=mock_embedding_service,
            vector_store=eager_vector_store,
            graph_store=eager_graph_store,
            graph_enabled=True,
            lazy_spacy=False,
        )
        
        start = time.perf_counter()
        for content in memories:
            await eager_service.remember(content)
        eager_time = time.perf_counter() - start
        
        speedup = eager_time / max(lazy_time, MIN_TIME_EPSILON)
        lazy_ms_per = (lazy_time * 1000) / len(memories)
        eager_ms_per = (eager_time * 1000) / len(memories)
        
        print(f"\n=== Small Benchmark (20 memories) ===")
        print(f"Lazy:  {lazy_time:.3f}s ({lazy_ms_per:.2f}ms/memory)")
        print(f"Eager: {eager_time:.3f}s ({eager_ms_per:.2f}ms/memory)")
        print(f"Speedup: {speedup:.1f}x")
        
        # Lazy should not be slower (allow variance)
        assert lazy_time <= eager_time * SPEEDUP_THRESHOLD_SMOKE, (
            f"Lazy unexpectedly slower: lazy={lazy_time:.3f}s, "
            f"eager={eager_time:.3f}s (speedup={speedup:.1f}x)"
        )

    @pytest.mark.slow
    @pytest.mark.asyncio
    async def test_ingest_benchmark_100(
        self, tmp_path, mock_embedding_service
    ):
        """Benchmark ingest with 100 memories.
        
        Expected: SPEEDUP_THRESHOLD_MEDIUM speedup with mocks (70x with real embeddings).
        """
        memories = generate_test_memories(100, seed=97)
        
        # Create separate vector stores
        lazy_vector_store = MockVectorStore(mock_embedding_service)
        eager_vector_store = MockVectorStore(mock_embedding_service)
        
        # Lazy mode
        lazy_graph_store = GraphStore(str(tmp_path / "lazy_graph.db"))
        lazy_service = TribalMemoryService(
            instance_id="lazy",
            embedding_service=mock_embedding_service,
            vector_store=lazy_vector_store,
            graph_store=lazy_graph_store,
            graph_enabled=True,
            lazy_spacy=True,
        )
        
        start = time.perf_counter()
        for content in memories:
            await lazy_service.remember(content)
        lazy_time = time.perf_counter() - start
        
        # Eager mode
        eager_graph_store = GraphStore(str(tmp_path / "eager_graph.db"))
        eager_service = TribalMemoryService(
            instance_id="eager",
            embedding_service=mock_embedding_service,
            vector_store=eager_vector_store,
            graph_store=eager_graph_store,
            graph_enabled=True,
            lazy_spacy=False,
        )
        
        start = time.perf_counter()
        for content in memories:
            await eager_service.remember(content)
        eager_time = time.perf_counter() - start
        
        speedup = eager_time / max(lazy_time, MIN_TIME_EPSILON)
        lazy_ms = (lazy_time * 1000) / len(memories)
        eager_ms = (eager_time * 1000) / len(memories)
        
        print(f"\n=== Benchmark (100 memories) ===")
        print(f"Lazy:  {lazy_time:.3f}s ({lazy_ms:.2f}ms/memory)")
        print(f"Eager: {eager_time:.3f}s ({eager_ms:.2f}ms/memory)")
        print(f"Speedup: {speedup:.1f}x")
        
        # With mock embeddings, expect at least SPEEDUP_THRESHOLD_MEDIUM improvement
        # Real-world with actual embeddings shows ~70x
        if SPACY_AVAILABLE:
            assert speedup >= SPEEDUP_THRESHOLD_MEDIUM, (
                f"Expected ≥{SPEEDUP_THRESHOLD_MEDIUM}x speedup with spaCy, "
                f"got {speedup:.1f}x. Real-world (with real embeddings) shows ~70x."
            )

    @pytest.mark.slow
    @pytest.mark.asyncio
    async def test_comprehensive_benchmark(
        self, tmp_path, mock_embedding_service
    ):
        """Comprehensive benchmark documenting lazy spaCy performance (Issue #97).
        
        This is the canonical performance documentation for the issue.
        Measures:
        - Ingest latency (total and per-memory)
        - Vector-only recall performance
        - Documents expected vs actual speedup
        """
        memories = generate_test_memories(200, seed=97)
        
        print(f"\n{'='*70}")
        print(f"Lazy spaCy Performance Benchmark (Issue #97)")
        print(f"{'='*70}")
        print(f"Corpus: {len(memories)} memories with entity-rich content")
        print()
        
        # Create separate vector stores
        lazy_vector_store = MockVectorStore(mock_embedding_service)
        eager_vector_store = MockVectorStore(mock_embedding_service)
        
        # === LAZY MODE ===
        lazy_graph_store = GraphStore(str(tmp_path / "lazy_graph.db"))
        lazy_service = TribalMemoryService(
            instance_id="lazy",
            embedding_service=mock_embedding_service,
            vector_store=lazy_vector_store,
            graph_store=lazy_graph_store,
            graph_enabled=True,
            lazy_spacy=True,
        )
        
        start = time.perf_counter()
        for content in memories:
            await lazy_service.remember(content)
        lazy_ingest_time = time.perf_counter() - start
        
        # Test vector-only recall (no graph expansion)
        lazy_recall_start = time.perf_counter()
        lazy_results = await lazy_service.recall(
            "api-gateway", limit=5, graph_expansion=False
        )
        lazy_recall_time = time.perf_counter() - lazy_recall_start
        
        # === EAGER MODE ===
        eager_graph_store = GraphStore(str(tmp_path / "eager_graph.db"))
        eager_service = TribalMemoryService(
            instance_id="eager",
            embedding_service=mock_embedding_service,
            vector_store=eager_vector_store,
            graph_store=eager_graph_store,
            graph_enabled=True,
            lazy_spacy=False,
        )
        
        start = time.perf_counter()
        for content in memories:
            await eager_service.remember(content)
        eager_ingest_time = time.perf_counter() - start
        
        # Test vector-only recall
        eager_recall_start = time.perf_counter()
        eager_results = await eager_service.recall(
            "api-gateway", limit=5, graph_expansion=False
        )
        eager_recall_time = time.perf_counter() - eager_recall_start
        
        # === METRICS ===
        speedup = eager_ingest_time / max(lazy_ingest_time, MIN_TIME_EPSILON)
        lazy_ms_per = (lazy_ingest_time * 1000) / len(memories)
        eager_ms_per = (eager_ingest_time * 1000) / len(memories)
        
        # === REPORT ===
        print(f"Ingest Performance:")
        print(f"  Lazy mode:  {lazy_ingest_time:.3f}s ({lazy_ms_per:.2f}ms/memory)")
        print(f"  Eager mode: {eager_ingest_time:.3f}s ({eager_ms_per:.2f}ms/memory)")
        print(f"  Speedup:    {speedup:.1f}x")
        print()
        print(f"Vector-Only Recall Performance:")
        print(f"  Lazy:  {lazy_recall_time*1000:.2f}ms ({len(lazy_results)} results)")
        print(f"  Eager: {eager_recall_time*1000:.2f}ms "
              f"({len(eager_results)} results)")
        print()
        print(f"Expected vs Actual:")
        print(f"  Target (real-world): ~70x faster ingest with lazy spaCy")
        print(f"  Actual (mock services): {speedup:.1f}x")
        print()
        if SPACY_AVAILABLE:
            print(f"  ✓ spaCy available: Ingest speedup from skipping NER")
            print(f"  Note: Mock embeddings reduce the gap. Real embeddings")
            print(f"        (FastEmbed/OpenAI) dominate ingest cost, amplifying")
            print(f"        the lazy spaCy benefit")
        else:
            print(f"  ⚠ spaCy not installed: Both modes use regex only")
        print()
        print(f"  Vector recall performance is identical (both use embeddings).")
        print(f"  Graph expansion may differ (lazy: regex entities, "
              f"eager: spaCy entities)")
        print(f"{'='*70}\n")
        
        # === ASSERTIONS ===
        # Lazy should be at least as fast (allow variance)
        assert lazy_ingest_time <= eager_ingest_time * SPEEDUP_THRESHOLD_SMOKE, (
            f"Lazy mode slower than expected: lazy={lazy_ingest_time:.3f}s, "
            f"eager={eager_ingest_time:.3f}s (speedup={speedup:.1f}x)"
        )
        
        # Vector-only recall should return similar results
        # (may differ by 1-2 due to tie-breaking in mock similarity scores)
        assert abs(len(lazy_results) - len(eager_results)) <= 2, (
            f"Vector recall differs significantly: lazy={len(lazy_results)}, "
            f"eager={len(eager_results)}"
        )
        
        # If spaCy available, expect measurable improvement
        # This threshold is conservative to account for variance in mock services
        if SPACY_AVAILABLE:
            assert speedup >= SPEEDUP_THRESHOLD_COMPREHENSIVE, (
                f"Expected ≥{SPEEDUP_THRESHOLD_COMPREHENSIVE}x speedup with spaCy, "
                f"got {speedup:.1f}x. Real-world shows ~70x with actual embeddings."
            )
        
        print(f"✅ Benchmark complete: {speedup:.1f}x faster ingest with lazy spaCy")

    # --- Recall tests ---

    @pytest.mark.asyncio
    async def test_recall_vector_only_identical(
        self, tmp_path, mock_embedding_service
    ):
        """Verify vector-only recall is identical between modes.
        
        Vector similarity search should return the same results regardless
        of lazy vs eager mode (both use the same embeddings).
        
        Graph expansion results will differ (by design) because lazy uses
        regex entities on ingest while eager uses spaCy entities.
        """
        memories = [
            "The auth-service uses PostgreSQL for user storage",
            "John deployed the api-gateway to production",
            "The payment-service connects to Stripe API",
        ]
        
        # Create separate vector stores
        lazy_vector_store = MockVectorStore(mock_embedding_service)
        eager_vector_store = MockVectorStore(mock_embedding_service)
        
        # Lazy mode
        lazy_graph_store = GraphStore(str(tmp_path / "lazy_graph.db"))
        lazy_service = TribalMemoryService(
            instance_id="lazy",
            embedding_service=mock_embedding_service,
            vector_store=lazy_vector_store,
            graph_store=lazy_graph_store,
            graph_enabled=True,
            lazy_spacy=True,
        )
        
        for content in memories:
            await lazy_service.remember(content)
        
        lazy_results = await lazy_service.recall(
            "auth-service", limit=10, graph_expansion=False
        )
        
        # Eager mode
        eager_graph_store = GraphStore(str(tmp_path / "eager_graph.db"))
        eager_service = TribalMemoryService(
            instance_id="eager",
            embedding_service=mock_embedding_service,
            vector_store=eager_vector_store,
            graph_store=eager_graph_store,
            graph_enabled=True,
            lazy_spacy=False,
        )
        
        for content in memories:
            await eager_service.remember(content)
        
        eager_results = await eager_service.recall(
            "auth-service", limit=10, graph_expansion=False
        )
        
        # Vector-only results should be identical
        assert len(lazy_results) == len(eager_results), (
            f"Vector recall differs: lazy={len(lazy_results)}, "
            f"eager={len(eager_results)}"
        )
        
        lazy_contents = {r.memory.content for r in lazy_results}
        eager_contents = {r.memory.content for r in eager_results}
        assert lazy_contents == eager_contents, "Vector recall content differs"

    @pytest.mark.asyncio
    async def test_graph_expansion_differs_between_modes(
        self, tmp_path, mock_embedding_service
    ):
        """Verify graph expansion may differ between lazy and eager modes.

        This documents a deliberate design tradeoff (Issue #97):
        - Lazy mode uses regex-only entity extraction on ingest → fewer graph entities
        - Eager mode uses spaCy NER on ingest → richer graph entities
        - Graph expansion at recall uses spaCy in both modes (query side)

        The tradeoff: lazy mode trades graph richness for dramatically faster
        ingest (~70x with real embeddings). In practice, regex captures the
        most important entities (service names, technologies, capitalized names),
        while spaCy additionally detects context-dependent entities.
        """
        # Sentences with entities that regex vs spaCy may extract differently
        memories = [
            "The auth-service uses PostgreSQL for user storage",
            "John deployed the api-gateway to production using Docker",
            "Sarah reviewed the payment-service integration with Stripe",
            "The api-gateway connects to Redis on port 6379",
            "Bob fixed a memory leak bug in the auth-service handler",
        ]

        # Create separate vector stores
        lazy_vector_store = MockVectorStore(mock_embedding_service)
        eager_vector_store = MockVectorStore(mock_embedding_service)

        # Lazy mode
        lazy_graph_store = GraphStore(str(tmp_path / "lazy_graph.db"))
        lazy_service = TribalMemoryService(
            instance_id="lazy",
            embedding_service=mock_embedding_service,
            vector_store=lazy_vector_store,
            graph_store=lazy_graph_store,
            graph_enabled=True,
            lazy_spacy=True,
        )

        for content in memories:
            await lazy_service.remember(content)

        # Eager mode
        eager_graph_store = GraphStore(str(tmp_path / "eager_graph.db"))
        eager_service = TribalMemoryService(
            instance_id="eager",
            embedding_service=mock_embedding_service,
            vector_store=eager_vector_store,
            graph_store=eager_graph_store,
            graph_enabled=True,
            lazy_spacy=False,
        )

        for content in memories:
            await eager_service.remember(content)

        # Count total graph entities stored by each mode
        lazy_entity_count = lazy_graph_store._conn.execute(
            "SELECT COUNT(*) FROM entities"
        ).fetchone()[0]
        eager_entity_count = eager_graph_store._conn.execute(
            "SELECT COUNT(*) FROM entities"
        ).fetchone()[0]

        print(f"\n=== Graph Entity Comparison ===")
        print(f"Lazy (regex-only ingest):  {lazy_entity_count} entities")
        print(f"Eager (spaCy ingest):      {eager_entity_count} entities")

        if SPACY_AVAILABLE:
            # When spaCy is available, eager mode should extract at least as many
            # entities as lazy mode (spaCy is a superset of regex extraction)
            assert eager_entity_count >= lazy_entity_count, (
                f"Eager mode should extract ≥ lazy entities: "
                f"eager={eager_entity_count}, lazy={lazy_entity_count}"
            )
            print(
                f"  ✓ Eager extracted ≥ lazy entities "
                f"(spaCy is superset of regex)"
            )
        else:
            # Without spaCy, both modes use regex only — entities should match
            assert eager_entity_count == lazy_entity_count, (
                f"Without spaCy, entity counts should match: "
                f"eager={eager_entity_count}, lazy={lazy_entity_count}"
            )
            print(f"  ⚠ spaCy not available — both modes use regex, counts match")

        # Both modes should extract SOME entities (regex catches capitalized names,
        # service names with hyphens, and known technology names)
        assert lazy_entity_count > 0, "Lazy mode should extract some entities via regex"
        assert eager_entity_count > 0, "Eager mode should extract entities"

        # Recall with graph expansion — results may differ
        lazy_results = await lazy_service.recall(
            "auth-service PostgreSQL", limit=10, graph_expansion=True
        )
        eager_results = await eager_service.recall(
            "auth-service PostgreSQL", limit=10, graph_expansion=True
        )

        print(f"Recall with graph expansion:")
        print(f"  Lazy:  {len(lazy_results)} results")
        print(f"  Eager: {len(eager_results)} results")
        print(
            f"  Design tradeoff: lazy trades graph richness for ~70x faster ingest"
        )

    @pytest.mark.asyncio
    async def test_graph_expansion_tradeoff_documentation(self, tmp_path):
        """Document the design tradeoff between lazy and eager modes (Issue #127).

        This test serves as living documentation of the lazy spaCy design decision:

        **Lazy Mode (lazy_spacy=True):**
        - Ingest: Regex-only entity extraction → ~70x faster with real embeddings
        - Graph: Captures high-signal entities (services, tech, capitalized names)
        - Tradeoff: Misses context-dependent entities that spaCy would detect

        **Eager Mode (lazy_spacy=False):**
        - Ingest: Full spaCy NER → richer entity graph, slower ingest
        - Graph: Captures all entities including context-dependent ones
        - Tradeoff: Ingest latency increases significantly

        **Recommendation:** Use lazy mode for most workloads. The regex extractor
        captures the most important entities while dramatically reducing ingest time.
        Use eager mode only when entity graph richness is critical.
        """
        # Use higher-dimensional embeddings to better simulate real-world usage
        embedding_service = MockEmbeddingService(embedding_dim=384)

        # Entity-rich memories designed to highlight extraction differences
        memories = [
            "Sarah deployed auth-service v2.1 to production using Docker Compose",
            "The api-gateway handles authentication via JWT tokens and Redis cache",
            "John discussed the migration plan with the DevOps team at the office",
            "PostgreSQL replication lag caused timeout errors in payment-service",
            "Alice configured monitoring with Prometheus and Grafana dashboards",
        ]

        # === Lazy Mode ===
        lazy_vector_store = MockVectorStore(embedding_service)
        lazy_graph_store = GraphStore(str(tmp_path / "lazy_graph.db"))
        lazy_service = TribalMemoryService(
            instance_id="lazy-tradeoff",
            embedding_service=embedding_service,
            vector_store=lazy_vector_store,
            graph_store=lazy_graph_store,
            graph_enabled=True,
            lazy_spacy=True,
        )

        for content in memories:
            await lazy_service.remember(content)

        # === Eager Mode ===
        eager_vector_store = MockVectorStore(embedding_service)
        eager_graph_store = GraphStore(str(tmp_path / "eager_graph.db"))
        eager_service = TribalMemoryService(
            instance_id="eager-tradeoff",
            embedding_service=embedding_service,
            vector_store=eager_vector_store,
            graph_store=eager_graph_store,
            graph_enabled=True,
            lazy_spacy=False,
        )

        for content in memories:
            await eager_service.remember(content)

        # === Compare Entity Extraction ===
        lazy_entities = lazy_graph_store._conn.execute(
            "SELECT name FROM entities ORDER BY name"
        ).fetchall()
        eager_entities = eager_graph_store._conn.execute(
            "SELECT name FROM entities ORDER BY name"
        ).fetchall()

        lazy_entity_names = {name for (name,) in lazy_entities}
        eager_entity_names = {name for (name,) in eager_entities}

        print(f"\n{'='*70}")
        print(f"Graph Expansion Tradeoff Analysis (Issue #127)")
        print(f"{'='*70}")
        print(f"Memories ingested: {len(memories)}")
        print(f"Embedding dimension: {embedding_service.embedding_dim}")
        print()
        print(f"Lazy mode entities ({len(lazy_entity_names)}):")
        print(f"  {sorted(lazy_entity_names)}")
        print()
        print(f"Eager mode entities ({len(eager_entity_names)}):")
        print(f"  {sorted(eager_entity_names)}")
        print()

        if SPACY_AVAILABLE:
            only_in_eager = eager_entity_names - lazy_entity_names
            only_in_lazy = lazy_entity_names - eager_entity_names
            common = lazy_entity_names & eager_entity_names

            print(f"Common entities: {len(common)}")
            print(f"Only in eager (spaCy-detected): {len(only_in_eager)}")
            if only_in_eager:
                print(f"  {sorted(only_in_eager)}")
            print(f"Only in lazy (regex-detected): {len(only_in_lazy)}")
            if only_in_lazy:
                print(f"  {sorted(only_in_lazy)}")
            print()
            print(f"Design decision: Lazy mode trades {len(only_in_eager)} "
                  f"spaCy-detected entities for ~70x faster ingest")
        else:
            print(f"⚠ spaCy not installed — both modes use regex")
            print(f"  Install spaCy to see the extraction difference:")
            print(f"    pip install tribalmemory[spacy] && "
                  f"python -m spacy download en_core_web_sm")

        print(f"{'='*70}\n")

        # === Test Recall with Graph Expansion ===
        # Use min_relevance=0.0 to retrieve all memories regardless of similarity
        lazy_results = await lazy_service.recall(
            "authentication system", limit=10, min_relevance=0.0, graph_expansion=True
        )
        eager_results = await eager_service.recall(
            "authentication system", limit=10, min_relevance=0.0, graph_expansion=True
        )

        print(f"Recall with graph expansion (min_relevance=0.0):")
        print(f"  Lazy:  {len(lazy_results)} results")
        print(f"  Eager: {len(eager_results)} results")

        # === Assertions ===
        # Both modes should extract some entities
        assert len(lazy_entity_names) > 0, "Lazy mode should extract entities"
        assert len(eager_entity_names) > 0, "Eager mode should extract entities"

        if SPACY_AVAILABLE:
            # With spaCy, eager should extract at least as many entities
            assert len(eager_entity_names) >= len(lazy_entity_names), (
                f"Eager mode should extract ≥ lazy entities: "
                f"eager={len(eager_entity_names)}, lazy={len(lazy_entity_names)}"
            )

        # Both modes should successfully recall memories
        assert len(lazy_results) > 0, "Lazy mode should return results"
        assert len(eager_results) > 0, "Eager mode should return results"

        print(f"✅ Graph expansion tradeoff documented and verified")
