"""Tests for the plugin system."""

from __future__ import annotations

import json
import sys
import tempfile
from pathlib import Path

from orxaq.kernel.plugins import (
    HookPoint,
    OrxaqPlugin,
    PluginLoader,
    PluginMeta,
    PluginRegistry,
    PluginType,
)

# ── Test fixtures ────────────────────────────────────────────


class DummyPlugin(OrxaqPlugin):
    """A test plugin."""

    meta = PluginMeta(
        name="test-plugin",
        version="1.0.0",
        plugin_type=PluginType.ALGORITHM,
        description="A test plugin",
        author="test",
        hooks=(HookPoint.BEFORE_DISCOVER, HookPoint.AFTER_DISCOVER),
    )

    def __init__(self):
        self.activated = False
        self.deactivated = False
        self.hook_calls: list[str] = []

    def activate(self, context: dict) -> None:
        self.activated = True

    def deactivate(self) -> None:
        self.deactivated = True

    def on_hook(self, hook: HookPoint, data: dict) -> dict:
        self.hook_calls.append(hook.value)
        data["plugin_touched"] = True
        return data


class AnotherPlugin(OrxaqPlugin):
    meta = PluginMeta(
        name="another-plugin",
        version="0.1.0",
        plugin_type=PluginType.CONNECTOR,
        hooks=(HookPoint.ON_DAG_CHANGE,),
    )


# ── PluginMeta ───────────────────────────────────────────────


class TestPluginMeta:
    def test_creation(self):
        m = PluginMeta(name="x", version="1.0", plugin_type=PluginType.SKIN)
        assert m.name == "x"
        assert m.plugin_type == PluginType.SKIN

    def test_to_dict(self):
        m = DummyPlugin.meta
        d = m.to_dict()
        assert d["name"] == "test-plugin"
        assert d["type"] == "algorithm"
        assert len(d["hooks"]) == 2


class TestPluginTypes:
    def test_all_types(self):
        assert len(PluginType) >= 6
        assert PluginType.ALGORITHM.value == "algorithm"

    def test_all_hooks(self):
        assert len(HookPoint) >= 7
        assert HookPoint.BEFORE_DISCOVER.value == "before_discover"


# ── PluginRegistry ───────────────────────────────────────────


class TestPluginRegistry:
    def test_register(self):
        reg = PluginRegistry()
        reg.register(DummyPlugin())
        assert "test-plugin" in reg
        assert len(reg) == 1

    def test_register_duplicate_raises(self):
        reg = PluginRegistry()
        reg.register(DummyPlugin())
        try:
            reg.register(DummyPlugin())
            assert False, "should raise"
        except ValueError:
            pass

    def test_get(self):
        reg = PluginRegistry()
        plugin = DummyPlugin()
        reg.register(plugin)
        assert reg.get("test-plugin") is plugin

    def test_get_missing(self):
        reg = PluginRegistry()
        assert reg.get("nope") is None

    def test_unregister(self):
        reg = PluginRegistry()
        plugin = DummyPlugin()
        reg.register(plugin)
        assert reg.unregister("test-plugin") is True
        assert "test-plugin" not in reg
        assert plugin.deactivated

    def test_unregister_missing(self):
        reg = PluginRegistry()
        assert reg.unregister("nope") is False

    def test_list_plugins(self):
        reg = PluginRegistry()
        reg.register(DummyPlugin())
        reg.register(AnotherPlugin())
        plugins = reg.list_plugins()
        assert len(plugins) == 2
        names = [p.name for p in plugins]
        assert "test-plugin" in names

    def test_list_by_type(self):
        reg = PluginRegistry()
        reg.register(DummyPlugin())
        reg.register(AnotherPlugin())
        algos = reg.list_by_type(PluginType.ALGORITHM)
        assert len(algos) == 1
        assert algos[0].name == "test-plugin"

    def test_activate(self):
        reg = PluginRegistry()
        plugin = DummyPlugin()
        reg.register(plugin)
        assert reg.activate("test-plugin", {"key": "value"})
        assert plugin.activated

    def test_activate_missing(self):
        reg = PluginRegistry()
        assert reg.activate("nope") is False

    def test_deactivate(self):
        reg = PluginRegistry()
        plugin = DummyPlugin()
        reg.register(plugin)
        assert reg.deactivate("test-plugin")
        assert plugin.deactivated

    def test_deactivate_missing(self):
        reg = PluginRegistry()
        assert reg.deactivate("nope") is False


