"""
Tests for the Novyx MCP local SQLite backend.

Integration tests using real SQLite databases via tmp_path.
No mocking — these test actual storage and retrieval.
"""

import json
import time

import pytest

from novyx_mcp.local_backend import CloudFeatureError, LocalBackend
from novyx_mcp.local_embeddings import LocalEmbedder, pack_embedding, unpack_embedding


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


@pytest.fixture
def backend(tmp_path):
    """Create a LocalBackend with an isolated temp database."""
    return LocalBackend(data_dir=str(tmp_path))


@pytest.fixture
def embedder():
    return LocalEmbedder()


# =========================================================================
# Embeddings
# =========================================================================


class TestLocalEmbedder:
    def test_strategy_is_tfidf_or_transformer(self, embedder):
        assert embedder.strategy in ("tfidf", "transformer")

    def test_embed_returns_384_dim(self, embedder):
        vec = embedder.embed("hello world")
        assert vec is not None
        assert len(vec) == 384

    def test_similarity_identical_texts(self, embedder):
        a = embedder.embed("the cat sat on the mat")
        b = embedder.embed("the cat sat on the mat")
        sim = embedder.similarity(a, b)
        assert sim == pytest.approx(1.0, abs=0.01)

    def test_similarity_different_texts(self, embedder):
        a = embedder.embed("machine learning algorithms")
        b = embedder.embed("cooking pasta recipes")
        sim = embedder.similarity(a, b)
        assert sim < 0.8  # Should be dissimilar

    def test_pack_unpack_roundtrip(self, embedder):
        vec = embedder.embed("roundtrip test")
        packed = pack_embedding(vec)
        unpacked = unpack_embedding(packed)
        for a, b in zip(vec, unpacked):
            assert a == pytest.approx(b, abs=1e-6)


# =========================================================================
# Core Memory
# =========================================================================


class TestRemember:
    def test_basic(self, backend):
        result = backend.remember("The sky is blue")
        assert "uuid" in result
        assert result["observation"] == "The sky is blue"
        assert result["importance"] == 5
        assert result["mode"] == "local"

    def test_with_tags(self, backend):
        result = backend.remember("Python is great", tags=["coding", "python"])
        assert result["tags"] == ["coding", "python"]

    def test_with_all_params(self, backend):
        result = backend.remember(
            "Important note",
            tags=["urgent"],
            importance=9,
            context="work meeting",
            ttl_seconds=3600,
        )
        assert result["importance"] == 9

    def test_creates_audit_entry(self, backend):
        backend.remember("Audited memory")
        entries = backend.audit(limit=10)
        assert len(entries) == 1
        assert entries[0]["operation"] == "CREATE"


class TestRecall:
    def test_basic(self, backend):
        backend.remember("Python is a programming language", tags=["code"])
        backend.remember("Cats are cute animals", tags=["pets"])
        backend.remember("JavaScript runs in browsers", tags=["code"])

        result = backend.recall("programming language")
        assert result["total_results"] > 0
        assert result["mode"] == "local"
        assert len(result["memories"]) > 0

    def test_with_tag_filter(self, backend):
        backend.remember("Python is great", tags=["code"])
        backend.remember("Cats are cute", tags=["pets"])

        result = backend.recall("great things", tags=["code"])
        for mem in result["memories"]:
            assert "code" in mem["tags"]

    def test_min_score_filter(self, backend):
        backend.remember("Hello world")
        result = backend.recall("hello", min_score=0.99)
        # With TF-IDF, very high min_score may filter everything
        assert isinstance(result["memories"], list)

    def test_empty_database(self, backend):
        result = backend.recall("anything")
        assert result["total_results"] == 0
        assert result["memories"] == []

    def test_scoring_includes_importance(self, backend):
        backend.remember("Low importance item", importance=1)
        backend.remember("High importance item", importance=10)

        result = backend.recall("importance item", limit=10)
        if len(result["memories"]) >= 2:
            # Higher importance should score higher when text similarity is equal
            scores = {m["observation"]: m["score"] for m in result["memories"]}
            assert scores.get("High importance item", 0) >= scores.get("Low importance item", 0)


class TestForget:
    def test_basic(self, backend):
        result = backend.remember("Temporary memory")
        mem_id = result["uuid"]

        success = backend.forget(mem_id)
        assert success is True

        # Should not appear in memories list
        mems = backend.memories()
        assert all(m["uuid"] != mem_id for m in mems)

    def test_nonexistent(self, backend):
        success = backend.forget("nonexistent-uuid")
        assert success is False

    def test_creates_audit_entry(self, backend):
        result = backend.remember("Will be deleted")
        backend.forget(result["uuid"])

        entries = backend.audit(limit=10, operation="DELETE")
        assert len(entries) == 1


