"""Tests for ai4gh.core.plugin."""

from __future__ import annotations

from typing import Any
from unittest.mock import MagicMock, patch

import pytest

from ai4gh.core.exceptions import PluginError
from ai4gh.core.plugin import GA4GHPlugin, PluginRegistry


class FakePlugin:
    """Test plugin implementing the GA4GHPlugin protocol."""

    name = "fake-spec"
    spec_version = "1.0.0"

    def get_tools(self) -> list[dict[str, Any]]:
        return [{"name": "fake_tool"}]

    def create_client(self, config: Any) -> Any:
        return None


class TestGA4GHPluginProtocol:
    def test_fake_plugin_implements_protocol(self) -> None:
        plugin = FakePlugin()
        assert isinstance(plugin, GA4GHPlugin)

    def test_non_plugin_does_not_match(self) -> None:
        assert not isinstance("not a plugin", GA4GHPlugin)


class TestPluginRegistry:
    def test_register_and_get(self) -> None:
        registry = PluginRegistry()
        plugin = FakePlugin()
        registry.register(plugin)
        assert registry.get("fake-spec") is plugin

    def test_get_missing_returns_none(self) -> None:
        registry = PluginRegistry()
        assert registry.get("nonexistent") is None

    def test_list_plugins(self) -> None:
        registry = PluginRegistry()
        assert registry.list_plugins() == []
        registry.register(FakePlugin())
        assert registry.list_plugins() == ["fake-spec"]

    def test_plugins_property_returns_copy(self) -> None:
        registry = PluginRegistry()
        plugin = FakePlugin()
        registry.register(plugin)
        plugins = registry.plugins
        assert plugins == {"fake-spec": plugin}
        # modifying returned dict doesn't affect internal state
        plugins["other"] = FakePlugin()
        assert "other" not in registry.plugins

    def test_register_overwrites(self) -> None:
        registry = PluginRegistry()
        p1 = FakePlugin()
        p2 = FakePlugin()
        registry.register(p1)
        registry.register(p2)
        assert registry.get("fake-spec") is p2
        assert len(registry.list_plugins()) == 1


class TestPluginDiscovery:
    def _make_entry_point(self, name: str, load_result: Any) -> MagicMock:
        ep = MagicMock()
        ep.name = name
        ep.load.return_value = load_result
        return ep

    @patch("importlib.metadata.entry_points")
    def test_discover_callable_plugin(self, mock_eps: MagicMock) -> None:
        """Test that discover() instantiates callable entry points."""
        mock_eps.return_value = [self._make_entry_point("fake", FakePlugin)]

        registry = PluginRegistry()
        count = registry.discover()
        assert count == 1
        assert registry.get("fake-spec") is not None

    @patch("importlib.metadata.entry_points")
    def test_discover_instance_plugin(self, mock_eps: MagicMock) -> None:
        """Test that discover() uses pre-instantiated entry points."""
        plugin_instance = FakePlugin()
        mock_eps.return_value = [self._make_entry_point("fake", plugin_instance)]

        registry = PluginRegistry()
        count = registry.discover()
        assert count == 1
        assert registry.get("fake-spec") is plugin_instance

    @patch("importlib.metadata.entry_points")
    def test_discover_non_protocol_warns(self, mock_eps: MagicMock) -> None:
        """Test that non-protocol objects are skipped with warning."""

        class NotAPlugin:
            pass

        mock_eps.return_value = [self._make_entry_point("bad", NotAPlugin)]

        registry = PluginRegistry()
        count = registry.discover()
        assert count == 0

    @patch("importlib.metadata.entry_points")
    def test_discover_load_error_raises(self, mock_eps: MagicMock) -> None:
        """Test that load errors are wrapped in PluginError."""
        ep = MagicMock()
        ep.name = "broken"
        ep.load.side_effect = ImportError("no module")
        mock_eps.return_value = [ep]

        registry = PluginRegistry()
        with pytest.raises(PluginError, match="Failed to load plugin 'broken'"):
            registry.discover()

    @patch("importlib.metadata.entry_points")
    def test_discover_empty(self, mock_eps: MagicMock) -> None:
        """Test discover() with no entry points."""
        mock_eps.return_value = []
        registry = PluginRegistry()
        count = registry.discover()
        assert count == 0
