"""Tests for list_tools.

Two failures this tool must not have. It must not advertise a tool the agent
cannot call — the list is built from the profile config, but the call goes to
the live registry, and an MCP server that never connected leaves a phantom
entry the agent then calls into a "tool not found". And it must not dump the
whole catalogue: an MCP-heavy profile carries ~125 tools / ~35 KB, which is
9k tokens spent to answer "do I have anything for ssh".
"""

import pytest

from navi.core.registry import ProfileRegistry, ToolRegistry
from navi.profiles.base import ToolConfig, ToolScopeConfig
from navi.tools._internal.base import ToolContext
from navi.tools.list_tools import ListToolsTool
from tests.conftest_factory import FakeTool, make_profile


@pytest.fixture(autouse=True)
def no_user_enabled_tools(monkeypatch):
    """Every profile also picks up tools/enabled.json, a real file on the machine
    running the tests — tests must see exactly what they declare."""
    monkeypatch.setattr("navi.core.tool_utils.load_user_enabled_tools", list)


class FakeMcpManager:
    """Stand-in for McpManager — group name to tool names, as the server config holds them."""

    def __init__(self, groups: dict[str, dict[str, list[str]]] | None = None) -> None:
        self._groups = groups or {}

    def resolve_group(self, server_name: str, group_name: str) -> list[str]:
        return list(self._groups.get(server_name, {}).get(group_name, []))


def make_profile_with(
    profile_id: str,
    native: list[str],
    mcp: dict | None = None,
    subagent_native: list[str] | None = None,
):
    """A profile holding exactly the declared tools — no enabled_tools migration on top."""
    subagent = (
        ToolScopeConfig(native=list(subagent_native))
        if subagent_native is not None
        else ToolScopeConfig()
    )
    return make_profile(
        profile_id,
        enabled_tools=[],
        tools=ToolConfig(
            agent=ToolScopeConfig(native=list(native), mcp=dict(mcp or {})),
            subagent=subagent,
        ),
    )


def make_profiles(*profiles) -> ProfileRegistry:
    registry = ProfileRegistry()
    for profile in profiles:
        registry.register(profile)
    return registry


def make_registry(*specs: tuple[str, str]) -> ToolRegistry:
    """specs: (name, description) pairs — the live, registered tools."""
    registry = ToolRegistry()
    for name, description in specs:
        registry.register(FakeTool(name, description=description), builtin=True)
    return registry


def make_tool(registry, profiles, groups=None) -> ListToolsTool:
    return ListToolsTool(
        registry=registry,
        profile_registry=profiles,
        mcp_manager=FakeMcpManager(groups),
    )


@pytest.mark.asyncio
async def test_lists_only_the_requested_profiles_tools():
    profiles = make_profiles(
        make_profile_with("secretary", ["todo"]),
        make_profile_with("developer", ["code_exec"]),
    )
    registry = make_registry(("todo", "notes"), ("code_exec", "runs"), ("weather", "forecast"))

    result = await make_tool(registry, profiles).execute({"profile_id": "secretary"})

    assert result.success is True
    assert "todo" in result.output
    assert "code_exec" not in result.output
    assert "weather" not in result.output


@pytest.mark.asyncio
async def test_compact_output_groups_names_by_source():
    groups = {"tgclient": {"read": ["dialogs_list", "messages_history"]}}
    profiles = make_profiles(
        make_profile_with("secretary", ["todo"], {"tgclient": ["read"]})
    )
    registry = make_registry(
        ("todo", "notes and tasks"),
        ("mcp__tgclient__dialogs_list", "list dialogs"),
        ("mcp__tgclient__messages_history", "read history"),
    )

    result = await make_tool(registry, profiles, groups).execute({"profile_id": "secretary"})

    assert "native (1): todo" in result.output
    assert (
        "mcp__tgclient__ (2): mcp__tgclient__dialogs_list, mcp__tgclient__messages_history"
        in result.output
    )
    # Descriptions are what makes the output huge — they stay out unless asked for.
    assert "notes and tasks" not in result.output


@pytest.mark.asyncio
async def test_verbose_is_an_order_of_magnitude_larger():
    """Descriptions are what costs the context — 125 tools of them is ~35 KB."""
    names = [f"tool_{i}" for i in range(20)]
    profiles = make_profiles(make_profile_with("secretary", names))
    registry = make_registry(*[(name, "d" * 200) for name in names])
    tool = make_tool(registry, profiles)

    compact = (await tool.execute({"profile_id": "secretary"})).output
    verbose = (await tool.execute({"profile_id": "secretary", "verbose": True})).output

    assert "d" * 200 not in compact
    assert "d" * 200 in verbose
    assert len(verbose) > len(compact) * 5


@pytest.mark.asyncio
async def test_query_filters_by_name_and_by_description():
    profiles = make_profiles(make_profile_with("secretary", ["todo", "sync_files"]))
    registry = make_registry(("todo", "notes and tasks"), ("sync_files", "copy files"))
    tool = make_tool(registry, profiles)

    by_name = (await tool.execute({"profile_id": "secretary", "query": "files"})).output
    assert "mcp__" not in by_name
    assert "sync_files" in by_name
    assert "todo" not in by_name
    assert "matching 'files'" in by_name

    by_description = (await tool.execute({"profile_id": "secretary", "query": "tasks"})).output
    assert "todo" in by_description
    assert "sync_files" not in by_description


