"""Route-level tests for the MCP tool definition schema.

These tests exercise the Pydantic request models (CreateToolRequest /
UpdateToolRequest) to catch schema gaps at the route/request-model layer —
the layer where the pre-fix defect lived (HTTP 422 on every MCP tool
creation attempt).

Test coverage:
- CreateToolRequest validates a valid MCP definition (was 422 before Part A).
- UpdateToolRequest validates a valid MCP definition.
- Invalid MCP bodies are rejected (ftp:// url, missing url).
- Round-trip: validated definition dict passes through validate_mcp_definition
  unchanged, proving the request schema and call-time validator agree.
- Full HTTP round-trip via the ASGI test client (POST /api/v1/tools/).
"""

from __future__ import annotations

from unittest.mock import AsyncMock, MagicMock

import pytest
from fastapi import HTTPException
from pydantic import ValidationError

from api.routes.tool import (
    CreateToolRequest,
    McpToolConfig,
    McpToolDefinition,
    ToolTestRequest,
    ToolTestResponse,
    UpdateToolRequest,
    _populate_discovered_tools,
    refresh_mcp_tools,
    router,
)
from api.routes.tool import (
    test_tool as call_test_tool_route,
)
from api.services.workflow.tools.mcp_tool import (
    validate_mcp_definition,
)

# ── Canonical valid MCP request body ─────────────────────────────────────────

VALID_MCP_DEFINITION = {
    "schema_version": 1,
    "type": "mcp",
    "config": {
        "transport": "streamable_http",
        "url": "https://x/mcp",
        "credential_uuid": None,
        "tools_filter": [],
    },
}


# ── Part A regression: CreateToolRequest / UpdateToolRequest validation ───────


def test_create_tool_request_accepts_mcp_definition():
    """CreateToolRequest must accept an MCP definition (was HTTP 422 before fix)."""
    req = CreateToolRequest(
        name="My MCP Tool",
        description="Integration via MCP",
        category="mcp",
        definition=VALID_MCP_DEFINITION,
    )
    assert isinstance(req.definition, McpToolDefinition)
    assert req.definition.type == "mcp"
    assert req.definition.config.url == "https://x/mcp"
    assert req.definition.config.transport == "streamable_http"
    assert req.definition.config.credential_uuid is None
    assert req.definition.config.tools_filter == []
    assert req.definition.config.timeout_secs == 30
    assert req.definition.config.sse_read_timeout_secs == 300


def test_update_tool_request_accepts_mcp_definition():
    """UpdateToolRequest must also accept an MCP definition."""
    req = UpdateToolRequest(
        name="Updated MCP Tool",
        definition=VALID_MCP_DEFINITION,
    )
    assert isinstance(req.definition, McpToolDefinition)
    assert req.definition.type == "mcp"
    assert req.definition.config.url == "https://x/mcp"


def test_update_tool_request_accepts_http_api_complex_parameter_types():
    """HTTP API tools may accept structured JSON parameters."""
    req = UpdateToolRequest(
        name="Check Availability New Multi",
        description="Check Availability when asked for it.",
        definition={
            "schema_version": 1,
            "type": "http_api",
            "config": {
                "method": "POST",
                "url": "https://automation.dograh.com/webhook/example",
                "parameters": [
                    {
                        "name": "params",
                        "type": "object",
                        "description": (
                            "An object containing the name and datetime in ISO format"
                        ),
                        "required": True,
                    },
                    {
                        "name": "slots",
                        "type": "array",
                        "description": "Candidate availability slots.",
                        "required": False,
                    },
                ],
                "preset_parameters": [
                    {
                        "name": "phone_number",
                        "type": "string",
                        "value_template": "{{initial_context.phone_number}}",
                        "required": True,
                    }
                ],
                "timeout_ms": 5000,
                "customMessageType": "text",
            },
        },
    )

    assert req.definition.type == "http_api"
    parameters = req.definition.config.parameters
    assert parameters[0].type == "object"
    assert parameters[1].type == "array"


