"""
Tests for Memory-Aware Plugin Management

These tests verify that the memory-aware plugin manager correctly handles
plugin loading, eviction, and memory limits.
"""

import gc
import os
import weakref
from datetime import datetime
from unittest.mock import Mock, patch

import pytest

from mcp_server.plugins.memory_aware_manager import (
    LoadedPlugin,
    MemoryAwarePluginManager,
    PluginMemoryInfo,
    get_memory_aware_manager,
)


class TestMemoryAwarePluginManager:
    """Test memory-aware plugin management functionality."""

    @pytest.fixture
    def manager(self):
        """Create a manager instance with small memory limit for testing."""
        return MemoryAwarePluginManager(
            max_memory_mb=100,  # Small limit for testing
            high_priority_langs=["python", "javascript"],
        )

    @pytest.fixture
    def mock_plugin(self):
        """Create a mock plugin."""
        plugin = Mock()
        plugin.language = "test"
        plugin.index = Mock()
        plugin.getDefinition = Mock()
        return plugin

    def test_initialization(self, manager):
        """Test manager initialization."""
        assert manager.max_memory_bytes == 100 * 1024 * 1024
        assert "python" in manager.high_priority_langs
        assert "javascript" in manager.high_priority_langs
        assert len(manager._plugins) == 0

    def test_plugin_loading(self, manager, mock_plugin):
        """Test plugin loading and caching."""
        with patch.object(manager._factory, "get_plugin", return_value=mock_plugin):
            # First load
            plugin1 = manager.get_plugin("test")
            assert plugin1 == mock_plugin
            assert "test" in manager._plugins
            assert "test" in manager._plugin_info

            # Second load (from cache)
            plugin2 = manager.get_plugin("test")
            assert plugin2 == mock_plugin
            assert manager._plugin_info["test"].usage_count == 2

    def test_lru_ordering(self, manager):
        """Test LRU ordering of plugins."""
        plugins = []
        for i in range(3):
            plugin = Mock()
            plugin.language = f"lang{i}"
            plugins.append(plugin)

        with patch.object(manager._factory, "get_plugin", side_effect=plugins):
            # Load plugins in order
            manager.get_plugin("lang0")
            manager.get_plugin("lang1")
            manager.get_plugin("lang2")

            # Access lang0 again (moves to end)
            manager.get_plugin("lang0")

            # Check order (lang1 should be oldest)
            plugin_names = list(manager._plugins.keys())
            assert plugin_names == ["lang1", "lang2", "lang0"]

    def test_memory_limit_enforcement(self, manager):
        """Test that memory limits are enforced."""
        # Mock memory usage to exceed limit
        with patch.object(
            manager, "_get_plugin_memory_usage", return_value=manager.max_memory_bytes + 1
        ):
            with patch.object(manager, "_get_eviction_candidates", return_value=["old_plugin"]):
                with patch.object(manager, "_evict_plugin", return_value=1024 * 1024) as mock_evict:
                    with patch.object(manager._factory, "get_plugin", return_value=Mock()):
                        plugin = manager.get_plugin("new_plugin")

                        # Should have tried to evict
                        mock_evict.assert_called_once_with("old_plugin")

    def test_high_priority_protection(self, manager):
        """Test that high-priority plugins are protected from eviction."""
        # Create plugins
        python_plugin = Mock()
        python_plugin.language = "python"
        other_plugin = Mock()
        other_plugin.language = "rust"

        with patch.object(
            manager._factory, "get_plugin", side_effect=[python_plugin, other_plugin]
        ):
            # Load both plugins
            manager.get_plugin("python")
            manager.get_plugin("rust")

            # Mark Python as high priority
            manager._plugin_info["python"].is_high_priority = True
            manager._plugin_info["rust"].is_high_priority = False

            # Get eviction candidates
            candidates = manager._get_eviction_candidates()

            # Python should not be in candidates
            assert "python" not in candidates
            assert "rust" in candidates

    def test_plugin_eviction(self, manager):
        """Test plugin eviction and memory reclamation."""
        plugin = Mock()
        plugin.language = "test"

        # Add plugin manually
        manager._plugins["test"] = LoadedPlugin(name="test", instance=plugin, metadata={})
        manager._plugin_info["test"] = PluginMemoryInfo(
            plugin_name="test",
            memory_bytes=1024 * 1024,  # 1MB
            last_used=datetime.now(),
            load_time=0.1,
            usage_count=1,
            is_high_priority=False,
        )

        # Evict plugin
        memory_freed = manager._evict_plugin("test")

        assert memory_freed == 1024 * 1024
        assert "test" not in manager._plugins
        assert "test" not in manager._plugin_info

    def test_memory_status(self, manager):
        """Test memory status reporting."""
        # Add some test plugins
        for i in range(2):
            plugin = Mock()
            manager._plugins[f"lang{i}"] = LoadedPlugin(
                name=f"lang{i}", instance=plugin, metadata={}
            )
            manager._plugin_info[f"lang{i}"] = PluginMemoryInfo(
                plugin_name=f"lang{i}",
                memory_bytes=1024 * 1024 * 10,  # 10MB each
                last_used=datetime.now(),
                load_time=0.1,
                usage_count=i + 1,
                is_high_priority=i == 0,
            )

        status = manager.get_memory_status()

        assert status["max_memory_mb"] == 100
        assert status["used_memory_mb"] == 20  # 2 * 10MB
        assert status["available_memory_mb"] == 80
        assert status["usage_percent"] == 20
        assert status["loaded_plugins"] == 2
        assert len(status["plugin_details"]) == 2

    def test_preload_high_priority(self, manager):
        """Test preloading of high-priority plugins."""
        mock_plugins = {
            "python": Mock(language="python"),
            "javascript": Mock(language="javascript"),
        }

        def get_plugin_side_effect(lang):
            return mock_plugins.get(lang)

        with patch.object(manager._factory, "get_plugin", side_effect=get_plugin_side_effect):
            manager.preload_high_priority()

            # Both high-priority languages should be loaded
            assert "python" in manager._plugins
            assert "javascript" in manager._plugins

    def test_clear_cache(self, manager):
        """Test cache clearing functionality."""
        # Add plugins
        for lang in ["python", "rust", "go"]:
            plugin = Mock()
            manager._plugins[lang] = LoadedPlugin(name=lang, instance=plugin, metadata={})
            manager._plugin_info[lang] = PluginMemoryInfo(
                plugin_name=lang,
                memory_bytes=1024 * 1024,
                last_used=datetime.now(),
                load_time=0.1,
                usage_count=1,
                is_high_priority=(lang == "python"),
            )

        # Clear cache keeping high priority
        manager.clear_cache(keep_high_priority=True)

        assert "python" in manager._plugins  # High priority kept
        assert "rust" not in manager._plugins
        assert "go" not in manager._plugins

        # Clear all
        manager.clear_cache(keep_high_priority=False)
        assert len(manager._plugins) == 0

    def test_set_high_priority_languages(self, manager):
        """Test updating high-priority languages."""
        # Add a plugin
        manager._plugin_info["rust"] = PluginMemoryInfo(
            plugin_name="rust",
            memory_bytes=1024 * 1024,
            last_used=datetime.now(),
            load_time=0.1,
            usage_count=1,
            is_high_priority=False,
        )

        # Update high priority list
        manager.set_high_priority_languages(["rust", "go"])

        assert "rust" in manager.high_priority_langs
        assert "go" in manager.high_priority_langs
        assert "python" not in manager.high_priority_langs

        # Rust plugin should now be high priority
        assert manager._plugin_info["rust"].is_high_priority is True

    def test_get_plugin_info(self, manager):
        """Test getting plugin information."""
        # Add a plugin
        plugin = Mock()
        manager._plugins["test"] = LoadedPlugin(name="test", instance=plugin, metadata={})
        manager._plugin_info["test"] = PluginMemoryInfo(
            plugin_name="test",
            memory_bytes=1024 * 1024 * 5,  # 5MB
            last_used=datetime.now(),
            load_time=0.25,
            usage_count=10,
            is_high_priority=False,
        )

        info = manager.get_plugin_info("test")

        assert info["language"] == "test"
        assert info["memory_mb"] == 5.0
        assert info["usage_count"] == 10
        assert info["load_time_seconds"] == 0.25
        assert info["is_high_priority"] is False
        assert info["is_loaded"] is True

        # Non-existent plugin
        assert manager.get_plugin_info("nonexistent") is None


