from enum import Enum
import pytest
from djpress.plugins import PluginRegistry, DJPressPlugin, Hooks
from djpress.plugins import registry
from djpress.exceptions import PluginLoadError

from djpress.conf import settings as djpress_settings


def test_registry_initialization():
    """Test that registry initializes with empty plugins and hooks."""
    registry = PluginRegistry()
    assert registry.plugins == []
    assert registry.hooks == {}
    assert registry._loaded is False


def test_register_hook():
    """Test registering a hook callback."""
    registry = PluginRegistry()

    def test_callback(content: str) -> str:
        return content

    registry.register_hook(Hooks.PRE_RENDER_CONTENT, test_callback)
    assert test_callback in registry.hooks[Hooks.PRE_RENDER_CONTENT]


def test_hook_enum_values():
    """Test that Hook enum has the expected values."""
    assert Hooks.PRE_RENDER_CONTENT.value == "pre_render_content"
    assert Hooks.POST_RENDER_CONTENT.value == "post_render_content"
    assert Hooks.POST_SAVE_POST.value == "post_save_post"
    assert Hooks.DJ_HEADER.value == "dj_header"
    assert Hooks.DJ_FOOTER.value == "dj_footer"


# Tests for plugin registration (flexible)
def test_register_hook_with_enum(clean_registry):
    """Test registering hook using Enum."""

    def test_callback(content: str) -> str:
        return content

    registry.register_hook(Hooks.PRE_RENDER_CONTENT, test_callback)
    assert Hooks.PRE_RENDER_CONTENT in registry.hooks
    assert test_callback in registry.hooks[Hooks.PRE_RENDER_CONTENT]


def test_register_hook_with_valid_string(clean_registry):
    """Test registering hook using valid string name."""

    def test_callback(content: str) -> str:
        return content

    registry.register_hook("pre_render_content", test_callback)
    assert Hooks.PRE_RENDER_CONTENT in registry.hooks
    assert test_callback in registry.hooks[Hooks.PRE_RENDER_CONTENT]


def test_register_unknown_str_hook_name(clean_registry, caplog):
    """Test registering unknown hook name - should accept but log a warning."""

    caplog.set_level("WARNING")

    def test_callback(content: str) -> str:
        return content

    registry._loaded = True  # Simulate that plugins are loaded
    registry.plugins = ["test_plugin"]  # Simulate a loaded plugin

    registry.register_hook("unknown_hook", test_callback)
    # Check the output from logger.warning
    assert "Invalid hook str name: unknown_hook. Hook not registered by callback." in caplog.text


def test_register_unknown_enum_hook_name(clean_registry, caplog):
    caplog.set_level("WARNING")

    def test_callback(content: str) -> str:
        return content

    registry._loaded = True  # Simulate that plugins are loaded
    registry.plugins = ["test_plugin"]  # Simulate a loaded plugin

    class Hooks(Enum):
        UNKNOWN_HOOK = "unknown_hook"

    registry.register_hook(Hooks.UNKNOWN_HOOK, test_callback)  # type: ignore
    assert "Invalid hook enum: Hooks.UNKNOWN_HOOK. Hook not registered by callback." in caplog.text


# Tests for hook execution (strict)
def test_run_hook_with_enum(clean_registry):
    """Test running hook with Enum succeeds."""

    def test_callback(content: str) -> str:
        return content + " modified"

    registry._loaded = True  # Simulate that plugins are loaded
    registry.plugins = ["test_plugin"]  # Simulate a loaded plugin
    registry.register_hook(Hooks.PRE_RENDER_CONTENT, test_callback)
    result = registry.run_hook(Hooks.PRE_RENDER_CONTENT, "test value")
    assert result == "test value modified"


def test_run_hook_not_registered(clean_registry):
    """Test running hook that hasn't been registered.

    Nothing happens the value just gets returned.
    """

    def test_callback(content: str) -> str:
        return content + " modified"

    registry._loaded = True  # Simulate that plugins are loaded
    registry.plugins = ["test_plugin"]  # Simulate a loaded plugin
    result = registry.run_hook(Hooks.PRE_RENDER_CONTENT, "test value")
    assert result == "test value"


