"""
Unit tests for the plugin system.

Tests:
- AdapterRegistry functionality
- PluginLoader operations
- Plugin discovery
- Hot-reloading
"""

import json
import pytest
import tempfile
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch

from agent_orchestrator.plugins import (
    AdapterRegistry,
    PluginMetadata,
    PluginStatus,
    PluginLoader,
    PluginConfig,
)


class TestPluginMetadata:
    """Tests for PluginMetadata dataclass."""

    def test_metadata_creation(self):
        """Test creating plugin metadata."""
        metadata = PluginMetadata(
            name="test-plugin",
            version="1.0.0",
            description="Test plugin",
            author="Test Author",
            adapter_type="cli",
            capabilities=["code_edit", "git"],
        )
        assert metadata.name == "test-plugin"
        assert metadata.version == "1.0.0"
        assert metadata.capabilities == ["code_edit", "git"]

    def test_metadata_defaults(self):
        """Test default values for metadata."""
        metadata = PluginMetadata(name="minimal")
        assert metadata.version == "1.0.0"
        assert metadata.description == ""
        assert metadata.author == ""
        assert metadata.adapter_type == ""
        assert metadata.capabilities == []


class TestAdapterRegistry:
    """Tests for the AdapterRegistry class."""

    def test_register_factory(self):
        """Test registering an adapter factory."""
        registry = AdapterRegistry()

        def mock_factory(config):
            return MagicMock()

        metadata = PluginMetadata(name="test-adapter")
        registry.register_factory("test-adapter", mock_factory, metadata)

        assert "test-adapter" in registry.list_factories()

    def test_register_factory_with_replace(self):
        """Test replacing an existing factory."""
        registry = AdapterRegistry()

        def factory1(config):
            return MagicMock(name="factory1")

        def factory2(config):
            return MagicMock(name="factory2")

        metadata = PluginMetadata(name="test")
        registry.register_factory("test", factory1, metadata)

        # Should fail without replace flag
        with pytest.raises(ValueError):
            registry.register_factory("test", factory2, metadata, replace=False)

        # Should succeed with replace flag
        registry.register_factory("test", factory2, metadata, replace=True)
        assert "test" in registry.list_factories()

    def test_unregister_factory(self):
        """Test unregistering a factory."""
        registry = AdapterRegistry()

        def mock_factory(config):
            return MagicMock()

        metadata = PluginMetadata(name="test")
        registry.register_factory("test", mock_factory, metadata)
        assert registry.unregister_factory("test")
        assert "test" not in registry.list_factories()

    def test_unregister_nonexistent(self):
        """Test unregistering a factory that doesn't exist."""
        registry = AdapterRegistry()
        assert not registry.unregister_factory("nonexistent")

    def test_create_adapter(self):
        """Test creating an adapter from factory."""
        registry = AdapterRegistry()
        mock_adapter = MagicMock()

        def mock_factory(**kwargs):
            return mock_adapter

        metadata = PluginMetadata(name="test")
        registry.register_factory("test", mock_factory, metadata)

        adapter = registry.create_adapter("test", "agent-1", option="value")
        assert adapter == mock_adapter

    def test_create_adapter_unknown_factory(self):
        """Test creating adapter from unknown factory."""
        registry = AdapterRegistry()
        with pytest.raises(KeyError):
            registry.create_adapter("unknown", "agent-1")

    def test_register_adapter_instance(self):
        """Test registering an adapter instance directly."""
        registry = AdapterRegistry()
        adapter = MagicMock()

        registry.register_adapter("agent-1", adapter)
        assert registry.get_adapter("agent-1") == adapter

    def test_get_nonexistent_adapter(self):
        """Test getting a non-existent adapter."""
        registry = AdapterRegistry()
        assert registry.get_adapter("nonexistent") is None

    def test_get_metadata(self):
        """Test getting factory metadata."""
        registry = AdapterRegistry()

        def mock_factory(**kwargs):
            return MagicMock()

        metadata = PluginMetadata(
            name="test",
            version="2.0.0",
            capabilities=["code_edit"],
        )
        registry.register_factory(
            "test",
            mock_factory,
            metadata,
            default_config={"timeout": 300},
        )

        meta = registry.get_metadata("test")
        assert meta is not None
        assert meta.version == "2.0.0"
        assert meta.capabilities == ["code_edit"]

    def test_list_adapters(self):
        """Test listing registered adapters."""
        registry = AdapterRegistry()
        registry.register_adapter("agent-1", MagicMock())
        registry.register_adapter("agent-2", MagicMock())

        adapters = registry.list_adapters()
        assert "agent-1" in adapters
        assert "agent-2" in adapters

    def test_get_by_capability(self):
        """Test finding adapters by capability."""
        registry = AdapterRegistry()

        def factory1(config):
            return MagicMock()

        def factory2(config):
            return MagicMock()

        metadata1 = PluginMetadata(name="plugin1", capabilities=["code_edit", "git"])
        metadata2 = PluginMetadata(name="plugin2", capabilities=["search", "git"])

        registry.register_factory("plugin1", factory1, metadata1)
        registry.register_factory("plugin2", factory2, metadata2)

        code_edit_plugins = registry.get_by_capability("code_edit")
        assert "plugin1" in code_edit_plugins
        assert "plugin2" not in code_edit_plugins

        git_plugins = registry.get_by_capability("git")
        assert "plugin1" in git_plugins
        assert "plugin2" in git_plugins

    def test_on_register_hook(self):
        """Test on_register event hook."""
        registry = AdapterRegistry()
        callback = MagicMock()
        registry.on_register(callback)

        def mock_factory(config):
            return MagicMock()

        metadata = PluginMetadata(name="test")
        registry.register_factory("test", mock_factory, metadata)

        callback.assert_called_once_with("test", metadata)

    def test_on_create_hook(self):
        """Test on_create event hook."""
        registry = AdapterRegistry()
        callback = MagicMock()
        registry.on_create(callback)

        mock_adapter = MagicMock()

        def mock_factory(**kwargs):
            return mock_adapter

        metadata = PluginMetadata(name="test")
        registry.register_factory("test", mock_factory, metadata)
        registry.create_adapter("test", "agent-1")

        # on_create is called with (agent_id, adapter)
        callback.assert_called_once_with("agent-1", mock_adapter)

    def test_get_status_via_metadata(self):
        """Test getting status via metadata."""
        registry = AdapterRegistry()

        def mock_factory(**kwargs):
            return MagicMock()

        metadata = PluginMetadata(name="test")
        registry.register_factory("test", mock_factory, metadata)

        meta = registry.get_metadata("test")
        assert meta.status == PluginStatus.REGISTERED

        # Non-existent factory
        assert registry.get_metadata("unknown") is None

    def test_disable_enable_adapter(self):
        """Test disabling and enabling adapter."""
        registry = AdapterRegistry()

        def mock_factory(**kwargs):
            return MagicMock()

        metadata = PluginMetadata(name="test")
        registry.register_factory("test", mock_factory, metadata)

        # Create adapter to make it active
        registry.create_adapter("test", "agent-1")

        # Disable it
        assert registry.disable_adapter("agent-1")
        meta = registry.get_metadata("agent-1")
        assert meta.status == PluginStatus.DISABLED

        # Enable it
        assert registry.enable_adapter("agent-1")
        meta = registry.get_metadata("agent-1")
        assert meta.status == PluginStatus.ACTIVE


