"""
Unit tests for MemoryManager — episodic store creation per user,
remember/recall, async save, encryption toggle, store isolation.
"""

import json
import os
import threading
from unittest.mock import MagicMock, patch

import pytest

from familiar.core.memory_manager import MemoryManager, _get_episodic_encryption


@pytest.fixture
def mock_agent(tmp_path):
    agent = MagicMock()
    agent.config.agent.memory_enabled = True
    agent.config.agent.episodic_max_memories = 100
    agent.config.agent.episodic_working_size = 10
    agent.config.agent.episodic_decay_rate = 0.05
    return agent


@pytest.fixture
def mm(mock_agent, tmp_path):
    """MemoryManager with a temporary episodic directory."""
    mgr = MemoryManager(mock_agent)
    mgr._episodic_dir = tmp_path / "episodic"
    mgr._episodic_dir.mkdir()
    return mgr


class TestEpisodicStoreCreation:
    """Per-user episodic memory store lifecycle."""

    def test_creates_store_on_first_access(self, mm):
        ms = mm.get_episodic_memory("user1")
        assert ms is not None

    def test_returns_same_store_for_same_user(self, mm):
        ms1 = mm.get_episodic_memory("user1")
        ms2 = mm.get_episodic_memory("user1")
        assert ms1 is ms2

    def test_different_stores_for_different_users(self, mm):
        ms1 = mm.get_episodic_memory("user1")
        ms2 = mm.get_episodic_memory("user2")
        assert ms1 is not ms2

    def test_disabled_returns_none(self, mock_agent):
        mock_agent.config.agent.memory_enabled = False
        mgr = MemoryManager(mock_agent)
        assert mgr.get_episodic_memory("user1") is None

    def test_sanitizes_user_id_for_filename(self, mm):
        """Special characters in user_id are sanitized for file paths."""
        ms = mm.get_episodic_memory("user@example.com")
        assert ms is not None
        # Store should be accessible
        assert "user_example.com" in str(mm._episodic_stores.keys()) or ms is not None


class TestStoreIsolation:
    """Memory stores are isolated between users."""

    def test_store_per_user(self, mm):
        ms1 = mm.get_episodic_memory("alice")
        ms2 = mm.get_episodic_memory("bob")
        ms1.store_conversation(
            conversation_id="c1",
            messages=[{"role": "user", "content": "alice secret"}],
            importance=0.8,
        )
        # Bob's store should not have Alice's data
        stats2 = ms2.get_stats()
        assert stats2.get("total_stored", 0) == 0

    def test_thread_safe_creation(self, mm):
        """Concurrent store creation for different users doesn't crash."""
        stores = {}
        errors = []

        def get_store(uid):
            try:
                stores[uid] = mm.get_episodic_memory(uid)
            except Exception as e:
                errors.append(e)

        threads = [threading.Thread(target=get_store, args=(f"user{i}",)) for i in range(10)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()
        assert not errors
        assert len(stores) == 10


class TestSaveAndLoad:
    """Persistence to disk (plaintext)."""

    def test_save_creates_file(self, mm):
        ms = mm.get_episodic_memory("persist_user")
        ms.store_conversation(
            conversation_id="c1",
            messages=[{"role": "user", "content": "remember this"}],
            importance=0.9,
        )
        mm.save_episodic_memory_async("persist_user")
        # Wait for async save (daemon thread)
        import time

        time.sleep(0.5)

        # Check file exists (plaintext)
        expected = mm._episodic_dir / "persist_user.json"
        enc_expected = mm._episodic_dir / "persist_user.json.enc"
        assert expected.exists() or enc_expected.exists()

    def test_save_noop_for_unknown_user(self, mm):
        """Saving for a user with no store is a no-op."""
        mm.save_episodic_memory_async("nonexistent")
        # No crash, no file created

    def test_load_from_plaintext(self, mm):
        """Store can be loaded from a pre-existing JSON file."""
        # Create a memory file manually
        import re

        uid = "preload_user"
        safe_uid = re.sub(r"[^a-zA-Z0-9_\-.]", "_", uid)
        path = mm._episodic_dir / f"{safe_uid}.json"

        from familiar.core.episodic_memory import MemorySystem

        ms = MemorySystem(max_memories=100)
        ms.store_conversation(
            conversation_id="c1",
            messages=[{"role": "user", "content": "pre-existing memory"}],
            importance=0.8,
        )
        data = ms.export()
        path.write_text(json.dumps(data))

        # Now load it
        loaded_ms = mm.get_episodic_memory(uid)
        stats = loaded_ms.get_stats()
        assert stats.get("total_stored", 0) > 0


class TestEncryption:
    """Encryption singleton behavior."""

    def test_no_key_returns_none(self):
        """Without FAMILIAR_ENCRYPTION_KEY, encryption returns None."""
        import familiar.core.memory_manager as mm_mod

        # Reset the singleton
        old_init = mm_mod._episodic_encryption_initialized
        old_enc = mm_mod._episodic_encryption
        try:
            mm_mod._episodic_encryption_initialized = False
            mm_mod._episodic_encryption = None
            with patch.dict(os.environ, {}, clear=False):
                os.environ.pop("FAMILIAR_ENCRYPTION_KEY", None)
                result = _get_episodic_encryption()
                assert result is None
        finally:
            mm_mod._episodic_encryption_initialized = old_init
            mm_mod._episodic_encryption = old_enc

    def test_singleton_returns_same_instance(self):
        """Second call returns cached result without re-init."""
        import familiar.core.memory_manager as mm_mod

        old_init = mm_mod._episodic_encryption_initialized
        old_enc = mm_mod._episodic_encryption
        try:
            mm_mod._episodic_encryption_initialized = True
            mm_mod._episodic_encryption = "cached_sentinel"
            assert _get_episodic_encryption() == "cached_sentinel"
        finally:
            mm_mod._episodic_encryption_initialized = old_init
            mm_mod._episodic_encryption = old_enc


class TestRememberRecall:
    """High-level remember/recall convenience methods."""

    def test_remember_stores_to_memory(self, mm):
        ms = mm.get_episodic_memory("recall_user")
        ms.store_conversation(
            conversation_id="conv-1",
            messages=[{"role": "user", "content": "My cat's name is Luna"}],
            importance=0.9,
        )
        results = ms.retrieve(query="cat name", top_k=5)
        assert len(results) > 0
        assert any("Luna" in m.content for m in results)
