"""
Tests for autopil.mcp_server — MCP tool dispatch logic.

Tests call dispatch_tool() directly, bypassing the MCP transport layer,
so the full test suite runs without an MCP client/server connection.

Coverage:
  Tools
  ─────
  evaluate_context
    - ALLOW: valid role + source within sensitivity ceiling
    - DENY: source on denylist
    - DENY: unknown agent role (no matching policy)
    - DENY: sensitivity level exceeds policy ceiling
    - DENY: task_type on denied_tasks list
    - DENY: task_type not in allowed_tasks list
    - ALLOW: task_type in allowed_tasks list
    - Cross-agent isolation: second role on same session is denied
    - Same role on same session is not blocked (not isolation violation)
    - Invalid sensitivity_level returns error message
    - Event is persisted in audit log after every call
    - event_id is returned in the JSON payload

  query_audit_log
    - Returns events after evaluate calls
    - Filters by agent_role
    - Filters by decision (ALLOW / DENY)
    - Filters by session_id
    - Respects limit parameter
    - Empty result when no events match filter

  get_audit_stats
    - Returns zero counts on empty log
    - Correct total / allowed / denied counts
    - Deny rate calculation
    - Top roles ranked by event count

  list_policies
    - Returns all policies with no filter
    - Filters by industry
    - Filters by agent_role
    - Returns empty list for unknown industry

  record_action
    - Succeeds and returns action_id after a valid evaluate_context call
    - Error on non-existent event_id
    - detail and outcome are stored

  reload_policies
    - Confirms policy count after reload
    - New policy written to disk is picked up after reload

  get_session_status
    - Returns owner, counts, and sources for a known session
    - Error on unknown session_id

  Server wiring
  ─────────────
  - create_mcp_server registers all 7 expected tools
"""

import json
import textwrap
import uuid

import pytest
import pytest_asyncio

# Skip the whole module if mcp is not installed
pytest.importorskip("mcp")

from autopil.mcp_server import (
    ALL_TOOLS,
    TOOL_AUDIT_QUERY,
    TOOL_AUDIT_STATS,
    TOOL_EVALUATE,
    TOOL_LIST_POLICIES,
    TOOL_RECORD_ACTION,
    TOOL_RELOAD_POLICIES,
    TOOL_SESSION_STATUS,
    create_mcp_server,
    dispatch_tool,
)
from autopil.audit_log import AuditLog
from autopil.db import create_stores
from autopil.models import Decision
from autopil.policy_engine import PolicyEngine


# ─────────────────────────────────────────────────────────────────────────────
# Fixtures
# ─────────────────────────────────────────────────────────────────────────────

POLICY_YAML = textwrap.dedent("""
    policies:
      - name: analyst_policy
        agent_role: analyst
        allowed_sources:
          - reports
          - market_data
        denied_sources:
          - executive_comms
        allowed_tasks:
          - market_analysis
          - report_generation
        denied_tasks:
          - trade_execution
        max_sensitivity: high

      - name: viewer_policy
        agent_role: viewer
        allowed_sources:
          - reports
        denied_sources:
          - market_data
          - executive_comms
        max_sensitivity: medium

      - name: admin_policy
        agent_role: admin
        allowed_sources: []
        denied_sources:
          - executive_comms
        max_sensitivity: restricted
""")


@pytest.fixture
def policy_file(tmp_path) -> str:
    p = tmp_path / "test_policies.yaml"
    p.write_text(POLICY_YAML)
    return str(p)


@pytest.fixture
def audit_db(tmp_path) -> str:
    return str(tmp_path / "test_audit.db")


@pytest.fixture
def stores(audit_db):
    return create_stores(db_path=audit_db)


@pytest.fixture
def audit_log_obj(stores):
    return stores[0]


@pytest.fixture
def lineage_store_obj(stores):
    return stores[5]


@pytest.fixture
def engine(policy_file) -> PolicyEngine:
    return PolicyEngine(policy_file)


@pytest.fixture
def state(engine) -> dict:
    return {"engine": engine}


@pytest.fixture
def dispatch_args(audit_log_obj, lineage_store_obj, state, policy_file):
    """Common kwargs for dispatch_tool calls."""
    return {
        "audit_log":     audit_log_obj,
        "lineage_store": lineage_store_obj,
        "state":         state,
        "policy_path":   policy_file,
        "tenant_id":     "default",
    }