def test_create_tool_request_accepts_mcp_with_all_fields():
    """All optional MCP config fields are accepted and preserved."""
    req = CreateToolRequest(
        name="Full MCP Tool",
        category="mcp",
        definition={
            "schema_version": 1,
            "type": "mcp",
            "config": {
                "transport": "streamable_http",
                "url": "https://acme.example.com/mcp",
                "credential_uuid": "cred-abc-123",
                "tools_filter": ["lookup_patient", "schedule_appointment"],
                "timeout_secs": 60,
                "sse_read_timeout_secs": 600,
            },
        },
    )
    cfg = req.definition.config  # type: ignore[union-attr]
    assert cfg.url == "https://acme.example.com/mcp"
    assert cfg.credential_uuid == "cred-abc-123"
    assert cfg.tools_filter == ["lookup_patient", "schedule_appointment"]
    assert cfg.timeout_secs == 60
    assert cfg.sse_read_timeout_secs == 600


# ── Invalid bodies are rejected ───────────────────────────────────────────────


@pytest.mark.parametrize(
    "definition",
    [
        # ftp:// URL — rejected by McpToolConfig.validate_url
        {
            "schema_version": 1,
            "type": "mcp",
            "config": {"transport": "streamable_http", "url": "ftp://x/mcp"},
        },
        # Empty url — rejected by McpToolConfig.validate_url
        {
            "schema_version": 1,
            "type": "mcp",
            "config": {"transport": "streamable_http", "url": ""},
        },
        # Missing url — rejected by McpToolConfig (required field)
        {
            "schema_version": 1,
            "type": "mcp",
            "config": {"transport": "streamable_http"},
        },
        # Unsupported transport — rejected because Literal["streamable_http"] constraint
        {
            "schema_version": 1,
            "type": "mcp",
            "config": {"url": "https://x/mcp", "transport": "stdio"},
        },
    ],
)
def test_create_tool_request_rejects_invalid_mcp_definition(definition):
    """Invalid MCP definitions must raise ValidationError."""
    with pytest.raises(ValidationError):
        CreateToolRequest(
            name="Bad MCP Tool",
            category="mcp",
            definition=definition,
        )


# ── Round-trip compatibility: request schema ↔ validate_mcp_definition ───────


def test_mcp_definition_round_trips_through_validate_mcp_definition():
    """The dict produced by CreateToolRequest.definition.model_dump() must be
    accepted by validate_mcp_definition without raising, and the result must
    contain the expected fields.  This proves the request-layer schema and the
    call-time validator agree on the stored config shape."""
    req = CreateToolRequest(
        name="Round-Trip MCP Tool",
        category="mcp",
        definition={
            "schema_version": 1,
            "type": "mcp",
            "config": {
                "transport": "streamable_http",
                "url": "https://roundtrip.example.com/mcp",
                "credential_uuid": "cred-rt-456",
                "tools_filter": ["ping"],
                "timeout_secs": 45,
                "sse_read_timeout_secs": 400,
            },
        },
    )

    # Simulate what the route does: persist definition as a plain dict
    persisted = req.definition.model_dump()  # type: ignore[union-attr]

    # validate_mcp_definition must accept the persisted shape without raising
    normalized = validate_mcp_definition(persisted)

    assert normalized["url"] == "https://roundtrip.example.com/mcp"
    assert normalized["transport"] == "streamable_http"
    assert normalized["credential_uuid"] == "cred-rt-456"
    assert normalized["tools_filter"] == ["ping"]
    assert normalized["timeout_secs"] == 45
    assert normalized["sse_read_timeout_secs"] == 400


def test_mcp_definition_round_trip_defaults():
    """Round-trip with minimal body: defaults fill in correctly and
    validate_mcp_definition agrees on them."""
    req = CreateToolRequest(
        name="Minimal MCP Tool",
        category="mcp",
        definition=VALID_MCP_DEFINITION,
    )

    persisted = req.definition.model_dump()  # type: ignore[union-attr]
    normalized = validate_mcp_definition(persisted)

    assert normalized["transport"] == "streamable_http"
    assert normalized["tools_filter"] == []
    assert normalized["timeout_secs"] == 30
    assert normalized["sse_read_timeout_secs"] == 300
    assert normalized["credential_uuid"] is None
    # Part B: auth_header / auth_scheme must NOT be present in the normalized
    # config dict (they were dead config removed in the fix)
    assert "auth_header" not in normalized
    assert "auth_scheme" not in normalized


# ── Full HTTP round-trip via ASGI test client ─────────────────────────────────