class TestMemories:
    def test_list_all(self, backend):
        backend.remember("First")
        backend.remember("Second")
        backend.remember("Third")

        mems = backend.memories()
        assert len(mems) == 3

    def test_limit(self, backend):
        for i in range(10):
            backend.remember(f"Memory {i}")

        mems = backend.memories(limit=3)
        assert len(mems) == 3

    def test_tag_filter(self, backend):
        backend.remember("Tagged", tags=["important"])
        backend.remember("Untagged")

        mems = backend.memories(tags=["important"])
        assert len(mems) == 1
        assert mems[0]["observation"] == "Tagged"

    def test_excludes_deleted(self, backend):
        result = backend.remember("Will delete")
        backend.forget(result["uuid"])

        mems = backend.memories()
        assert len(mems) == 0


class TestMemory:
    def test_get_by_id(self, backend):
        result = backend.remember("Specific memory")
        mem = backend.memory(result["uuid"])
        assert mem["observation"] == "Specific memory"

    def test_not_found(self, backend):
        with pytest.raises(ValueError, match="Memory not found"):
            backend.memory("nonexistent-uuid")


class TestStats:
    def test_basic(self, backend):
        backend.remember("First", importance=3, tags=["a"])
        backend.remember("Second", importance=7, tags=["b"])

        stats = backend.stats()
        assert stats["total_memories"] == 2
        assert stats["average_importance"] == 5.0
        assert stats["mode"] == "local"
        assert "a" in stats["tag_distribution"]


# =========================================================================
# Rollback
# =========================================================================


class TestRollback:
    def test_basic(self, backend):
        backend.remember("Before rollback")
        time.sleep(0.05)

        # Record time between creates
        import datetime
        cutoff = datetime.datetime.now(datetime.timezone.utc).isoformat()
        time.sleep(0.05)

        backend.remember("After rollback")
        assert len(backend.memories()) == 2

        result = backend.rollback(cutoff)
        assert result["operations_undone"] == 1
        assert len(backend.memories()) == 1

    def test_dry_run(self, backend):
        backend.remember("Memory one")
        time.sleep(0.05)

        import datetime
        cutoff = datetime.datetime.now(datetime.timezone.utc).isoformat()
        time.sleep(0.05)

        backend.remember("Memory two")

        result = backend.rollback(cutoff, dry_run=True)
        assert result["dry_run"] is True
        assert result["operations_to_undo"] == 1
        # Dry run should NOT actually undo
        assert len(backend.memories()) == 2

    def test_relative_time(self, backend):
        backend.remember("Old memory")
        time.sleep(0.05)
        backend.remember("Recent memory")
        time.sleep(0.05)

        result = backend.rollback("1 second ago")
        # Should undo at least the most recent creation
        assert result["operations_undone"] >= 1

    def test_nothing_to_undo(self, backend):
        result = backend.rollback("2099-01-01T00:00:00Z")
        assert result["operations_undone"] == 0

    def test_undo_delete(self, backend):
        """Rollback should restore deleted memories."""
        result = backend.remember("Will be deleted then restored")
        mem_id = result["uuid"]
        time.sleep(0.05)

        import datetime
        cutoff = datetime.datetime.now(datetime.timezone.utc).isoformat()
        time.sleep(0.05)

        backend.forget(mem_id)
        assert len(backend.memories()) == 0

        backend.rollback(cutoff)
        mems = backend.memories()
        assert len(mems) == 1
        assert mems[0]["uuid"] == mem_id


# =========================================================================
# Audit
# =========================================================================


class TestAudit:
    def test_records_operations(self, backend):
        backend.remember("Test")
        entries = backend.audit()
        assert len(entries) == 1
        assert entries[0]["operation"] == "CREATE"

    def test_operation_filter(self, backend):
        result = backend.remember("To delete")
        backend.forget(result["uuid"])

        creates = backend.audit(operation="CREATE")
        deletes = backend.audit(operation="DELETE")
        assert len(creates) == 1
        assert len(deletes) == 1

    def test_limit(self, backend):
        for i in range(10):
            backend.remember(f"Mem {i}")

        entries = backend.audit(limit=3)
        assert len(entries) == 3


# =========================================================================
# Links
# =========================================================================


class TestLinks:
    def test_create_link(self, backend):
        m1 = backend.remember("Source")
        m2 = backend.remember("Target")

        link = backend.link(m1["uuid"], m2["uuid"])
        assert link["source_id"] == m1["uuid"]
        assert link["target_id"] == m2["uuid"]
        assert link["relation"] == "related"

    def test_custom_relation(self, backend):
        m1 = backend.remember("Cause")
        m2 = backend.remember("Effect")

        link = backend.link(m1["uuid"], m2["uuid"], relation="causes")
        assert link["relation"] == "causes"


# =========================================================================
# Knowledge Graph
# =========================================================================