def _eval_args(**overrides) -> dict:
    """Build a minimal evaluate_context argument dict."""
    base = {
        "agent_role":        "analyst",
        "user_id":           "u1",
        "source_id":         "reports",
        "sensitivity_level": "low",
        "session_id":        str(uuid.uuid4()),
    }
    base.update(overrides)
    return base


def _parse_json(result: list) -> dict:
    """Extract and parse the JSON payload embedded in the tool response text."""
    text = result[0].text
    json_start = text.index("{")
    return json.loads(text[json_start:])


def _text(result: list) -> str:
    return result[0].text


# ─────────────────────────────────────────────────────────────────────────────
# evaluate_context
# ─────────────────────────────────────────────────────────────────────────────

class TestEvaluateContext:

    @pytest.mark.asyncio
    async def test_allow_valid_role_and_source(self, dispatch_args):
        result = await dispatch_tool(name=TOOL_EVALUATE, arguments=_eval_args(), **dispatch_args)
        data = _parse_json(result)
        assert data["decision"] == "ALLOW"
        assert data["policy_name"] == "analyst_policy"
        assert "event_id" in data

    @pytest.mark.asyncio
    async def test_deny_denied_source(self, dispatch_args):
        result = await dispatch_tool(
            name=TOOL_EVALUATE,
            arguments=_eval_args(source_id="executive_comms"),
            **dispatch_args,
        )
        data = _parse_json(result)
        assert data["decision"] == "DENY"
        assert "denylist" in data["reason"].lower() or "denied" in data["reason"].lower()
        assert "event_id" in data

    @pytest.mark.asyncio
    async def test_deny_unknown_role(self, dispatch_args):
        result = await dispatch_tool(
            name=TOOL_EVALUATE,
            arguments=_eval_args(agent_role="unknown_role"),
            **dispatch_args,
        )
        data = _parse_json(result)
        assert data["decision"] == "DENY"

    @pytest.mark.asyncio
    async def test_deny_sensitivity_ceiling_exceeded(self, dispatch_args):
        # viewer has max_sensitivity: medium — restricted should be denied
        result = await dispatch_tool(
            name=TOOL_EVALUATE,
            arguments=_eval_args(agent_role="viewer", source_id="reports", sensitivity_level="restricted"),
            **dispatch_args,
        )
        data = _parse_json(result)
        assert data["decision"] == "DENY"

    @pytest.mark.asyncio
    async def test_allow_within_sensitivity_ceiling(self, dispatch_args):
        result = await dispatch_tool(
            name=TOOL_EVALUATE,
            arguments=_eval_args(agent_role="viewer", source_id="reports", sensitivity_level="medium"),
            **dispatch_args,
        )
        data = _parse_json(result)
        assert data["decision"] == "ALLOW"

    @pytest.mark.asyncio
    async def test_deny_task_on_denied_tasks(self, dispatch_args):
        result = await dispatch_tool(
            name=TOOL_EVALUATE,
            arguments=_eval_args(task_type="trade_execution"),
            **dispatch_args,
        )
        data = _parse_json(result)
        assert data["decision"] == "DENY"

    @pytest.mark.asyncio
    async def test_allow_task_in_allowed_tasks(self, dispatch_args):
        result = await dispatch_tool(
            name=TOOL_EVALUATE,
            arguments=_eval_args(task_type="market_analysis"),
            **dispatch_args,
        )
        data = _parse_json(result)
        assert data["decision"] == "ALLOW"

    @pytest.mark.asyncio
    async def test_deny_task_not_in_allowed_tasks(self, dispatch_args):
        # analyst has allowed_tasks — a task not in that list should be denied
        result = await dispatch_tool(
            name=TOOL_EVALUATE,
            arguments=_eval_args(task_type="some_unlisted_task"),
            **dispatch_args,
        )
        data = _parse_json(result)
        assert data["decision"] == "DENY"

    @pytest.mark.asyncio
    async def test_cross_agent_isolation_deny(self, dispatch_args):
        sid = str(uuid.uuid4())
        # First call — analyst claims the session
        await dispatch_tool(
            name=TOOL_EVALUATE,
            arguments=_eval_args(agent_role="analyst", session_id=sid),
            **dispatch_args,
        )
        # Second call — different role on same session
        result = await dispatch_tool(
            name=TOOL_EVALUATE,
            arguments=_eval_args(agent_role="viewer", session_id=sid),
            **dispatch_args,
        )
        data = _parse_json(result)
        assert data["decision"] == "DENY"
        assert data["policy_name"] == "cross_agent_isolation"

    @pytest.mark.asyncio
    async def test_same_role_same_session_not_blocked(self, dispatch_args):
        sid = str(uuid.uuid4())
        for _ in range(3):
            result = await dispatch_tool(
                name=TOOL_EVALUATE,
                arguments=_eval_args(agent_role="analyst", session_id=sid),
                **dispatch_args,
            )
            data = _parse_json(result)
            assert data["decision"] == "ALLOW"

    @pytest.mark.asyncio
    async def test_invalid_sensitivity_returns_error(self, dispatch_args):
        result = await dispatch_tool(
            name=TOOL_EVALUATE,
            arguments=_eval_args(sensitivity_level="ultra_secret"),
            **dispatch_args,
        )
        assert "❌ Error" in _text(result)
        assert "sensitivity_level" in _text(result)

    @pytest.mark.asyncio
    async def test_event_persisted_in_audit_log(self, dispatch_args, audit_log_obj):
        await dispatch_tool(name=TOOL_EVALUATE, arguments=_eval_args(), **dispatch_args)
        events = audit_log_obj.get_all_events("default")
        assert len(events) == 1
        assert events[0].agent_role == "analyst"

    @pytest.mark.asyncio
    async def test_deny_event_also_persisted(self, dispatch_args, audit_log_obj):
        await dispatch_tool(
            name=TOOL_EVALUATE,
            arguments=_eval_args(source_id="executive_comms"),
            **dispatch_args,
        )
        events = audit_log_obj.get_all_events("default")
        assert len(events) == 1
        assert events[0].decision == Decision.DENY

    @pytest.mark.asyncio
    async def test_allow_message_contains_event_id(self, dispatch_args):
        result = await dispatch_tool(name=TOOL_EVALUATE, arguments=_eval_args(), **dispatch_args)
        text = _text(result)
        assert "✅ ALLOW" in text
        assert "Event ID:" in text

    @pytest.mark.asyncio
    async def test_deny_message_instructs_agent(self, dispatch_args):
        result = await dispatch_tool(
            name=TOOL_EVALUATE,
            arguments=_eval_args(source_id="executive_comms"),
            **dispatch_args,
        )
        text = _text(result)
        assert "🚫 DENY" in text
        assert "Do not proceed" in text


