"""Tests for memory portability — export and import."""

import json
from datetime import datetime, timedelta, timezone

import pytest

from locidb.core.edge import MemoryEdge
from locidb.core.node import MemoryNode
from locidb.core.store import MemoryStore
from locidb.core.portability import (
    export_context_prompt,
    export_native,
    export_snapshot,
    import_context_md,
    import_native,
)


@pytest.fixture
def store(tmp_path):
    s = MemoryStore(tmp_path / "test.loci")
    yield s
    s.close()


@pytest.fixture
def populated_store(store):
    """A store with nodes and edges for export tests."""
    now = datetime.now(timezone.utc)

    n1 = MemoryNode(
        id="node1",
        content="Decided to use hub-spoke model for routing",
        embedding=[0.1, 0.2, 0.3],
        importance=0.9,
        timestamp=now,
        entity_tags=["Dropzi", "routing"],
        context_scope="project:dropzi",
        summary_level=0,
        access_count=5,
    )
    n2 = MemoryNode(
        id="node2",
        content="Delivery radius capped at 15km per hub",
        embedding=[0.4, 0.5, 0.6],
        importance=0.8,
        timestamp=now - timedelta(days=10),
        entity_tags=["Dropzi", "delivery"],
        context_scope="project:dropzi",
        summary_level=1,
    )
    n3 = MemoryNode(
        id="node3",
        content="Neetish prefers concise responses",
        embedding=[0.7, 0.8, 0.9],
        importance=0.75,
        timestamp=now - timedelta(days=5),
        entity_tags=["Neetish", "preferences"],
        context_scope="user:neetish",
    )
    n4 = MemoryNode(
        id="node4",
        content="Old memory that should have low importance",
        embedding=[0.01, 0.02, 0.03],
        importance=0.2,
        timestamp=now - timedelta(days=100),
        entity_tags=["old"],
        context_scope="global",
    )

    for n in [n1, n2, n3, n4]:
        store.save_node(n)

    e1 = MemoryEdge(
        id="edge1", source_id="node1", target_id="node2",
        relationship_type="related_to", strength=0.85,
    )
    e2 = MemoryEdge(
        id="edge2", source_id="node3", target_id="node1",
        relationship_type="decided", strength=0.9,
    )
    store.save_edge(e1)
    store.save_edge(e2)

    return store


# ── Native export ─────────────────────────────────────────────────────

class TestNativeExport:
    def test_exports_all_nodes_and_edges(self, populated_store, tmp_path):
        out = export_native(populated_store, tmp_path / "backup.loci.json")
        data = json.loads(out.read_text())

        assert data["locidb_version"] == "0.1.0"
        assert data["stats"]["total_nodes"] == 4
        assert data["stats"]["total_edges"] == 2
        assert len(data["nodes"]) == 4
        assert len(data["edges"]) == 2

    def test_includes_embeddings(self, populated_store, tmp_path):
        out = export_native(populated_store, tmp_path / "backup.loci.json")
        data = json.loads(out.read_text())

        node = next(n for n in data["nodes"] if n["id"] == "node1")
        assert node["embedding"] == [0.1, 0.2, 0.3]

    def test_scope_filter(self, populated_store, tmp_path):
        out = export_native(populated_store, tmp_path / "dropzi.loci.json", scope="project:dropzi")
        data = json.loads(out.read_text())

        assert data["stats"]["total_nodes"] == 2
        assert all(n["context_scope"] == "project:dropzi" for n in data["nodes"])

    def test_min_importance_filter(self, populated_store, tmp_path):
        out = export_native(populated_store, tmp_path / "important.loci.json", min_importance=0.5)
        data = json.loads(out.read_text())

        assert all(n["importance"] >= 0.5 for n in data["nodes"])
        # node4 (importance=0.2) should be excluded
        ids = {n["id"] for n in data["nodes"]}
        assert "node4" not in ids

    def test_time_window_filter(self, populated_store, tmp_path):
        out = export_native(populated_store, tmp_path / "recent.loci.json", time_window_days=30)
        data = json.loads(out.read_text())

        # node4 is 100 days old, should be excluded
        ids = {n["id"] for n in data["nodes"]}
        assert "node4" not in ids
        assert "node1" in ids

    def test_deterministic_output(self, populated_store, tmp_path):
        out1 = export_native(populated_store, tmp_path / "a.loci.json")
        d1 = json.loads(out1.read_text())
        out2 = export_native(populated_store, tmp_path / "b.loci.json")
        d2 = json.loads(out2.read_text())

        # Same nodes (sorted by importance desc then id)
        assert [n["id"] for n in d1["nodes"]] == [n["id"] for n in d2["nodes"]]


# ── Native import ─────────────────────────────────────────────────────