async def test_post_tool_mcp_returns_200(test_client_factory, db_session):
    """POST /api/v1/tools/ with an MCP definition must return HTTP 200 and
    persist the definition with type='mcp'.  Before Part A this always
    returned 422."""
    # Create a user and an organization, then link them so the route's
    # selected_organization_id check passes.
    user, _ = await db_session.get_or_create_user_by_provider_id("mcp_route_test_user")
    org, _ = await db_session.get_or_create_organization_by_provider_id(
        "mcp_route_test_org", user.id
    )
    await db_session.update_user_selected_organization(user.id, org.id)
    # Reload the user so selected_organization_id is populated on the object.
    user = await db_session.get_user_by_id(user.id)

    async with test_client_factory(user) as client:
        response = await client.post(
            "/api/v1/tools/",
            json={
                "name": "HTTP Round-Trip MCP Tool",
                "description": "Testing the full route",
                "category": "mcp",
                "definition": {
                    "schema_version": 1,
                    "type": "mcp",
                    "config": {
                        "transport": "streamable_http",
                        "url": "https://roundtrip.example.com/mcp",
                        "credential_uuid": None,
                        "tools_filter": [],
                    },
                },
            },
        )

    assert response.status_code == 200, (
        f"Expected 200, got {response.status_code}: {response.text}"
    )
    body = response.json()
    assert body["definition"]["type"] == "mcp"
    assert body["definition"]["config"]["url"] == "https://roundtrip.example.com/mcp"
    assert body["category"] == "mcp"


async def test_post_tool_mcp_invalid_url_returns_422(test_client_factory, db_session):
    """POST /api/v1/tools/ with an ftp:// URL must return HTTP 422."""
    user, _ = await db_session.get_or_create_user_by_provider_id(
        "mcp_route_test_user_422"
    )
    org, _ = await db_session.get_or_create_organization_by_provider_id(
        "mcp_route_test_org_422", user.id
    )
    await db_session.update_user_selected_organization(user.id, org.id)
    user = await db_session.get_user_by_id(user.id)

    async with test_client_factory(user) as client:
        response = await client.post(
            "/api/v1/tools/",
            json={
                "name": "Bad MCP Tool",
                "category": "mcp",
                "definition": {
                    "schema_version": 1,
                    "type": "mcp",
                    "config": {
                        "transport": "streamable_http",
                        "url": "ftp://invalid.example.com/mcp",
                    },
                },
            },
        )

    assert response.status_code == 422


# ── Task 6: discovered_tools field and _populate_discovered_tools helper ──────


def test_mcp_config_accepts_discovered_tools():
    cfg = McpToolConfig(
        url="https://x/mcp",
        discovered_tools=[{"name": "echo", "description": "Echo"}],
    )
    assert cfg.discovered_tools == [{"name": "echo", "description": "Echo"}]
    # Defaults to [] when omitted
    assert McpToolConfig(url="https://x/mcp").discovered_tools == []


@pytest.mark.asyncio
async def test_populate_discovered_tools_overwrites_cache(monkeypatch):
    import api.services.tool_management as tool_svc

    monkeypatch.setattr(
        tool_svc,
        "discover_mcp_tools",
        AsyncMock(return_value=[{"name": "echo", "description": "Echo"}]),
    )
    definition = {
        "schema_version": 1,
        "type": "mcp",
        "config": {
            "url": "https://x/mcp",
            "tools_filter": [],
            "discovered_tools": [{"name": "stale", "description": "old"}],
        },
    }
    out = await _populate_discovered_tools(definition, organization_id=1)
    assert out["config"]["discovered_tools"] == [
        {"name": "echo", "description": "Echo"}
    ]


@pytest.mark.asyncio
async def test_populate_discovered_tools_non_mcp_is_noop():
    definition = {"schema_version": 1, "type": "http_api", "config": {}}
    out = await _populate_discovered_tools(definition, organization_id=1)
    assert out == definition  # untouched


@pytest.mark.asyncio
async def test_populate_discovered_tools_server_down_sets_empty(monkeypatch):
    import api.services.tool_management as tool_svc

    monkeypatch.setattr(
        tool_svc,
        "discover_mcp_tools",
        AsyncMock(side_effect=RuntimeError("connection refused")),
    )
    definition = {
        "schema_version": 1,
        "type": "mcp",
        "config": {"url": "https://x/mcp", "tools_filter": []},
    }
    out = await _populate_discovered_tools(definition, organization_id=1)
    assert out["config"]["discovered_tools"] == []


# ── Task 7: POST /{tool_uuid}/mcp/refresh ─────────────────────────────────────