# ─────────────────────────────────────────────────────────────────────────────
# query_audit_log
# ─────────────────────────────────────────────────────────────────────────────

class TestQueryAuditLog:

    @pytest_asyncio.fixture(autouse=True)
    async def seed_events(self, dispatch_args):
        """Seed: 2 analyst-allow, 1 analyst-deny, 1 viewer-allow."""
        sid = str(uuid.uuid4())
        for source in ("reports", "market_data"):
            await dispatch_tool(
                name=TOOL_EVALUATE,
                arguments=_eval_args(agent_role="analyst", source_id=source, session_id=sid),
                **dispatch_args,
            )
        await dispatch_tool(
            name=TOOL_EVALUATE,
            arguments=_eval_args(agent_role="analyst", source_id="executive_comms", session_id=sid),
            **dispatch_args,
        )
        viewer_sid = str(uuid.uuid4())
        await dispatch_tool(
            name=TOOL_EVALUATE,
            arguments=_eval_args(agent_role="viewer", source_id="reports", session_id=viewer_sid),
            **dispatch_args,
        )

    @pytest.mark.asyncio
    async def test_returns_all_events_by_default(self, dispatch_args):
        result = await dispatch_tool(name=TOOL_AUDIT_QUERY, arguments={}, **dispatch_args)
        data = json.loads(_text(result).split("\n\n", 1)[1])
        assert len(data) == 4

    @pytest.mark.asyncio
    async def test_filter_by_agent_role(self, dispatch_args):
        result = await dispatch_tool(
            name=TOOL_AUDIT_QUERY,
            arguments={"agent_role": "viewer"},
            **dispatch_args,
        )
        data = json.loads(_text(result).split("\n\n", 1)[1])
        assert len(data) == 1
        assert data[0]["agent_role"] == "viewer"

    @pytest.mark.asyncio
    async def test_filter_by_decision_deny(self, dispatch_args):
        result = await dispatch_tool(
            name=TOOL_AUDIT_QUERY,
            arguments={"decision": "DENY"},
            **dispatch_args,
        )
        data = json.loads(_text(result).split("\n\n", 1)[1])
        assert len(data) == 1
        assert data[0]["decision"] == "DENY"

    @pytest.mark.asyncio
    async def test_filter_by_decision_allow(self, dispatch_args):
        result = await dispatch_tool(
            name=TOOL_AUDIT_QUERY,
            arguments={"decision": "ALLOW"},
            **dispatch_args,
        )
        data = json.loads(_text(result).split("\n\n", 1)[1])
        assert all(e["decision"] == "ALLOW" for e in data)
        assert len(data) == 3

    @pytest.mark.asyncio
    async def test_limit_respected(self, dispatch_args):
        result = await dispatch_tool(
            name=TOOL_AUDIT_QUERY,
            arguments={"limit": 2},
            **dispatch_args,
        )
        data = json.loads(_text(result).split("\n\n", 1)[1])
        assert len(data) == 2

    @pytest.mark.asyncio
    async def test_filter_by_session_id(self, dispatch_args, audit_log_obj):
        events = audit_log_obj.get_all_events("default")
        target_sid = events[0].session_id
        result = await dispatch_tool(
            name=TOOL_AUDIT_QUERY,
            arguments={"session_id": target_sid},
            **dispatch_args,
        )
        data = json.loads(_text(result).split("\n\n", 1)[1])
        assert all(e["session_id"] == target_sid for e in data)

    @pytest.mark.asyncio
    async def test_empty_result_unknown_role(self, dispatch_args):
        result = await dispatch_tool(
            name=TOOL_AUDIT_QUERY,
            arguments={"agent_role": "ghost_role"},
            **dispatch_args,
        )
        data = json.loads(_text(result).split("\n\n", 1)[1])
        assert data == []