class TestNativeImport:
    def test_replace_import(self, populated_store, tmp_path):
        # Export first
        export_native(populated_store, tmp_path / "export.loci.json")

        # Create a fresh store and import
        new_store = MemoryStore(tmp_path / "new.loci")
        result = import_native(new_store, tmp_path / "export.loci.json", mode="replace")

        assert result["imported_nodes"] == 4
        assert result["imported_edges"] == 2
        assert result["mode"] == "replace"

        # Verify data
        node1 = new_store.get_node("node1")
        assert node1 is not None
        assert node1.content == "Decided to use hub-spoke model for routing"
        assert node1.embedding == [0.1, 0.2, 0.3]
        new_store.close()

    def test_merge_import(self, populated_store, tmp_path):
        # Export
        export_native(populated_store, tmp_path / "export.loci.json")

        # Create a store with one overlapping node
        merge_store = MemoryStore(tmp_path / "merge.loci")
        existing = MemoryNode(
            id="node1", content="EXISTING CONTENT", embedding=[9.9],
            context_scope="project:dropzi",
        )
        merge_store.save_node(existing)

        result = import_native(merge_store, tmp_path / "export.loci.json", mode="merge")

        # node1 should NOT be overwritten
        node1 = merge_store.get_node("node1")
        assert node1.content == "EXISTING CONTENT"

        # Other nodes should be imported
        assert result["imported_nodes"] == 3  # 4 total minus 1 existing
        merge_store.close()

    def test_roundtrip_fidelity(self, populated_store, tmp_path):
        export_native(populated_store, tmp_path / "rt.loci.json")

        rt_store = MemoryStore(tmp_path / "rt.loci")
        import_native(rt_store, tmp_path / "rt.loci.json")

        for nid in ["node1", "node2", "node3", "node4"]:
            orig = populated_store.get_node(nid)
            restored = rt_store.get_node(nid)
            assert restored is not None
            assert restored.content == orig.content
            assert restored.importance == orig.importance
            assert restored.entity_tags == orig.entity_tags
            assert restored.context_scope == orig.context_scope
            assert restored.summary_level == orig.summary_level

        rt_store.close()


# ── Context prompt export ─────────────────────────────────────────────

class TestContextPromptExport:
    def test_produces_markdown(self, populated_store, tmp_path):
        out = export_context_prompt(populated_store, tmp_path / "memory.context.md")
        content = out.read_text()

        assert content.startswith("# Memory Context")
        assert "Exported from LociDB" in content
        assert "memories distilled" in content

    def test_contains_sections(self, populated_store, tmp_path):
        out = export_context_prompt(populated_store, tmp_path / "memory.context.md")
        content = out.read_text()

        assert "## Core identity & background" in content
        assert "## Active projects" in content
        assert "## Key decisions & preferences" in content
        assert "## Recent context (last 30 days)" in content

    def test_contains_memories(self, populated_store, tmp_path):
        out = export_context_prompt(populated_store, tmp_path / "memory.context.md")
        content = out.read_text()

        assert "hub-spoke" in content
        assert "Neetish" in content

    def test_no_embeddings_in_output(self, populated_store, tmp_path):
        out = export_context_prompt(populated_store, tmp_path / "memory.context.md")
        content = out.read_text()

        assert "[0.1, 0.2, 0.3]" not in content

    def test_footer(self, populated_store, tmp_path):
        out = export_context_prompt(populated_store, tmp_path / "memory.context.md")
        content = out.read_text()

        assert "paste this as a system prompt" in content
        assert ".loci.json" in content

    def test_scope_filter(self, populated_store, tmp_path):
        out = export_context_prompt(
            populated_store, tmp_path / "dropzi.md", scope="project:dropzi"
        )
        content = out.read_text()

        assert "hub-spoke" in content
        # user:neetish content should not appear
        assert "Neetish prefers" not in content

    def test_max_tokens_budget(self, populated_store, tmp_path):
        out = export_context_prompt(
            populated_store, tmp_path / "small.md", max_tokens=200
        )
        content = out.read_text()
        # Should be reasonably small
        assert len(content) < 2000

    def test_graph_highlights(self, populated_store, tmp_path):
        out = export_context_prompt(populated_store, tmp_path / "graph.md")
        content = out.read_text()

        assert "Knowledge graph highlights" in content


# ── Snapshot export ───────────────────────────────────────────────────

class TestSnapshotExport:
    def test_produces_plain_text(self, populated_store, tmp_path):
        out = export_snapshot(populated_store, tmp_path / "snap.txt")
        content = out.read_text()

        assert "LociDB snapshot" in content
        assert "•" in content

    def test_contains_memories(self, populated_store, tmp_path):
        out = export_snapshot(populated_store, tmp_path / "snap.txt")
        content = out.read_text()

        assert "hub-spoke" in content

    def test_no_embeddings(self, populated_store, tmp_path):
        out = export_snapshot(populated_store, tmp_path / "snap.txt")
        content = out.read_text()

        assert "0.1, 0.2" not in content

    def test_token_budget(self, populated_store, tmp_path):
        out = export_snapshot(populated_store, tmp_path / "tiny.txt", max_tokens=100)
        content = out.read_text()
        # Very small budget → fewer entries
        assert len(content) < 800


