"""reload_all() — the single implementation behind reload_tools and the admin button.

The MCP half is the delicate one: register_mcp_tools only adds registrations, so
a reload that reconnects without dropping the old ones leaves ghost tools — for
every server taken out of the config, and for every tool a server stopped
exposing. They stay visible to the model and fail only when called.
"""

import textwrap

import pytest

from navi.context_providers._loader import ContextProviderRegistry
from navi.core.registry import ToolRegistry
from navi.core.reload import ReloadReport, reload_all


def _write(path, name, version="v1", broken=False):
    if broken:
        path.write_text("not python (")
        return
    path.write_text(
        textwrap.dedent(
            f'''
            name = "{name}"
            description = "{version}"
            parameters = {{"type": "object", "properties": {{}}}}

            async def execute(params):
                return "{version}"
            '''
        ).lstrip()
    )


class FakeClient:
    def __init__(self, server, tools):
        self.server = server
        self.connected = True
        self._tools = tools

    async def list_tools(self):
        return [
            type("T", (), {"name": n, "description": "d", "inputSchema": {}})
            for n in self._tools
        ]


class FakeMcpManager:
    """Reconnects to whatever servers the test put in ``servers``."""

    def __init__(self, servers: dict[str, list[str]]):
        self.servers = servers
        self.reloads = 0
        self.fail = False

    @property
    def clients(self):
        return {s: FakeClient(s, t) for s, t in self.servers.items()}

    async def reload_all(self):
        self.reloads += 1
        if self.fail:
            raise RuntimeError("mcp is down")


@pytest.fixture
def isolated_settings(tmp_path, monkeypatch):
    """Point tools_dir at a scratch directory and control enabled.json."""
    import navi.core.reload as reload_mod

    # Settings is frozen — replace the object the module sees, not the field.
    monkeypatch.setattr(
        reload_mod, "settings", reload_mod.settings.model_copy(update={"tools_dir": str(tmp_path)})
    )
    # enabled.json is read through tool_utils by both reload and the agent.
    monkeypatch.setattr("navi.core.tool_utils.load_user_enabled_tools", list)
    return tmp_path


class FakeBuiltin:
    name = "reload_tools"
    description = "d"
    parameters = {}


async def test_reports_what_was_loaded(isolated_settings):
    _write(isolated_settings / "alpha.py", "alpha")
    _write(isolated_settings / "beta.py", "beta")
    registry = ToolRegistry()
    registry.register(FakeBuiltin(), builtin=True)

    report = await reload_all(registry, None, None)

    assert report.ok is True
    assert sorted(report.tools) == ["alpha", "beta"]
    assert report.total_tools == 3
    assert report.mcp_error is None
    assert "Tools (2): alpha, beta." in report.lines()


async def test_enabled_json_naming_a_tool_that_does_not_exist_is_reported(isolated_settings, monkeypatch):
    """The gmail case: the name stayed in enabled.json after its file stopped
    loading, and nothing said so — the tool just vanished from every profile."""
    _write(isolated_settings / "alpha.py", "alpha")
    monkeypatch.setattr("navi.core.tool_utils.load_user_enabled_tools", lambda: ["ghost"])
    registry = ToolRegistry()

    report = await reload_all(registry, None, None)

    assert report.missing_enabled == ["ghost"]
    assert report.ok is True  # the reload itself worked; the config is wrong
    assert any("ghost" in line for line in report.lines())


async def test_one_broken_file_does_not_hide_the_rest(isolated_settings):
    _write(isolated_settings / "alpha.py", "alpha")
    _write(isolated_settings / "broken.py", "broken", broken=True)
    registry = ToolRegistry()

    report = await reload_all(registry, None, None)

    assert report.ok is False
    assert report.tools == ["alpha"]
    assert list(report.tool_errors) == ["broken.py"]
    assert registry.get("alpha") is not None


async def test_context_providers_are_reloaded(isolated_settings, tmp_path, monkeypatch):
    import navi.core.reload as reload_mod

    providers = tmp_path / "providers"
    providers.mkdir()
    (providers / "clock.py").write_text(
        textwrap.dedent(
            '''
            name = "clock"
            description = "the time"
            global_provider = False

            async def get_context():
                return "12:00"
            '''
        ).lstrip()
    )
    monkeypatch.setattr(
        reload_mod,
        "settings",
        reload_mod.settings.model_copy(update={"context_providers_dir": str(providers)}),
    )
    cp_registry = ContextProviderRegistry()

    report = await reload_all(ToolRegistry(), cp_registry, None)

    assert report.providers == ["clock"]
    assert cp_registry.get("clock") is not None


async def test_mcp_tools_of_a_removed_server_do_not_survive(isolated_settings):
    registry = ToolRegistry()
    manager = FakeMcpManager({"alpha": ["ping", "pong"]})

    first = await reload_all(registry, None, manager)
    assert first.mcp_tools == ["mcp__alpha__ping", "mcp__alpha__pong"]

    # The server now exposes only one tool...
    manager.servers = {"alpha": ["ping"]}
    second = await reload_all(registry, None, manager)
    assert second.mcp_tools == ["mcp__alpha__ping"]
    assert "mcp__alpha__pong" not in [t.name for t in registry.all()]

    # ...and then leaves the config entirely.
    manager.servers = {}
    third = await reload_all(registry, None, manager)
    assert third.mcp_tools == []
    assert registry.all() == []


async def test_builtin_tools_survive_an_mcp_reload(isolated_settings):
    registry = ToolRegistry()
    registry.register(FakeBuiltin(), builtin=True)

    await reload_all(registry, None, FakeMcpManager({"alpha": ["ping"]}))

    assert [t.name for t in registry.all()] == ["reload_tools", "mcp__alpha__ping"]


async def test_an_mcp_failure_is_isolated_and_reported(isolated_settings):
    """A dead MCP server must not erase what the tool pass already loaded."""
    _write(isolated_settings / "alpha.py", "alpha")
    registry = ToolRegistry()
    manager = FakeMcpManager({})
    manager.fail = True

    report = await reload_all(registry, None, manager)

    assert report.ok is False
    assert report.tools == ["alpha"]
    assert "mcp is down" in report.mcp_error
    assert registry.get("alpha") is not None


def test_report_serialises_for_the_api():
    report = ReloadReport(tools=["a"], total_tools=2, tool_errors={"x.py": "boom"})
    data = report.as_dict()
    assert data["ok"] is False
    assert data["tools"] == ["a"] and data["total_tools"] == 2
    assert data["tool_errors"] == {"x.py": "boom"}
    assert set(data) == {
        "ok", "tools", "total_tools", "tool_errors", "missing_enabled",
        "providers", "provider_errors", "mcp_tools", "mcp_error",
    }
