import json
import sys

import pytest
from marshmallow import Schema
from marshmallow.fields import (
    DateTime,
    Dict,
    Field,
    Int,
    List,
    Nested,
    String,
    TimeDelta,
)

from apispec import APISpec
from apispec.exceptions import APISpecError
from apispec.ext.marshmallow import MarshmallowPlugin, common

from .schemas import (
    AnalysisSchema,
    AnalysisWithListSchema,
    DefaultValuesSchema,
    PatternedObjectSchema,
    PetSchema,
    RunSchema,
    SampleSchema,
    SelfReferencingSchema,
)
from .utils import (
    build_ref,
    get_headers,
    get_parameters,
    get_paths,
    get_responses,
    get_schemas,
)


class TestDefinitionHelper:
    @pytest.mark.parametrize("schema", [PetSchema, PetSchema()])
    def test_can_use_schema_as_definition(self, spec, schema):
        spec.components.schema("Pet", schema=schema)
        definitions = get_schemas(spec)
        props = definitions["Pet"]["properties"]

        assert props["id"]["type"] == "integer"
        assert props["name"]["type"] == "string"

    def test_schema_helper_without_schema(self, spec):
        spec.components.schema("Pet", {"properties": {"key": {"type": "integer"}}})
        definitions = get_schemas(spec)
        assert definitions["Pet"]["properties"] == {"key": {"type": "integer"}}

    @pytest.mark.parametrize("schema", [AnalysisSchema, AnalysisSchema()])
    def test_resolve_schema_dict_auto_reference(self, schema):
        def resolver(schema):
            schema_cls = common.resolve_schema_cls(schema)
            return schema_cls.__name__

        spec = APISpec(
            title="Test auto-reference",
            version="0.1",
            openapi_version="2.0",
            plugins=(MarshmallowPlugin(schema_name_resolver=resolver),),
        )
        with pytest.raises(KeyError):
            get_schemas(spec)

        spec.components.schema("analysis", schema=schema)
        spec.path(
            "/test",
            operations={
                "get": {
                    "responses": {
                        "200": {"schema": build_ref(spec, "schema", "analysis")}
                    }
                }
            },
        )
        definitions = get_schemas(spec)
        assert 3 == len(definitions)

        assert "analysis" in definitions
        assert "SampleSchema" in definitions
        assert "RunSchema" in definitions

    @pytest.mark.parametrize(
        "schema", [AnalysisWithListSchema, AnalysisWithListSchema()]
    )
    def test_resolve_schema_dict_auto_reference_in_list(self, schema):
        def resolver(schema):
            schema_cls = common.resolve_schema_cls(schema)
            return schema_cls.__name__

        spec = APISpec(
            title="Test auto-reference",
            version="0.1",
            openapi_version="2.0",
            plugins=(MarshmallowPlugin(schema_name_resolver=resolver),),
        )
        with pytest.raises(KeyError):
            get_schemas(spec)

        spec.components.schema("analysis", schema=schema)
        spec.path(
            "/test",
            operations={
                "get": {
                    "responses": {
                        "200": {"schema": build_ref(spec, "schema", "analysis")}
                    }
                }
            },
        )
        definitions = get_schemas(spec)
        assert 3 == len(definitions)

        assert "analysis" in definitions
        assert "SampleSchema" in definitions
        assert "RunSchema" in definitions

    @pytest.mark.parametrize("schema", [AnalysisSchema, AnalysisSchema()])
    def test_resolve_schema_dict_auto_reference_return_none(self, schema):
        def resolver(schema):
            return None

        spec = APISpec(
            title="Test auto-reference",
            version="0.1",
            openapi_version="2.0",
            plugins=(MarshmallowPlugin(schema_name_resolver=resolver),),
        )
        with pytest.raises(KeyError):
            get_schemas(spec)

        with pytest.raises(
            APISpecError, match="Name resolver returned None for schema"
        ):
            spec.components.schema("analysis", schema=schema)

    @pytest.mark.parametrize("schema", [AnalysisSchema, AnalysisSchema()])
    def test_warning_when_schema_added_twice(self, spec, schema):
        spec.components.schema("Analysis", schema=schema)
        with pytest.warns(UserWarning, match="has already been added to the spec"):
            spec.components.schema("DuplicateAnalysis", schema=schema)

    def test_schema_instances_with_different_modifiers_added(self, spec):
        class MultiModifierSchema(Schema):
            pet_unmodified = Nested(PetSchema)
            pet_exclude = Nested(PetSchema, exclude=("name",))

        spec.components.schema("Pet", schema=PetSchema())
        spec.components.schema("Pet_Exclude", schema=PetSchema(exclude=("name",)))

        spec.components.schema("MultiModifierSchema", schema=MultiModifierSchema)

        definitions = get_schemas(spec)
        pet_unmodified_ref = definitions["MultiModifierSchema"]["properties"][
            "pet_unmodified"
        ]
        assert pet_unmodified_ref == build_ref(spec, "schema", "Pet")

        pet_exclude = definitions["MultiModifierSchema"]["properties"]["pet_exclude"]
        assert pet_exclude == build_ref(spec, "schema", "Pet_Exclude")

    def test_schema_instance_with_different_modifers_custom_resolver(self, recwarn):
        class MultiModifierSchema(Schema):
            pet_unmodified = Nested(PetSchema)
            pet_exclude = Nested(PetSchema(partial=True))

        def resolver(schema):
            schema_instance = common.resolve_schema_instance(schema)
            prefix = "Partial-" if schema_instance.partial else ""
            schema_cls = common.resolve_schema_cls(schema)
            name = prefix + schema_cls.__name__
            if name.endswith("Schema"):
                return name[:-6] or name
            return name

        spec = APISpec(
            title="Test Custom Resolver for Partial",
            version="0.1",
            openapi_version="2.0",
            plugins=(MarshmallowPlugin(schema_name_resolver=resolver),),
        )

        spec.components.schema("NameClashSchema", schema=MultiModifierSchema)
        assert not recwarn

    def test_schema_with_clashing_names(self, spec):
        class Pet(PetSchema):
            another_field = String()

        class NameClashSchema(Schema):
            pet_1 = Nested(PetSchema)
            pet_2 = Nested(Pet)

        with pytest.warns(
            UserWarning, match="Multiple schemas resolved to the name Pet"
        ):
            spec.components.schema("NameClashSchema", schema=NameClashSchema)

        definitions = get_schemas(spec)

        assert "Pet" in definitions
        assert "Pet1" in definitions

    def test_resolve_nested_schema_many_true_resolver_return_none(self):
        def resolver(schema):
            return None

        class PetFamilySchema(Schema):
            pets_1 = Nested(PetSchema, many=True)
            pets_2 = List(Nested(PetSchema))

        spec = APISpec(
            title="Test auto-reference",
            version="0.1",
            openapi_version="2.0",
            plugins=(MarshmallowPlugin(schema_name_resolver=resolver),),
        )

        spec.components.schema("PetFamily", schema=PetFamilySchema)
        props = get_schemas(spec)["PetFamily"]["properties"]
        pets_1 = props["pets_1"]
        pets_2 = props["pets_2"]
        assert pets_1["type"] == pets_2["type"] == "array"