class TestSingletonManager:
    """Test singleton manager functionality."""

    def test_singleton_instance(self):
        """Test that get_memory_aware_manager returns singleton."""
        manager1 = get_memory_aware_manager()
        manager2 = get_memory_aware_manager()

        assert manager1 is manager2

    def test_environment_configuration(self):
        """Test configuration from environment variables."""
        # Clear any existing instance
        import mcp_server.plugins.memory_aware_manager as module

        module._manager_instance = None

        # Set environment variables
        os.environ["MCP_MAX_MEMORY_MB"] = "512"
        os.environ["MCP_HIGH_PRIORITY_LANGS"] = "python,go,rust"
        os.environ["MCP_PRELOAD_PLUGINS"] = "false"

        try:
            manager = get_memory_aware_manager()

            assert manager.max_memory_bytes == 512 * 1024 * 1024
            assert "python" in manager.high_priority_langs
            assert "go" in manager.high_priority_langs
            assert "rust" in manager.high_priority_langs

        finally:
            # Clean up
            del os.environ["MCP_MAX_MEMORY_MB"]
            del os.environ["MCP_HIGH_PRIORITY_LANGS"]
            del os.environ["MCP_PRELOAD_PLUGINS"]
            module._manager_instance = None

    def test_thread_safety(self):
        """Test thread-safe access to singleton."""
        import threading

        import mcp_server.plugins.memory_aware_manager as module

        module._manager_instance = None

        managers = []

        def get_manager():
            manager = get_memory_aware_manager()
            managers.append(manager)

        # Create multiple threads
        threads = []
        for _ in range(10):
            thread = threading.Thread(target=get_manager)
            threads.append(thread)
            thread.start()

        # Wait for all threads
        for thread in threads:
            thread.join()

        # All should be the same instance
        assert len(set(id(m) for m in managers)) == 1

        # Clean up
        module._manager_instance = None


