"""Tests for plugin discovery and registry."""

import sys
from pathlib import Path
from unittest.mock import Mock

import pytest

from cleave.tui.backends import register_backend
from cleave.tui.backends.base import Backend, BackendConfig, BackendState
from cleave.tui.backends.plugin import (
    PluginDiscovery,
    PluginInfo,
    PluginRegistry,
    discover_plugins,
    load_plugin,
)


@pytest.fixture
def plugin_dir(tmp_path):
    """Create a temporary plugin directory."""
    plugins = tmp_path / "backends"
    plugins.mkdir()
    return plugins


@pytest.fixture
def valid_plugin(plugin_dir):
    """Create a valid plugin module."""
    plugin_code = '''
from cleave.tui.backends.base import Backend, BackendConfig, BackendState
from collections.abc import AsyncIterator
from cleave.tui.backends.base import Message

class TestBackend(Backend):
    """Test backend for plugin system."""

    def __init__(self):
        self._state = BackendState.DISCONNECTED
        self._error = None

    @property
    def name(self) -> str:
        return "test-plugin"

    @property
    def state(self) -> BackendState:
        return self._state

    @property
    def error(self) -> str | None:
        return self._error

    async def connect(self, config: BackendConfig) -> None:
        self._state = BackendState.CONNECTED

    async def disconnect(self) -> None:
        self._state = BackendState.DISCONNECTED

    async def query(self, prompt: str) -> AsyncIterator[Message]:
        yield Message(role="assistant", content="Test response")

    @classmethod
    def check_auth(cls) -> tuple[bool, str]:
        return (True, "Test backend always authenticated")

    @classmethod
    def available_models(cls, config: BackendConfig | None = None) -> list[str]:
        return ["test-model-1", "test-model-2"]

__plugin_name__ = "test-plugin"
__plugin_version__ = "1.0.0"
__plugin_author__ = "Test Author"
__plugin_backend__ = TestBackend
'''
    plugin_file = plugin_dir / "test_plugin.py"
    plugin_file.write_text(plugin_code)
    return plugin_file


@pytest.fixture
def invalid_plugin_no_backend(plugin_dir):
    """Create an invalid plugin (missing backend class)."""
    plugin_code = '''
__plugin_name__ = "broken-plugin"
__plugin_version__ = "1.0.0"
# Missing __plugin_backend__
'''
    plugin_file = plugin_dir / "broken_plugin.py"
    plugin_file.write_text(plugin_code)
    return plugin_file


@pytest.fixture
def invalid_plugin_syntax_error(plugin_dir):
    """Create an invalid plugin (syntax error)."""
    plugin_code = "def broken( syntax error"
    plugin_file = plugin_dir / "syntax_error.py"
    plugin_file.write_text(plugin_code)
    return plugin_file


class TestPluginInfo:
    """Tests for PluginInfo dataclass."""

    def test_plugin_info_creation(self):
        info = PluginInfo(
            name="test",
            version="1.0.0",
            author="Test Author",
            module_path=Path("/path/to/plugin.py"),
            backend_class=Mock(),
        )
        assert info.name == "test"
        assert info.version == "1.0.0"
        assert info.author == "Test Author"
        assert info.description == ""
        assert info.is_builtin is False


class TestPluginDiscovery:
    """Tests for plugin discovery."""

    def test_discover_valid_plugin(self, plugin_dir, valid_plugin):
        """Test discovering a valid plugin."""
        plugins = discover_plugins(plugin_dir)
        assert len(plugins) == 1
        assert plugins[0].name == "test-plugin"
        assert plugins[0].version == "1.0.0"
        assert plugins[0].author == "Test Author"
        assert plugins[0].backend_class is not None

    def test_discover_skips_invalid_plugin(
        self, plugin_dir, valid_plugin, invalid_plugin_no_backend
    ):
        """Test that invalid plugins are skipped."""
        plugins = discover_plugins(plugin_dir)
        assert len(plugins) == 1  # Only valid plugin
        assert plugins[0].name == "test-plugin"

    def test_discover_handles_syntax_errors(
        self, plugin_dir, valid_plugin, invalid_plugin_syntax_error
    ):
        """Test that syntax errors don't crash discovery."""
        plugins = discover_plugins(plugin_dir)
        assert len(plugins) == 1  # Only valid plugin
        assert plugins[0].name == "test-plugin"

    def test_discover_empty_directory(self, plugin_dir):
        """Test discovering plugins in empty directory."""
        plugins = discover_plugins(plugin_dir)
        assert len(plugins) == 0

    def test_discover_nonexistent_directory(self, tmp_path):
        """Test discovering plugins in non-existent directory."""
        plugins = discover_plugins(tmp_path / "nonexistent")
        assert len(plugins) == 0

    def test_discover_ignores_init_files(self, plugin_dir):
        """Test that __init__.py files are ignored."""
        (plugin_dir / "__init__.py").write_text("# Init file")
        plugins = discover_plugins(plugin_dir)
        assert len(plugins) == 0

    def test_discover_ignores_non_python_files(self, plugin_dir):
        """Test that non-Python files are ignored."""
        (plugin_dir / "readme.txt").write_text("Not a plugin")
        plugins = discover_plugins(plugin_dir)
        assert len(plugins) == 0