class TestComponentParameterHelper:
    @pytest.mark.parametrize("schema", [PetSchema, PetSchema()])
    def test_can_use_schema_in_parameter(self, spec, schema):
        param = {"schema": schema}
        spec.components.parameter("Pet", "body", param)
        parameter = get_parameters(spec)["Pet"]
        assert parameter["in"] == "body"
        reference = parameter["schema"]
        assert reference == build_ref(spec, "schema", "Pet")

        resolved_schema = spec.components.schemas["Pet"]
        assert resolved_schema["properties"]["name"]["type"] == "string"
        assert resolved_schema["properties"]["password"]["type"] == "string"
        assert resolved_schema["properties"]["id"]["type"] == "integer"

    @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True)
    @pytest.mark.parametrize("schema", [PetSchema, PetSchema()])
    def test_can_use_schema_in_parameter_with_content(self, spec, schema):
        param = {"content": {"application/json": {"schema": schema}}}
        spec.components.parameter("Pet", "body", param)
        parameter = get_parameters(spec)["Pet"]
        assert parameter["in"] == "body"
        reference = parameter["content"]["application/json"]["schema"]
        assert reference == build_ref(spec, "schema", "Pet")

        resolved_schema = spec.components.schemas["Pet"]
        assert resolved_schema["properties"]["name"]["type"] == "string"
        assert resolved_schema["properties"]["password"]["type"] == "string"
        assert resolved_schema["properties"]["id"]["type"] == "integer"


class TestComponentResponseHelper:
    @pytest.mark.parametrize("schema", [PetSchema, PetSchema()])
    def test_can_use_schema_in_response(self, spec, schema):
        if spec.openapi_version.major < 3:
            resp = {"schema": schema}
        else:
            resp = {"content": {"application/json": {"schema": schema}}}
        spec.components.response("GetPetOk", resp)
        response = get_responses(spec)["GetPetOk"]
        if spec.openapi_version.major < 3:
            reference = response["schema"]
        else:
            reference = response["content"]["application/json"]["schema"]
        assert reference == build_ref(spec, "schema", "Pet")

        resolved_schema = spec.components.schemas["Pet"]
        assert resolved_schema["properties"]["id"]["type"] == "integer"
        assert resolved_schema["properties"]["name"]["type"] == "string"
        assert resolved_schema["properties"]["password"]["type"] == "string"

    @pytest.mark.parametrize("schema", [PetSchema, PetSchema()])
    def test_can_use_schema_in_response_header(self, spec, schema):
        resp = {"headers": {"PetHeader": {"schema": schema}}}
        spec.components.response("GetPetOk", resp)
        response = get_responses(spec)["GetPetOk"]
        reference = response["headers"]["PetHeader"]["schema"]
        assert reference == build_ref(spec, "schema", "Pet")

        resolved_schema = spec.components.schemas["Pet"]
        assert resolved_schema["properties"]["id"]["type"] == "integer"
        assert resolved_schema["properties"]["name"]["type"] == "string"
        assert resolved_schema["properties"]["password"]["type"] == "string"

    @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True)
    def test_content_without_schema(self, spec):
        resp = {"content": {"application/json": {"example": {"name": "Example"}}}}
        spec.components.response("GetPetOk", resp)
        response = get_responses(spec)["GetPetOk"]
        assert response == resp


class TestComponentHeaderHelper:
    @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True)
    @pytest.mark.parametrize("schema", [PetSchema, PetSchema()])
    def test_can_use_schema_in_header(self, spec, schema):
        param = {"schema": schema}
        spec.components.header("Pet", param)
        header = get_headers(spec)["Pet"]
        reference = header["schema"]
        assert reference == build_ref(spec, "schema", "Pet")

        resolved_schema = spec.components.schemas["Pet"]
        assert resolved_schema["properties"]["name"]["type"] == "string"
        assert resolved_schema["properties"]["password"]["type"] == "string"
        assert resolved_schema["properties"]["id"]["type"] == "integer"