def _fake_user(org_id=1):
    u = MagicMock()
    u.selected_organization_id = org_id
    u.id = 1
    u.provider_id = "p1"
    return u


def _mcp_tool_model(org_id=1):
    t = MagicMock()
    t.tool_uuid = "tu-mcp"
    t.name = "Mock MCP"
    t.category = "mcp"
    t.definition = {
        "schema_version": 1,
        "type": "mcp",
        "config": {"url": "https://x/mcp", "tools_filter": []},
    }
    return t


def _http_tool_model(method="GET"):
    t = MagicMock()
    t.tool_uuid = "tu-http"
    t.name = "Mock HTTP"
    t.category = "http_api"
    t.definition = {
        "schema_version": 1,
        "type": "http_api",
        "config": {"method": method, "url": "https://example.com/search"},
    }
    return t


@pytest.mark.asyncio
async def test_tool_executes_http_api_tool_with_llm_and_preset_params(monkeypatch):
    import api.routes.tool as tool_route

    tool = _http_tool_model()
    monkeypatch.setattr(
        tool_route.db_client, "get_tool_by_uuid", AsyncMock(return_value=tool)
    )
    executor = AsyncMock(
        return_value={
            "status": "success",
            "status_code": 200,
            "data": {"ok": True},
        }
    )
    monkeypatch.setattr(tool_route, "execute_http_tool", executor)

    resp = await call_test_tool_route(
        "tu-http",
        request=ToolTestRequest(
            llm_params={"query": "cart"},
            preset_params={
                "customer_id": "c_123",
                "sentiment": "cooperative",
            },
        ),
        user=_fake_user(),
    )

    assert resp.status == "success"
    assert resp.status_code == 200
    assert resp.data == {"ok": True}
    assert resp.error is None
    executor.assert_awaited_once_with(
        tool,
        {"query": "cart"},
        preset_params={
            "customer_id": "c_123",
            "sentiment": "cooperative",
        },
        organization_id=1,
        include_request_headers=True,
    )
    assert resp.hint is None
    assert resp.request_method == "GET"
    assert resp.request_url == "https://example.com/search"
    assert resp.request_headers == {}
    assert resp.request_body is None
    assert resp.request_params == {
        "query": "cart",
        "customer_id": "c_123",
        "sentiment": "cooperative",
    }


@pytest.mark.asyncio
async def test_tool_test_sets_request_body_for_post_method(monkeypatch):
    import api.routes.tool as tool_route

    tool = _http_tool_model(method="POST")
    monkeypatch.setattr(
        tool_route.db_client, "get_tool_by_uuid", AsyncMock(return_value=tool)
    )
    monkeypatch.setattr(
        tool_route,
        "execute_http_tool",
        AsyncMock(
            return_value={"status": "success", "status_code": 200, "data": {"id": 1}}
        ),
    )

    resp = await call_test_tool_route(
        "tu-http",
        request=ToolTestRequest(llm_params={"name": "Ada"}),
        user=_fake_user(),
    )

    assert resp.request_method == "POST"
    assert resp.request_body == {"name": "Ada"}
    assert resp.request_params is None


@pytest.mark.asyncio
async def test_tool_test_returns_masked_effective_request_headers(monkeypatch):
    import api.routes.tool as tool_route

    tool = _http_tool_model(method="POST")
    monkeypatch.setattr(
        tool_route.db_client, "get_tool_by_uuid", AsyncMock(return_value=tool)
    )
    monkeypatch.setattr(
        tool_route,
        "execute_http_tool",
        AsyncMock(
            return_value={
                "status": "success",
                "status_code": 200,
                "data": {"ok": True},
                "request_headers": {
                    "X-Tenant": "acme",
                    "Authorization": "****************oken",
                },
            }
        ),
    )

    resp = await call_test_tool_route(
        "tu-http", request=ToolTestRequest(), user=_fake_user()
    )

    assert resp.request_headers == {
        "X-Tenant": "acme",
        "Authorization": "****************oken",
    }