# ── Context markdown import ──────────────────────────────────────────

class TestContextMdImport:
    def test_import_bullets(self, store, tmp_path):
        md = tmp_path / "import.md"
        md.write_text(
            "# Memory Context\n\n"
            "## Decisions\n"
            "- [0.87] Use hub-spoke for routing\n"
            "- Delivery radius is 15km\n"
            "- [2026-03-20] Added real-time tracking\n"
            "\n"
            "## Other\n"
            "• Prefers Python over TypeScript\n"
        )
        result = import_context_md(store, md)

        assert result["imported_nodes"] == 4
        # Verify tags were stripped
        nodes = store.list_nodes(limit=100)
        contents = {n.content for n in nodes}
        assert "Use hub-spoke for routing" in contents
        assert "Delivery radius is 15km" in contents
        assert "Added real-time tracking" in contents
        assert "Prefers Python over TypeScript" in contents


# ── LociDB-level export/import ────────────────────────────────────────

class TestLociDBPortability:
    @pytest.fixture
    def loci_db(self, tmp_path):
        """A LociDB instance with pre-populated data (no embeddings needed)."""
        from locidb.core.db import LociDB
        db = LociDB.__new__(LociDB)
        db.path = tmp_path / "test.loci"
        db._store = MemoryStore(db.path)
        # We skip the indexer to avoid loading sentence-transformers in tests
        db._indexer = None

        now = datetime.now(timezone.utc)
        for i, (content, scope, imp) in enumerate([
            ("Hub-spoke model chosen", "project:dropzi", 0.9),
            ("Delivery radius 15km", "project:dropzi", 0.8),
            ("User prefers Python", "user:neetish", 0.7),
        ]):
            node = MemoryNode(
                id=f"test{i}",
                content=content,
                embedding=[float(i)] * 3,
                importance=imp,
                timestamp=now - timedelta(days=i),
                entity_tags=["test"],
                context_scope=scope,
            )
            db._store.save_node(node)

        yield db
        db.close()

    def test_export_native_via_db(self, loci_db, tmp_path):
        out = loci_db.export(tmp_path / "backup.loci.json", format="native")
        assert out.exists()
        data = json.loads(out.read_text())
        assert data["stats"]["total_nodes"] == 3

    def test_export_context_prompt_via_db(self, loci_db, tmp_path):
        out = loci_db.export(tmp_path / "ctx.md", format="context_prompt")
        assert out.exists()
        content = out.read_text()
        assert "# Memory Context" in content

    def test_export_snapshot_via_db(self, loci_db, tmp_path):
        out = loci_db.export(tmp_path / "snap.txt", format="snapshot")
        assert out.exists()
        assert "LociDB snapshot" in out.read_text()

    def test_export_scope_all(self, loci_db, tmp_path):
        out = loci_db.export(tmp_path / "all.loci.json", format="native", scope="all")
        data = json.loads(out.read_text())
        assert data["stats"]["total_nodes"] == 3

    def test_export_invalid_format(self, loci_db, tmp_path):
        with pytest.raises(ValueError, match="Unknown export format"):
            loci_db.export(tmp_path / "bad.xyz", format="invalid")

    def test_import_from_json(self, loci_db, tmp_path):
        loci_db.export(tmp_path / "backup.loci.json", format="native")

        # Import into a new DB
        from locidb.core.db import LociDB
        db2 = LociDB.__new__(LociDB)
        db2.path = tmp_path / "restored.loci"
        db2._store = MemoryStore(db2.path)
        db2._indexer = None

        result = db2.import_from(tmp_path / "backup.loci.json")
        assert result["imported_nodes"] == 3
        db2.close()

    def test_import_from_md(self, loci_db, tmp_path):
        loci_db.export(tmp_path / "ctx.context.md", format="context_prompt")

        from locidb.core.db import LociDB
        db2 = LociDB.__new__(LociDB)
        db2.path = tmp_path / "from_md.loci"
        db2._store = MemoryStore(db2.path)
        db2._indexer = None

        result = db2.import_from(tmp_path / "ctx.context.md")
        assert result["imported_nodes"] > 0
        db2.close()

    def test_import_merge_mode(self, loci_db, tmp_path):
        loci_db.export(tmp_path / "backup.loci.json", format="native")

        result = loci_db.import_from(tmp_path / "backup.loci.json", mode="merge")
        # All 3 already exist → 0 imported
        assert result["imported_nodes"] == 0
