"""The generated spec has to be usable by a machine, not just readable.

docs/openapi.json is the only machine-readable description of this API, and it
is what the MCP server builds its tools from. It was published with no
`parameters` and no `requestBody` on any operation - invalid OpenAPI 3.1, and
worse in practice: a tool built from an operation with no parameters has nowhere
to put the id its caller supplied, so every argument was dropped in silence. A
call for one asset returned the list.

These tests are the floor. They check the spec's shape, not its prose.
"""

import json
import re
from pathlib import Path

import pytest

REPO = Path(__file__).resolve().parents[1]
SPEC = REPO / 'docs' / 'openapi.json'

pytestmark = pytest.mark.skipif(
    not SPEC.is_file(),
    reason='no generated spec in this checkout')


@pytest.fixture(scope='module')
def spec():
    return json.loads(SPEC.read_text())


def operations(spec):
    for path, item in spec['paths'].items():
        for verb, operation in item.items():
            yield path, verb, operation


def test_every_templated_path_declares_its_parameters(spec):
    """`/api/assets/{id}` with no parameters is not a description of anything."""
    offenders = [
        '%s %s' % (verb.upper(), path)
        for path, verb, operation in operations(spec)
        if '{' in path and not operation.get('parameters')
    ]
    assert not offenders, (
        'These operations have a templated path and no parameters, so a '
        'generated client cannot fill it in:\n  ' + '\n  '.join(offenders[:15]))


def test_declared_parameters_match_the_template(spec):
    """A parameter that is not in the path, or a placeholder with no parameter,
    is the same defect wearing different clothes."""
    offenders = []
    for path, verb, operation in operations(spec):
        intemplate = set(re.findall(r'\{([^}]+)\}', path))
        declared = {p['name'] for p in operation.get('parameters', [])
                    if p.get('in') == 'path'}
        if intemplate != declared:
            offenders.append('%s %s: template %s, declared %s'
                             % (verb.upper(), path, sorted(intemplate), sorted(declared)))
    assert not offenders, '\n  '.join(offenders[:15])


def test_write_verbs_can_carry_a_body(spec):
    offenders = [
        '%s %s' % (verb.upper(), path)
        for path, verb, operation in operations(spec)
        if verb in ('post', 'put') and 'requestBody' not in operation
    ]
    assert not offenders, (
        'These write operations declare no request body, so a client built '
        'from this spec cannot send one:\n  ' + '\n  '.join(offenders[:15]))


def test_optional_auth_is_expressed_as_optional(spec):
    """An endpoint that works logged out must not be published as requiring a
    token - that is a lie to every reader, and it is the majority case here."""
    optional = [operation for _, _, operation in operations(spec)
                if any(item == {} for item in operation.get('security') or [])]
    assert len(optional) > 50, (
        'The inventory documents over a hundred jwt-optional endpoints; the '
        'spec should express them with an empty security requirement '
        'alongside the scheme, not as bearer-required.')


def test_an_endpoint_that_needs_no_token_cannot_fail_with_401(spec):
    offenders = [
        '%s %s' % (verb.upper(), path)
        for path, verb, operation in operations(spec)
        if not operation.get('security') and '401' in operation.get('responses', {})
    ]
    assert not offenders, '\n  '.join(offenders[:15])


def test_every_operation_documents_failure(spec):
    offenders = [
        '%s %s' % (verb.upper(), path)
        for path, verb, operation in operations(spec)
        if 'default' not in operation.get('responses', {})
    ]
    assert not offenders, (
        'These operations document only success:\n  ' + '\n  '.join(offenders[:15]))


def test_the_envelopes_are_defined_and_referenced(spec):
    schemas = spec.get('components', {}).get('schemas', {})
    assert 'SuccessEnvelope' in schemas and 'ErrorEnvelope' in schemas
    # The error shape is the one people get wrong: nested under data.
    error = schemas['ErrorEnvelope']['properties']['data']['properties']['error']
    assert set(error['required']) == {'code', 'message'}


def test_summaries_are_not_cut_mid_word(spec):
    """A truncated summary is what a tool picker shows as the whole description
    of a call."""
    offenders = []
    for path, verb, operation in operations(spec):
        summary = operation.get('summary', '')
        if summary.endswith('...'):
            body = summary[:-3]
            if body and not body[-1].isalnum() and body[-1] not in ')]"\'':
                offenders.append('%s %s: %r' % (verb.upper(), path, summary[-40:]))
    assert not offenders, '\n  '.join(offenders[:15])
