"""Tests for Phase 80: Dynamic Tool Loading."""

import json
import types
import pytest
from pathlib import Path

import brynq.dynamic_tools as dt


@pytest.fixture(autouse=True)
def _isolate(tmp_path, monkeypatch):
    """Redirect storage to tmp_path and reset in-memory state."""
    monkeypatch.setattr(dt, "TOOLS_DIR", tmp_path)
    monkeypatch.setattr(dt, "REGISTRY_FILE", tmp_path / "registry.json")
    monkeypatch.setattr(dt, "STATS_FILE", tmp_path / "stats.jsonl")
    dt.reset()


# ── register_tool ──────────────────────────────────────────────────────────

class TestRegisterTool:
    def test_register_basic(self):
        t = dt.register_tool("echo", "Echo input", lambda args: args)
        assert t["name"] == "echo"
        assert t["description"] == "Echo input"
        assert t["category"] == "custom"
        assert t["active"] is True

    def test_register_with_category(self):
        t = dt.register_tool("greet", "Say hi", lambda a: "hi", category="util")
        assert t["category"] == "util"

    def test_register_with_parameters(self):
        params = {"message": {"type": "string"}}
        t = dt.register_tool("say", "Say something", lambda a: a, parameters=params)
        assert t["parameters"] == params

    def test_register_overwrites(self):
        dt.register_tool("x", "first", lambda a: 1)
        dt.register_tool("x", "second", lambda a: 2)
        t = dt.get_tool("x")
        assert t["description"] == "second"

    def test_register_invalid_name(self):
        with pytest.raises(ValueError):
            dt.register_tool("", "empty name", lambda a: a)

    def test_register_invalid_handler(self):
        with pytest.raises(ValueError):
            dt.register_tool("bad", "not callable", "not_a_function")

    def test_register_persists_to_file(self, tmp_path):
        dt.register_tool("persist", "test", lambda a: a)
        reg = json.loads((tmp_path / "registry.json").read_text("utf-8"))
        assert "persist" in reg["tools"]


# ── unregister_tool ────────────────────────────────────────────────────────

class TestUnregisterTool:
    def test_unregister_existing(self):
        dt.register_tool("bye", "remove me", lambda a: a)
        assert dt.unregister_tool("bye") is True
        assert dt.get_tool("bye") is None

    def test_unregister_missing(self):
        assert dt.unregister_tool("ghost") is False


# ── get_tool / list_tools ──────────────────────────────────────────────────

class TestGetAndList:
    def test_get_existing(self):
        dt.register_tool("alpha", "a", lambda a: a)
        t = dt.get_tool("alpha")
        assert t["name"] == "alpha"

    def test_get_missing(self):
        assert dt.get_tool("nope") is None

    def test_list_all(self):
        dt.register_tool("a", "a", lambda a: a, category="cat1")
        dt.register_tool("b", "b", lambda a: a, category="cat2")
        assert len(dt.list_tools()) == 2

    def test_list_by_category(self):
        dt.register_tool("a", "a", lambda a: a, category="cat1")
        dt.register_tool("b", "b", lambda a: a, category="cat2")
        dt.register_tool("c", "c", lambda a: a, category="cat1")
        assert len(dt.list_tools(category="cat1")) == 2
        assert len(dt.list_tools(category="cat2")) == 1

    def test_list_empty(self):
        assert dt.list_tools() == []


# ── execute_tool ───────────────────────────────────────────────────────────

