"""Tests for the custom-tool HTTP executor: path templating and HTTP methods.

Pure module (tool_handlers) — no server import, no API key needed.
"""

import json

import pytest

import tool_handlers
from tool_handlers import fill_endpoint_path, execute_tool_endpoint


class TestFillEndpointPath:

    def test_no_placeholders_passthrough(self):
        ep, args, missing = fill_endpoint_path('https://x/api/v1/messages', {'a': 1})
        assert ep == 'https://x/api/v1/messages'
        assert args == {'a': 1}
        assert missing == []

    def test_substitutes_and_consumes_argument(self):
        ep, args, missing = fill_endpoint_path(
            'https://x/api/ai/v1/appointments/{id}', {'id': 'a_12', 'start': 'T'})
        assert ep == 'https://x/api/ai/v1/appointments/a_12'
        assert args == {'start': 'T'}           # id removed from body
        assert missing == []

    def test_url_encodes_value(self):
        ep, _, _ = fill_endpoint_path('https://x/{id}', {'id': 'a/1 2'})
        assert ep == 'https://x/a%2F1%202'

    def test_missing_or_blank_reported(self):
        ep, args, missing = fill_endpoint_path('https://x/{id}/{sub}', {'id': ''})
        assert missing == ['id', 'sub']
        assert '{id}' in ep                       # untouched when missing

    def test_does_not_mutate_input(self):
        src = {'id': 'a_1', 'k': 'v'}
        fill_endpoint_path('https://x/{id}', src)
        assert src == {'id': 'a_1', 'k': 'v'}


class _Resp:
    def __init__(self, status=200, body=None, text=''):
        self.status_code = status
        self.ok = 200 <= status < 300
        self._body = body
        self.text = text or (json.dumps(body) if body is not None else '')

    def json(self):
        if self._body is None:
            raise ValueError('no json')
        return self._body


class TestExecuteToolEndpoint:

    @pytest.fixture
    def spy(self, monkeypatch):
        calls = []

        def fake_request(method, url, **kw):
            calls.append((method, url, kw))
            return _Resp(200, {'ok': True, 'id': 'a_12'})
        monkeypatch.setattr(tool_handlers.requests, 'request', fake_request)
        return calls

    def test_post_default_sends_json_body(self, spy):
        tool = {'x_endpoint': 'https://x/api/ai/v1/messages',
                'x_headers': {'Authorization': 'Bearer test'}}
        out = execute_tool_endpoint(tool, {'extension': '8704', 'type': 'note'})
        method, url, kw = spy[0]
        assert method == 'POST' and url == 'https://x/api/ai/v1/messages'
        assert kw['json'] == {'extension': '8704', 'type': 'note'}
        assert kw['headers']['Authorization'] == 'Bearer test'
        assert kw['headers']['Content-Type'] == 'application/json'
        assert json.loads(out)['id'] == 'a_12'

    def test_get_sends_query_params(self, spy):
        tool = {'x_endpoint': 'https://x/api/ai/v1/appointments', 'x_method': 'GET'}
        execute_tool_endpoint(tool, {'extension': '8704', 'from': '2026-10-02T00:00:00+08:00'})
        method, url, kw = spy[0]
        assert method == 'GET'
        assert kw['params'] == {'extension': '8704', 'from': '2026-10-02T00:00:00+08:00'}
        assert 'json' not in kw

    def test_get_with_path_param(self, spy):
        tool = {'x_endpoint': 'https://x/api/ai/v1/appointments/{id}', 'x_method': 'GET'}
        execute_tool_endpoint(tool, {'id': 'a_12'})
        method, url, kw = spy[0]
        assert url == 'https://x/api/ai/v1/appointments/a_12'
        assert kw['params'] is None               # nothing left after templating

    def test_put_with_path_param_sends_remaining_body(self, spy):
        tool = {'x_endpoint': 'https://x/api/ai/v1/appointments/{id}', 'x_method': 'PUT'}
        execute_tool_endpoint(tool, {'id': 'a_12', 'start': '2026-10-02T16:00:00+08:00'})
        method, url, kw = spy[0]
        assert method == 'PUT'
        assert url.endswith('/appointments/a_12')
        assert kw['json'] == {'start': '2026-10-02T16:00:00+08:00'}

    def test_delete_with_path_param(self, spy):
        tool = {'x_endpoint': 'https://x/api/ai/v1/appointments/{id}', 'x_method': 'delete'}
        execute_tool_endpoint(tool, {'id': 'a_12'})
        method, url, kw = spy[0]
        assert method == 'DELETE'
        assert url.endswith('/appointments/a_12')
        assert 'json' not in kw

    def test_missing_path_param_does_not_call(self, spy):
        tool = {'x_endpoint': 'https://x/api/ai/v1/appointments/{id}', 'x_method': 'DELETE'}
        out = execute_tool_endpoint(tool, {})
        assert spy == []
        assert 'missing argument' in out and 'id' in out

    def test_x_query_params_forces_get_as_before(self, spy):
        # Legacy semantics: x_query_params always meant GET with params,
        # even when x_method says otherwise (references/booking_agent.json relies on it).
        tool = {'x_endpoint': 'https://x/slots', 'x_method': 'POST', 'x_query_params': True}
        execute_tool_endpoint(tool, {'date': '2026-10-02'})
        method, url, kw = spy[0]
        assert method == 'GET' and kw['params'] == {'date': '2026-10-02'} and 'json' not in kw

    def test_unknown_method_falls_back_to_post(self, spy):
        tool = {'x_endpoint': 'https://x/y', 'x_method': 'BREW'}
        execute_tool_endpoint(tool, {'a': 1})
        assert spy[0][0] == 'POST'

    def test_non_2xx_returns_api_error_with_status(self, monkeypatch):
        monkeypatch.setattr(tool_handlers.requests, 'request',
                            lambda m, u, **kw: _Resp(409, {'ok': False, 'error': 'taken'}))
        out = execute_tool_endpoint({'x_endpoint': 'https://x/y'}, {'a': 1})
        assert out.startswith('API error: 409')

    def test_no_endpoint_returns_none(self):
        assert execute_tool_endpoint({'function': {'name': 'dial_department'}}, {'department': 'Accounts'}) is None
