"""
Unit tests for scheduling/schedule/plugins.py

Tests plugin system base classes and registry.
"""
from unittest.mock import Mock, patch
import pytest


class TestPluginExecutionError:
    """Tests for PluginExecutionError exception."""

    def test_is_exception_class(self):
        """Should be an Exception subclass."""
        from smoothschedule.scheduling.schedule.plugins import PluginExecutionError

        assert issubclass(PluginExecutionError, Exception)

    def test_can_be_raised_and_caught(self):
        """Should be raisable with a message."""
        from smoothschedule.scheduling.schedule.plugins import PluginExecutionError

        with pytest.raises(PluginExecutionError) as exc_info:
            raise PluginExecutionError("Plugin failed")

        assert str(exc_info.value) == "Plugin failed"


class TestBasePlugin:
    """Tests for BasePlugin abstract class."""

    def test_class_exists(self):
        """Should have BasePlugin class."""
        from smoothschedule.scheduling.schedule.plugins import BasePlugin

        assert BasePlugin is not None

    def test_has_required_attributes(self):
        """Should define required class attributes."""
        from smoothschedule.scheduling.schedule.plugins import BasePlugin

        assert hasattr(BasePlugin, 'name')
        assert hasattr(BasePlugin, 'display_name')
        assert hasattr(BasePlugin, 'description')
        assert hasattr(BasePlugin, 'category')
        assert hasattr(BasePlugin, 'config_schema')

    def test_init_stores_config(self):
        """Should store config on initialization."""
        from smoothschedule.scheduling.schedule.plugins import BasePlugin

        # Create concrete implementation for testing
        class TestPlugin(BasePlugin):
            name = "test_plugin"
            display_name = "Test Plugin"

            def execute(self, context):
                return {"success": True}

        plugin = TestPlugin(config={"key": "value"})

        assert plugin.config == {"key": "value"}

    def test_init_defaults_to_empty_config(self):
        """Should default to empty config when None provided."""
        from smoothschedule.scheduling.schedule.plugins import BasePlugin

        class TestPlugin(BasePlugin):
            name = "test_plugin"
            display_name = "Test Plugin"

            def execute(self, context):
                return {"success": True}

        plugin = TestPlugin(config=None)

        assert plugin.config == {}

    def test_validate_config_checks_required_keys(self):
        """Should raise ValueError for missing required config keys."""
        from smoothschedule.scheduling.schedule.plugins import BasePlugin

        class TestPlugin(BasePlugin):
            name = "test_plugin"
            display_name = "Test Plugin"
            config_schema = {
                "api_key": {"required": True},
            }

            def execute(self, context):
                return {"success": True}

        with pytest.raises(ValueError) as exc_info:
            TestPlugin(config={})

        assert "Required config key 'api_key' missing" in str(exc_info.value)

    def test_validate_config_allows_optional_keys(self):
        """Should not raise for missing optional config keys."""
        from smoothschedule.scheduling.schedule.plugins import BasePlugin

        class TestPlugin(BasePlugin):
            name = "test_plugin"
            display_name = "Test Plugin"
            config_schema = {
                "optional_key": {"required": False},
            }

            def execute(self, context):
                return {"success": True}

        # Should not raise
        plugin = TestPlugin(config={})
        assert plugin.config == {}

    def test_validate_config_passes_with_required_keys_present(self):
        """Should not raise when required keys are provided."""
        from smoothschedule.scheduling.schedule.plugins import BasePlugin

        class TestPlugin(BasePlugin):
            name = "test_plugin"
            display_name = "Test Plugin"
            config_schema = {
                "api_key": {"required": True},
            }

            def execute(self, context):
                return {"success": True}

        # Should not raise
        plugin = TestPlugin(config={"api_key": "secret"})
        assert plugin.config["api_key"] == "secret"

    def test_can_execute_returns_true_by_default(self):
        """Should return (True, None) by default."""
        from smoothschedule.scheduling.schedule.plugins import BasePlugin

        class TestPlugin(BasePlugin):
            name = "test_plugin"
            display_name = "Test Plugin"

            def execute(self, context):
                return {"success": True}

        plugin = TestPlugin()
        can_exec, reason = plugin.can_execute({})

        assert can_exec is True
        assert reason is None

    def test_on_success_does_nothing_by_default(self):
        """Should not raise on success callback."""
        from smoothschedule.scheduling.schedule.plugins import BasePlugin

        class TestPlugin(BasePlugin):
            name = "test_plugin"
            display_name = "Test Plugin"

            def execute(self, context):
                return {"success": True}

        plugin = TestPlugin()
        # Should not raise
        plugin.on_success({"success": True})

    def test_on_failure_logs_error(self):
        """Should log error on failure."""
        from smoothschedule.scheduling.schedule.plugins import BasePlugin

        class TestPlugin(BasePlugin):
            name = "test_plugin"
            display_name = "Test Plugin"

            def execute(self, context):
                return {"success": True}

        plugin = TestPlugin()

        with patch('smoothschedule.scheduling.schedule.plugins.logger') as mock_logger:
            plugin.on_failure(Exception("Something failed"))
            mock_logger.error.assert_called()

    def test_get_next_run_time_returns_none_by_default(self):
        """Should return None for next run time by default."""
        from smoothschedule.scheduling.schedule.plugins import BasePlugin

        class TestPlugin(BasePlugin):
            name = "test_plugin"
            display_name = "Test Plugin"

            def execute(self, context):
                return {"success": True}

        plugin = TestPlugin()
        result = plugin.get_next_run_time(None)

        assert result is None

    def test_str_representation(self):
        """Should return display_name and name."""
        from smoothschedule.scheduling.schedule.plugins import BasePlugin

        class TestPlugin(BasePlugin):
            name = "test_plugin"
            display_name = "Test Plugin"

            def execute(self, context):
                return {"success": True}

        plugin = TestPlugin()
        result = str(plugin)

        assert "Test Plugin" in result
        assert "test_plugin" in result