# ─────────────────────────────────────────────────────────────────────────────
# get_audit_stats
# ─────────────────────────────────────────────────────────────────────────────

class TestGetAuditStats:

    @pytest.mark.asyncio
    async def test_empty_log_returns_zeros(self, dispatch_args):
        result = await dispatch_tool(name=TOOL_AUDIT_STATS, arguments={}, **dispatch_args)
        data = _parse_json(result)
        assert data["total_events"] == 0
        assert data["allowed"] == 0
        assert data["denied"] == 0
        assert data["deny_rate_pct"] == 0.0

    @pytest.mark.asyncio
    async def test_correct_counts_after_events(self, dispatch_args):
        sid = str(uuid.uuid4())
        # 2 allows, 1 deny
        for source in ("reports", "market_data"):
            await dispatch_tool(
                name=TOOL_EVALUATE,
                arguments=_eval_args(source_id=source, session_id=sid),
                **dispatch_args,
            )
        await dispatch_tool(
            name=TOOL_EVALUATE,
            arguments=_eval_args(source_id="executive_comms", session_id=sid),
            **dispatch_args,
        )

        result = await dispatch_tool(name=TOOL_AUDIT_STATS, arguments={}, **dispatch_args)
        data = _parse_json(result)
        assert data["total_events"] == 3
        assert data["allowed"] == 2
        assert data["denied"] == 1
        assert data["deny_rate_pct"] == pytest.approx(33.3, abs=0.1)

    @pytest.mark.asyncio
    async def test_top_roles_sorted_by_count(self, dispatch_args):
        sid = str(uuid.uuid4())
        # 3 analyst events, 1 viewer event
        for _ in range(3):
            await dispatch_tool(name=TOOL_EVALUATE, arguments=_eval_args(session_id=sid), **dispatch_args)
        await dispatch_tool(
            name=TOOL_EVALUATE,
            arguments=_eval_args(agent_role="viewer", source_id="reports", session_id=str(uuid.uuid4())),
            **dispatch_args,
        )

        result = await dispatch_tool(name=TOOL_AUDIT_STATS, arguments={}, **dispatch_args)
        data = _parse_json(result)
        roles = [r["role"] for r in data["top_agent_roles"]]
        assert roles[0] == "analyst"

    @pytest.mark.asyncio
    async def test_summary_text_present(self, dispatch_args):
        result = await dispatch_tool(name=TOOL_AUDIT_STATS, arguments={}, **dispatch_args)
        assert "Audit summary" in _text(result)