# Tests for hook execution (strict)
def test_run_hook_with_multiple_hooks(clean_registry):
    """Test running hook with multiple hooks."""

    def test_callback(content: str) -> str:
        return content + " modified by test_callback"

    def second_test_callback(content: str) -> str:
        return content + " modified by second test_callback"

    registry._loaded = True  # Simulate that plugins are loaded
    registry.plugins = ["test_plugin"]  # Simulate a loaded plugin
    registry.register_hook(Hooks.PRE_RENDER_CONTENT, test_callback)
    registry.register_hook(Hooks.PRE_RENDER_CONTENT, second_test_callback)
    result = registry.run_hook(Hooks.PRE_RENDER_CONTENT, "test value")
    assert result == "test value modified by test_callback modified by second test_callback"


def test_run_hook_with_exception(clean_registry, caplog):
    """Test running hook with an exception doesn't modify the value."""
    caplog.set_level("WARNING")

    def test_callback(content: str) -> str:
        raise ValueError("Test error")

    registry._loaded = True  # Simulate that plugins are loaded
    registry.plugins = ["test_plugin"]  # Simulate a loaded plugin
    registry.register_hook(Hooks.PRE_RENDER_CONTENT, test_callback)
    result = registry.run_hook(Hooks.PRE_RENDER_CONTENT, "test value")
    assert result == "test value"

    assert "Error running callback" in caplog.text


def test_run_hook_with_string_fails(clean_registry, caplog):
    caplog.set_level("WARNING")
    """Test running hook with string fails."""
    registry.run_hook("pre_render_content", "test")  # type: ignore
    assert "hook_name must be a Hooks enum member" in caplog.text


def test_run_hook_type_error_message(clean_registry, caplog):
    caplog.set_level("WARNING")
    """Test specific error message when running hook with wrong type."""
    registry.run_hook("pre_render_content", "test")  # type: ignore
    assert "hook_name must be a Hooks enum member" in caplog.text
    assert "got <class 'str'>" in caplog.text


# For line 90 (value is None in hooks dict)
def test_run_hook_nonexistent_hook(clean_registry):
    """Test running hook that hasn't been registered."""
    result = registry.run_hook(Hooks.PRE_RENDER_CONTENT, "test")
    assert result == "test"  # Should return original value unchanged


# For lines 96-98 (plugin loading already done)
def test_load_plugins_already_loaded(clean_registry):
    """Test that load_plugins doesn't reload if already loaded."""
    registry._loaded = True
    registry.load_plugins()
    assert len(registry.plugins) == 0  # Should not have loaded any plugins


def test_load_plugins_not_loaded(clean_registry):
    """Test that load_plugins doesn't reload if already loaded."""
    registry._loaded = False
    registry.load_plugins()
    assert len(registry.plugins) == 0  # Should not have loaded any plugins


# For lines 115-131 (_import_plugin_class)
def test_import_plugin_standard_location(clean_registry, tmp_path):
    """Test importing plugin from standard location."""
    # Create a temporary plugin package
    plugin_dir = tmp_path / "test_plugin"
    plugin_dir.mkdir()
    (plugin_dir / "__init__.py").write_text("")
    (plugin_dir / "plugin.py").write_text("""
from djpress.plugins import DJPressPlugin
class Plugin(DJPressPlugin):
    name = "test_plugin"
    """)

    # Add to Python path and try to import
    import sys

    sys.path.insert(0, str(tmp_path))

    try:
        plugin_class = registry._import_plugin_class("test_plugin")
        assert plugin_class.__name__ == "Plugin"
    finally:
        sys.path.pop(0)


def test_import_plugin_custom_location(clean_registry, tmp_path):
    """Test importing plugin from custom location."""
    # Similar setup but with custom path
    # Test both successful and failing imports


# For lines 147-165 and 175-178 (_instantiate_plugin)
def test_instantiate_plugin_with_config(clean_registry):
    """Test instantiating plugin with configuration."""

    class TestPlugin(DJPressPlugin):
        name = "test_plugin"

    plugin_settings = {"test_plugin": {"setting": "value"}}
    plugin = registry._instantiate_plugin(TestPlugin, "test_plugin", plugin_settings)
    assert isinstance(plugin, DJPressPlugin)
    assert plugin.config == {"setting": "value"}


def test_instantiate_plugin_fails(clean_registry):
    """Test handling of plugin instantiation failure."""

    class BrokenPlugin(DJPressPlugin):
        name = "broken_plugin"

        def __init__(self, config=None):
            raise RuntimeError("Plugin broken")

    from django.core.exceptions import ImproperlyConfigured

    with pytest.raises(ImproperlyConfigured) as exc_info:
        registry._instantiate_plugin(BrokenPlugin, "broken_plugin", {})
    assert "Error initializing plugin" in str(exc_info.value)


