# #region Test.PluginLoader [C:3] [TYPE Module] [SEMANTICS test, plugin, loader, dynamic, import]
# @BRIEF Tests for core/plugin_loader.py — PluginLoader.
# @RELATION BINDS_TO -> [PluginLoader]

from pathlib import Path
import sys

sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))

import tempfile
from unittest.mock import MagicMock, patch

import pytest


class TestPluginLoader:
    """PluginLoader: dynamic plugin discovery and registration."""

    @pytest.fixture
    def temp_plugin_dir(self):
        """Create a temporary plugin directory."""
        with tempfile.TemporaryDirectory() as tmpdir:
            yield tmpdir

    def test_init_creates_directory_if_not_exists(self, temp_plugin_dir):
        from src.core.plugin_loader import PluginLoader

        nonexistent = Path(temp_plugin_dir) / "nonexistent"
        loader = PluginLoader(str(nonexistent))
        assert nonexistent.exists()

    def test_loads_single_file_plugin(self, temp_plugin_dir):
        from src.core.plugin_loader import PluginLoader

        # Create a simple plugin file
        plugin_code = '''
from src.core.plugin_base import PluginBase
from typing import Any

class TestPlugin(PluginBase):
    @property
    def id(self) -> str:
        return "test-plugin"
    @property
    def name(self) -> str:
        return "Test Plugin"
    @property
    def description(self) -> str:
        return "A test plugin"
    @property
    def version(self) -> str:
        return "1.0.0"
    def get_schema(self) -> dict:
        return {"type": "object", "properties": {}}
    async def execute(self, params: dict[str, Any]):
        return {"status": "ok"}
'''
        plugin_file = Path(temp_plugin_dir) / "test_plugin.py"
        plugin_file.write_text(plugin_code)

        loader = PluginLoader(temp_plugin_dir)
        assert loader.has_plugin("test-plugin")
        assert loader.get_plugin("test-plugin") is not None

    def test_ignores_init_py(self, temp_plugin_dir):
        from src.core.plugin_loader import PluginLoader

        init_file = Path(temp_plugin_dir) / "__init__.py"
        init_file.write_text("# init")

        loader = PluginLoader(temp_plugin_dir)
        # Should not crash, no plugins loaded
        assert len(loader.get_all_plugin_configs()) == 0

    @patch("src.core.plugin_loader._logger")
    def test_loads_package_plugin(self, mock_logger, temp_plugin_dir):
        from src.core.plugin_loader import PluginLoader

        # Create a package
        pkg_dir = Path(temp_plugin_dir) / "my_package"
        pkg_dir.mkdir()
        init_file = pkg_dir / "__init__.py"
        init_file.write_text('''
from src.core.plugin_base import PluginBase
from typing import Any

class PackagePlugin(PluginBase):
    @property
    def id(self) -> str:
        return "package-plugin"
    @property
    def name(self) -> str:
        return "Package Plugin"
    @property
    def description(self) -> str:
        return "A package plugin"
    @property
    def version(self) -> str:
        return "2.0.0"
    def get_schema(self) -> dict:
        return {}
    async def execute(self, params: dict[str, Any]):
        return {"status": "ok"}
''')

        loader = PluginLoader(temp_plugin_dir)
        assert loader.has_plugin("package-plugin")

    def test_skips_non_py_files(self, temp_plugin_dir):
        from src.core.plugin_loader import PluginLoader

        txt_file = Path(temp_plugin_dir) / "readme.txt"
        txt_file.write_text("not a plugin")

        loader = PluginLoader(temp_plugin_dir)
        assert len(loader.get_all_plugin_configs()) == 0

    def test_duplicate_plugin_id_skipped(self, temp_plugin_dir):
        from src.core.plugin_loader import PluginLoader

        plugin_code = '''
from src.core.plugin_base import PluginBase
from typing import Any

class DupPlugin(PluginBase):
    @property
    def id(self) -> str:
        return "dup-plugin"
    @property
    def name(self) -> str:
        return "Dup Plugin"
    @property
    def description(self) -> str:
        return "Duplicate plugin"
    @property
    def version(self) -> str:
        return "1.0.0"
    def get_schema(self) -> dict:
        return {}
    async def execute(self, params: dict[str, Any]):
        return {"status": "ok"}
'''
        # Create two files with the same plugin ID
        p1 = Path(temp_plugin_dir) / "plugin_a.py"
        p1.write_text(plugin_code)
        p2 = Path(temp_plugin_dir) / "plugin_b.py"
        p2.write_text(plugin_code)

        loader = PluginLoader(temp_plugin_dir)
        # Only one should be registered (first loaded)
        assert loader.has_plugin("dup-plugin")

    def test_get_plugin_returns_none_for_missing(self, temp_plugin_dir):
        from src.core.plugin_loader import PluginLoader

        loader = PluginLoader(temp_plugin_dir)
        result = loader.get_plugin("nonexistent")
        assert result is None

    def test_get_all_plugin_configs(self, temp_plugin_dir):
        from src.core.plugin_loader import PluginLoader

        plugin_code = '''
from src.core.plugin_base import PluginBase
from typing import Any

class ConfigPlugin(PluginBase):
    @property
    def id(self) -> str:
        return "config-plugin"
    @property
    def name(self) -> str:
        return "Config Plugin"
    @property
    def description(self) -> str:
        return "A config plugin"
    @property
    def version(self) -> str:
        return "3.0.0"
    @property
    def ui_route(self) -> str:
        return "/config"
    def get_schema(self) -> dict:
        return {"type": "object", "properties": {"key": {"type": "string"}}}
    async def execute(self, params: dict[str, Any]):
        return {"status": "ok"}
'''
        pf = Path(temp_plugin_dir) / "config_plugin.py"
        pf.write_text(plugin_code)

        loader = PluginLoader(temp_plugin_dir)
        configs = loader.get_all_plugin_configs()
        assert len(configs) == 1
        assert configs[0].id == "config-plugin"
        assert configs[0].name == "Config Plugin"
        assert configs[0].ui_route == "/config"
        assert configs[0].input_schema == {"type": "object", "properties": {"key": {"type": "string"}}}

    def test_module_load_failure_logged(self, temp_plugin_dir):
        from src.core.plugin_loader import PluginLoader

        # Create a file with syntax error
        bad_file = Path(temp_plugin_dir) / "bad_plugin.py"
        bad_file.write_text("this is not valid python {{{")

        with patch("src.core.plugin_loader._logger") as mock_log:
            loader = PluginLoader(temp_plugin_dir)
            mock_log.error.assert_called_once()

    def test_instantiation_failure_logged(self, temp_plugin_dir):
        from src.core.plugin_loader import PluginLoader

        plugin_code = '''
from src.core.plugin_base import PluginBase
from typing import Any

class BrokenPlugin(PluginBase):
    @property
    def id(self) -> str:
        return "broken"
    @property
    def name(self) -> str:
        return "Broken"
    @property
    def description(self) -> str:
        return "A broken plugin"
    @property
    def version(self) -> str:
        return "1.0.0"
    def get_schema(self) -> dict:
        return {}
    async def execute(self, params: dict[str, Any]):
        return {"status": "ok"}

class NonPlugin:
    pass
'''
        pf = Path(temp_plugin_dir) / "good_plugin.py"
        pf.write_text(plugin_code)

        with patch("src.core.plugin_loader._logger") as mock_log:
            loader = PluginLoader(temp_plugin_dir)
            # Should have loaded successfully
            assert loader.has_plugin("broken")
            # NonPlugin should not trigger error (it's not a PluginBase subclass)

    def test_schema_not_dict_raises_error(self, temp_plugin_dir):
        from src.core.plugin_loader import PluginLoader

        plugin_code = '''
from src.core.plugin_base import PluginBase
from typing import Any

class BadSchemaPlugin(PluginBase):
    @property
    def id(self) -> str:
        return "bad-schema"
    @property
    def name(self) -> str:
        return "Bad Schema"
    @property
    def description(self) -> str:
        return "Bad schema plugin"
    @property
    def version(self) -> str:
        return "1.0.0"
    def get_schema(self) -> dict:
        return "not_a_dict"
    async def execute(self, params: dict[str, Any]):
        return {"status": "ok"}
'''
        pf = Path(temp_plugin_dir) / "bad_schema.py"
        pf.write_text(plugin_code)

        with patch("src.core.plugin_loader._logger") as mock_log:
            loader = PluginLoader(temp_plugin_dir)
            # Schema validation fails -> logged as error, plugin not registered
            assert not loader.has_plugin("bad-schema")
            mock_log.error.assert_called_once()
# #endregion Test.PluginLoader