class TestTriples:
    def test_add_triple(self, backend):
        result = backend.triple("Python", "is_a", "programming_language")
        assert result["subject"] == "Python"
        assert result["predicate"] == "is_a"
        assert result["object"] == "programming_language"

    def test_query_by_subject(self, backend):
        backend.triple("Python", "is_a", "language")
        backend.triple("Python", "created_by", "Guido")
        backend.triple("JavaScript", "is_a", "language")

        result = backend.triples(subject="Python")
        assert result["total"] == 2

    def test_query_by_predicate(self, backend):
        backend.triple("Python", "is_a", "language")
        backend.triple("JavaScript", "is_a", "language")

        result = backend.triples(predicate="is_a")
        assert result["total"] == 2

    def test_query_by_object(self, backend):
        backend.triple("Python", "is_a", "language")
        backend.triple("JavaScript", "is_a", "language")

        result = backend.triples(object="language")
        assert result["total"] == 2

    def test_auto_creates_entities(self, backend):
        backend.triple("NewEntity", "relates_to", "AnotherEntity")
        # Entities should exist in the database
        row = backend._conn.execute(
            "SELECT * FROM entities WHERE name = ?", ("NewEntity",)
        ).fetchone()
        assert row is not None


# =========================================================================
# Context Spaces
# =========================================================================


class TestSpaces:
    def test_create_space(self, backend):
        result = backend.create_space("Test Space", description="For testing")
        assert "space_id" in result
        assert result["name"] == "Test Space"

    def test_list_spaces(self, backend):
        backend.create_space("Space 1")
        backend.create_space("Space 2")

        result = backend.list_spaces()
        assert result["total_count"] == 2

    def test_get_space(self, backend):
        created = backend.create_space("My Space")
        fetched = backend.get_space(created["space_id"])
        assert fetched["name"] == "My Space"

    def test_update_space(self, backend):
        created = backend.create_space("Original")
        updated = backend.update_space(created["space_id"], name="Updated")
        assert updated["name"] == "Updated"

    def test_delete_space(self, backend):
        created = backend.create_space("To Delete")
        backend.delete_space(created["space_id"])

        result = backend.list_spaces()
        assert result["total_count"] == 0

    def test_space_not_found(self, backend):
        with pytest.raises(ValueError, match="Space not found"):
            backend.get_space("nonexistent")


# =========================================================================
# Usage
# =========================================================================


class TestUsage:
    def test_local_usage(self, backend):
        result = backend.usage()
        assert result["tier"] == "local"
        assert result["mode"] == "local"
        assert "upgrade_url" in result


# =========================================================================
# Cloud-only Features
# =========================================================================


class TestCloudOnlyFeatures:
    def test_share_context(self, backend):
        with pytest.raises(CloudFeatureError):
            backend.share_context("tag", "user@example.com")

    def test_replay_timeline(self, backend):
        with pytest.raises(CloudFeatureError):
            backend.replay_timeline()

    def test_replay_snapshot(self, backend):
        with pytest.raises(CloudFeatureError):
            backend.replay_snapshot("2026-01-01T00:00:00Z")

    def test_replay_lifecycle(self, backend):
        with pytest.raises(CloudFeatureError):
            backend.replay_lifecycle("mem-1")

    def test_replay_diff(self, backend):
        with pytest.raises(CloudFeatureError):
            backend.replay_diff("2026-01-01", "2026-01-02")

    def test_cortex_status(self, backend):
        with pytest.raises(CloudFeatureError):
            backend.cortex_status()

    def test_cortex_run(self, backend):
        with pytest.raises(CloudFeatureError):
            backend.cortex_run()

    def test_cortex_insights(self, backend):
        with pytest.raises(CloudFeatureError):
            backend.cortex_insights()


# =========================================================================
# TTL Expiry
# =========================================================================


class TestTTLExpiry:
    def test_expired_memories_purged(self, backend):
        backend.remember("Ephemeral", ttl_seconds=1)
        assert len(backend.memories()) == 1

        # Manually set expires_at to the past
        backend._conn.execute(
            "UPDATE memories SET expires_at = '2000-01-01T00:00:00+00:00'"
        )
        backend._conn.commit()

        # Should be purged on next access
        assert len(backend.memories()) == 0


# =========================================================================
# Backend Routing
# =========================================================================


class TestBackendRouting:
    def test_local_mode_without_api_key(self, monkeypatch, tmp_path):
        """When NOVYX_API_KEY is NOT set, local backend is used."""
        monkeypatch.delenv("NOVYX_API_KEY", raising=False)
        monkeypatch.setenv("NOVYX_DATA_DIR", str(tmp_path))

        # Reset the singleton
        import novyx_mcp.server as srv
        srv._backend_instance = None

        backend = srv._get_backend()
        assert isinstance(backend, LocalBackend)

        # Cleanup
        srv._backend_instance = None

    def test_cloud_mode_with_api_key(self, monkeypatch):
        """When NOVYX_API_KEY is set, cloud backend is used."""
        monkeypatch.setenv("NOVYX_API_KEY", "nram_test_123_abc")

        import novyx_mcp.server as srv
        srv._backend_instance = None

        try:
            from novyx_mcp.cloud_backend import CloudBackend
            backend = srv._get_backend()
            assert isinstance(backend, CloudBackend)
        except Exception:
            # SDK may validate the key format — that's fine, it proves cloud path was taken
            pass
        finally:
            srv._backend_instance = None
