#!/usr/bin/env python3
"""
Tests for tree_sitter_analyzer.plugins.manager module.

This module tests the PluginManager class which handles dynamic
plugin discovery and management in the new architecture.
"""

import pytest
from unittest.mock import Mock, patch, MagicMock
from typing import List, Dict, Any, Optional, Type
from pathlib import Path

from tree_sitter_analyzer.plugins.manager import PluginManager
from tree_sitter_analyzer.plugins.base import LanguagePlugin


class MockLanguagePlugin(LanguagePlugin):
    """Mock language plugin for testing"""
    
    def __init__(self, language: str, extensions: List[str]):
        self._language = language
        self._extensions = extensions
    
    def get_language_name(self) -> str:
        return self._language
    
    def get_file_extensions(self) -> List[str]:
        return self._extensions
    
    def create_extractor(self):
        return Mock()
    
    def is_applicable(self, file_path: str) -> bool:
        return any(file_path.endswith(ext) for ext in self._extensions)


class TestPluginManager:
    """Test cases for PluginManager class"""

    @pytest.fixture
    def plugin_manager(self) -> PluginManager:
        """Create a PluginManager instance for testing"""
        return PluginManager()

    @pytest.fixture
    def mock_java_plugin(self) -> MockLanguagePlugin:
        """Create a mock Java plugin"""
        return MockLanguagePlugin('java', ['.java'])

    @pytest.fixture
    def mock_python_plugin(self) -> MockLanguagePlugin:
        """Create a mock Python plugin"""
        return MockLanguagePlugin('python', ['.py', '.pyi'])

    @pytest.fixture
    def mock_javascript_plugin(self) -> MockLanguagePlugin:
        """Create a mock JavaScript plugin"""
        return MockLanguagePlugin('javascript', ['.js', '.jsx'])

    def test_plugin_manager_initialization(self, plugin_manager: PluginManager) -> None:
        """Test PluginManager initialization"""
        assert plugin_manager is not None
        assert hasattr(plugin_manager, 'load_plugins')
        assert hasattr(plugin_manager, 'get_plugin')
        assert hasattr(plugin_manager, 'register_plugin')

    def test_load_plugins_success(self, plugin_manager: PluginManager) -> None:
        """Test successful plugin loading"""
        with patch.object(plugin_manager, '_load_from_entry_points') as mock_entry_points, \
             patch.object(plugin_manager, '_load_from_local_directory') as mock_local:
            
            mock_entry_points.return_value = [MockLanguagePlugin('java', ['.java'])]
            mock_local.return_value = [MockLanguagePlugin('python', ['.py'])]
            
            plugins = plugin_manager.load_plugins()
            
            assert isinstance(plugins, list)
            assert len(plugins) == 2
            mock_entry_points.assert_called_once()
            mock_local.assert_called_once()

    def test_load_from_entry_points_success(self, plugin_manager: PluginManager) -> None:
        """Test loading plugins from entry points"""
        mock_entry_point = Mock()
        mock_entry_point.name = 'java'
        mock_entry_point.load.return_value = MockLanguagePlugin
        
        with patch('importlib.metadata.entry_points') as mock_iter:
            mock_iter.return_value = [mock_entry_point]
            
            plugins = plugin_manager._load_from_entry_points()
            
            assert isinstance(plugins, list)
            assert len(plugins) == 0

    def test_load_from_entry_points_with_exception(self, plugin_manager: PluginManager) -> None:
        """Test loading plugins from entry points with exception"""
        mock_entry_point = Mock()
        mock_entry_point.name = 'java'
        mock_entry_point.load.side_effect = Exception("Load failed")
        
        with patch('importlib.metadata.entry_points') as mock_iter:
            mock_iter.return_value = [mock_entry_point]
            
            plugins = plugin_manager._load_from_entry_points()
            
            # Should handle exceptions gracefully
            assert isinstance(plugins, list)
            assert len(plugins) == 0

    def test_load_from_local_directory_success(self, plugin_manager: PluginManager) -> None:
        """Test loading plugins from local directory"""
        with patch('pathlib.Path.iterdir') as mock_iterdir, \
             patch('importlib.util.spec_from_file_location') as mock_spec, \
             patch('importlib.util.module_from_spec') as mock_module, \
             patch.object(plugin_manager, '_find_plugin_classes') as mock_find:
            
            # Mock directory structure
            mock_file = Mock()
            mock_file.name = 'java_plugin.py'
            mock_file.is_file.return_value = True
            mock_file.suffix = '.py'
            mock_iterdir.return_value = [mock_file]
            
            # Mock module loading
            mock_spec.return_value = Mock()
            mock_module_instance = Mock()
            mock_module.return_value = mock_module_instance
            
            # Mock plugin class finding
            mock_find.return_value = [MockLanguagePlugin]
            
            plugins = plugin_manager._load_from_local_directory()
            
            assert isinstance(plugins, list)

    def test_load_from_local_directory_with_exception(self, plugin_manager: PluginManager) -> None:
        """Test loading plugins from local directory with exception"""
        with patch('pkgutil.iter_modules') as mock_iter_modules:
            mock_iter_modules.side_effect = Exception("Module iteration failed")

            plugins = plugin_manager._load_from_local_directory()

            # Should handle exceptions gracefully
            assert isinstance(plugins, list)
            assert len(plugins) == 0

    def test_find_plugin_classes(self, plugin_manager: PluginManager) -> None:
        """Test finding plugin classes in a module"""
        # Create a mock module with plugin classes
        mock_module = Mock()
        
        # Mock plugin class
        mock_plugin_class = Mock()
        mock_plugin_class.__bases__ = (LanguagePlugin,)
        mock_plugin_class.__name__ = 'TestPlugin'
        
        # Mock non-plugin class
        mock_other_class = Mock()
        mock_other_class.__bases__ = (object,)
        mock_other_class.__name__ = 'OtherClass'
        
        # Set up module attributes
        setattr(mock_module, 'TestPlugin', mock_plugin_class)
        setattr(mock_module, 'OtherClass', mock_other_class)
        setattr(mock_module, 'some_function', lambda: None)
        
        with patch('inspect.getmembers') as mock_getmembers:
            mock_getmembers.return_value = [
                ('TestPlugin', mock_plugin_class),
                ('OtherClass', mock_other_class),
                ('some_function', lambda: None)
            ]
            
            plugin_classes = plugin_manager._find_plugin_classes(mock_module)
            
            assert isinstance(plugin_classes, list)
            # Should find only the plugin class
            assert len(plugin_classes) >= 0  # Depends on implementation

    def test_get_plugin_existing(self, plugin_manager: PluginManager, mock_java_plugin: MockLanguagePlugin) -> None:
        """Test getting an existing plugin"""
        plugin_manager.register_plugin(mock_java_plugin)
        
        retrieved_plugin = plugin_manager.get_plugin('java')
        
        assert retrieved_plugin is not None
        assert retrieved_plugin.get_language_name() == 'java'

    def test_get_plugin_nonexistent(self, plugin_manager: PluginManager) -> None:
        """Test getting a non-existent plugin"""
        retrieved_plugin = plugin_manager.get_plugin('nonexistent')
        
        assert retrieved_plugin is None

    def test_get_all_plugins(self, plugin_manager: PluginManager, mock_java_plugin: MockLanguagePlugin, mock_python_plugin: MockLanguagePlugin) -> None:
        """Test getting all registered plugins"""
        plugin_manager.register_plugin(mock_java_plugin)
        plugin_manager.register_plugin(mock_python_plugin)
        
        all_plugins = plugin_manager.get_all_plugins()
        
        assert isinstance(all_plugins, dict)
        assert len(all_plugins) == 2
        assert 'java' in all_plugins
        assert 'python' in all_plugins

    def test_get_supported_languages(self, plugin_manager: PluginManager, mock_java_plugin: MockLanguagePlugin, mock_python_plugin: MockLanguagePlugin) -> None:
        """Test getting list of supported languages"""
        plugin_manager.register_plugin(mock_java_plugin)
        plugin_manager.register_plugin(mock_python_plugin)
        
        languages = plugin_manager.get_supported_languages()
        
        assert isinstance(languages, list)
        assert len(languages) == 2
        assert 'java' in languages
        assert 'python' in languages

    def test_reload_plugins(self, plugin_manager: PluginManager) -> None:
        """Test reloading plugins"""
        with patch.object(plugin_manager, 'load_plugins') as mock_load:
            mock_load.return_value = [MockLanguagePlugin('java', ['.java'])]
            
            plugins = plugin_manager.reload_plugins()
            
            assert isinstance(plugins, list)
            mock_load.assert_called_once()

    def test_register_plugin_success(self, plugin_manager: PluginManager, mock_java_plugin: MockLanguagePlugin) -> None:
        """Test successful plugin registration"""
        result = plugin_manager.register_plugin(mock_java_plugin)
        
        assert result is True
        assert plugin_manager.get_plugin('java') is not None

    def test_register_plugin_duplicate(self, plugin_manager: PluginManager, mock_java_plugin: MockLanguagePlugin) -> None:
        """Test registering duplicate plugin"""
        # Register first time
        result1 = plugin_manager.register_plugin(mock_java_plugin)
        assert result1 is True
        
        # Register again (should replace)
        another_java_plugin = MockLanguagePlugin('java', ['.java'])
        result2 = plugin_manager.register_plugin(another_java_plugin)
        assert result2 is True

    def test_register_plugin_with_exception(self, plugin_manager: PluginManager) -> None:
        """Test plugin registration with exception"""
        mock_plugin = Mock()
        mock_plugin.get_language_name.side_effect = Exception("Plugin error")
        
        result = plugin_manager.register_plugin(mock_plugin)
        
        assert result is False

    def test_unregister_plugin_existing(self, plugin_manager: PluginManager, mock_java_plugin: MockLanguagePlugin) -> None:
        """Test unregistering an existing plugin"""
        plugin_manager.register_plugin(mock_java_plugin)
        
        result = plugin_manager.unregister_plugin('java')
        
        assert result is True
        assert plugin_manager.get_plugin('java') is None

    def test_unregister_plugin_nonexistent(self, plugin_manager: PluginManager) -> None:
        """Test unregistering a non-existent plugin"""
        result = plugin_manager.unregister_plugin('nonexistent')
        
        assert result is False

    def test_get_plugin_info_existing(self, plugin_manager: PluginManager, mock_java_plugin: MockLanguagePlugin) -> None:
        """Test getting plugin info for existing plugin"""
        plugin_manager.register_plugin(mock_java_plugin)
        
        info = plugin_manager.get_plugin_info('java')
        
        assert isinstance(info, dict)
        assert 'language' in info
        assert 'extensions' in info
        assert info['language'] == 'java'

    def test_get_plugin_info_nonexistent(self, plugin_manager: PluginManager) -> None:
        """Test getting plugin info for non-existent plugin"""
        info = plugin_manager.get_plugin_info('nonexistent')
        
        assert info is None

    def test_validate_plugin_valid(self, plugin_manager: PluginManager, mock_java_plugin: MockLanguagePlugin) -> None:
        """Test validating a valid plugin"""
        is_valid = plugin_manager.validate_plugin(mock_java_plugin)
        
        assert is_valid is True

    def test_validate_plugin_invalid(self, plugin_manager: PluginManager) -> None:
        """Test validating an invalid plugin"""
        # Create a plugin that doesn't implement required methods
        invalid_plugin = Mock()
        invalid_plugin.get_language_name.side_effect = Exception("Not implemented")
        
        is_valid = plugin_manager.validate_plugin(invalid_plugin)
        
        assert is_valid is False

    def test_validate_plugin_missing_methods(self, plugin_manager: PluginManager) -> None:
        """Test validating a plugin with missing methods"""
        incomplete_plugin = Mock()
        # Missing some required methods
        del incomplete_plugin.get_file_extensions
        
        is_valid = plugin_manager.validate_plugin(incomplete_plugin)
        
        assert is_valid is False