@pytest.mark.asyncio
async def test_declared_but_unregistered_tools_are_not_advertised():
    """The navi_ui case: the profile config declares it, the server never connected."""
    groups = {"navi_ui": {"ui": ["render_component"]}}
    profiles = make_profiles(
        make_profile_with("secretary", ["todo"], {"navi_ui": ["ui"]})
    )
    registry = make_registry(("todo", "notes"))  # render_component was never registered

    result = await make_tool(registry, profiles, groups).execute({"profile_id": "secretary"})

    assert "Not registered (1): mcp__navi_ui__render_component" in result.output
    # Counted as neither available nor part of a source section.
    assert "(1 available)" in result.output
    assert "mcp__navi_ui__ (1)" not in result.output


@pytest.mark.asyncio
async def test_query_still_reports_a_matching_unregistered_tool():
    """"Do I have anything for render" must answer "declared, but not callable"."""
    groups = {"navi_ui": {"ui": ["render_component"]}}
    profiles = make_profiles(
        make_profile_with("secretary", ["todo"], {"navi_ui": ["ui"]})
    )
    registry = make_registry(("todo", "notes"))

    result = await make_tool(registry, profiles, groups).execute(
        {"profile_id": "secretary", "query": "render"}
    )

    assert "Not registered (1): mcp__navi_ui__render_component" in result.output
    assert "0 of 2 declared" in result.output


@pytest.mark.asyncio
async def test_wildcard_group_takes_every_registered_tool_of_that_server():
    profiles = make_profiles(
        make_profile_with("secretary", ["todo"], {"tgclient": ["*"]})
    )
    registry = make_registry(
        ("todo", "notes"),
        ("mcp__tgclient__dialogs_list", "dialogs"),
        ("mcp__other__whatever", "not this server"),
    )

    result = await make_tool(registry, profiles).execute({"profile_id": "secretary"})

    assert "mcp__tgclient__dialogs_list" in result.output
    assert "mcp__other__whatever" not in result.output


@pytest.mark.asyncio
async def test_no_profile_in_context_is_an_error_that_says_what_to_do():
    result = await make_tool(make_registry(), make_profiles()).execute({})

    assert result.success is False
    assert result.error == "no_active_profile"
    assert "list_profiles" in result.output


@pytest.mark.asyncio
async def test_unknown_profile_is_rejected():
    registry = make_registry(("todo", "notes"))
    profiles = make_profiles(make_profile_with("secretary", ["todo"]))

    result = await make_tool(registry, profiles).execute({"profile_id": "nope"})

    assert result.success is False
    assert result.error == "profile_not_found"


@pytest.mark.asyncio
async def test_defaults_to_the_profile_the_run_executes_as():
    """Omitted profile_id means "mine" — the agent answers "what do I have" with no argument."""
    profiles = make_profiles(
        make_profile_with("secretary", ["todo"]),
        make_profile_with("server_admin", ["ssh_exec"]),
    )
    registry = make_registry(("todo", "notes"), ("ssh_exec", "remote"))
    tool = make_tool(registry, profiles)

    result = await tool.execute({}, ctx=ToolContext(profile_id="server_admin"))

    assert "ssh_exec" in result.output
    assert "todo" not in result.output
    # Which profile was assumed, so the answer cannot be misread as another's.
    assert "'server_admin' (current profile)" in result.output


@pytest.mark.asyncio
async def test_explicit_profile_id_beats_the_running_profile():
    profiles = make_profiles(
        make_profile_with("secretary", ["todo"]),
        make_profile_with("server_admin", ["ssh_exec"]),
    )
    registry = make_registry(("todo", "notes"), ("ssh_exec", "remote"))

    result = await make_tool(registry, profiles).execute(
        {"profile_id": "secretary"}, ctx=ToolContext(profile_id="server_admin")
    )

    assert "todo" in result.output
    assert "ssh_exec" not in result.output
    assert "current profile" not in result.output


@pytest.mark.asyncio
async def test_scope_subagent_lists_what_a_spawned_agent_would_get():
    profiles = make_profiles(
        make_profile_with("server_admin", ["ssh_exec", "todo"], subagent_native=["ssh_exec"])
    )
    registry = make_registry(("ssh_exec", "remote"), ("todo", "planning"))

    agent_side = await make_tool(registry, profiles).execute({"profile_id": "server_admin"})
    subagent_side = await make_tool(registry, profiles).execute(
        {"profile_id": "server_admin", "scope": "subagent"}
    )

    assert "todo" in agent_side.output
    assert "todo" not in subagent_side.output
    assert "ssh_exec" in subagent_side.output
    assert "(subagent scope)" in subagent_side.output


@pytest.mark.asyncio
async def test_unknown_scope_is_rejected():
    profiles = make_profiles(make_profile_with("secretary", ["todo"]))
    registry = make_registry(("todo", "notes"))

    result = await make_tool(registry, profiles).execute(
        {"profile_id": "secretary", "scope": "worker"}
    )

    assert result.success is False
    assert result.error == "unknown_scope"
