"""Integration tests using real stores with mocked LLM/embedding API.

All tests are marked @pytest.mark.integration and use temporary directories
so they never touch production data.
"""

from __future__ import annotations

from contextlib import ExitStack
from unittest.mock import AsyncMock, patch

import pytest

from engram.config import EmbeddingConfig, EpisodicConfig, SemanticConfig
from engram.episodic.store import EpisodicStore
from engram.models import MemoryType, SemanticEdge, SemanticNode
from engram.semantic import create_graph

# Fixed 384-dim embedding for all integration tests
_FIXED_VEC = [0.1] * 384

_EMBED_TARGETS = [
    "engram.episodic.embeddings._get_embeddings",
    "engram.episodic.store._get_embeddings",
    "engram.episodic.episodic_crud._get_embeddings",
    "engram.episodic.episodic_search._get_embeddings",
    "engram.episodic.batch_operations._get_embeddings",
]


def _mock_embeddings(_model, texts, _dim=None):
    return [_FIXED_VEC for _ in texts]


def _patch_embeddings():
    stack = ExitStack()
    for target in _EMBED_TARGETS:
        stack.enter_context(patch(target, side_effect=_mock_embeddings))
    return stack


def _make_store(tmp_path, subdir, namespace=None):
    cfg = EpisodicConfig(
        path=str(tmp_path / subdir),
        mode="embedded",
        dedup_enabled=False,
        fts_db_path=str(tmp_path / f"{subdir}_fts.db"),
    )
    embed = EmbeddingConfig(provider="test", model="all-MiniLM-L6-v2")
    store = EpisodicStore(config=cfg, embedding_config=embed, namespace=namespace) if namespace else EpisodicStore(config=cfg, embedding_config=embed)
    store._embedding_dim = 384
    return store


# --- Fixtures ---

@pytest.fixture
def episodic(tmp_path):
    with _patch_embeddings():
        yield _make_store(tmp_path, "episodic")


@pytest.fixture
def graph(tmp_path):
    cfg = SemanticConfig(path=str(tmp_path / "semantic.db"))
    return create_graph(cfg)


@pytest.fixture
def tenant_a(tmp_path):
    with _patch_embeddings():
        yield _make_store(tmp_path, "episodic_a", namespace="tenant_a")


@pytest.fixture
def tenant_b(tmp_path):
    with _patch_embeddings():
        yield _make_store(tmp_path, "episodic_b", namespace="tenant_b")


# --- Full episodic workflow ---

@pytest.mark.integration
async def test_remember_then_recall_finds_content(episodic):
    """remember → search → content found in results."""
    with _patch_embeddings():
        mem_id = await episodic.remember("Python is great for data science", memory_type=MemoryType.FACT)
    assert mem_id

    with _patch_embeddings():
        results = await episodic.search("Python data science", limit=5)

    assert len(results) >= 1
    contents = [r.content for r in results]
    assert any("Python" in c for c in contents)


@pytest.mark.integration
async def test_remember_with_tags_then_filter_by_tag(episodic):
    """remember with tags → recall filtered by tag → only tagged results."""
    with _patch_embeddings():
        await episodic.remember("Deploy to production", memory_type=MemoryType.DECISION, tags=["deploy", "prod"])
        await episodic.remember("Refactor auth module", memory_type=MemoryType.FACT, tags=["auth"])

    with _patch_embeddings():
        results = await episodic.search("production deploy", limit=5, tags=["deploy"])

    assert len(results) >= 1
    for r in results:
        assert "deploy" in r.tags


@pytest.mark.integration
async def test_remember_multiple_then_count_in_stats(episodic):
    """Multiple remember calls → stats count increases correctly."""
    with _patch_embeddings():
        await episodic.remember("First memory")
        await episodic.remember("Second memory")
        await episodic.remember("Third memory")

    stats = await episodic.stats()
    assert stats["count"] >= 3


@pytest.mark.integration
async def test_cleanup_removes_expired_memories(episodic):
    """remember with past expiry → cleanup → count decreases."""
    from datetime import datetime, timedelta, timezone

    past = datetime.now(timezone.utc) - timedelta(hours=1)
    with _patch_embeddings():
        await episodic.remember("This should expire", expires_at=past)
        await episodic.remember("This is permanent")

    deleted = await episodic.cleanup_expired()
    assert deleted >= 1

    stats = await episodic.stats()
    assert stats["count"] >= 1


# --- Graph workflow ---

@pytest.mark.integration
async def test_add_nodes_then_query_finds_them(graph):
    """add_node × 2 → query by keyword → nodes found."""
    node1 = SemanticNode(type="Service", name="auth-service")
    node2 = SemanticNode(type="Service", name="payment-service")
    await graph.add_node(node1)
    await graph.add_node(node2)

    results = await graph.query("auth")
    assert len(results) >= 1
    names = [n.name for n in results]
    assert "auth-service" in names