class TestLoadPlugin:
    """Tests for loading individual plugins."""

    def test_load_valid_plugin(self, valid_plugin):
        """Test loading a valid plugin."""
        info = load_plugin(valid_plugin)
        assert info is not None
        assert info.name == "test-plugin"
        assert info.version == "1.0.0"
        assert info.backend_class is not None

    def test_load_plugin_missing_backend(self, invalid_plugin_no_backend):
        """Test loading plugin without backend class."""
        info = load_plugin(invalid_plugin_no_backend)
        assert info is None

    def test_load_plugin_syntax_error(self, invalid_plugin_syntax_error):
        """Test loading plugin with syntax error."""
        info = load_plugin(invalid_plugin_syntax_error)
        assert info is None


class TestPluginRegistry:
    """Tests for plugin registry."""

    def test_registry_initialization(self):
        """Test registry starts empty."""
        registry = PluginRegistry()
        assert len(registry.list_plugins()) == 0
        assert len(registry.list_backends()) >= 2  # claude, openai at minimum

    def test_register_plugin(self):
        """Test registering a plugin."""
        registry = PluginRegistry()

        mock_backend = Mock(spec=Backend)
        mock_backend.name = "test-backend"

        info = PluginInfo(
            name="test",
            version="1.0.0",
            author="Test",
            module_path=Path("/test.py"),
            backend_class=mock_backend,
        )

        registry.register_plugin(info)

        assert "test" in registry.list_plugins()
        assert "test" in registry.list_backends()
        assert registry.get_plugin_info("test") == info

    def test_get_backend_from_plugin(self):
        """Test retrieving backend from registered plugin."""
        registry = PluginRegistry()

        mock_backend_class = Mock()
        mock_backend_instance = Mock(spec=Backend)
        mock_backend_class.return_value = mock_backend_instance

        info = PluginInfo(
            name="test",
            version="1.0.0",
            author="Test",
            module_path=Path("/test.py"),
            backend_class=mock_backend_class,
        )

        registry.register_plugin(info)
        backend = registry.get_backend("test")

        assert backend == mock_backend_instance
        mock_backend_class.assert_called_once()

    def test_discover_and_register_plugins(self, plugin_dir, valid_plugin):
        """Test discovering and registering plugins."""
        registry = PluginRegistry()
        count = registry.discover_and_register(plugin_dir)

        assert count == 1
        assert "test-plugin" in registry.list_plugins()
        assert "test-plugin" in registry.list_backends()

    def test_get_nonexistent_plugin(self):
        """Test retrieving non-existent plugin."""
        registry = PluginRegistry()
        assert registry.get_plugin_info("nonexistent") is None

    def test_get_nonexistent_backend_raises(self):
        """Test retrieving non-existent backend raises error."""
        registry = PluginRegistry()
        with pytest.raises(KeyError, match="Unknown backend"):
            registry.get_backend("nonexistent")

    def test_is_plugin_backend(self):
        """Test checking if backend is from plugin."""
        registry = PluginRegistry()

        info = PluginInfo(
            name="test",
            version="1.0.0",
            author="Test",
            module_path=Path("/test.py"),
            backend_class=Mock(),
        )

        registry.register_plugin(info)

        assert registry.is_plugin_backend("test") is True
        assert registry.is_plugin_backend("claude") is False

    def test_get_backend_status(self):
        """Test getting backend status."""
        registry = PluginRegistry()

        # Built-in backend should exist
        status = registry.get_backend_status("claude")
        assert status["exists"] is True
        assert status["is_plugin"] is False

        # Non-existent backend
        status = registry.get_backend_status("nonexistent")
        assert status["exists"] is False


class TestPluginDiscoveryIntegration:
    """Integration tests for the full plugin discovery flow."""

    def test_full_plugin_lifecycle(self, plugin_dir, valid_plugin):
        """Test complete plugin discovery and usage."""
        # Discover plugins
        discovery = PluginDiscovery(plugin_dir)
        plugins = discovery.discover()

        assert len(plugins) == 1
        plugin_info = plugins[0]

        # Register plugin
        registry = PluginRegistry()
        registry.register_plugin(plugin_info)

        # Use plugin backend
        backend = registry.get_backend("test-plugin")
        assert backend is not None
        assert backend.name == "test-plugin"

    def test_default_plugin_directory(self):
        """Test using default plugin directory."""
        discovery = PluginDiscovery()
        # Should not crash even if directory doesn't exist
        plugins = discovery.discover()
        assert isinstance(plugins, list)