class TestPluginRegistry:
    """Tests for PluginRegistry class."""

    def test_init_creates_empty_plugins_dict(self):
        """Should start with empty plugins."""
        from smoothschedule.scheduling.schedule.plugins import PluginRegistry

        registry = PluginRegistry()
        assert len(registry._plugins) == 0

    def test_register_adds_plugin(self):
        """Should register a plugin class."""
        from smoothschedule.scheduling.schedule.plugins import PluginRegistry, BasePlugin

        class TestPlugin(BasePlugin):
            name = "test_plugin"
            display_name = "Test Plugin"

            def execute(self, context):
                return {"success": True}

        registry = PluginRegistry()
        registry.register(TestPlugin)

        assert "test_plugin" in registry._plugins
        assert registry._plugins["test_plugin"] is TestPlugin

    def test_register_raises_for_no_name(self):
        """Should raise ValueError if plugin has no name."""
        from smoothschedule.scheduling.schedule.plugins import PluginRegistry, BasePlugin

        class NoNamePlugin(BasePlugin):
            name = ""  # Empty name
            display_name = "No Name"

            def execute(self, context):
                return {"success": True}

        registry = PluginRegistry()

        with pytest.raises(ValueError) as exc_info:
            registry.register(NoNamePlugin)

        assert "must define a 'name' attribute" in str(exc_info.value)

    def test_register_raises_for_duplicate_name(self):
        """Should raise ValueError for duplicate plugin names."""
        from smoothschedule.scheduling.schedule.plugins import PluginRegistry, BasePlugin

        class Plugin1(BasePlugin):
            name = "duplicate_name"
            display_name = "Plugin 1"

            def execute(self, context):
                return {"success": True}

        class Plugin2(BasePlugin):
            name = "duplicate_name"  # Same name
            display_name = "Plugin 2"

            def execute(self, context):
                return {"success": True}

        registry = PluginRegistry()
        registry.register(Plugin1)

        with pytest.raises(ValueError) as exc_info:
            registry.register(Plugin2)

        assert "already registered" in str(exc_info.value)

    def test_unregister_removes_plugin(self):
        """Should unregister a plugin by name."""
        from smoothschedule.scheduling.schedule.plugins import PluginRegistry, BasePlugin

        class TestPlugin(BasePlugin):
            name = "test_plugin"
            display_name = "Test Plugin"

            def execute(self, context):
                return {"success": True}

        registry = PluginRegistry()
        registry.register(TestPlugin)
        assert "test_plugin" in registry._plugins

        registry.unregister("test_plugin")
        assert "test_plugin" not in registry._plugins

    def test_unregister_does_nothing_for_unknown_plugin(self):
        """Should not raise when unregistering unknown plugin."""
        from smoothschedule.scheduling.schedule.plugins import PluginRegistry

        registry = PluginRegistry()

        # Should not raise
        registry.unregister("nonexistent_plugin")

    def test_get_returns_plugin_class(self):
        """Should return plugin class by name."""
        from smoothschedule.scheduling.schedule.plugins import PluginRegistry, BasePlugin

        class TestPlugin(BasePlugin):
            name = "test_plugin"
            display_name = "Test Plugin"

            def execute(self, context):
                return {"success": True}

        registry = PluginRegistry()
        registry.register(TestPlugin)

        result = registry.get("test_plugin")

        assert result is TestPlugin

    def test_get_returns_none_for_unknown(self):
        """Should return None for unknown plugin name."""
        from smoothschedule.scheduling.schedule.plugins import PluginRegistry

        registry = PluginRegistry()
        result = registry.get("nonexistent")

        assert result is None

    def test_get_instance_returns_plugin_instance(self):
        """Should return configured plugin instance."""
        from smoothschedule.scheduling.schedule.plugins import PluginRegistry, BasePlugin

        class TestPlugin(BasePlugin):
            name = "test_plugin"
            display_name = "Test Plugin"

            def execute(self, context):
                return {"success": True}

        registry = PluginRegistry()
        registry.register(TestPlugin)

        instance = registry.get_instance("test_plugin", config={"key": "value"})

        assert isinstance(instance, TestPlugin)
        assert instance.config == {"key": "value"}

    def test_get_instance_returns_none_for_unknown(self):
        """Should return None for unknown plugin name."""
        from smoothschedule.scheduling.schedule.plugins import PluginRegistry

        registry = PluginRegistry()
        result = registry.get_instance("nonexistent")

        assert result is None

    def test_list_all_returns_plugin_metadata(self):
        """Should return list of plugin metadata."""
        from smoothschedule.scheduling.schedule.plugins import PluginRegistry, BasePlugin

        class TestPlugin(BasePlugin):
            name = "test_plugin"
            display_name = "Test Plugin"
            description = "A test plugin"
            category = "testing"

            def execute(self, context):
                return {"success": True}

        registry = PluginRegistry()
        registry.register(TestPlugin)

        result = registry.list_all()

        assert len(result) == 1
        assert result[0]['name'] == 'test_plugin'
        assert result[0]['display_name'] == 'Test Plugin'
        assert result[0]['description'] == 'A test plugin'
        assert result[0]['category'] == 'testing'

    def test_list_all_returns_empty_list_when_no_plugins(self):
        """Should return empty list when no plugins registered."""
        from smoothschedule.scheduling.schedule.plugins import PluginRegistry

        registry = PluginRegistry()
        result = registry.list_all()

        assert result == []

    def test_list_by_category_groups_plugins(self):
        """Should group plugins by category."""
        from smoothschedule.scheduling.schedule.plugins import PluginRegistry, BasePlugin

        class Plugin1(BasePlugin):
            name = "plugin1"
            display_name = "Plugin 1"
            category = "category_a"

            def execute(self, context):
                return {"success": True}

        class Plugin2(BasePlugin):
            name = "plugin2"
            display_name = "Plugin 2"
            category = "category_b"

            def execute(self, context):
                return {"success": True}

        class Plugin3(BasePlugin):
            name = "plugin3"
            display_name = "Plugin 3"
            category = "category_a"

            def execute(self, context):
                return {"success": True}

        registry = PluginRegistry()
        registry.register(Plugin1)
        registry.register(Plugin2)
        registry.register(Plugin3)

        result = registry.list_by_category()

        assert 'category_a' in result
        assert 'category_b' in result
        assert len(result['category_a']) == 2
        assert len(result['category_b']) == 1