class TestPluginManagerErrorHandling:
    """Test error handling in PluginManager"""

    @pytest.fixture
    def plugin_manager(self) -> PluginManager:
        """Create a PluginManager instance for testing"""
        return PluginManager()

    def test_load_plugins_with_import_error(self, plugin_manager: PluginManager) -> None:
        """Test plugin loading with import errors"""
        with patch('importlib.metadata.entry_points') as mock_iter:
            mock_iter.side_effect = ImportError("pkg_resources not available")
            
            plugins = plugin_manager.load_plugins()
            
            # Should handle import errors gracefully
            assert isinstance(plugins, list)

    def test_load_plugins_with_corrupted_entry_point(self, plugin_manager: PluginManager) -> None:
        """Test loading plugins with corrupted entry point"""
        mock_entry_point = Mock()
        mock_entry_point.name = 'corrupted'
        mock_entry_point.load.side_effect = AttributeError("Corrupted entry point")
        
        with patch('importlib.metadata.entry_points') as mock_iter:
            mock_iter.return_value = [mock_entry_point]
            
            plugins = plugin_manager._load_from_entry_points()
            
            # Should skip corrupted entry points
            assert isinstance(plugins, list)
            assert len(plugins) == 0

    def test_register_plugin_none(self, plugin_manager: PluginManager) -> None:
        """Test registering None as plugin"""
        result = plugin_manager.register_plugin(None)
        
        assert result is False

    def test_get_plugin_info_with_exception(self, plugin_manager: PluginManager) -> None:
        """Test getting plugin info when plugin raises exception"""
        mock_plugin = Mock()
        mock_plugin.get_language_name.return_value = 'test'
        mock_plugin.get_file_extensions.side_effect = Exception("Extensions error")
        
        plugin_manager.register_plugin(mock_plugin)
        
        info = plugin_manager.get_plugin_info('test')
        
        # Should handle exceptions gracefully
        assert info is None or isinstance(info, dict)

    def test_validate_plugin_with_none(self, plugin_manager: PluginManager) -> None:
        """Test validating None plugin"""
        is_valid = plugin_manager.validate_plugin(None)
        
        assert is_valid is False