class TestCustomField:
    def test_can_use_custom_field_decorator(self, spec_fixture):
        class CustomNameA(Field):
            pass

        spec_fixture.marshmallow_plugin.map_to_openapi_type(CustomNameA, DateTime)

        class CustomNameB(Field):
            pass

        spec_fixture.marshmallow_plugin.map_to_openapi_type(
            CustomNameB, "integer", "int32"
        )

        class BadCustomField(Field):
            pass

        with pytest.raises(TypeError):
            spec_fixture.marshmallow_plugin.map_to_openapi_type(
                BadCustomField, "integer"
            )

        class CustomPetASchema(PetSchema):
            name = CustomNameA()

        class CustomPetBSchema(PetSchema):
            name = CustomNameB()

        spec_fixture.spec.components.schema("Pet", schema=PetSchema)
        spec_fixture.spec.components.schema("CustomPetA", schema=CustomPetASchema)
        spec_fixture.spec.components.schema("CustomPetB", schema=CustomPetBSchema)

        props_0 = get_schemas(spec_fixture.spec)["Pet"]["properties"]
        props_a = get_schemas(spec_fixture.spec)["CustomPetA"]["properties"]
        props_b = get_schemas(spec_fixture.spec)["CustomPetB"]["properties"]

        assert props_0["name"]["type"] == "string"
        assert "format" not in props_0["name"]

        assert props_a["name"]["type"] == "string"
        assert props_a["name"]["format"] == "date-time"

        assert props_b["name"]["type"] == "integer"
        assert props_b["name"]["format"] == "int32"


def get_nested_schema(schema, field_name):
    try:
        return schema._declared_fields[field_name]._schema
    except AttributeError:
        return schema._declared_fields[field_name]._Nested__schema