class TestGlobalRegistry:
    """Tests for global plugin registry."""

    def test_registry_is_plugin_registry_instance(self):
        """Should have a global registry instance."""
        from smoothschedule.scheduling.schedule.plugins import registry, PluginRegistry

        assert isinstance(registry, PluginRegistry)


class TestRegisterPluginDecorator:
    """Tests for register_plugin decorator."""

    def test_decorator_registers_plugin(self):
        """Should register plugin when used as decorator."""
        from smoothschedule.scheduling.schedule.plugins import PluginRegistry, BasePlugin

        registry = PluginRegistry()

        # Create a decorator that uses this specific registry
        def register_plugin(plugin_class):
            registry.register(plugin_class)
            return plugin_class

        @register_plugin
        class DecoratedPlugin(BasePlugin):
            name = "decorated_plugin"
            display_name = "Decorated Plugin"

            def execute(self, context):
                return {"success": True}

        assert "decorated_plugin" in registry._plugins

    def test_decorator_returns_same_class(self):
        """Should return the same class after decoration."""
        from smoothschedule.scheduling.schedule.plugins import PluginRegistry, BasePlugin

        registry = PluginRegistry()

        def register_plugin(plugin_class):
            registry.register(plugin_class)
            return plugin_class

        @register_plugin
        class DecoratedPlugin(BasePlugin):
            name = "decorated_plugin2"
            display_name = "Decorated Plugin 2"

            def execute(self, context):
                return {"success": True}

        assert DecoratedPlugin.name == "decorated_plugin2"