@pytest.mark.asyncio
async def test_tool_test_request_body_includes_resolved_preset_parameters(
    monkeypatch,
):
    """The request preview gives LLM values precedence over preset values."""
    import api.routes.tool as tool_route

    tool = _http_tool_model(method="POST")
    tool.definition["config"]["preset_parameters"] = [
        {
            "name": "source",
            "type": "string",
            "value_template": "{{initial_context.metadata.channel}}",
            "required": True,
        }
    ]
    monkeypatch.setattr(
        tool_route.db_client, "get_tool_by_uuid", AsyncMock(return_value=tool)
    )
    monkeypatch.setattr(
        tool_route,
        "execute_http_tool",
        AsyncMock(
            return_value={"status": "success", "status_code": 200, "data": {"ok": True}}
        ),
    )

    resp = await call_test_tool_route(
        "tu-http",
        request=ToolTestRequest(
            llm_params={"name": "Ada", "source": "llm"},
            preset_params={"source": "web_widget"},
        ),
        user=_fake_user(),
    )

    assert resp.request_body == {"name": "Ada", "source": "llm"}


@pytest.mark.asyncio
async def test_tool_test_no_arguments_post_shows_empty_body(monkeypatch):
    import api.routes.tool as tool_route

    tool = _http_tool_model(method="POST")
    monkeypatch.setattr(
        tool_route.db_client, "get_tool_by_uuid", AsyncMock(return_value=tool)
    )
    monkeypatch.setattr(
        tool_route,
        "execute_http_tool",
        AsyncMock(return_value={"status": "success", "status_code": 200, "data": None}),
    )

    resp = await call_test_tool_route(
        "tu-http", request=ToolTestRequest(), user=_fake_user()
    )

    # POST with no arguments sends json={} over the wire; preview must show {}
    # so callers can distinguish an absent body from an empty one.
    assert resp.request_body == {}
    assert resp.request_params is None


@pytest.mark.asyncio
@pytest.mark.parametrize(
    "status_code,expected_snippet",
    [
        (400, "HTTP 400 Bad Request"),
        (401, "HTTP 401 Unauthorized"),
        (403, "HTTP 403 Forbidden"),
        (404, "HTTP 404 Not Found"),
        (405, "HTTP 405 Method Not Allowed"),
        (408, "HTTP 408 Request Timeout"),
        (409, "HTTP 409 Conflict"),
        (415, "HTTP 415 Unsupported Media Type"),
        (422, "HTTP 422 Unprocessable Entity"),
        (429, "HTTP 429 Too Many Requests"),
        (500, "HTTP 500"),
        (503, "HTTP 503"),
    ],
)
async def test_tool_test_hint_for_status_code(
    monkeypatch, status_code, expected_snippet
):
    import api.routes.tool as tool_route

    tool = _http_tool_model(method="POST")
    monkeypatch.setattr(
        tool_route.db_client, "get_tool_by_uuid", AsyncMock(return_value=tool)
    )
    monkeypatch.setattr(
        tool_route,
        "execute_http_tool",
        AsyncMock(
            return_value={
                "status": "error",
                "status_code": status_code,
                "error": "boom",
            }
        ),
    )

    resp = await call_test_tool_route(
        "tu-http", request=ToolTestRequest(llm_params={"a": 1}), user=_fake_user()
    )

    assert resp.hint is not None
    assert resp.hint.startswith(expected_snippet)


@pytest.mark.asyncio
async def test_tool_test_no_hint_on_success(monkeypatch):
    import api.routes.tool as tool_route

    tool = _http_tool_model(method="GET")
    monkeypatch.setattr(
        tool_route.db_client, "get_tool_by_uuid", AsyncMock(return_value=tool)
    )
    monkeypatch.setattr(
        tool_route,
        "execute_http_tool",
        AsyncMock(return_value={"status": "success", "status_code": 200, "data": {}}),
    )

    resp = await call_test_tool_route(
        "tu-http", request=ToolTestRequest(), user=_fake_user()
    )

    assert resp.hint is None


@pytest.mark.asyncio
async def test_tool_test_no_hint_for_uncovered_status_code(monkeypatch):
    import api.routes.tool as tool_route

    tool = _http_tool_model(method="GET")
    monkeypatch.setattr(
        tool_route.db_client, "get_tool_by_uuid", AsyncMock(return_value=tool)
    )
    monkeypatch.setattr(
        tool_route,
        "execute_http_tool",
        AsyncMock(
            return_value={"status": "error", "status_code": 418, "error": "teapot"}
        ),
    )

    resp = await call_test_tool_route(
        "tu-http", request=ToolTestRequest(), user=_fake_user()
    )

    assert resp.hint is None


def test_tool_test_route_is_registered():
    assert any(
        route.path == "/tools/{tool_uuid}/test" and "POST" in route.methods
        for route in router.routes
    )