# For lines 96-98 (early return if already loaded)
def test_load_plugins_early_return(clean_registry):
    """Test that load_plugins returns early if already loaded."""
    registry._loaded = True
    original_plugins = registry.plugins.copy()
    registry.load_plugins()
    assert registry.plugins == original_plugins


# For lines 123-131 (import error handling)
def test_import_plugin_both_paths_fail(clean_registry):
    """Test when both import attempts fail."""
    from django.core.exceptions import ImproperlyConfigured

    with pytest.raises(ImproperlyConfigured) as exc_info:
        registry._import_plugin_class("nonexistent_plugin")

    assert "Could not load plugin 'nonexistent_plugin'" in str(exc_info.value)
    assert "Tried both custom path and standard plugin.py location" in str(exc_info.value)


# For lines 176-177 (plugin instantiation error)
def test_instantiate_plugin_setup_fails(clean_registry):
    """Test handling of plugin setup failure."""

    class PluginWithBadSetup(DJPressPlugin):
        name = "bad_setup"

        def setup(self, registry):
            raise RuntimeError("Setup failed")

    from django.core.exceptions import ImproperlyConfigured

    with pytest.raises(ImproperlyConfigured) as exc_info:
        registry._instantiate_plugin(PluginWithBadSetup, "bad_setup", {})

    assert "Error initializing plugin 'bad_setup'" in str(exc_info.value)


def test_register_hook_first_callback(clean_registry):
    """Test registering first callback for a hook."""

    def callback(content: str) -> str:
        return content

    # This should create new list and add callback
    registry.register_hook(Hooks.PRE_RENDER_CONTENT, callback)
    assert len(registry.hooks[Hooks.PRE_RENDER_CONTENT]) == 1
    assert registry.hooks[Hooks.PRE_RENDER_CONTENT][0] == callback


def test_load_plugins_successful(clean_registry, monkeypatch):
    """Test successful plugin loading process."""

    class TestPlugin(DJPressPlugin):
        name = "test"

        def setup(self, registry):
            pass

    # Mock the import and instantiate methods to return known values
    def mock_import(self, path):
        return TestPlugin

    def mock_instantiate(self, plugin_class, path, settings):
        return TestPlugin()

    monkeypatch.setattr(PluginRegistry, "_import_plugin_class", mock_import)
    monkeypatch.setattr(PluginRegistry, "_instantiate_plugin", mock_instantiate)

    # Set up test settings
    monkeypatch.setattr(djpress_settings, "PLUGINS", ["test_plugin"])
    monkeypatch.setattr(djpress_settings, "PLUGIN_SETTINGS", {})

    # Load plugins
    registry.load_plugins()

    # Verify plugin was added
    assert len(registry.plugins) == 1
    assert isinstance(registry.plugins[0], TestPlugin)


def test_load_plugins_exception(clean_registry, monkeypatch, caplog):
    """Test plugin loading with exception."""
    caplog.set_level("WARNING")

    class TestPlugin(DJPressPlugin):
        name = "test"

        def setup(self, registry):
            pass

    # Mock the import and instantiate methods to return known values
    def mock_import(self, path):
        return TestPlugin

    def mock_instantiate(self, plugin_class, path, settings):
        raise RuntimeError("Plugin setup failed")

    monkeypatch.setattr(PluginRegistry, "_import_plugin_class", mock_import)
    monkeypatch.setattr(PluginRegistry, "_instantiate_plugin", mock_instantiate)

    # Set up test settings
    monkeypatch.setattr(djpress_settings, "PLUGINS", ["test_plugin"])
    monkeypatch.setattr(djpress_settings, "PLUGIN_SETTINGS", {})

    # Load plugins - will log a warning
    registry.load_plugins()
    assert "Failed to load plugins:" in caplog.text


def test_plugin_missing_name():
    """Test plugin initialization with missing name."""

    class NoNamePlugin(DJPressPlugin):
        pass  # No name defined

    with pytest.raises(ValueError) as exc_info:
        NoNamePlugin()
    assert str(exc_info.value) == "Plugin must define a name"