class TestHooks:
    def test_fire_hook(self):
        reg = PluginRegistry()
        plugin = DummyPlugin()
        reg.register(plugin)

        result = reg.fire_hook(HookPoint.BEFORE_DISCOVER, {"variables": ["a", "b"]})
        assert result["plugin_touched"] is True
        assert "before_discover" in plugin.hook_calls

    def test_fire_hook_no_subscribers(self):
        reg = PluginRegistry()
        result = reg.fire_hook(HookPoint.ON_ENGINE_START, {"ready": True})
        assert result["ready"] is True

    def test_hook_chaining(self):
        reg = PluginRegistry()
        reg.register(DummyPlugin())
        result = reg.fire_hook(HookPoint.BEFORE_DISCOVER, {})
        assert result.get("plugin_touched") is True

    def test_inactive_plugin_skipped(self):
        reg = PluginRegistry()
        plugin = DummyPlugin()
        reg.register(plugin)
        reg.deactivate("test-plugin")

        result = reg.fire_hook(HookPoint.BEFORE_DISCOVER, {"data": 1})
        assert "plugin_touched" not in result

    def test_unregister_removes_hooks(self):
        reg = PluginRegistry()
        reg.register(DummyPlugin())
        reg.unregister("test-plugin")

        result = reg.fire_hook(HookPoint.BEFORE_DISCOVER, {"x": 1})
        assert "plugin_touched" not in result


class TestHealthCheck:
    def test_health_check(self):
        reg = PluginRegistry()
        reg.register(DummyPlugin())
        reg.register(AnotherPlugin())
        reg.deactivate("another-plugin")

        h = reg.health_check()
        assert h["total_plugins"] == 2
        assert h["active_plugins"] == 1
        assert len(h["plugins"]) == 2


# ── PluginLoader ─────────────────────────────────────────────


class TestPluginLoader:
    def test_load_file(self):
        reg = PluginRegistry()
        loader = PluginLoader(reg)

        # Create a temp plugin file
        code = """
from orxaq.kernel.plugins import OrxaqPlugin, PluginMeta, PluginType

class MyPlugin(OrxaqPlugin):
    meta = PluginMeta(name="file-plugin", version="1.0", plugin_type=PluginType.SKIN)

PLUGIN = MyPlugin()
"""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
            f.write(code)
            path = f.name

        loaded = loader.load_file(path)
        assert len(loaded) == 1
        assert "file-plugin" in reg

    def test_load_file_with_register(self):
        reg = PluginRegistry()
        loader = PluginLoader(reg)

        code = """
from orxaq.kernel.plugins import OrxaqPlugin, PluginMeta, PluginType

class RegPlugin(OrxaqPlugin):
    meta = PluginMeta(name="reg-plugin", version="1.0", plugin_type=PluginType.COMMAND)

def register(registry):
    registry.register(RegPlugin())
"""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
            f.write(code)
            path = f.name

        loaded = loader.load_file(path)
        assert len(loaded) == 1
        assert "reg-plugin" in reg

    def test_load_missing_file(self):
        reg = PluginRegistry()
        loader = PluginLoader(reg)
        loaded = loader.load_file("/nonexistent/plugin.py")
        assert loaded == []

    def test_load_directory(self):
        reg = PluginRegistry()
        loader = PluginLoader(reg)

        with tempfile.TemporaryDirectory() as tmpdir:
            code = """
from orxaq.kernel.plugins import OrxaqPlugin, PluginMeta, PluginType

class DirPlugin(OrxaqPlugin):
    meta = PluginMeta(name="dir-plugin", version="1.0", plugin_type=PluginType.VALIDATOR)

PLUGIN = DirPlugin()
"""
            (Path(tmpdir) / "my_plugin.py").write_text(code)
            # Underscore files should be skipped
            (Path(tmpdir) / "_private.py").write_text("# skip me")

            loaded = loader.load_directory(tmpdir)
            assert len(loaded) == 1
            assert "dir-plugin" in reg

    def test_load_missing_directory(self):
        reg = PluginRegistry()
        loader = PluginLoader(reg)
        loaded = loader.load_directory("/nonexistent/dir")
        assert loaded == []

    def test_load_from_config(self):
        """Test loading from JSON config — only testing the config parsing."""
        reg = PluginRegistry()
        loader = PluginLoader(reg)

        config = {"plugins": [{"module": "nonexistent.module", "enabled": False}]}
        with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
            json.dump(config, f)
            path = f.name

        # No plugins should load since all disabled
        loaded = loader.load_from_config(path)
        assert loaded == []

    def test_load_from_missing_config(self):
        reg = PluginRegistry()
        loader = PluginLoader(reg)
        loaded = loader.load_from_config("/nonexistent/config.json")
        assert loaded == []


# ── OrxaqPlugin base ─────────────────────────────────────────


class TestOrxaqPluginBase:
    def test_default_activate(self):
        p = OrxaqPlugin()
        p.activate({})  # Should not raise

    def test_default_deactivate(self):
        p = OrxaqPlugin()
        p.deactivate()  # Should not raise

    def test_default_on_hook(self):
        p = OrxaqPlugin()
        result = p.on_hook(HookPoint.ON_ENGINE_START, {"x": 1})
        assert result == {"x": 1}  # Pass-through


# ── Coverage: PluginLoader.load_module (lines 211-222) ────────