class TestExecuteTool:
    def test_execute_success(self):
        dt.register_tool("add", "add", lambda a: a.get("x", 0) + a.get("y", 0))
        result = dt.execute_tool("add", {"x": 3, "y": 4})
        assert result["ok"] is True
        assert result["result"] == 7
        assert "elapsed_ms" in result

    def test_execute_no_args(self):
        dt.register_tool("ping", "ping", lambda a: "pong")
        result = dt.execute_tool("ping")
        assert result["ok"] is True
        assert result["result"] == "pong"

    def test_execute_error_isolated(self):
        def bad(a):
            raise RuntimeError("boom")
        dt.register_tool("bad", "fails", bad)
        result = dt.execute_tool("bad")
        assert result["ok"] is False
        assert "boom" in result["error"]

    def test_execute_missing_tool(self):
        result = dt.execute_tool("nonexistent")
        assert result["ok"] is False
        assert "not found" in result["error"]

    def test_execute_logs_to_stats(self, tmp_path):
        dt.register_tool("log_me", "l", lambda a: "ok")
        dt.execute_tool("log_me")
        stats_file = tmp_path / "stats.jsonl"
        assert stats_file.exists()
        lines = stats_file.read_text("utf-8").strip().splitlines()
        assert len(lines) >= 1
        entry = json.loads(lines[0])
        assert entry["tool"] == "log_me"
        assert entry["ok"] is True


# ── import_tools_from_module ───────────────────────────────────────────────

class TestImportTools:
    def test_import_decorated_tools(self, tmp_path, monkeypatch):
        # Create a temporary module with a decorated function
        mod_code = '''
def helper(args):
    """A helper tool."""
    return "helped"
helper._brynq_tool = {
    "name": "helper",
    "description": "A helper tool",
    "parameters": None,
    "category": "imported",
}
'''
        mod_file = tmp_path / "fake_tools.py"
        mod_file.write_text(mod_code)
        import sys
        monkeypatch.syspath_prepend(str(tmp_path))
        registered = dt.import_tools_from_module("fake_tools")
        assert "helper" in registered
        t = dt.get_tool("helper")
        assert t["category"] == "imported"

    def test_import_no_decorated_functions(self, tmp_path, monkeypatch):
        mod_code = "def plain(x): return x\n"
        (tmp_path / "no_tools.py").write_text(mod_code)
        import sys
        monkeypatch.syspath_prepend(str(tmp_path))
        registered = dt.import_tools_from_module("no_tools")
        assert registered == []


# ── export_tool_schema ─────────────────────────────────────────────────────

class TestExportSchema:
    def test_export_single(self):
        dt.register_tool("s1", "schema test", lambda a: a, parameters={"x": {"type": "int"}})
        schema = dt.export_tool_schema("s1")
        assert schema["name"] == "s1"
        assert "parameters" in schema

    def test_export_all(self):
        dt.register_tool("s1", "a", lambda a: a)
        dt.register_tool("s2", "b", lambda a: a)
        schemas = dt.export_tool_schema()
        assert "s1" in schemas
        assert "s2" in schemas

    def test_export_missing(self):
        result = dt.export_tool_schema("nope")
        assert "error" in result


# ── get_tool_stats ─────────────────────────────────────────────────────────

class TestToolStats:
    def test_stats_after_calls(self):
        dt.register_tool("counter", "c", lambda a: 1)
        dt.execute_tool("counter")
        dt.execute_tool("counter")
        stats = dt.get_tool_stats()
        assert stats["counter"]["calls"] == 2
        assert stats["counter"]["errors"] == 0

    def test_stats_with_errors(self):
        dt.register_tool("err", "e", lambda a: 1 / 0)
        dt.execute_tool("err")
        dt.execute_tool("err")
        stats = dt.get_tool_stats()
        assert stats["err"]["calls"] == 2
        assert stats["err"]["errors"] == 2

    def test_stats_empty(self):
        stats = dt.get_tool_stats()
        assert stats == {}


# ── tool decorator ─────────────────────────────────────────────────────────

class TestToolDecorator:
    def test_decorator_marks_function(self):
        @dt.tool(name="deco", description="decorated")
        def my_fn(args):
            return args
        assert hasattr(my_fn, "_brynq_tool")
        assert my_fn._brynq_tool["name"] == "deco"

    def test_decorator_uses_fn_name(self):
        @dt.tool()
        def auto_named(args):
            """Auto doc."""
            return args
        assert auto_named._brynq_tool["name"] == "auto_named"
        assert auto_named._brynq_tool["description"] == "Auto doc."