class TestPluginConfig:
    """Tests for PluginConfig dataclass."""

    def test_from_dict(self):
        """Test creating config from dictionary."""
        data = {
            "name": "test-plugin",
            "enabled": True,
            "factory_module": "test.module",
            "factory_function": "create_adapter",
            "metadata": {"version": "1.0.0"},
            "default_config": {"timeout": 300},
        }

        config = PluginConfig.from_dict(data)
        assert config.name == "test-plugin"
        assert config.factory_module == "test.module"
        assert config.default_config["timeout"] == 300

    def test_to_dict(self):
        """Test converting config to dictionary."""
        config = PluginConfig(
            name="test",
            factory_module="test.module",
            factory_function="create",
        )

        data = config.to_dict()
        assert data["name"] == "test"
        assert data["factory_module"] == "test.module"


class TestPluginLoader:
    """Tests for the PluginLoader class."""

    def test_loader_init(self):
        """Test initializing the plugin loader."""
        registry = AdapterRegistry()
        loader = PluginLoader(registry)
        assert loader.registry == registry

    def test_load_from_config(self):
        """Test loading plugins from config dictionary."""
        registry = AdapterRegistry()
        loader = PluginLoader(registry)

        with patch.object(loader, "_load_plugin", return_value=True) as mock_load:
            config = {
                "plugins": [
                    {
                        "name": "test-plugin",
                        "factory_module": "test.module",
                        "factory_function": "create",
                    }
                ]
            }

            loaded = loader.load_from_config(config)
            assert "test-plugin" in loaded
            mock_load.assert_called_once()

    def test_load_from_file(self):
        """Test loading plugins from JSON file."""
        registry = AdapterRegistry()
        loader = PluginLoader(registry)

        config = {
            "plugins": [
                {
                    "name": "file-plugin",
                    "enabled": True,
                    "factory_module": "test.module",
                    "factory_function": "create",
                }
            ]
        }

        with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
            json.dump(config, f)
            f.flush()

            with patch.object(loader, "_load_plugin", return_value=True):
                loaded = loader.load_from_file(Path(f.name))
                assert "file-plugin" in loaded

    def test_load_from_nonexistent_file(self):
        """Test loading from non-existent file."""
        registry = AdapterRegistry()
        loader = PluginLoader(registry)

        loaded = loader.load_from_file(Path("/nonexistent/path.json"))
        assert loaded == []

    def test_load_disabled_plugin(self):
        """Test that disabled plugins are skipped."""
        registry = AdapterRegistry()
        loader = PluginLoader(registry)

        config = PluginConfig(
            name="disabled-plugin",
            enabled=False,
            factory_module="test.module",
            factory_function="create",
        )

        result = loader._load_plugin(config)
        assert result is False

    def test_load_plugin_missing_factory(self):
        """Test loading plugin with missing factory info."""
        registry = AdapterRegistry()
        loader = PluginLoader(registry)

        config = PluginConfig(
            name="incomplete-plugin",
            enabled=True,
            factory_module="",  # Missing
            factory_function="",
        )

        result = loader._load_plugin(config)
        assert result is False

    def test_get_loaded_plugins(self):
        """Test getting list of loaded plugins."""
        registry = AdapterRegistry()
        loader = PluginLoader(registry)

        # Manually add to plugin configs
        loader._plugin_configs["plugin1"] = PluginConfig(name="plugin1")
        loader._plugin_configs["plugin2"] = PluginConfig(name="plugin2")

        loaded = loader.get_loaded_plugins()
        assert "plugin1" in loaded
        assert "plugin2" in loaded

    def test_get_plugin_config(self):
        """Test getting config for a loaded plugin."""
        registry = AdapterRegistry()
        loader = PluginLoader(registry)

        config = PluginConfig(name="test", factory_module="test.module")
        loader._plugin_configs["test"] = config

        retrieved = loader.get_plugin_config("test")
        assert retrieved == config
        assert loader.get_plugin_config("unknown") is None

    def test_unload_plugin(self):
        """Test unloading a plugin."""
        registry = AdapterRegistry()
        loader = PluginLoader(registry)

        # Setup mock loaded plugin
        mock_module = MagicMock()
        loader._loaded_modules["test"] = mock_module
        loader._plugin_configs["test"] = PluginConfig(name="test")

        result = loader.unload_plugin("test")
        assert result is True
        assert "test" not in loader._loaded_modules
        assert "test" not in loader._plugin_configs

    def test_unload_nonexistent_plugin(self):
        """Test unloading a plugin that doesn't exist."""
        registry = AdapterRegistry()
        loader = PluginLoader(registry)

        result = loader.unload_plugin("nonexistent")
        assert result is False

    def test_create_default_config(self):
        """Test creating default plugin configuration."""
        registry = AdapterRegistry()
        loader = PluginLoader(registry)

        config = loader.create_default_config()

        assert "plugins" in config
        plugin_names = [p["name"] for p in config["plugins"]]
        assert "claude-code" in plugin_names
        assert "gemini-cli" in plugin_names
        assert "codex-cli" in plugin_names

    def test_save_config(self):
        """Test saving plugin configuration."""
        registry = AdapterRegistry()
        loader = PluginLoader(registry)

        loader._plugin_configs["test"] = PluginConfig(
            name="test",
            factory_module="test.module",
            factory_function="create",
        )

        with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
            loader.save_config(Path(f.name))

            with open(f.name) as rf:
                saved = json.load(rf)

            assert "plugins" in saved
            assert len(saved["plugins"]) == 1
            assert saved["plugins"][0]["name"] == "test"

    def test_discover_nonexistent_directory(self):
        """Test discovering plugins in non-existent directory."""
        registry = AdapterRegistry()
        loader = PluginLoader(registry)

        discovered = loader.discover_plugins(Path("/nonexistent/plugins"))
        assert discovered == []


class TestPluginIntegration:
    """Integration tests for plugin system."""

    def test_full_plugin_lifecycle(self):
        """Test complete plugin lifecycle."""
        registry = AdapterRegistry()

        # Register callback
        registered_plugins = []
        registry.on_register(lambda name, meta: registered_plugins.append(name))

        # Create and register factory - accepts **kwargs since create_adapter passes config
        def create_test_adapter(**kwargs):
            adapter = MagicMock()
            adapter.name = "test-adapter"
            adapter.config = kwargs
            return adapter

        metadata = PluginMetadata(
            name="test-plugin",
            version="1.0.0",
            capabilities=["code_edit"],
        )

        registry.register_factory(
            "test-plugin",
            create_test_adapter,
            metadata,
            default_config={"timeout": 300},
        )

        assert "test-plugin" in registered_plugins

        # Create adapter instance
        adapter = registry.create_adapter(
            "test-plugin",
            "agent-1",
            custom_option="value",
        )

        assert adapter.name == "test-adapter"
        # Verify config was passed correctly
        assert adapter.config["timeout"] == 300
        assert adapter.config["custom_option"] == "value"

        # Unregister
        registry.unregister_factory("test-plugin")
        assert "test-plugin" not in registry.list_factories()