@pytest.mark.asyncio
async def test_tool_rejects_non_http_api_tool(monkeypatch):
    import api.routes.tool as tool_route

    monkeypatch.setattr(
        tool_route.db_client,
        "get_tool_by_uuid",
        AsyncMock(return_value=_mcp_tool_model()),
    )

    with pytest.raises(HTTPException) as ei:
        await call_test_tool_route(
            "tu-mcp", request=ToolTestRequest(), user=_fake_user()
        )

    assert ei.value.status_code == 400


@pytest.mark.asyncio
async def test_refresh_success(monkeypatch):
    import api.services.tool_management as tool_svc

    tool = _mcp_tool_model()
    monkeypatch.setattr(
        tool_svc.db_client, "get_tool_by_uuid", AsyncMock(return_value=tool)
    )
    monkeypatch.setattr(
        tool_svc.db_client,
        "update_tool",
        AsyncMock(return_value=tool),
    )
    monkeypatch.setattr(
        tool_svc,
        "discover_mcp_tools",
        AsyncMock(return_value=[{"name": "echo", "description": "Echo"}]),
    )
    resp = await refresh_mcp_tools("tu-mcp", user=_fake_user())
    assert resp.discovered_tools == [{"name": "echo", "description": "Echo"}]
    assert resp.error is None


@pytest.mark.asyncio
async def test_refresh_server_down_returns_200_with_error(monkeypatch):
    import api.services.tool_management as tool_svc

    tool = _mcp_tool_model()
    monkeypatch.setattr(
        tool_svc.db_client, "get_tool_by_uuid", AsyncMock(return_value=tool)
    )
    monkeypatch.setattr(tool_svc.db_client, "update_tool", AsyncMock(return_value=tool))
    monkeypatch.setattr(tool_svc, "discover_mcp_tools", AsyncMock(return_value=[]))
    resp = await refresh_mcp_tools("tu-mcp", user=_fake_user())
    assert resp.discovered_tools == []
    assert resp.error  # non-empty human-readable message
    # update_tool should NOT be called when discovery returns empty
    tool_svc.db_client.update_tool.assert_not_called()


@pytest.mark.asyncio
async def test_refresh_non_mcp_is_400(monkeypatch):
    import api.services.tool_management as tool_svc

    tool = _mcp_tool_model()
    tool.category = "http_api"
    monkeypatch.setattr(
        tool_svc.db_client, "get_tool_by_uuid", AsyncMock(return_value=tool)
    )
    with pytest.raises(HTTPException) as ei:
        await refresh_mcp_tools("tu-mcp", user=_fake_user())
    assert ei.value.status_code == 400


@pytest.mark.asyncio
async def test_refresh_not_found_is_404(monkeypatch):
    import api.services.tool_management as tool_svc

    monkeypatch.setattr(
        tool_svc.db_client, "get_tool_by_uuid", AsyncMock(return_value=None)
    )
    with pytest.raises(HTTPException) as ei:
        await refresh_mcp_tools("nope", user=_fake_user())
    assert ei.value.status_code == 404


def test_tool_test_response_has_hint_and_request_fields():
    """ToolTestResponse must carry hint + request_method/url/body/params
    so the frontend can show what was sent and why it may have failed."""
    resp = ToolTestResponse(
        status="error",
        status_code=405,
        data=None,
        error="Method Not Allowed",
        duration_ms=12,
        hint="HTTP 405 Method Not Allowed — the endpoint rejected the configured method (POST).",
        request_method="POST",
        request_url="https://example.com/thing",
        request_headers={"Authorization": "********oken"},
        request_body={"a": 1},
        request_params=None,
    )
    assert resp.hint.startswith("HTTP 405")
    assert resp.request_method == "POST"
    assert resp.request_url == "https://example.com/thing"
    assert resp.request_headers == {"Authorization": "********oken"}
    assert resp.request_body == {"a": 1}
    assert resp.request_params is None


def test_tool_test_response_request_fields_default_to_none_or_required():
    """hint/request_body/request_params are optional; request_method/url are required."""
    resp = ToolTestResponse(
        status="success",
        duration_ms=5,
        request_method="GET",
        request_url="https://example.com/thing",
    )
    assert resp.hint is None
    assert resp.request_headers == {}
    assert resp.request_body is None
    assert resp.request_params is None
