import json

import openapi_spec_validator as osv
import pytest
from flask import request

from .schemas import Bar
from .schemas import Baz
from .schemas import Foo
from apiflask import APIBlueprint
from apiflask import Schema
from apiflask.commands import spec_command
from apiflask.fields import Integer


def test_spec(app):
    assert app.spec
    assert 'openapi' in app.spec


def test_spec_processor(app, client):
    @app.spec_processor
    def edit_spec(spec):
        assert spec['openapi'] == '3.0.3'
        spec['openapi'] = '3.0.2'
        assert app.title == 'APIFlask'
        assert spec['info']['title'] == 'APIFlask'
        spec['info']['title'] = 'Foo'
        return spec

    rv = client.get('/openapi.json')
    assert rv.status_code == 200
    osv.validate(rv.json)
    assert rv.json['openapi'] == '3.0.2'
    assert rv.json['info']['title'] == 'Foo'


def test_spec_processor_pass_object(app, client):
    app.config['SPEC_PROCESSOR_PASS_OBJECT'] = True

    class NotUsedSchema(Schema):
        id = Integer()

    @app.spec_processor
    def process_spec(spec):
        spec.title = 'Foo'
        spec.components.schema('NotUsed', schema=NotUsedSchema)
        return spec

    rv = client.get('/openapi.json')
    assert rv.status_code == 200
    osv.validate(rv.json)
    assert rv.json['info']['title'] == 'Foo'
    assert 'NotUsed' in rv.json['components']['schemas']
    assert 'id' in rv.json['components']['schemas']['NotUsed']['properties']


@pytest.mark.parametrize('spec_format', ['json', 'yaml', 'yml'])
def test_get_spec(app, spec_format):
    spec = app._get_spec(spec_format)

    if spec_format == 'json':
        assert isinstance(spec, dict)
    else:
        assert 'title: APIFlask' in spec


def test_get_spec_force_update(app):
    app._get_spec()

    @app.route('/foo')
    @app.output(Foo)
    def foo():
        pass

    spec = app._get_spec()
    assert '/foo' not in spec['paths']

    new_spec = app._get_spec(force_update=True)
    assert '/foo' in new_spec['paths']


def test_spec_bypass_endpoints(app):
    bp = APIBlueprint('foo', __name__, static_folder='static', url_prefix='/foo')
    app.register_blueprint(bp)

    spec = app._get_spec()
    assert '/static' not in spec['paths']
    assert '/foo/static' not in spec['paths']
    assert '/docs' not in spec['paths']
    assert '/openapi.json' not in spec['paths']
    assert '/docs/oauth2-redirect' not in spec['paths']


def test_spec_bypass_methods(app):
    class Foo:
        def bar(self):
            pass

    app.add_url_rule('/foo', 'foo', Foo().bar)

    spec = app._get_spec()
    assert '/foo' not in spec['paths']


def test_spec_attribute(app):
    spec = app._get_spec()

    @app.route('/foo')
    @app.output(Foo)
    def foo():
        pass

    assert '/foo' not in spec['paths']
    assert '/foo' in app.spec['paths']


def test_spec_schemas(app):
    @app.route('/foo')
    @app.output(Foo(partial=True))
    def foo():
        pass

    @app.route('/bar')
    @app.output(Bar(many=True))
    def bar():
        pass

    @app.route('/baz')
    @app.output(Baz)
    def baz():
        pass

    class Spam(Schema):
        id = Integer()

    @app.route('/spam')
    @app.output(Spam)
    def spam():
        pass

    class Ham(Schema):
        id = Integer()

    @app.route('/ham')
    @app.output(Ham)
    def ham():
        pass

    spec = app.spec
    assert len(spec['components']['schemas']) == 5
    assert 'FooUpdate' in spec['components']['schemas']
    assert 'Bar' in spec['components']['schemas']
    assert 'Baz' in spec['components']['schemas']
    assert 'Spam' in spec['components']['schemas']
    assert 'Ham' in spec['components']['schemas']


def test_servers_and_externaldocs(app):
    assert app.external_docs is None
    assert app.servers is None

    app.external_docs = {'description': 'Find more info here', 'url': 'https://docs.example.com/'}
    app.servers = [
        {'url': 'http://localhost:5000/', 'description': 'Development server'},
        {'url': 'https://api.example.com/', 'description': 'Production server'},
    ]

    rv = app.test_client().get('/openapi.json')
    assert rv.status_code == 200
    osv.validate(rv.json)
    assert rv.json['externalDocs'] == {
        'description': 'Find more info here',
        'url': 'https://docs.example.com/',
    }
    assert rv.json['servers'] == [
        {'url': 'http://localhost:5000/', 'description': 'Development server'},
        {'url': 'https://api.example.com/', 'description': 'Production server'},
    ]


def test_default_servers(app):
    assert app.servers is None

    rv = app.test_client().get('/openapi.json')
    assert rv.status_code == 200
    osv.validate(rv.json)
    with app.test_request_context():
        assert rv.json['servers'] == [
            {
                'url': f'{request.url_root}',
            },
        ]


def test_default_servers_without_req_context(cli_runner):
    result = cli_runner.invoke(spec_command)
    assert 'openapi' in result.output
    assert 'servers' not in json.loads(result.output)


def test_auto_200_response(app, client):
    @app.get('/foo')
    def bare():
        pass

    @app.get('/bar')
    @app.input(Foo)
    def only_input():
        pass

    @app.get('/baz')
    @app.doc(summary='some summary')
    def only_doc():
        pass

    @app.get('/eggs')
    @app.output(Foo, status_code=204)
    def output_204():
        pass

    @app.get('/spam')
    @app.doc(responses={204: 'empty'})
    def doc_responses():
        pass

    rv = client.get('/openapi.json')
    assert rv.status_code == 200
    osv.validate(rv.json)
    assert '200' in rv.json['paths']['/foo']['get']['responses']
    assert '200' in rv.json['paths']['/bar']['get']['responses']
    assert '200' in rv.json['paths']['/baz']['get']['responses']
    assert '200' not in rv.json['paths']['/eggs']['get']['responses']
    assert '200' not in rv.json['paths']['/spam']['get']['responses']
    assert rv.json['paths']['/spam']['get']['responses']['204']['description'] == 'empty'


def test_sync_local_json_spec(app, client, tmp_path):
    app.config['AUTO_SERVERS'] = False

    local_spec_path = tmp_path / 'openapi.json'
    app.config['SYNC_LOCAL_SPEC'] = True
    app.config['LOCAL_SPEC_PATH'] = local_spec_path
    app.config['SPEC_FORMAT'] = 'json'

    rv = client.get('/openapi.json')
    assert rv.status_code == 200
    osv.validate(rv.json)

    with open(local_spec_path) as f:
        spec_content = json.loads(f.read())
        assert spec_content == app.spec
        assert 'info' in spec_content
        assert 'paths' in spec_content


def test_sync_local_yaml_spec(app, client, tmp_path):
    app.config['AUTO_SERVERS'] = False

    local_spec_path = tmp_path / 'openapi.json'
    app.config['SYNC_LOCAL_SPEC'] = True
    app.config['LOCAL_SPEC_PATH'] = local_spec_path
    app.config['SPEC_FORMAT'] = 'yaml'

    rv = client.get('/openapi.json')
    assert rv.status_code == 200

    with open(local_spec_path) as f:
        spec_content = f.read()
        assert spec_content == str(app.spec)
        assert 'title: APIFlask' in spec_content


def test_sync_local_spec_no_path(app):
    app.config['SYNC_LOCAL_SPEC'] = True

    with pytest.raises(TypeError):
        app.spec
