from unittest.mock import MagicMock, patch

import pytest

from nanobot.agent.tools.base import Tool
from nanobot.agent.tools.context import ToolContext
from nanobot.agent.tools.loader import ToolLoader
from nanobot.agent.tools.registry import ToolRegistry, is_tool_error_result


def test_loader_discovers_entry_point_tools():
    """Simulate an entry-point plugin being discovered."""
    mock_ep = MagicMock()
    mock_ep.name = "my_plugin"

    class _FakeTool(Tool):
        __name__ = "FakeTool"
        _plugin_discoverable = True
        _scopes = {"core"}

        @property
        def name(self) -> str:
            return "fake_tool"

        @property
        def description(self) -> str:
            return "A fake tool for testing."

        @property
        def parameters(self) -> dict:
            return {"type": "object"}

        @classmethod
        def enabled(cls, ctx):
            return True

        @classmethod
        def create(cls, ctx):
            return MagicMock()

        async def execute(self, **_):
            return "ok"

    mock_ep.load.return_value = _FakeTool

    with patch("nanobot.agent.tools.loader.entry_points", return_value=[mock_ep]):
        loader = ToolLoader()
        discovered = loader._discover_plugins()

    assert "my_plugin" in discovered
    assert discovered["my_plugin"] is _FakeTool


def test_loader_skips_abstract_entry_point_tools():
    """Verify abstract tool classes registered via entry_points are skipped."""
    mock_ep = MagicMock()
    mock_ep.name = "abstract_plugin"

    class _AbstractTool(Tool):
        __name__ = "AbstractTool"
        _plugin_discoverable = True
        _scopes = {"core"}

        @classmethod
        def enabled(cls, ctx):
            return True

        @classmethod
        def create(cls, ctx):
            return MagicMock()

        # Intentionally missing abstract properties (name, description, parameters, execute)

    mock_ep.load.return_value = _AbstractTool

    with patch("nanobot.agent.tools.loader.entry_points", return_value=[mock_ep]):
        loader = ToolLoader()
        discovered = loader._discover_plugins()

    assert "abstract_plugin" not in discovered


@pytest.mark.asyncio
async def test_loader_entry_point_error_wrapper_preserves_tool_api(tmp_path):
    """Only adapt legacy plugin error strings; keep the wrapped tool API intact."""
    mock_ep = MagicMock()
    mock_ep.name = "api_plugin"

    class _ApiPluginTool(Tool):
        config_key = "api_plugin"

        @property
        def name(self) -> str:
            return "api_plugin"

        @property
        def description(self) -> str:
            return "Entry-point plugin with custom tool API methods."

        @property
        def parameters(self) -> dict:
            return {"type": "object", "properties": {"value": {"type": "string"}}}

        @property
        def read_only(self) -> bool:
            return True

        @property
        def concurrency_safe(self) -> bool:
            return False

        def cast_params(self, params: dict) -> dict:
            return {"value": str(params["value"])}

        def validate_params(self, params: dict) -> list[str]:
            return [] if params == {"value": "1"} else ["bad value"]

        def to_schema(self) -> dict:
            return {"name": self.name, "custom": True}

        async def execute(self, **_):
            return "Error: plugin failed"

    mock_ep.load.return_value = _ApiPluginTool

    registry = ToolRegistry()
    with patch("nanobot.agent.tools.loader.entry_points", return_value=[mock_ep]):
        ToolLoader(test_classes=[]).load(
            ToolContext(config=None, workspace=str(tmp_path)),
            registry,
        )

    tool = registry.get("api_plugin")
    assert tool is not None
    assert tool.config_key == "api_plugin"
    assert tool.read_only is True
    assert tool.concurrency_safe is False
    assert tool.cast_params({"value": 1}) == {"value": "1"}
    assert tool.validate_params({"value": "1"}) == []
    assert tool.to_schema() == {"name": "api_plugin", "custom": True}

    result = await tool.execute(value="1")
    assert is_tool_error_result(result) is True
    assert str(result) == "Error: plugin failed"
