"""
Unit tests for scheduling/automations/registry.py

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


class TestAutomationExecutionError:
    """Tests for AutomationExecutionError exception."""

    def test_is_exception_class(self):
        """Should be an Exception subclass."""
        from smoothschedule.scheduling.automations.registry import AutomationExecutionError

        assert issubclass(AutomationExecutionError, Exception)

    def test_can_be_raised_and_caught(self):
        """Should be raisable with a message."""
        from smoothschedule.scheduling.automations.registry import AutomationExecutionError

        with pytest.raises(AutomationExecutionError) as exc_info:
            raise AutomationExecutionError("Automation failed")

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


class TestBaseAutomation:
    """Tests for BaseAutomation abstract class."""

    def test_class_exists(self):
        """Should have BaseAutomation class."""
        from smoothschedule.scheduling.automations.registry import BaseAutomation

        assert BaseAutomation is not None

    def test_has_required_attributes(self):
        """Should define required class attributes."""
        from smoothschedule.scheduling.automations.registry import BaseAutomation

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

    def test_init_stores_config(self):
        """Should store config on initialization."""
        from smoothschedule.scheduling.automations.registry import BaseAutomation

        # Create concrete implementation for testing
        class TestAutomation(BaseAutomation):
            name = "test_automation"
            display_name = "Test Automation"

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

        automation = TestAutomation(config={"key": "value"})

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

    def test_init_defaults_to_empty_config(self):
        """Should default to empty config when None provided."""
        from smoothschedule.scheduling.automations.registry import BaseAutomation

        class TestAutomation(BaseAutomation):
            name = "test_automation"
            display_name = "Test Automation"

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

        automation = TestAutomation(config=None)

        assert automation.config == {}

    def test_validate_config_checks_required_keys(self):
        """Should raise ValueError for missing required config keys."""
        from smoothschedule.scheduling.automations.registry import BaseAutomation

        class TestAutomation(BaseAutomation):
            name = "test_automation"
            display_name = "Test Automation"
            config_schema = {
                "api_key": {"required": True},
            }

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

        with pytest.raises(ValueError) as exc_info:
            TestAutomation(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.automations.registry import BaseAutomation

        class TestAutomation(BaseAutomation):
            name = "test_automation"
            display_name = "Test Automation"
            config_schema = {
                "optional_key": {"required": False},
            }

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

        # Should not raise
        automation = TestAutomation(config={})
        assert automation.config == {}

    def test_validate_config_passes_with_required_keys_present(self):
        """Should not raise when required keys are provided."""
        from smoothschedule.scheduling.automations.registry import BaseAutomation

        class TestAutomation(BaseAutomation):
            name = "test_automation"
            display_name = "Test Automation"
            config_schema = {
                "api_key": {"required": True},
            }

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

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

    def test_can_execute_returns_true_by_default(self):
        """Should return (True, None) by default."""
        from smoothschedule.scheduling.automations.registry import BaseAutomation

        class TestAutomation(BaseAutomation):
            name = "test_automation"
            display_name = "Test Automation"

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

        automation = TestAutomation()
        can_exec, reason = automation.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.automations.registry import BaseAutomation

        class TestAutomation(BaseAutomation):
            name = "test_automation"
            display_name = "Test Automation"

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

        automation = TestAutomation()
        # Should not raise
        automation.on_success({"success": True})

    def test_on_failure_logs_error(self):
        """Should log error on failure."""
        from smoothschedule.scheduling.automations.registry import BaseAutomation

        class TestAutomation(BaseAutomation):
            name = "test_automation"
            display_name = "Test Automation"

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

        automation = TestAutomation()

        with patch('smoothschedule.scheduling.automations.registry.logger') as mock_logger:
            automation.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.automations.registry import BaseAutomation

        class TestAutomation(BaseAutomation):
            name = "test_automation"
            display_name = "Test Automation"

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

        automation = TestAutomation()
        result = automation.get_next_run_time(None)

        assert result is None

    def test_str_representation(self):
        """Should return display_name and name."""
        from smoothschedule.scheduling.automations.registry import BaseAutomation

        class TestAutomation(BaseAutomation):
            name = "test_automation"
            display_name = "Test Automation"

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

        automation = TestAutomation()
        result = str(automation)

        assert "Test Automation" in result
        assert "test_automation" in result


class TestAutomationRegistry:
    """Tests for AutomationRegistry class."""

    def test_init_creates_empty_automations_dict(self):
        """Should start with empty automations."""
        from smoothschedule.scheduling.automations.registry import AutomationRegistry

        registry = AutomationRegistry()
        assert len(registry._automations) == 0

    def test_register_adds_automation(self):
        """Should register an automation class."""
        from smoothschedule.scheduling.automations.registry import AutomationRegistry, BaseAutomation

        class TestAutomation(BaseAutomation):
            name = "test_automation"
            display_name = "Test Automation"

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

        registry = AutomationRegistry()
        registry.register(TestAutomation)

        assert "test_automation" in registry._automations
        assert registry._automations["test_automation"] is TestAutomation

    def test_register_raises_for_no_name(self):
        """Should raise ValueError if automation has no name."""
        from smoothschedule.scheduling.automations.registry import AutomationRegistry, BaseAutomation

        class NoNameAutomation(BaseAutomation):
            name = ""  # Empty name
            display_name = "No Name"

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

        registry = AutomationRegistry()

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

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

    def test_register_raises_for_duplicate_name(self):
        """Should raise ValueError for duplicate automation names."""
        from smoothschedule.scheduling.automations.registry import AutomationRegistry, BaseAutomation

        class Automation1(BaseAutomation):
            name = "duplicate_name"
            display_name = "Automation 1"

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

        class Automation2(BaseAutomation):
            name = "duplicate_name"  # Same name
            display_name = "Automation 2"

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

        registry = AutomationRegistry()
        registry.register(Automation1)

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

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

    def test_unregister_removes_automation(self):
        """Should unregister an automation by name."""
        from smoothschedule.scheduling.automations.registry import AutomationRegistry, BaseAutomation

        class TestAutomation(BaseAutomation):
            name = "test_automation"
            display_name = "Test Automation"

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

        registry = AutomationRegistry()
        registry.register(TestAutomation)
        assert "test_automation" in registry._automations

        registry.unregister("test_automation")
        assert "test_automation" not in registry._automations

    def test_unregister_does_nothing_for_unknown_automation(self):
        """Should not raise when unregistering unknown automation."""
        from smoothschedule.scheduling.automations.registry import AutomationRegistry

        registry = AutomationRegistry()

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

    def test_get_returns_automation_class(self):
        """Should return automation class by name."""
        from smoothschedule.scheduling.automations.registry import AutomationRegistry, BaseAutomation

        class TestAutomation(BaseAutomation):
            name = "test_automation"
            display_name = "Test Automation"

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

        registry = AutomationRegistry()
        registry.register(TestAutomation)

        result = registry.get("test_automation")

        assert result is TestAutomation

    def test_get_returns_none_for_unknown(self):
        """Should return None for unknown automation name."""
        from smoothschedule.scheduling.automations.registry import AutomationRegistry

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

        assert result is None

    def test_get_instance_returns_automation_instance(self):
        """Should return configured automation instance."""
        from smoothschedule.scheduling.automations.registry import AutomationRegistry, BaseAutomation

        class TestAutomation(BaseAutomation):
            name = "test_automation"
            display_name = "Test Automation"

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

        registry = AutomationRegistry()
        registry.register(TestAutomation)

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

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

    def test_get_instance_returns_none_for_unknown(self):
        """Should return None for unknown automation name."""
        from smoothschedule.scheduling.automations.registry import AutomationRegistry

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

        assert result is None

    def test_list_all_returns_automation_metadata(self):
        """Should return list of automation metadata."""
        from smoothschedule.scheduling.automations.registry import AutomationRegistry, BaseAutomation

        class TestAutomation(BaseAutomation):
            name = "test_automation"
            display_name = "Test Automation"
            description = "A test automation"
            category = "testing"

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

        registry = AutomationRegistry()
        registry.register(TestAutomation)

        result = registry.list_all()

        assert len(result) == 1
        assert result[0]['name'] == 'test_automation'
        assert result[0]['display_name'] == 'Test Automation'
        assert result[0]['description'] == 'A test automation'
        assert result[0]['category'] == 'testing'

    def test_list_all_returns_empty_list_when_no_automations(self):
        """Should return empty list when no automations registered."""
        from smoothschedule.scheduling.automations.registry import AutomationRegistry

        registry = AutomationRegistry()
        result = registry.list_all()

        assert result == []

    def test_list_by_category_groups_automations(self):
        """Should group automations by category."""
        from smoothschedule.scheduling.automations.registry import AutomationRegistry, BaseAutomation

        class Automation1(BaseAutomation):
            name = "automation1"
            display_name = "Automation 1"
            category = "category_a"

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

        class Automation2(BaseAutomation):
            name = "automation2"
            display_name = "Automation 2"
            category = "category_b"

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

        class Automation3(BaseAutomation):
            name = "automation3"
            display_name = "Automation 3"
            category = "category_a"

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

        registry = AutomationRegistry()
        registry.register(Automation1)
        registry.register(Automation2)
        registry.register(Automation3)

        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 automation registry."""

    def test_registry_is_automation_registry_instance(self):
        """Should have a global registry instance."""
        from smoothschedule.scheduling.automations.registry import registry, AutomationRegistry

        assert isinstance(registry, AutomationRegistry)