# ─────────────────────────────────────────────────────────────────────────────
# list_policies
# ─────────────────────────────────────────────────────────────────────────────

class TestListPolicies:

    @pytest.mark.asyncio
    async def test_returns_all_policies_no_filter(self, dispatch_args):
        result = await dispatch_tool(name=TOOL_LIST_POLICIES, arguments={}, **dispatch_args)
        data = json.loads(_text(result).split("\n\n", 1)[1])
        # 3 policies in the test YAML
        assert len(data) == 3

    @pytest.mark.asyncio
    async def test_filter_by_agent_role(self, dispatch_args):
        result = await dispatch_tool(
            name=TOOL_LIST_POLICIES,
            arguments={"agent_role": "viewer"},
            **dispatch_args,
        )
        data = json.loads(_text(result).split("\n\n", 1)[1])
        assert len(data) == 1
        assert data[0]["agent_role"] == "viewer"

    @pytest.mark.asyncio
    async def test_filter_by_unknown_industry_returns_empty(self, dispatch_args):
        result = await dispatch_tool(
            name=TOOL_LIST_POLICIES,
            arguments={"industry": "nonexistent_industry"},
            **dispatch_args,
        )
        data = json.loads(_text(result).split("\n\n", 1)[1])
        assert data == []

    @pytest.mark.asyncio
    async def test_policy_fields_present(self, dispatch_args):
        result = await dispatch_tool(name=TOOL_LIST_POLICIES, arguments={}, **dispatch_args)
        data = json.loads(_text(result).split("\n\n", 1)[1])
        for policy in data:
            assert "name" in policy
            assert "agent_role" in policy
            assert "allowed_sources" in policy
            assert "denied_sources" in policy
            assert "max_sensitivity" in policy


# ─────────────────────────────────────────────────────────────────────────────
# record_action
# ─────────────────────────────────────────────────────────────────────────────

class TestRecordAction:

    @pytest.mark.asyncio
    async def test_records_action_after_valid_event(self, dispatch_args):
        # First get a valid event_id
        eval_result = await dispatch_tool(
            name=TOOL_EVALUATE, arguments=_eval_args(), **dispatch_args
        )
        event_id = _parse_json(eval_result)["event_id"]

        result = await dispatch_tool(
            name=TOOL_RECORD_ACTION,
            arguments={
                "event_id":    event_id,
                "action_type": "loan_decision",
                "detail":      {"outcome": "approved", "amount": 50000},
                "outcome":     "approved",
            },
            **dispatch_args,
        )
        data = _parse_json(result)
        assert "action_id" in data
        assert data["event_id"] == event_id
        assert data["action_type"] == "loan_decision"
        assert data["outcome"] == "approved"

    @pytest.mark.asyncio
    async def test_error_on_nonexistent_event_id(self, dispatch_args):
        result = await dispatch_tool(
            name=TOOL_RECORD_ACTION,
            arguments={
                "event_id":    "evt_does_not_exist",
                "action_type": "loan_decision",
                "detail":      {},
            },
            **dispatch_args,
        )
        assert "❌ Error" in _text(result)
        assert "event_id" in _text(result)

    @pytest.mark.asyncio
    async def test_action_without_outcome_succeeds(self, dispatch_args):
        eval_result = await dispatch_tool(
            name=TOOL_EVALUATE, arguments=_eval_args(), **dispatch_args
        )
        event_id = _parse_json(eval_result)["event_id"]

        result = await dispatch_tool(
            name=TOOL_RECORD_ACTION,
            arguments={
                "event_id":    event_id,
                "action_type": "tool_call",
                "detail":      {"tool": "get_balance"},
            },
            **dispatch_args,
        )
        assert "✅" in _text(result)
        data = _parse_json(result)
        assert data["outcome"] is None

    @pytest.mark.asyncio
    async def test_success_message_present(self, dispatch_args):
        eval_result = await dispatch_tool(
            name=TOOL_EVALUATE, arguments=_eval_args(), **dispatch_args
        )
        event_id = _parse_json(eval_result)["event_id"]
        result = await dispatch_tool(
            name=TOOL_RECORD_ACTION,
            arguments={"event_id": event_id, "action_type": "response", "detail": {}},
            **dispatch_args,
        )
        assert "Provenance trail updated" in _text(result)


# ─────────────────────────────────────────────────────────────────────────────
# reload_policies
# ─────────────────────────────────────────────────────────────────────────────