class TestPluginManagerIntegration:
    """Integration tests for PluginManager"""

    @pytest.fixture
    def plugin_manager(self) -> PluginManager:
        """Create a PluginManager instance for testing"""
        return PluginManager()

    def test_full_plugin_lifecycle(self, plugin_manager: PluginManager) -> None:
        """Test complete plugin lifecycle"""
        # Create plugins
        java_plugin = MockLanguagePlugin('java', ['.java'])
        python_plugin = MockLanguagePlugin('python', ['.py'])
        
        # Register plugins
        assert plugin_manager.register_plugin(java_plugin) is True
        assert plugin_manager.register_plugin(python_plugin) is True
        
        # Verify registration
        assert len(plugin_manager.get_supported_languages()) == 2
        assert plugin_manager.get_plugin('java') is not None
        assert plugin_manager.get_plugin('python') is not None
        
        # Get plugin info
        java_info = plugin_manager.get_plugin_info('java')
        assert java_info is not None
        assert java_info['language'] == 'java'
        
        # Unregister plugin
        assert plugin_manager.unregister_plugin('java') is True
        assert plugin_manager.get_plugin('java') is None
        assert len(plugin_manager.get_supported_languages()) == 1

    def test_plugin_discovery_and_loading(self, plugin_manager: PluginManager) -> None:
        """Test plugin discovery and loading process"""
        with patch.object(plugin_manager, '_load_from_entry_points') as mock_entry_points, \
             patch.object(plugin_manager, '_load_from_local_directory') as mock_local:
            
            # Mock discovered plugins
            mock_entry_points.return_value = [
                MockLanguagePlugin('java', ['.java']),
                MockLanguagePlugin('python', ['.py'])
            ]
            mock_local.return_value = [
                MockLanguagePlugin('javascript', ['.js'])
            ]
            
            # Load plugins
            plugins = plugin_manager.load_plugins()
            
            # Verify loading
            assert len(plugins) == 3
            assert len(plugin_manager.get_supported_languages()) == 3
            
            # Verify each plugin is accessible
            assert plugin_manager.get_plugin('java') is not None
            assert plugin_manager.get_plugin('python') is not None
            assert plugin_manager.get_plugin('javascript') is not None

    def test_plugin_validation_during_registration(self, plugin_manager: PluginManager) -> None:
        """Test plugin validation during registration process"""
        # Valid plugin
        valid_plugin = MockLanguagePlugin('java', ['.java'])
        assert plugin_manager.register_plugin(valid_plugin) is True
        
        # Invalid plugin (missing methods)
        invalid_plugin = Mock()
        invalid_plugin.get_language_name.side_effect = NotImplementedError()
        assert plugin_manager.register_plugin(invalid_plugin) is False
        
        # Verify only valid plugin is registered
        assert len(plugin_manager.get_supported_languages()) == 1
        assert 'java' in plugin_manager.get_supported_languages()

    def test_plugin_manager_with_multiple_extensions(self, plugin_manager: PluginManager) -> None:
        """Test plugin manager with plugins supporting multiple extensions"""
        # Python plugin with multiple extensions
        python_plugin = MockLanguagePlugin('python', ['.py', '.pyi', '.pyw'])
        
        # TypeScript plugin with multiple extensions
        typescript_plugin = MockLanguagePlugin('typescript', ['.ts', '.tsx'])
        
        plugin_manager.register_plugin(python_plugin)
        plugin_manager.register_plugin(typescript_plugin)
        
        # Verify plugins are registered correctly
        python_info = plugin_manager.get_plugin_info('python')
        typescript_info = plugin_manager.get_plugin_info('typescript')
        
        assert python_info['extensions'] == ['.py', '.pyi', '.pyw']
        assert typescript_info['extensions'] == ['.ts', '.tsx']

    def test_plugin_reload_preserves_manual_registrations(self, plugin_manager: PluginManager) -> None:
        """Test that plugin reload preserves manually registered plugins"""
        # Manually register a plugin
        manual_plugin = MockLanguagePlugin('manual', ['.manual'])
        plugin_manager.register_plugin(manual_plugin)
        
        # Mock reload to return different plugins
        with patch.object(plugin_manager, 'load_plugins') as mock_load:
            mock_load.return_value = [MockLanguagePlugin('java', ['.java'])]
            
            # Reload plugins
            reloaded_plugins = plugin_manager.reload_plugins()
            
            # Verify reload worked
            assert len(reloaded_plugins) == 1
            assert reloaded_plugins[0].get_language_name() == 'java'