class TestRegisterAutomationDecorator:
    """Tests for register_automation decorator."""

    def test_decorator_registers_automation(self):
        """Should register automation when used as decorator."""
        from smoothschedule.scheduling.automations.registry import AutomationRegistry, BaseAutomation

        registry = AutomationRegistry()

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

        @register_automation
        class DecoratedAutomation(BaseAutomation):
            name = "decorated_automation"
            display_name = "Decorated Automation"

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

        assert "decorated_automation" in registry._automations

    def test_decorator_returns_same_class(self):
        """Should return the same class after decoration."""
        from smoothschedule.scheduling.automations.registry import AutomationRegistry, BaseAutomation

        registry = AutomationRegistry()

        def register_automation(automation_class):
            registry.register(automation_class)
            return automation_class

        @register_automation
        class DecoratedAutomation(BaseAutomation):
            name = "decorated_automation2"
            display_name = "Decorated Automation 2"

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

        assert DecoratedAutomation.name == "decorated_automation2"


class TestAutomationExecution:
    """Tests for automation execution functionality."""

    def test_execute_abstract_method(self):
        """Should have execute as abstract method."""
        from smoothschedule.scheduling.automations.registry import BaseAutomation
        from abc import ABC

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

    def test_concrete_automation_can_execute(self):
        """Should allow concrete automation execution."""
        from smoothschedule.scheduling.automations.registry import BaseAutomation

        class ConcreteAutomation(BaseAutomation):
            name = "concrete_automation"
            display_name = "Concrete Automation"

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

        automation = ConcreteAutomation()
        result = automation.execute({"input": "test data"})

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

    def test_automation_with_custom_can_execute(self):
        """Should allow overriding can_execute."""
        from smoothschedule.scheduling.automations.registry import BaseAutomation

        class ConditionalAutomation(BaseAutomation):
            name = "conditional_automation"
            display_name = "Conditional Automation"

            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}

        automation = ConditionalAutomation()

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

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

    def test_automation_with_custom_next_run_time(self):
        """Should allow overriding get_next_run_time."""
        from smoothschedule.scheduling.automations.registry import BaseAutomation
        from django.utils import timezone
        from datetime import timedelta

        class ScheduledAutomation(BaseAutomation):
            name = "scheduled_automation"
            display_name = "Scheduled Automation"

            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}

        automation = ScheduledAutomation()

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

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