class TestPluginExecution:
    """Tests for plugin execution functionality."""

    def test_execute_abstract_method(self):
        """Should have execute as abstract method."""
        from smoothschedule.scheduling.schedule.plugins import BasePlugin
        from abc import ABC

        # BasePlugin should be abstract
        assert issubclass(BasePlugin, ABC)

    def test_concrete_plugin_can_execute(self):
        """Should allow concrete plugin execution."""
        from smoothschedule.scheduling.schedule.plugins import BasePlugin

        class ConcretePlugin(BasePlugin):
            name = "concrete_plugin"
            display_name = "Concrete Plugin"

            def execute(self, context):
                return {
                    "success": True,
                    "data": context.get("input", "no input")
                }

        plugin = ConcretePlugin()
        result = plugin.execute({"input": "test data"})

        assert result["success"] is True
        assert result["data"] == "test data"

    def test_plugin_with_custom_can_execute(self):
        """Should allow overriding can_execute."""
        from smoothschedule.scheduling.schedule.plugins import BasePlugin

        class ConditionalPlugin(BasePlugin):
            name = "conditional_plugin"
            display_name = "Conditional Plugin"

            def can_execute(self, context):
                if not context.get("has_permission"):
                    return False, "Missing permission"
                return True, None

            def execute(self, context):
                return {"success": True}

        plugin = ConditionalPlugin()

        # Without permission
        can_exec, reason = plugin.can_execute({})
        assert can_exec is False
        assert reason == "Missing permission"

        # With permission
        can_exec, reason = plugin.can_execute({"has_permission": True})
        assert can_exec is True
        assert reason is None

    def test_plugin_with_custom_next_run_time(self):
        """Should allow overriding get_next_run_time."""
        from smoothschedule.scheduling.schedule.plugins import BasePlugin
        from django.utils import timezone
        from datetime import timedelta

        class ScheduledPlugin(BasePlugin):
            name = "scheduled_plugin"
            display_name = "Scheduled Plugin"

            def get_next_run_time(self, last_run):
                if last_run is None:
                    return timezone.now()
                return last_run + timedelta(hours=1)

            def execute(self, context):
                return {"success": True}

        plugin = ScheduledPlugin()

        # First run (no last_run)
        next_run = plugin.get_next_run_time(None)
        assert next_run is not None

        # Subsequent run
        now = timezone.now()
        next_run = plugin.get_next_run_time(now)
        assert next_run == now + timedelta(hours=1)