def test_register_multiple_callbacks(clean_registry):
    """Test registering multiple callbacks for the same hook."""

    def callback1(content: str) -> str:
        return content + "1"

    def callback2(content: str) -> str:
        return content + "2"

    registry._loaded = True  # Simulate that plugins are loaded
    registry.plugins = ["test_plugin"]  # Simulate a loaded plugin

    # Register first callback - should create new list
    registry.register_hook(Hooks.PRE_RENDER_CONTENT, callback1)
    assert len(registry.hooks[Hooks.PRE_RENDER_CONTENT]) == 1
    assert registry.hooks[Hooks.PRE_RENDER_CONTENT][0] == callback1

    # Register second callback - should append to existing list
    registry.register_hook(Hooks.PRE_RENDER_CONTENT, callback2)
    assert len(registry.hooks[Hooks.PRE_RENDER_CONTENT]) == 2
    assert registry.hooks[Hooks.PRE_RENDER_CONTENT][0] == callback1
    assert registry.hooks[Hooks.PRE_RENDER_CONTENT][1] == callback2

    # Verify both callbacks are executed in order
    result = registry.run_hook(Hooks.PRE_RENDER_CONTENT, "test")
    assert result == "test12"


def test_register_multiple_callbacks_with_error(clean_registry, caplog):
    """Test registering multiple callbacks for the same hook.

    The first and third callbacks should execute normally by appending text to the value.
    The second callback should raise an error, which should be logged but not stop the execution of the other
    callbacks.
    """
    caplog.set_level("WARNING")

    def callback1(content: str) -> str:
        return content + "1"

    def callback2(content: str) -> str:
        raise ValueError("Test error in callback2")

    def callback3(content: str) -> str:
        return content + "3"

    registry._loaded = True  # Simulate that plugins are loaded
    registry.plugins = ["test_plugin"]  # Simulate a loaded plugin

    # Register first callback - should create new list
    registry.register_hook(Hooks.PRE_RENDER_CONTENT, callback1)
    assert len(registry.hooks[Hooks.PRE_RENDER_CONTENT]) == 1
    assert registry.hooks[Hooks.PRE_RENDER_CONTENT][0] == callback1

    # Register second callback - should append to existing list
    registry.register_hook(Hooks.PRE_RENDER_CONTENT, callback2)
    assert len(registry.hooks[Hooks.PRE_RENDER_CONTENT]) == 2
    assert registry.hooks[Hooks.PRE_RENDER_CONTENT][0] == callback1
    assert registry.hooks[Hooks.PRE_RENDER_CONTENT][1] == callback2

    # Register third callback - should append to existing list
    registry.register_hook(Hooks.PRE_RENDER_CONTENT, callback3)
    assert len(registry.hooks[Hooks.PRE_RENDER_CONTENT]) == 3
    assert registry.hooks[Hooks.PRE_RENDER_CONTENT][0] == callback1
    assert registry.hooks[Hooks.PRE_RENDER_CONTENT][1] == callback2
    assert registry.hooks[Hooks.PRE_RENDER_CONTENT][2] == callback3

    # Verify all callbacks are executed in order with the error logged and ignored
    result = registry.run_hook(Hooks.PRE_RENDER_CONTENT, "test")
    assert result == "test13"

    assert "Error running callback" in caplog.text


def test_content_modification_hook(clean_registry):
    """Test hooks that modify and return content."""

    def test_callback(content: str) -> str:
        return content + " modified"

    registry._loaded = True  # Simulate that plugins are loaded
    registry.plugins = ["test_plugin"]  # Simulate a loaded plugin

    registry.register_hook(Hooks.PRE_RENDER_CONTENT, test_callback)
    result = registry.run_hook(Hooks.PRE_RENDER_CONTENT, "test")
    assert result == "test modified"


@pytest.mark.django_db
def test_plugin_storage_interface():
    """Test plugin storage interface methods."""

    class TestPlugin(DJPressPlugin):
        name = "test_plugin"

    plugin = TestPlugin()

    # Test get_data with no storage
    assert plugin.get_data() == {}

    # Test save_data and get_data
    plugin.save_data({"key": "value"})
    assert plugin.get_data() == {"key": "value"}

    # Test save_data with update
    plugin.save_data({"new_key": "new_value"})
    assert plugin.get_data() == {"new_key": "new_value"}

    # Test save additional data
    data = plugin.get_data()
    data["extra"] = "data"
    plugin.save_data(data)
    assert plugin.get_data() == {"new_key": "new_value", "extra": "data"}