class TestReloadPolicies:

    @pytest.mark.asyncio
    async def test_reload_returns_count(self, dispatch_args):
        result = await dispatch_tool(name=TOOL_RELOAD_POLICIES, arguments={}, **dispatch_args)
        text = _text(result)
        assert "✅" in text
        assert "active" in text

    @pytest.mark.asyncio
    async def test_new_policy_picked_up_after_reload(self, dispatch_args, state, tmp_path, policy_file):
        # Add a new policy to the file (no dedent — must preserve 2-space indent
        # to stay within the top-level `policies:` list in the YAML)
        extra = """
  - name: trader_policy
    agent_role: trader
    allowed_sources:
      - market_data
    denied_sources: []
    max_sensitivity: high
"""
        with open(policy_file, "a") as f:
            f.write(extra)

        # Reload
        await dispatch_tool(name=TOOL_RELOAD_POLICIES, arguments={}, **dispatch_args)

        # New role should now get ALLOW
        result = await dispatch_tool(
            name=TOOL_EVALUATE,
            arguments=_eval_args(agent_role="trader", source_id="market_data"),
            **dispatch_args,
        )
        data = _parse_json(result)
        assert data["decision"] == "ALLOW"
        assert data["policy_name"] == "trader_policy"


# ─────────────────────────────────────────────────────────────────────────────
# get_session_status
# ─────────────────────────────────────────────────────────────────────────────

class TestGetSessionStatus:

    @pytest.mark.asyncio
    async def test_returns_session_summary(self, dispatch_args):
        sid = str(uuid.uuid4())
        for source in ("reports", "market_data", "executive_comms"):
            await dispatch_tool(
                name=TOOL_EVALUATE,
                arguments=_eval_args(session_id=sid, source_id=source),
                **dispatch_args,
            )

        result = await dispatch_tool(
            name=TOOL_SESSION_STATUS,
            arguments={"session_id": sid},
            **dispatch_args,
        )
        data = _parse_json(result)
        assert data["session_id"] == sid
        assert data["owner_role"] == "analyst"
        assert data["total_events"] == 3
        assert data["allowed"] == 2
        assert data["denied"] == 1
        assert set(data["sources_accessed"]) == {"reports", "market_data", "executive_comms"}

    @pytest.mark.asyncio
    async def test_error_on_unknown_session(self, dispatch_args):
        result = await dispatch_tool(
            name=TOOL_SESSION_STATUS,
            arguments={"session_id": "sess_does_not_exist"},
            **dispatch_args,
        )
        assert "❌ Error" in _text(result)

    @pytest.mark.asyncio
    async def test_summary_text_present(self, dispatch_args):
        sid = str(uuid.uuid4())
        await dispatch_tool(
            name=TOOL_EVALUATE,
            arguments=_eval_args(session_id=sid),
            **dispatch_args,
        )
        result = await dispatch_tool(
            name=TOOL_SESSION_STATUS,
            arguments={"session_id": sid},
            **dispatch_args,
        )
        text = _text(result)
        assert "owned by" in text
        assert "analyst" in text


# ─────────────────────────────────────────────────────────────────────────────
# Unknown tool
# ─────────────────────────────────────────────────────────────────────────────

class TestUnknownTool:

    @pytest.mark.asyncio
    async def test_unknown_tool_returns_error(self, dispatch_args):
        result = await dispatch_tool(
            name="does_not_exist",
            arguments={},
            **dispatch_args,
        )
        assert "❌ Error" in _text(result)
        assert "does_not_exist" in _text(result)


# ─────────────────────────────────────────────────────────────────────────────
# Server wiring — create_mcp_server
# ─────────────────────────────────────────────────────────────────────────────

class TestServerWiring:

    @pytest.mark.asyncio
    async def test_all_tools_registered(self, policy_file, audit_db):
        from mcp import types as mcp_types
        server = create_mcp_server(policy_path=policy_file, audit_db=audit_db)
        # Call the registered handler directly via request_handlers
        handler = server.request_handlers[mcp_types.ListToolsRequest]
        server_result = await handler(mcp_types.ListToolsRequest(method="tools/list"))
        tool_names = {t.name for t in server_result.root.tools}
        assert tool_names == set(ALL_TOOLS)

    def test_all_tools_constant_has_seven_entries(self):
        assert len(ALL_TOOLS) == 7