class TestPluginLoaderLoadModule:
    """Cover PluginLoader.load_module."""

    def test_load_module_with_register_function(self, tmp_path):
        """Module with register(registry) function."""
        code = (
            "from orxaq.kernel.plugins import "
            "OrxaqPlugin, PluginMeta, PluginType\n"
            "\n"
            "class ModPlugin(OrxaqPlugin):\n"
            "    meta = PluginMeta(\n"
            "        name='mod-reg-plugin', version='1.0',\n"
            "        plugin_type=PluginType.ALGORITHM,\n"
            "    )\n"
            "\n"
            "def register(registry):\n"
            "    registry.register(ModPlugin())\n"
        )
        mod_dir = tmp_path / "mypkg"
        mod_dir.mkdir()
        (mod_dir / "__init__.py").write_text("")
        (mod_dir / "plug.py").write_text(code)
        sys.path.insert(0, str(tmp_path))
        try:
            reg = PluginRegistry()
            loader = PluginLoader(reg)
            loaded = loader.load_module("mypkg.plug")
            assert loaded == ["mypkg.plug"]
            assert "mod-reg-plugin" in reg
        finally:
            sys.path.remove(str(tmp_path))
            sys.modules.pop("mypkg.plug", None)
            sys.modules.pop("mypkg", None)

    def test_load_module_with_plugin_attribute(self, tmp_path):
        """Module with PLUGIN attribute (no register function)."""
        code = (
            "from orxaq.kernel.plugins import "
            "OrxaqPlugin, PluginMeta, PluginType\n"
            "\n"
            "class AttrPlugin(OrxaqPlugin):\n"
            "    meta = PluginMeta(\n"
            "        name='attr-plugin', version='2.0',\n"
            "        plugin_type=PluginType.SKIN,\n"
            "    )\n"
            "\n"
            "PLUGIN = AttrPlugin()\n"
        )
        mod_dir = tmp_path / "attrpkg"
        mod_dir.mkdir()
        (mod_dir / "__init__.py").write_text("")
        (mod_dir / "aplug.py").write_text(code)
        sys.path.insert(0, str(tmp_path))
        try:
            reg = PluginRegistry()
            loader = PluginLoader(reg)
            loaded = loader.load_module("attrpkg.aplug")
            assert loaded == ["attr-plugin"]
            assert "attr-plugin" in reg
        finally:
            sys.path.remove(str(tmp_path))
            sys.modules.pop("attrpkg.aplug", None)
            sys.modules.pop("attrpkg", None)


# ── Coverage: load_file invalid spec (line 233) ──────────────


class TestPluginLoaderInvalidSpec:
    """Cover load_file when spec is None."""

    def test_load_file_invalid_spec(self, tmp_path):
        bad_file = tmp_path / "plugin.xyz"
        bad_file.write_text("PLUGIN = None")
        reg = PluginRegistry()
        loader = PluginLoader(reg)
        loaded = loader.load_file(str(bad_file))
        assert loaded == []


# ── Coverage: load_from_config enabled module (lines 278-280) ─


class TestPluginLoaderFromConfigEnabled:
    """Cover load_from_config with enabled modules."""

    def test_load_from_config_enabled_module(self, tmp_path):
        mod_dir = tmp_path / "cfgpkg"
        mod_dir.mkdir()
        (mod_dir / "__init__.py").write_text("")
        plug_code = (
            "from orxaq.kernel.plugins import "
            "OrxaqPlugin, PluginMeta, PluginType\n"
            "\n"
            "class CfgPlugin(OrxaqPlugin):\n"
            "    meta = PluginMeta(\n"
            "        name='cfg-plugin', version='1.0',\n"
            "        plugin_type=PluginType.COMMAND,\n"
            "    )\n"
            "\n"
            "def register(registry):\n"
            "    registry.register(CfgPlugin())\n"
        )
        (mod_dir / "mplug.py").write_text(plug_code)

        config = {"plugins": [{"module": "cfgpkg.mplug", "enabled": True}]}
        config_file = tmp_path / "plugins.json"
        config_file.write_text(json.dumps(config))

        sys.path.insert(0, str(tmp_path))
        try:
            reg = PluginRegistry()
            loader = PluginLoader(reg)
            loaded = loader.load_from_config(str(config_file))
            assert len(loaded) == 1
            assert "cfg-plugin" in reg
        finally:
            sys.path.remove(str(tmp_path))
            sys.modules.pop("cfgpkg.mplug", None)
            sys.modules.pop("cfgpkg", None)

    def test_load_from_config_empty_module_name(self, tmp_path):
        """Config entry with empty module name is skipped."""
        config = {"plugins": [{"module": "", "enabled": True}]}
        config_file = tmp_path / "plugins.json"
        config_file.write_text(json.dumps(config))

        reg = PluginRegistry()
        loader = PluginLoader(reg)
        loaded = loader.load_from_config(str(config_file))
        assert loaded == []