class TestOperationHelper:
    @pytest.fixture
    def make_pet_callback_spec(self, spec_fixture):
        def _make_pet_spec(operations):
            spec_fixture.spec.path(
                path="/pet",
                operations={
                    "post": {"callbacks": {"petEvent": {"petCallbackUrl": operations}}}
                },
            )
            return spec_fixture

        return _make_pet_spec

    @pytest.mark.parametrize(
        "pet_schema",
        (PetSchema, PetSchema(), PetSchema(many=True), "tests.schemas.PetSchema"),
    )
    @pytest.mark.parametrize("spec_fixture", ("2.0",), indirect=True)
    def test_schema_v2(self, spec_fixture, pet_schema):
        spec_fixture.spec.path(
            path="/pet",
            operations={
                "get": {
                    "responses": {
                        200: {
                            "schema": pet_schema,
                            "description": "successful operation",
                            "headers": {"PetHeader": {"schema": pet_schema}},
                        }
                    }
                }
            },
        )
        get = get_paths(spec_fixture.spec)["/pet"]["get"]
        if isinstance(pet_schema, Schema) and pet_schema.many is True:
            assert get["responses"]["200"]["schema"]["type"] == "array"
            schema_reference = get["responses"]["200"]["schema"]["items"]
            assert (
                get["responses"]["200"]["headers"]["PetHeader"]["schema"]["type"]
                == "array"
            )
            header_reference = get["responses"]["200"]["headers"]["PetHeader"][
                "schema"
            ]["items"]
        else:
            schema_reference = get["responses"]["200"]["schema"]
            header_reference = get["responses"]["200"]["headers"]["PetHeader"]["schema"]
        assert schema_reference == build_ref(spec_fixture.spec, "schema", "Pet")
        assert header_reference == build_ref(spec_fixture.spec, "schema", "Pet")
        assert len(spec_fixture.spec.components.schemas) == 1
        resolved_schema = spec_fixture.spec.components.schemas["Pet"]
        assert resolved_schema == spec_fixture.openapi.schema2jsonschema(PetSchema)
        assert get["responses"]["200"]["description"] == "successful operation"

    @pytest.mark.parametrize(
        "pet_schema",
        (PetSchema, PetSchema(), PetSchema(many=True), "tests.schemas.PetSchema"),
    )
    @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True)
    def test_schema_v3(self, spec_fixture, pet_schema):
        spec_fixture.spec.path(
            path="/pet",
            operations={
                "get": {
                    "responses": {
                        200: {
                            "content": {"application/json": {"schema": pet_schema}},
                            "description": "successful operation",
                            "headers": {"PetHeader": {"schema": pet_schema}},
                        }
                    }
                }
            },
        )
        get = get_paths(spec_fixture.spec)["/pet"]["get"]
        if isinstance(pet_schema, Schema) and pet_schema.many is True:
            assert (
                get["responses"]["200"]["content"]["application/json"]["schema"]["type"]
                == "array"
            )
            schema_reference = get["responses"]["200"]["content"]["application/json"][
                "schema"
            ]["items"]
            assert (
                get["responses"]["200"]["headers"]["PetHeader"]["schema"]["type"]
                == "array"
            )
            header_reference = get["responses"]["200"]["headers"]["PetHeader"][
                "schema"
            ]["items"]
        else:
            schema_reference = get["responses"]["200"]["content"]["application/json"][
                "schema"
            ]
            header_reference = get["responses"]["200"]["headers"]["PetHeader"]["schema"]

        assert schema_reference == build_ref(spec_fixture.spec, "schema", "Pet")
        assert header_reference == build_ref(spec_fixture.spec, "schema", "Pet")
        assert len(spec_fixture.spec.components.schemas) == 1
        resolved_schema = spec_fixture.spec.components.schemas["Pet"]
        assert resolved_schema == spec_fixture.openapi.schema2jsonschema(PetSchema)
        assert get["responses"]["200"]["description"] == "successful operation"

    @pytest.mark.parametrize(
        "pet_schema",
        (PetSchema, PetSchema(), PetSchema(many=True), "tests.schemas.PetSchema"),
    )
    @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True)
    def test_callback_schema_v3(self, make_pet_callback_spec, pet_schema):
        spec_fixture = make_pet_callback_spec(
            {
                "get": {
                    "responses": {
                        "200": {
                            "content": {"application/json": {"schema": pet_schema}},
                            "description": "successful operation",
                            "headers": {"PetHeader": {"schema": pet_schema}},
                        }
                    }
                }
            }
        )
        p = get_paths(spec_fixture.spec)["/pet"]
        c = p["post"]["callbacks"]["petEvent"]["petCallbackUrl"]
        get = c["get"]
        if isinstance(pet_schema, Schema) and pet_schema.many is True:
            assert (
                get["responses"]["200"]["content"]["application/json"]["schema"]["type"]
                == "array"
            )
            schema_reference = get["responses"]["200"]["content"]["application/json"][
                "schema"
            ]["items"]
            assert (
                get["responses"]["200"]["headers"]["PetHeader"]["schema"]["type"]
                == "array"
            )
            header_reference = get["responses"]["200"]["headers"]["PetHeader"][
                "schema"
            ]["items"]
        else:
            schema_reference = get["responses"]["200"]["content"]["application/json"][
                "schema"
            ]
            header_reference = get["responses"]["200"]["headers"]["PetHeader"]["schema"]

        assert schema_reference == build_ref(spec_fixture.spec, "schema", "Pet")
        assert header_reference == build_ref(spec_fixture.spec, "schema", "Pet")
        assert len(spec_fixture.spec.components.schemas) == 1
        resolved_schema = spec_fixture.spec.components.schemas["Pet"]
        assert resolved_schema == spec_fixture.openapi.schema2jsonschema(PetSchema)
        assert get["responses"]["200"]["description"] == "successful operation"

    @pytest.mark.parametrize("spec_fixture", ("2.0",), indirect=True)
    def test_schema_expand_parameters_v2(self, spec_fixture):
        spec_fixture.spec.path(
            path="/pet",
            operations={
                "get": {"parameters": [{"in": "query", "schema": PetSchema}]},
                "post": {
                    "parameters": [
                        {
                            "in": "body",
                            "description": "a pet schema",
                            "required": True,
                            "name": "pet",
                            "schema": PetSchema,
                        }
                    ]
                },
            },
        )
        p = get_paths(spec_fixture.spec)["/pet"]
        get = p["get"]
        assert get["parameters"] == spec_fixture.openapi.schema2parameters(
            PetSchema(), location="query"
        )
        post = p["post"]
        assert post["parameters"] == spec_fixture.openapi.schema2parameters(
            PetSchema,
            location="body",
            required=True,
            name="pet",
            description="a pet schema",
        )

    @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True)
    def test_schema_expand_parameters_v3(self, spec_fixture):
        spec_fixture.spec.path(
            path="/pet",
            operations={
                "get": {"parameters": [{"in": "query", "schema": PetSchema}]},
                "post": {
                    "requestBody": {
                        "description": "a pet schema",
                        "required": True,
                        "content": {"application/json": {"schema": PetSchema}},
                    }
                },
            },
        )
        p = get_paths(spec_fixture.spec)["/pet"]
        get = p["get"]
        assert get["parameters"] == spec_fixture.openapi.schema2parameters(
            PetSchema(), location="query"
        )
        for parameter in get["parameters"]:
            description = parameter.get("description", False)
            assert description
            name = parameter["name"]
            assert description == PetSchema.description[name]
        post = p["post"]
        post_schema = spec_fixture.marshmallow_plugin.resolver.resolve_schema_dict(
            PetSchema
        )
        assert (
            post["requestBody"]["content"]["application/json"]["schema"] == post_schema
        )
        assert post["requestBody"]["description"] == "a pet schema"
        assert post["requestBody"]["required"]

    @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True)
    def test_callback_schema_expand_parameters_v3(self, make_pet_callback_spec):
        spec_fixture = make_pet_callback_spec(
            {
                "get": {"parameters": [{"in": "query", "schema": PetSchema}]},
                "post": {
                    "requestBody": {
                        "description": "a pet schema",
                        "required": True,
                        "content": {"application/json": {"schema": PetSchema}},
                    }
                },
            }
        )
        p = get_paths(spec_fixture.spec)["/pet"]
        c = p["post"]["callbacks"]["petEvent"]["petCallbackUrl"]
        get = c["get"]
        assert get["parameters"] == spec_fixture.openapi.schema2parameters(
            PetSchema(), location="query"
        )
        for parameter in get["parameters"]:
            description = parameter.get("description", False)
            assert description
            name = parameter["name"]
            assert description == PetSchema.description[name]
        post = c["post"]
        post_schema = spec_fixture.marshmallow_plugin.resolver.resolve_schema_dict(
            PetSchema
        )
        assert (
            post["requestBody"]["content"]["application/json"]["schema"] == post_schema
        )
        assert post["requestBody"]["description"] == "a pet schema"
        assert post["requestBody"]["required"]

    @pytest.mark.parametrize("spec_fixture", ("2.0",), indirect=True)
    def test_schema_uses_ref_if_available_v2(self, spec_fixture):
        spec_fixture.spec.components.schema("Pet", schema=PetSchema)
        spec_fixture.spec.path(
            path="/pet", operations={"get": {"responses": {200: {"schema": PetSchema}}}}
        )
        get = get_paths(spec_fixture.spec)["/pet"]["get"]
        assert get["responses"]["200"]["schema"] == build_ref(
            spec_fixture.spec, "schema", "Pet"
        )

    @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True)
    def test_schema_uses_ref_if_available_v3(self, spec_fixture):
        spec_fixture.spec.components.schema("Pet", schema=PetSchema)
        spec_fixture.spec.path(
            path="/pet",
            operations={
                "get": {
                    "responses": {
                        200: {"content": {"application/json": {"schema": PetSchema}}}
                    }
                }
            },
        )
        get = get_paths(spec_fixture.spec)["/pet"]["get"]
        assert get["responses"]["200"]["content"]["application/json"][
            "schema"
        ] == build_ref(spec_fixture.spec, "schema", "Pet")

    @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True)
    def test_callback_schema_uses_ref_if_available_v3(self, make_pet_callback_spec):
        spec_fixture = make_pet_callback_spec(
            {
                "get": {
                    "responses": {
                        "200": {"content": {"application/json": {"schema": PetSchema}}}
                    }
                }
            }
        )
        p = get_paths(spec_fixture.spec)["/pet"]
        c = p["post"]["callbacks"]["petEvent"]["petCallbackUrl"]
        get = c["get"]
        assert get["responses"]["200"]["content"]["application/json"][
            "schema"
        ] == build_ref(spec_fixture.spec, "schema", "Pet")

    def test_schema_uses_ref_if_available_name_resolver_returns_none_v2(self):
        def resolver(schema):
            return None

        spec = APISpec(
            title="Test auto-reference",
            version="0.1",
            openapi_version="2.0",
            plugins=(MarshmallowPlugin(schema_name_resolver=resolver),),
        )
        spec.components.schema("Pet", schema=PetSchema)
        spec.path(
            path="/pet", operations={"get": {"responses": {200: {"schema": PetSchema}}}}
        )
        get = get_paths(spec)["/pet"]["get"]
        assert get["responses"]["200"]["schema"] == build_ref(spec, "schema", "Pet")

    def test_schema_uses_ref_if_available_name_resolver_returns_none_v3(self):
        def resolver(schema):
            return None

        spec = APISpec(
            title="Test auto-reference",
            version="0.1",
            openapi_version="3.0.0",
            plugins=(MarshmallowPlugin(schema_name_resolver=resolver),),
        )
        spec.components.schema("Pet", schema=PetSchema)
        spec.path(
            path="/pet",
            operations={
                "get": {
                    "responses": {
                        200: {"content": {"application/json": {"schema": PetSchema}}}
                    }
                }
            },
        )
        get = get_paths(spec)["/pet"]["get"]
        assert get["responses"]["200"]["content"]["application/json"][
            "schema"
        ] == build_ref(spec, "schema", "Pet")

    @pytest.mark.parametrize("spec_fixture", ("2.0",), indirect=True)
    def test_schema_resolver_allof_v2(self, spec_fixture):
        spec_fixture.spec.components.schema("Pet", schema=PetSchema)
        spec_fixture.spec.components.schema("Sample", schema=SampleSchema)
        spec_fixture.spec.path(
            path="/pet",
            operations={
                "get": {
                    "responses": {200: {"schema": {"allOf": [PetSchema, SampleSchema]}}}
                }
            },
        )
        get = get_paths(spec_fixture.spec)["/pet"]["get"]
        assert get["responses"]["200"]["schema"] == {
            "allOf": [
                build_ref(spec_fixture.spec, "schema", "Pet"),
                build_ref(spec_fixture.spec, "schema", "Sample"),
            ]
        }

    @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True)
    @pytest.mark.parametrize("combinator", ["oneOf", "anyOf", "allOf"])
    def test_schema_resolver_oneof_anyof_allof_v3(self, spec_fixture, combinator):
        spec_fixture.spec.components.schema("Pet", schema=PetSchema)
        spec_fixture.spec.path(
            path="/pet",
            operations={
                "get": {
                    "responses": {
                        200: {
                            "content": {
                                "application/json": {
                                    "schema": {combinator: [PetSchema, SampleSchema]}
                                }
                            }
                        }
                    }
                }
            },
        )
        get = get_paths(spec_fixture.spec)["/pet"]["get"]
        assert get["responses"]["200"]["content"]["application/json"]["schema"] == {
            combinator: [
                build_ref(spec_fixture.spec, "schema", "Pet"),
                build_ref(spec_fixture.spec, "schema", "Sample"),
            ]
        }

    @pytest.mark.parametrize("spec_fixture", ("2.0",), indirect=True)
    def test_schema_resolver_not_v2(self, spec_fixture):
        spec_fixture.spec.components.schema("Pet", schema=PetSchema)
        spec_fixture.spec.path(
            path="/pet",
            operations={"get": {"responses": {200: {"schema": {"not": PetSchema}}}}},
        )
        get = get_paths(spec_fixture.spec)["/pet"]["get"]
        assert get["responses"]["200"]["schema"] == {
            "not": build_ref(spec_fixture.spec, "schema", "Pet"),
        }

    @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True)
    def test_schema_resolver_not_v3(self, spec_fixture):
        spec_fixture.spec.components.schema("Pet", schema=PetSchema)
        spec_fixture.spec.path(
            path="/pet",
            operations={
                "get": {
                    "responses": {
                        200: {
                            "content": {
                                "application/json": {"schema": {"not": PetSchema}}
                            }
                        }
                    }
                }
            },
        )
        get = get_paths(spec_fixture.spec)["/pet"]["get"]
        assert get["responses"]["200"]["content"]["application/json"]["schema"] == {
            "not": build_ref(spec_fixture.spec, "schema", "Pet"),
        }

    @pytest.mark.parametrize(
        "pet_schema",
        (PetSchema, PetSchema(), "tests.schemas.PetSchema"),
    )
    def test_schema_name_resolver_returns_none_v2(self, pet_schema):
        def resolver(schema):
            return None

        spec = APISpec(
            title="Test resolver returns None",
            version="0.1",
            openapi_version="2.0",
            plugins=(MarshmallowPlugin(schema_name_resolver=resolver),),
        )
        spec.path(
            path="/pet",
            operations={"get": {"responses": {200: {"schema": pet_schema}}}},
        )
        get = get_paths(spec)["/pet"]["get"]
        assert "properties" in get["responses"]["200"]["schema"]

    @pytest.mark.parametrize(
        "pet_schema",
        (PetSchema, PetSchema(), "tests.schemas.PetSchema"),
    )
    def test_schema_name_resolver_returns_none_v3(self, pet_schema):
        def resolver(schema):
            return None

        spec = APISpec(
            title="Test resolver returns None",
            version="0.1",
            openapi_version="3.0.0",
            plugins=(MarshmallowPlugin(schema_name_resolver=resolver),),
        )
        spec.path(
            path="/pet",
            operations={
                "get": {
                    "responses": {
                        200: {"content": {"application/json": {"schema": pet_schema}}}
                    }
                }
            },
        )
        get = get_paths(spec)["/pet"]["get"]
        assert (
            "properties"
            in get["responses"]["200"]["content"]["application/json"]["schema"]
        )

    def test_callback_schema_uses_ref_if_available_name_resolver_returns_none_v3(self):
        def resolver(schema):
            return None

        spec = APISpec(
            title="Test auto-reference",
            version="0.1",
            openapi_version="3.0.0",
            plugins=(MarshmallowPlugin(schema_name_resolver=resolver),),
        )
        spec.components.schema("Pet", schema=PetSchema)
        spec.path(
            path="/pet",
            operations={
                "post": {
                    "callbacks": {
                        "petEvent": {
                            "petCallbackUrl": {
                                "get": {
                                    "responses": {
                                        "200": {
                                            "content": {
                                                "application/json": {
                                                    "schema": PetSchema
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            },
        )
        p = get_paths(spec)["/pet"]
        c = p["post"]["callbacks"]["petEvent"]["petCallbackUrl"]
        get = c["get"]
        assert get["responses"]["200"]["content"]["application/json"][
            "schema"
        ] == build_ref(spec, "schema", "Pet")

    @pytest.mark.parametrize("spec_fixture", ("2.0",), indirect=True)
    def test_schema_uses_ref_in_parameters_and_request_body_if_available_v2(
        self, spec_fixture
    ):
        spec_fixture.spec.components.schema("Pet", schema=PetSchema)
        spec_fixture.spec.path(
            path="/pet",
            operations={
                "get": {"parameters": [{"in": "query", "schema": PetSchema}]},
                "post": {"parameters": [{"in": "body", "schema": PetSchema}]},
            },
        )
        p = get_paths(spec_fixture.spec)["/pet"]
        assert "schema" not in p["get"]["parameters"][0]
        post = p["post"]
        assert len(post["parameters"]) == 1
        assert post["parameters"][0]["schema"] == build_ref(
            spec_fixture.spec, "schema", "Pet"
        )

    @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True)
    def test_schema_uses_ref_in_parameters_and_request_body_if_available_v3(
        self, spec_fixture
    ):
        spec_fixture.spec.components.schema("Pet", schema=PetSchema)
        spec_fixture.spec.path(
            path="/pet",
            operations={
                "get": {"parameters": [{"in": "query", "schema": PetSchema}]},
                "post": {
                    "requestBody": {
                        "content": {"application/json": {"schema": PetSchema}}
                    }
                },
            },
        )
        p = get_paths(spec_fixture.spec)["/pet"]
        assert "schema" in p["get"]["parameters"][0]
        post = p["post"]
        schema_ref = post["requestBody"]["content"]["application/json"]["schema"]
        assert schema_ref == build_ref(spec_fixture.spec, "schema", "Pet")

    @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True)
    def test_callback_schema_uses_ref_in_parameters_and_request_body_if_available_v3(
        self, make_pet_callback_spec
    ):
        spec_fixture = make_pet_callback_spec(
            {
                "get": {"parameters": [{"in": "query", "schema": PetSchema}]},
                "post": {
                    "requestBody": {
                        "content": {"application/json": {"schema": PetSchema}}
                    }
                },
            }
        )
        p = get_paths(spec_fixture.spec)["/pet"]
        c = p["post"]["callbacks"]["petEvent"]["petCallbackUrl"]
        assert "schema" in c["get"]["parameters"][0]
        post = c["post"]
        schema_ref = post["requestBody"]["content"]["application/json"]["schema"]
        assert schema_ref == build_ref(spec_fixture.spec, "schema", "Pet")

    @pytest.mark.parametrize("spec_fixture", ("2.0",), indirect=True)
    def test_schema_array_uses_ref_if_available_v2(self, spec_fixture):
        spec_fixture.spec.components.schema("Pet", schema=PetSchema)
        spec_fixture.spec.path(
            path="/pet",
            operations={
                "get": {
                    "parameters": [
                        {
                            "name": "petSchema",
                            "in": "body",
                            "schema": {"type": "array", "items": PetSchema},
                        }
                    ],
                    "responses": {
                        200: {"schema": {"type": "array", "items": PetSchema}}
                    },
                }
            },
        )
        get = get_paths(spec_fixture.spec)["/pet"]["get"]
        assert len(get["parameters"]) == 1
        resolved_schema = {
            "type": "array",
            "items": build_ref(spec_fixture.spec, "schema", "Pet"),
        }
        assert get["parameters"][0]["schema"] == resolved_schema
        assert get["responses"]["200"]["schema"] == resolved_schema

    @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True)
    def test_schema_array_uses_ref_if_available_v3(self, spec_fixture):
        spec_fixture.spec.components.schema("Pet", schema=PetSchema)
        spec_fixture.spec.path(
            path="/pet",
            operations={
                "get": {
                    "parameters": [
                        {
                            "name": "Pet",
                            "in": "query",
                            "content": {
                                "application/json": {
                                    "schema": {"type": "array", "items": PetSchema}
                                }
                            },
                        }
                    ],
                    "responses": {
                        200: {
                            "content": {
                                "application/json": {
                                    "schema": {"type": "array", "items": PetSchema}
                                }
                            }
                        }
                    },
                }
            },
        )
        get = get_paths(spec_fixture.spec)["/pet"]["get"]
        assert len(get["parameters"]) == 1
        resolved_schema = {
            "type": "array",
            "items": build_ref(spec_fixture.spec, "schema", "Pet"),
        }
        request_schema = get["parameters"][0]["content"]["application/json"]["schema"]
        assert request_schema == resolved_schema
        response_schema = get["responses"]["200"]["content"]["application/json"][
            "schema"
        ]
        assert response_schema == resolved_schema

    @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True)
    def test_callback_schema_array_uses_ref_if_available_v3(
        self, make_pet_callback_spec
    ):
        spec_fixture = make_pet_callback_spec(
            {
                "get": {
                    "parameters": [
                        {
                            "name": "Pet",
                            "in": "query",
                            "content": {
                                "application/json": {
                                    "schema": {"type": "array", "items": PetSchema}
                                }
                            },
                        }
                    ],
                    "responses": {
                        "200": {
                            "content": {
                                "application/json": {
                                    "schema": {"type": "array", "items": PetSchema}
                                }
                            }
                        }
                    },
                }
            }
        )
        p = get_paths(spec_fixture.spec)["/pet"]
        c = p["post"]["callbacks"]["petEvent"]["petCallbackUrl"]
        get = c["get"]
        assert len(get["parameters"]) == 1
        resolved_schema = {
            "type": "array",
            "items": build_ref(spec_fixture.spec, "schema", "Pet"),
        }
        request_schema = get["parameters"][0]["content"]["application/json"]["schema"]
        assert request_schema == resolved_schema
        response_schema = get["responses"]["200"]["content"]["application/json"][
            "schema"
        ]
        assert response_schema == resolved_schema

    @pytest.mark.parametrize("spec_fixture", ("2.0",), indirect=True)
    def test_schema_partially_v2(self, spec_fixture):
        spec_fixture.spec.components.schema("Pet", schema=PetSchema)
        spec_fixture.spec.path(
            path="/parents",
            operations={
                "get": {
                    "responses": {
                        200: {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "mother": PetSchema,
                                    "father": PetSchema,
                                },
                            }
                        }
                    }
                }
            },
        )
        get = get_paths(spec_fixture.spec)["/parents"]["get"]
        assert get["responses"]["200"]["schema"] == {
            "type": "object",
            "properties": {
                "mother": build_ref(spec_fixture.spec, "schema", "Pet"),
                "father": build_ref(spec_fixture.spec, "schema", "Pet"),
            },
        }

    @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True)
    def test_schema_partially_v3(self, spec_fixture):
        spec_fixture.spec.components.schema("Pet", schema=PetSchema)
        spec_fixture.spec.path(
            path="/parents",
            operations={
                "get": {
                    "responses": {
                        200: {
                            "content": {
                                "application/json": {
                                    "schema": {
                                        "type": "object",
                                        "properties": {
                                            "mother": PetSchema,
                                            "father": PetSchema,
                                        },
                                    }
                                }
                            }
                        }
                    }
                }
            },
        )
        get = get_paths(spec_fixture.spec)["/parents"]["get"]
        assert get["responses"]["200"]["content"]["application/json"]["schema"] == {
            "type": "object",
            "properties": {
                "mother": build_ref(spec_fixture.spec, "schema", "Pet"),
                "father": build_ref(spec_fixture.spec, "schema", "Pet"),
            },
        }

    @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True)
    def test_callback_schema_partially_v3(self, make_pet_callback_spec):
        spec_fixture = make_pet_callback_spec(
            {
                "get": {
                    "responses": {
                        "200": {
                            "content": {
                                "application/json": {
                                    "schema": {
                                        "type": "object",
                                        "properties": {
                                            "mother": PetSchema,
                                            "father": PetSchema,
                                        },
                                    }
                                }
                            }
                        }
                    }
                }
            }
        )
        p = get_paths(spec_fixture.spec)["/pet"]
        c = p["post"]["callbacks"]["petEvent"]["petCallbackUrl"]
        get = c["get"]
        assert get["responses"]["200"]["content"]["application/json"]["schema"] == {
            "type": "object",
            "properties": {
                "mother": build_ref(spec_fixture.spec, "schema", "Pet"),
                "father": build_ref(spec_fixture.spec, "schema", "Pet"),
            },
        }

    def test_parameter_reference(self, spec_fixture):
        if spec_fixture.spec.openapi_version.major < 3:
            param = {"schema": PetSchema}
        else:
            param = {"content": {"application/json": {"schema": PetSchema}}}
        spec_fixture.spec.components.parameter("Pet", "body", param)
        spec_fixture.spec.path(
            path="/parents", operations={"get": {"parameters": ["Pet"]}}
        )
        get = get_paths(spec_fixture.spec)["/parents"]["get"]
        assert get["parameters"] == [build_ref(spec_fixture.spec, "parameter", "Pet")]

    def test_response_reference(self, spec_fixture):
        if spec_fixture.spec.openapi_version.major < 3:
            resp = {"schema": PetSchema}
        else:
            resp = {"content": {"application/json": {"schema": PetSchema}}}
        spec_fixture.spec.components.response("Pet", resp)
        spec_fixture.spec.path(
            path="/parents", operations={"get": {"responses": {"200": "Pet"}}}
        )
        get = get_paths(spec_fixture.spec)["/parents"]["get"]
        assert get["responses"] == {
            "200": build_ref(spec_fixture.spec, "response", "Pet")
        }

    def test_schema_global_state_untouched_2json(self, spec_fixture):
        assert get_nested_schema(RunSchema, "sample") is None
        data = spec_fixture.openapi.schema2jsonschema(RunSchema)
        json.dumps(data)
        assert get_nested_schema(RunSchema, "sample") is None

    def test_schema_global_state_untouched_2parameters(self, spec_fixture):
        assert get_nested_schema(RunSchema, "sample") is None
        data = spec_fixture.openapi.schema2parameters(RunSchema, location="json")
        json.dumps(data)
        assert get_nested_schema(RunSchema, "sample") is None

    def test_resolve_schema_dict_ref_as_string(self, spec):
        """Test schema ref passed as string"""
        # The case tested here is a reference passed as string, not a
        # marshmallow Schema passed by name as string. We want to ensure the
        # MarshmallowPlugin does not interfere with the feature interpreting
        # strings as references. Therefore, we use a specific name to ensure
        # there is no Schema with that name in the marshmallow registry from
        # somewhere else in the tests.
        # e.g. PetSchema is in the registry already so it wouldn't work.
        schema = {"schema": "SomeSpecificPetSchema"}
        if spec.openapi_version.major >= 3:
            schema = {"content": {"application/json": schema}}
        spec.path("/pet/{petId}", operations={"get": {"responses": {"200": schema}}})
        resp = get_paths(spec)["/pet/{petId}"]["get"]["responses"]["200"]
        if spec.openapi_version.major < 3:
            schema = resp["schema"]
        else:
            schema = resp["content"]["application/json"]["schema"]
        assert schema == build_ref(spec, "schema", "SomeSpecificPetSchema")


class TestCircularReference:
    def test_circular_referencing_schemas(self, spec):
        spec.components.schema("Analysis", schema=AnalysisSchema)
        definitions = get_schemas(spec)
        ref = definitions["Analysis"]["properties"]["sample"]
        assert ref == build_ref(spec, "schema", "Sample")


# Regression tests for issue #55
class TestSelfReference:
    def test_self_referencing_field_single(self, spec):
        spec.components.schema("SelfReference", schema=SelfReferencingSchema)
        definitions = get_schemas(spec)
        ref = definitions["SelfReference"]["properties"]["single"]
        assert ref == build_ref(spec, "schema", "SelfReference")

    def test_self_referencing_field_many(self, spec):
        spec.components.schema("SelfReference", schema=SelfReferencingSchema)
        definitions = get_schemas(spec)
        result = definitions["SelfReference"]["properties"]["multiple"]
        assert result == {
            "type": "array",
            "items": build_ref(spec, "schema", "SelfReference"),
        }


class TestFieldOrdering:
    def test_field_order_preserved(self, spec):
        class OrderedSchema(Schema):
            field1 = Int()
            field2 = Int()
            field3 = Int()
            field4 = Int()
            field5 = Int()
            if sys.version_info < (3, 10):

                class Meta:
                    ordered = True

        spec.components.schema("Ordered", schema=OrderedSchema)
        result = get_schemas(spec)["Ordered"]["properties"]
        assert list(result.keys()) == ["field1", "field2", "field3", "field4", "field5"]


class TestFieldWithCustomProps:
    def test_field_with_custom_props(self, spec):
        spec.components.schema("PatternedObject", schema=PatternedObjectSchema)
        result = get_schemas(spec)["PatternedObject"]["properties"]["count"]
        assert "x-count" in result
        assert result["x-count"] == 1

    def test_field_with_custom_props_passed_as_snake_case(self, spec):
        spec.components.schema("PatternedObject", schema=PatternedObjectSchema)
        result = get_schemas(spec)["PatternedObject"]["properties"]["count2"]
        assert "x-count2" in result
        assert result["x-count2"] == 2


class TestSchemaWithDefaultValues:
    def test_schema_with_default_values(self, spec):
        spec.components.schema("DefaultValuesSchema", schema=DefaultValuesSchema)
        definitions = get_schemas(spec)
        props = definitions["DefaultValuesSchema"]["properties"]
        assert props["number_auto_default"]["default"] == 12
        assert props["number_manual_default"]["default"] == 42
        assert "default" not in props["string_callable_default"]
        assert props["string_manual_default"]["default"] == "Manual"
        assert "default" not in props["numbers"]


class TestDictValues:
    def test_dict_values_resolve_to_additional_properties(self, spec):
        class SchemaWithDict(Schema):
            dict_field = Dict(values=String())

        spec.components.schema("SchemaWithDict", schema=SchemaWithDict)
        result = get_schemas(spec)["SchemaWithDict"]["properties"]["dict_field"]
        assert result == {"type": "object", "additionalProperties": {"type": "string"}}

    def test_dict_with_empty_values_field(self, spec):
        class SchemaWithDict(Schema):
            dict_field = Dict()

        spec.components.schema("SchemaWithDict", schema=SchemaWithDict)
        result = get_schemas(spec)["SchemaWithDict"]["properties"]["dict_field"]
        assert result == {"type": "object", "additionalProperties": {}}

    def test_dict_with_empty_values_field_and_metadata(self, spec):
        class SchemaWithDict(Schema):
            dict_field = Dict(metadata={"additionalProperties": True})

        spec.components.schema("SchemaWithDict", schema=SchemaWithDict)
        result = get_schemas(spec)["SchemaWithDict"]["properties"]["dict_field"]
        assert result == {"type": "object", "additionalProperties": True}

    def test_dict_with_nested(self, spec):
        class SchemaWithDict(Schema):
            dict_field = Dict(values=Nested(PetSchema))

        spec.components.schema("SchemaWithDict", schema=SchemaWithDict)

        assert len(get_schemas(spec)) == 2

        result = get_schemas(spec)["SchemaWithDict"]["properties"]["dict_field"]
        assert result == {
            "additionalProperties": build_ref(spec, "schema", "Pet"),
            "type": "object",
        }


class TestList:
    def test_list_with_nested(self, spec):
        class SchemaWithList(Schema):
            list_field = List(Nested(PetSchema))

        spec.components.schema("SchemaWithList", schema=SchemaWithList)

        assert len(get_schemas(spec)) == 2

        result = get_schemas(spec)["SchemaWithList"]["properties"]["list_field"]
        assert result == {"items": build_ref(spec, "schema", "Pet"), "type": "array"}


class TestTimeDelta:
    def test_timedelta_x_unit(self, spec):
        class SchemaWithTimeDelta(Schema):
            sec = TimeDelta("seconds")
            day = TimeDelta("days")

        spec.components.schema("SchemaWithTimeDelta", schema=SchemaWithTimeDelta)

        assert (
            get_schemas(spec)["SchemaWithTimeDelta"]["properties"]["sec"]["x-unit"]
            == "seconds"
        )
        assert (
            get_schemas(spec)["SchemaWithTimeDelta"]["properties"]["day"]["x-unit"]
            == "days"
        )