class TestMemoryMonitoring:
    """Test memory monitoring functionality."""

    def test_memory_measurement(self):
        """Test that memory measurement works."""
        manager = MemoryAwarePluginManager(max_memory_mb=100)

        # Should be able to get current memory
        current_memory = manager._get_current_memory()
        assert current_memory > 0

        # Plugin memory should start at 0
        plugin_memory = manager._get_plugin_memory_usage()
        assert plugin_memory == 0

    def test_weak_reference_cleanup(self):
        """Test that weak references allow garbage collection."""
        manager = MemoryAwarePluginManager(max_memory_mb=100)

        # Create and add a plugin
        plugin = Mock()
        plugin.language = "test"

        manager._plugins["test"] = LoadedPlugin(name="test", instance=plugin, metadata={})
        manager._plugin_info["test"] = PluginMemoryInfo(
            plugin_name="test",
            memory_bytes=1024,
            last_used=datetime.now(),
            load_time=0.1,
            usage_count=1,
            is_high_priority=False,
        )

        # Create weak reference
        manager._weak_refs["test"] = weakref.ref(
            plugin, lambda ref: manager._on_plugin_deleted("test")
        )

        # Remove strong reference
        del manager._plugins["test"]
        del plugin

        # Force garbage collection
        gc.collect()

        # Plugin info should be cleaned up by callback
        # Note: This is timing-dependent and may not always work in tests

    def test_memory_limit_calculation(self):
        """Test memory limit calculations."""
        manager = MemoryAwarePluginManager(max_memory_mb=100)

        # Add some fake plugin memory usage
        manager._plugin_info["test1"] = PluginMemoryInfo(
            plugin_name="test1",
            memory_bytes=30 * 1024 * 1024,  # 30MB
            last_used=datetime.now(),
            load_time=0.1,
            usage_count=1,
            is_high_priority=False,
        )

        manager._plugin_info["test2"] = PluginMemoryInfo(
            plugin_name="test2",
            memory_bytes=50 * 1024 * 1024,  # 50MB
            last_used=datetime.now(),
            load_time=0.1,
            usage_count=1,
            is_high_priority=False,
        )

        # Total should be 80MB
        total_memory = manager._get_plugin_memory_usage()
        assert total_memory == 80 * 1024 * 1024

        # Should need to free memory
        available = manager._ensure_memory_available()
        # This depends on eviction logic
