# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# Licensed under the MIT License

"""Tests for MemoryTree encryption at rest (Sprint 7)."""

import pytest

from familiar.core.memory_tree import MemoryTree

# Try to import cryptography — skip tests if unavailable
try:
    from cryptography.fernet import Fernet

    CRYPTO_AVAILABLE = True
except ImportError:
    CRYPTO_AVAILABLE = False

skip_no_crypto = pytest.mark.skipif(
    not CRYPTO_AVAILABLE,
    reason="cryptography package not installed",
)


@pytest.fixture
def fernet_key():
    return Fernet.generate_key().decode() if CRYPTO_AVAILABLE else None


@pytest.fixture
def tree_with_data(tmp_path):
    """Create a tree with some data, return (tree, db_path)."""
    db_path = tmp_path / "enc_test.db"
    tree = MemoryTree(db_path=db_path)
    tree.store(title="Secret Note", body="Top secret content", tier="working")
    tree.store(title="Recipe", body="Flour, water, salt", tier="long", tags=["food"])
    return tree, db_path


@skip_no_crypto
class TestEncryptionAtRest:
    def test_shutdown_encrypts_db(self, tmp_path, fernet_key, monkeypatch):
        monkeypatch.setenv("FAMILIAR_ENCRYPTION_KEY", fernet_key)
        db_path = tmp_path / "shutdown.db"
        tree = MemoryTree(db_path=db_path, encrypt=True)
        tree.store(title="Test", body="data")

        assert db_path.exists()
        tree.shutdown()

        # Plaintext DB should be gone, encrypted should exist
        assert not db_path.exists()
        assert db_path.with_suffix(".db.enc").exists()

    def test_decrypt_on_open(self, tmp_path, fernet_key, monkeypatch):
        monkeypatch.setenv("FAMILIAR_ENCRYPTION_KEY", fernet_key)
        db_path = tmp_path / "reopen.db"

        # Create, populate, encrypt
        tree1 = MemoryTree(db_path=db_path, encrypt=True)
        tree1.store(title="Persistent", body="survives restart")
        tree1.shutdown()
        assert not db_path.exists()

        # Re-open — should auto-decrypt
        tree2 = MemoryTree(db_path=db_path, encrypt=True)
        results = tree2.search("Persistent")
        assert len(results) >= 1
        assert results[0].body == "survives restart"

    def test_roundtrip_preserves_all_data(self, tmp_path, fernet_key, monkeypatch):
        monkeypatch.setenv("FAMILIAR_ENCRYPTION_KEY", fernet_key)
        db_path = tmp_path / "roundtrip.db"

        tree1 = MemoryTree(db_path=db_path, encrypt=True)
        e1 = tree1.store(
            title="Full Entry",
            body="full body",
            tier="working",
            tags=["a", "b"],
            importance=0.9,
            source="test:1",
        )
        e2 = tree1.store(title="Second", body="second body")
        tree1.link(e1.memory_id, e2.memory_id, "related")
        tree1.shutdown()

        tree2 = MemoryTree(db_path=db_path, encrypt=True)
        entry = tree2.get(e1.memory_id)
        assert entry is not None
        assert entry.title == "Full Entry"
        assert entry.importance == 0.9
        assert set(entry.tags) == {"a", "b"}

        edges = tree2.get_edges(e1.memory_id)
        assert len(edges) == 1

    def test_no_encrypt_flag_skips(self, tmp_path, fernet_key, monkeypatch):
        monkeypatch.setenv("FAMILIAR_ENCRYPTION_KEY", fernet_key)
        db_path = tmp_path / "noenc.db"

        tree = MemoryTree(db_path=db_path, encrypt=False)
        tree.store(title="Plain", body="text")
        tree.shutdown()

        # DB should still be plaintext
        assert db_path.exists()
        assert not db_path.with_suffix(".db.enc").exists()

    def test_no_key_skips_encryption(self, tmp_path, monkeypatch):
        monkeypatch.delenv("FAMILIAR_ENCRYPTION_KEY", raising=False)
        monkeypatch.delenv("FAMILIAR_ENCRYPTION_PASSWORD", raising=False)
        db_path = tmp_path / "nokey.db"

        tree = MemoryTree(db_path=db_path, encrypt=True)
        tree.store(title="NoKey", body="still plain")
        tree.shutdown()

        # No key available — DB should remain plaintext
        assert db_path.exists()

    def test_encrypted_path_convention(self, tmp_path):
        db_path = tmp_path / "convention.db"
        tree = MemoryTree(db_path=db_path)
        assert tree._encrypted_path() == tmp_path / "convention.db.enc"

    def test_shutdown_idempotent(self, tmp_path, fernet_key, monkeypatch):
        monkeypatch.setenv("FAMILIAR_ENCRYPTION_KEY", fernet_key)
        db_path = tmp_path / "idempotent.db"

        tree = MemoryTree(db_path=db_path, encrypt=True)
        tree.store(title="Idem", body="potent")
        tree.shutdown()
        # Second shutdown should not crash (DB already gone)
        tree.shutdown()

    def test_plaintext_db_not_encrypted_on_open(self, tmp_path, fernet_key, monkeypatch):
        """If plaintext DB exists (no .enc), open it normally."""
        monkeypatch.setenv("FAMILIAR_ENCRYPTION_KEY", fernet_key)
        db_path = tmp_path / "existing.db"

        # Create without encryption
        tree1 = MemoryTree(db_path=db_path, encrypt=False)
        tree1.store(title="Existing", body="data")

        # Open with encryption — should work on plaintext DB
        tree2 = MemoryTree(db_path=db_path, encrypt=True)
        results = tree2.search("Existing")
        assert len(results) >= 1