@pytest.mark.integration
async def test_add_edge_then_get_related(graph):
    """add nodes + edge → get_related by name → edge appears in result."""
    team = SemanticNode(type="Team", name="platform-team")
    svc = SemanticNode(type="Service", name="api-gateway")
    await graph.add_node(team)
    await graph.add_node(svc)

    edge = SemanticEdge(from_node=team.key, to_node=svc.key, relation="owns")
    await graph.add_edge(edge)

    # get_related searches by plain name (suffix match on node keys)
    related = await graph.get_related(["platform-team"])
    assert "platform-team" in related
    edges = related["platform-team"].get("edges", [])
    assert len(edges) >= 1
    assert any(e.relation == "owns" for e in edges)


@pytest.mark.integration
async def test_remove_node_no_longer_in_query(graph):
    """add node → remove node → query returns nothing."""
    node = SemanticNode(type="Technology", name="legacy-db")
    await graph.add_node(node)

    results_before = await graph.query("legacy-db")
    assert len(results_before) >= 1

    await graph.remove_node(node.key)
    results_after = await graph.query("legacy-db")
    names_after = [n.name for n in results_after]
    assert "legacy-db" not in names_after


@pytest.mark.integration
async def test_graph_stats_reflect_adds(graph):
    """add 2 nodes, 1 edge → stats show correct counts."""
    node1 = SemanticNode(type="Service", name="svc-a")
    node2 = SemanticNode(type="Service", name="svc-b")
    await graph.add_node(node1)
    await graph.add_node(node2)
    edge = SemanticEdge(from_node=node1.key, to_node=node2.key, relation="calls")
    await graph.add_edge(edge)

    stats = await graph.stats()
    assert stats["node_count"] >= 2
    assert stats["edge_count"] >= 1


# --- Multi-tenant isolation ---

@pytest.mark.integration
async def test_tenant_isolation_store_a_not_visible_from_b(tenant_a, tenant_b):
    """Data stored by tenant A is not visible to tenant B."""
    with _patch_embeddings():
        await tenant_a.remember("Tenant A secret data", memory_type=MemoryType.FACT)

    with _patch_embeddings():
        results_b = await tenant_b.search("Tenant A secret data", limit=5)

    # Tenant B must not see tenant A's memories
    contents_b = [r.content for r in results_b]
    assert not any("Tenant A" in c for c in contents_b)


@pytest.mark.integration
async def test_tenant_isolation_separate_stats(tenant_a, tenant_b):
    """Each tenant's stats are independent."""
    with _patch_embeddings():
        await tenant_a.remember("A memory 1")
        await tenant_a.remember("A memory 2")

    stats_a = await tenant_a.stats()
    stats_b = await tenant_b.stats()

    assert stats_a["count"] >= 2
    assert stats_b["count"] == 0


# --- Health check ---

@pytest.mark.integration
async def test_health_check_returns_ok(episodic, graph):
    """deep_check with real stores returns overall healthy status."""
    from engram.health import deep_check

    result = await deep_check(episodic, graph)
    assert result["status"] in ("healthy", "unhealthy")  # just verifies it runs
    assert "components" in result
    assert "episodic_store" in result["components"]
    assert "semantic" in result["components"]


@pytest.mark.integration
async def test_health_check_episodic_store_component(episodic, graph):
    """Episodic store component health reports count correctly."""
    with _patch_embeddings():
        await episodic.remember("health check memory")

    from engram.health import check_episodic_store

    component = await check_episodic_store(episodic)
    assert component.status == "healthy"
    assert component.details.get("count", 0) >= 1


# --- Backup / restore roundtrip ---

@pytest.mark.integration
async def test_backup_restore_roundtrip(episodic, graph, tmp_path):
    """backup → restore → verify episodic data intact."""
    from engram.backup import backup, restore
    from engram.config import EpisodicConfig, EmbeddingConfig

    with _patch_embeddings():
        await episodic.remember("Backup test memory", memory_type=MemoryType.FACT)

    archive = str(tmp_path / "test_backup.tar.gz")
    manifest = await backup(episodic, graph, archive)
    assert manifest["episodic_count"] >= 1

    # Restore into a fresh store
    fresh_cfg = EpisodicConfig(
        path=str(tmp_path / "restored_episodic"),
        mode="embedded",
        dedup_enabled=False,
        fts_db_path=str(tmp_path / "restored_fts.db"),
    )
    fresh_embed = EmbeddingConfig(provider="test", model="all-MiniLM-L6-v2")
    with _patch_embeddings():
        fresh_store = EpisodicStore(config=fresh_cfg, embedding_config=fresh_embed)
        fresh_store._embedding_dim = 384

    from engram.config import SemanticConfig
    fresh_graph = create_graph(SemanticConfig(path=str(tmp_path / "restored_semantic.db")))

    result = await restore(fresh_store, fresh_graph, archive)
    assert result["episodic_restored"] >= 1
