from typing import Any, Callable, Dict, List, Optional, Union

from tartiflette.resolver.default import (
    default_type_resolver,
    gather_arguments_coercer,
)
from tartiflette.schema.introspection import (
    SCHEMA_ROOT_FIELD_DEFINITION,
    TYPENAME_ROOT_FIELD_DEFINITION,
    prepare_type_root_field,
)
from tartiflette.schema.registry import SchemaRegistry
from tartiflette.types.enum import GraphQLEnumType, GraphQLEnumTypeExtension
from tartiflette.types.exceptions.tartiflette import (
    GraphQLSchemaError,
    ImproperlyConfigured,
    RedefinedImplementation,
    UnknownSchemaFieldResolver,
)
from tartiflette.types.helpers.get_directive_instances import (
    compute_directive_nodes,
)
from tartiflette.types.helpers.reduce_type import reduce_type
from tartiflette.types.input_object import (
    GraphQLInputObjectType,
    GraphQLInputObjectTypeExtension,
)
from tartiflette.types.interface import (
    GraphQLInterfaceType,
    GraphQLInterfaceTypeExtension,
)
from tartiflette.types.list import GraphQLList
from tartiflette.types.non_null import GraphQLNonNull
from tartiflette.types.object import (
    GraphQLObjectType,
    GraphQLObjectTypeExtension,
)
from tartiflette.types.scalar import (
    GraphQLScalarType,
    GraphQLScalarTypeExtension,
)
from tartiflette.types.schema_extension import GraphQLSchemaExtension
from tartiflette.types.union import GraphQLUnionType, GraphQLUnionTypeExtension
from tartiflette.utils.callables import (
    is_valid_async_generator,
    is_valid_coroutine,
)
from tartiflette.utils.directives import wraps_with_directives
from tartiflette.utils.errors import graphql_error_from_nodes

__all__ = ("GraphQLSchema",)

_DEFAULT_QUERY_OPERATION_NAME = "Query"
_DEFAULT_MUTATION_OPERATION_NAME = "Mutation"
_DEFAULT_SUBSCRIPTION_OPERATION_NAME = "Subscription"


_IMPLEMENTABLE_DIRECTIVE_FUNCTION_HOOKS = (
    "on_post_bake",
    "on_pre_output_coercion",
    "on_introspection",
    "on_post_input_coercion",
    "on_argument_execution",
    "on_field_execution",
    "on_field_collection",
    "on_fragment_spread_collection",
    "on_inline_fragment_collection",
    "on_schema_execution",
)

_IMPLEMENTABLE_DIRECTIVE_GENERATOR_HOOKS = ("on_schema_subscription",)


def _validate_extension(extended, name, ext_type, message):
    if not extended:
        return [f"Can't extend a non existing type < {name} >."]

    if not isinstance(extended, ext_type):
        return [
            f"Can't extend {message} < {extended.name} >"
            f" cause it's not an {message}."
        ]

    return []


def _validate_extension_directives(extension, extended, message):
    errors = []

    extended_dir = [x.name.value for x in extended.directives]

    for directive in extension.directives:
        if directive.name.value in extended_dir:
            errors.append(
                f"Can't add < {directive.name.value} > Directive to < "
                f"{extension.name} > {message}, cause it's already there."
            )

    return errors


def _format_schema_error_message(errors: List[str]) -> str:
    result = "\n"
    for index, err in enumerate(errors):
        result = "{result}\n{index}: {err}".format(
            result=result, index=index, err=err
        )
    return result


def _value_uniqueness(values: List[str]) -> List[str]:
    seen = []
    double = []

    for value in values:
        if value in seen and value not in double:
            double.append(value)
        seen.append(value)

    return double


def _validated_field_args_are_same_as_interface_args(
    obj_type_name, obj_field, iface_name, iface_field, errors
) -> bool:
    for (
        iface_field_arg_name,
        iface_field_arg,
    ) in iface_field.arguments.items():
        try:
            obj_field_arg = obj_field.arguments[iface_field_arg_name]
        except KeyError:
            errors.append(
                f"Field < {obj_type_name}.{obj_field.name} > is missing interface "
                f"field argument < {iface_name}.{iface_field.name}"
                f"({iface_field_arg_name}) >."
            )
        else:
            if obj_field_arg.gql_type != iface_field_arg.gql_type:
                errors.append(
                    f"Field argument < {obj_type_name}.{obj_field.name}"
                    f"({iface_field_arg_name}) >"
                    f" is not of type < {iface_field_arg.gql_type}"
                    f" > as required by the interface < {iface_name} >."
                )

    # Makes sure no argument are added to the object field as non-null,
    # only nullable args are allowed, so for each arguments that are not
    # in the interface, validate that they are not "NonNullable"
    for arg_name in set(obj_field.arguments.keys()) - set(
        iface_field.arguments.keys()
    ):
        if isinstance(obj_field.arguments[arg_name].gql_type, GraphQLNonNull):
            errors.append(
                f"Field < {obj_type_name}.{obj_field.name}"
                f"({arg_name}) > isn't required in interface"
                f" field < {iface_name}.{iface_field.name} >,"
                f" so it cannot be NonNullable."
            )


class GraphQLSchema:
    """
    GraphQL Schema

    Contains the complete GraphQL Schema: types, entrypoints and directives.
    """

    # pylint: disable=too-many-instance-attributes

    # Introspection attributes
    description = "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations."

    def __init__(self, name: str = "default") -> None:
        """
        :param name: name of the schema
        :type name: str
        """
        self.name = name
        self.default_type_resolver: Optional[Callable] = None
        self.default_arguments_coercer: Optional[Callable] = None

        # Operation type names
        self.query_operation_name: str = _DEFAULT_QUERY_OPERATION_NAME
        self.mutation_operation_name: str = _DEFAULT_MUTATION_OPERATION_NAME
        self.subscription_operation_name: str = _DEFAULT_SUBSCRIPTION_OPERATION_NAME

        # Type, directive, enum, scalar & input type definitions
        self.type_definitions: Dict[str, "GraphQLType"] = {}
        self._directive_definitions: Dict[str, "GraphQLDirective"] = {}
        self._scalar_definitions: Dict[str, "GraphQLScalarType"] = {}
        self._enum_definitions: Dict[str, "GraphQLEnumType"] = {}
        self._input_types: List[
            Union[
                "GraphQLScalarType",
                "GraphQLEnumType",
                "GraphQLInputObjectType",
            ]
        ] = []
        self._operation_types: Dict[str, "GraphQLObjectType"] = {}

        # Introspection attributes
        self.types: List["GraphQLType"] = []
        self.queryType: Optional[  # pylint: disable=invalid-name
            "GraphQLType"
        ] = None
        self.mutationType: Optional[  # pylint: disable=invalid-name
            "GraphQLType"
        ] = None
        self.subscriptionType: Optional[  # pylint: disable=invalid-name
            "GraphQLType"
        ] = None
        self.directives: List[  # pylint: disable=invalid-name
            "GraphQLDirective"
        ] = []

        self.extensions: List["GraphQLExtension"] = []
        self._schema_directives: List["DirectiveNode"] = []
        self._json_loader = None

    @property
    def json_loader(self):
        return self._json_loader

    @json_loader.setter
    def json_loader(self, loader):
        self._json_loader = loader

    def add_schema_directives(
        self, directives_instances: List["DirectiveNode"]
    ) -> None:
        self._schema_directives.extend(directives_instances)

    def __eq__(self, other: Any) -> bool:
        """
        Returns True if `other` instance is identical to `self`.
        :param other: object instance to compare to `self`
        :type other: Any
        :return: whether or not `other` is identical to `self`
        :rtype: bool
        """
        return self is other or (
            isinstance(other, GraphQLSchema)
            and self.name == other.name
            and self.query_operation_name == other.query_operation_name
            and self.mutation_operation_name == other.mutation_operation_name
            and self.subscription_operation_name
            == other.subscription_operation_name
            and self.type_definitions == other.type_definitions
        )

    def __repr__(self) -> str:
        """
        Returns the representation of a GraphQLSchema instance.
        :return: the representation of a GraphQLSchema instance
        :rtype: str
        """
        return "GraphQLSchema(name={!r})".format(self.name)

    def __str__(self) -> str:
        """
        Returns a human-readable representation of the schema.
        :return: a human-readable representation of the schema
        :rtype: str
        """
        return self.name

    def __hash__(self) -> int:
        """
        Hash the name of the schema as a unique representation of a
        GraphQLSchema.
        :return: hash of the schema name
        :rtype: int
        """
        return hash(self.name)

    def add_type_definition(self, type_definition: "GraphQLType") -> None:
        """
        Adds a GraphQLType to the defined type list.
        :param type_definition: GraphQLType to add
        :type type_definition: GraphQLType
        """
        if type_definition.name in self.type_definitions:
            raise RedefinedImplementation(
                "new GraphQL type definition `{}` "
                "overrides existing type definition `{}`.".format(
                    type_definition.name,
                    repr(self.type_definitions.get(type_definition.name)),
                )
            )
        self.type_definitions[type_definition.name] = type_definition
        if isinstance(type_definition, GraphQLInputObjectType):
            self._input_types.append(type_definition.name)

    def has_type(self, name: str) -> bool:
        """
        Determines whether or not the name corresponds to a defined type.
        :param name: name of the type to find
        :type name: str
        :return: whether or not the name corresponds to a defined type
        :rtype: bool
        """
        return name in self.type_definitions

    def find_type(self, name: str) -> "GraphQLType":
        """
        Returns the defined type corresponding to the name.
        :param name: name of the type to return
        :type name: str
        :return: the defined type
        :rtype: GraphQLType
        """
        return self.type_definitions[name]

    def add_directive_definition(
        self, directive_definition: "GraphQLDirective"
    ) -> None:
        """
        Adds a GraphQLDirective to the defined directive list.
        :param directive_definition: GraphQLDirective to add
        :type directive_definition: GraphQLDirective
        """
        if directive_definition.name in self._directive_definitions:
            raise RedefinedImplementation(
                "new GraphQL directive definition `{}` "
                "overrides existing directive definition `{}`.".format(
                    directive_definition.name,
                    repr(
                        self._directive_definitions.get(
                            directive_definition.name
                        )
                    ),
                )
            )

        self._directive_definitions[
            directive_definition.name
        ] = directive_definition

    def has_directive(self, name: str) -> bool:
        """
        Determines whether or not the name corresponds to a defined directive.
        :param name: name of the directive to find
        :type name: str
        :return: whether or not the name corresponds to a defined directive
        :rtype: bool
        """
        return name in self._directive_definitions

    def find_directive(self, name: str) -> "GraphQLDirective":
        """
        Returns the defined directive corresponding to the name.
        :param name: name of the directive to return
        :type name: str
        :return: the defined directive
        :rtype: GraphQLDirective
        """
        return self._directive_definitions[name]

    def add_scalar_definition(
        self, scalar_definition: "GraphQLScalarType"
    ) -> None:
        """
        Adds a GraphQLScalarType to the defined scalar list.
        :param scalar_definition: GraphQLScalarType to add
        :type scalar_definition: GraphQLScalarType
        """
        if scalar_definition.name in self._scalar_definitions:
            raise RedefinedImplementation(
                "new GraphQL scalar definition `{}` "
                "overrides existing scalar definition `{}`.".format(
                    scalar_definition.name,
                    repr(self._scalar_definitions.get(scalar_definition.name)),
                )
            )

        self._scalar_definitions[scalar_definition.name] = scalar_definition
        self._input_types.append(scalar_definition.name)
        self.add_type_definition(scalar_definition)

    def find_scalar(self, name: str) -> Optional["GraphQLScalarType"]:
        """
        Returns the defined scalar corresponding to the name.
        :param name: name of the scalar to return
        :type name: str
        :return: the defined scalar
        :rtype: GraphQLScalarType
        """
        return self._scalar_definitions.get(name)

    def add_enum_definition(self, enum_definition: "GraphQLEnumType") -> None:
        """
        Adds a GraphQLScalarType to the defined scalar list.
        :param enum_definition: GraphQLEnumType to add
        :type enum_definition: GraphQLEnumType
        """
        if enum_definition.name in self._enum_definitions:
            raise RedefinedImplementation(
                "new GraphQL enum definition `{}` "
                "overrides existing enum definition `{}`.".format(
                    enum_definition.name,
                    repr(self._enum_definitions.get(enum_definition.name)),
                )
            )

        self._enum_definitions[enum_definition.name] = enum_definition
        self._input_types.append(enum_definition.name)
        self.add_type_definition(enum_definition)

    def add_extension(self, extension: "GraphQLExtension") -> None:
        """TODO"""
        self.extensions.append(extension)

    def get_field_by_name(self, name: str) -> "GraphQLField":
        """
        Returns the field corresponding to the filled in name.
        :param name: name of the field with the following format "Parent.field"
        :type name: str
        :return: the field corresponding to the filled in name
        :rtype: GraphQLField
        """
        try:
            parent_name, field_name = name.split(".")
        except ValueError:
            raise ImproperlyConfigured(
                "field name must be of the format `TypeName.fieldName` got "
                f"`{name}`."
            )

        try:
            return self.type_definitions[parent_name].find_field(field_name)
        except (AttributeError, KeyError):
            raise UnknownSchemaFieldResolver(
                f"field `{name}` was not found in GraphQL schema."
            )

    def _inject_introspection_fields(self) -> None:
        """
        Injects introspection fields to the query type and to defined object
        and union types.
        """
        query_type = self.type_definitions.get(self.query_operation_name)
        if not query_type:
            return

        query_type.add_field(
            SCHEMA_ROOT_FIELD_DEFINITION(
                gql_type=GraphQLNonNull("__Schema", schema=self)
            )
        )
        query_type.add_field(prepare_type_root_field(self))

        for type_definition in self.type_definitions.values():
            try:
                type_definition.add_field(
                    TYPENAME_ROOT_FIELD_DEFINITION(
                        gql_type=GraphQLNonNull(gql_type="String", schema=self)
                    )
                )
            except AttributeError:
                pass

    def _validate_schema_named_types(self) -> List[str]:
        """
        Validates that all type with fields refers to known GraphQL types.
        :return: a list of errors
        :rtype: List[str]
        """
        errors = []
        for type_name, gql_type in self.type_definitions.items():
            try:
                for field in gql_type.implemented_fields.values():
                    reduced_type = reduce_type(field.gql_type)
                    if str(reduced_type) not in self.type_definitions:
                        errors.append(
                            f"Field < {type_name}.{field.name} > is Invalid: "
                            f"the given Type < {reduced_type} > does not exist!"
                        )
            except AttributeError:
                pass
        return errors

    def _validate_field_type_is_same_as_interface_type(
        self, field_type, interface_field_type
    ) -> bool:
        # If they are the same simple type
        if field_type == interface_field_type:
            return True

        # If field_type is a nonnull variant of interface_type then it's ok
        if isinstance(field_type, GraphQLNonNull):
            return self._validate_field_type_is_same_as_interface_type(
                field_type.gql_type, interface_field_type
            )

        # If interface says !Null but field is not non null
        if isinstance(interface_field_type, GraphQLNonNull):
            return False

        # If interface says list but field is not the same list
        # because firt the == condition is false (or else we wouldn't be here)
        # and field_type isn't a non_null of interface type
        # then if interface is a list, they aren't the same type
        if isinstance(interface_field_type, GraphQLList):
            return False

        # Then, look at the possible type for the interface
        interface = self.type_definitions[interface_field_type]
        if isinstance(interface, GraphQLInterfaceType):
            return interface.is_possible_type(field_type)
        return False

    def _validate_field_follow_interface(
        self, iface_name, object_type, iface_field, errors
    ):
        try:
            object_field = object_type.find_field(iface_field.name)
        except KeyError:
            errors.append(
                f"Field < {object_type.name}.{iface_field.name} > is missing "
                f"as defined in the < {iface_name} > Interface."
            )
        else:
            if not self._validate_field_type_is_same_as_interface_type(
                object_field.gql_type, iface_field.gql_type
            ):
                errors.append(
                    f"Field < {object_type.name}.{iface_field.name} > "
                    f"should be of Type < {iface_field.gql_type} > "
                    f"as defined in the < {iface_name} > Interface."
                )

            _validated_field_args_are_same_as_interface_args(
                object_type.name, object_field, iface_name, iface_field, errors
            )

    def _validate_object_follow_interfaces(self) -> List[str]:
        """
        Validates that object types which implements interfaces does follow
        their implementations.
        :return: a list of errors
        :rtype: List[str]
        """
        # pylint: disable=too-complex
        errors = []
        for gql_type in self.type_definitions.values():
            try:
                ifaces_names = gql_type.interfaces_names
            except AttributeError:
                continue

            for iface_name in ifaces_names:
                try:
                    iface_type = self.type_definitions[iface_name]
                    if not isinstance(iface_type, GraphQLInterfaceType):
                        errors.append(
                            f"Type < {gql_type.name} > "
                            f"implements < {iface_name} > "
                            f"which is not an interface!"
                        )
                        continue
                except KeyError:
                    errors.append(
                        f"Type < {gql_type.name} > "
                        f"implements < {iface_name} > "
                        f"which does not exist!"
                    )
                    continue

                for iface_field in iface_type.implemented_fields.values():
                    self._validate_field_follow_interface(
                        iface_name, gql_type, iface_field, errors
                    )
        return errors

    def _validate_schema_root_types_exist(self) -> List[str]:
        """
        Validates that schema operation types are linked to defined types.
        :return: a list of errors
        :rtype: List[str]
        """
        errors = []
        # Check "query" which is the only mandatory root type
        if self.query_operation_name not in self.type_definitions:
            errors.append(
                f"Missing Query Type < {self.query_operation_name} >."
            )
        if (
            self.mutation_operation_name != "Mutation"
            and self.mutation_operation_name not in self.type_definitions
        ):
            errors.append(
                f"Missing Mutation Type < {self.mutation_operation_name} >."
            )
        if (
            self.subscription_operation_name != "Subscription"
            and self.subscription_operation_name not in self.type_definitions
        ):
            errors.append(
                f"Missing Subscription Type < {self.subscription_operation_name} >."
            )
        return errors

    def _validate_non_empty_object(self) -> List[str]:
        """
        Validates that object types implement at least one fields.
        :return: a list of errors
        :rtype: List[str]
        """
        errors = []
        for type_name, gql_type in self.type_definitions.items():
            if isinstance(gql_type, GraphQLObjectType) and not [
                field_name
                for field_name in gql_type.implemented_fields
                if not field_name.startswith("__")
            ]:
                errors.append(f"Type < {type_name} > has no fields.")
        return errors

    def _validate_union_is_acceptable(self) -> List[str]:
        """
        Validates that union types are valid.
        :return: a list of errors
        :rtype: List[str]
        """
        errors = []
        for type_name, gql_type in self.type_definitions.items():
            if isinstance(gql_type, GraphQLUnionType):
                for contained_type_name in gql_type.types:
                    if contained_type_name == type_name:
                        errors.append(
                            f"Union Type < {type_name} > contains itself."
                        )
                        # TODO: Are there other restrictions for `Union`s ?
                        # can they contain interfaces ?
                        # can they mix types: interface | object | scalar
        return errors

    def _validate_all_scalars_have_implementations(self) -> List[str]:
        """
        Validates that defined scalar types provide a proper implementation.
        :return: a list of errors
        :rtype: List[str]
        """
        errors = []
        for type_name, gql_type in self.type_definitions.items():
            if isinstance(gql_type, GraphQLScalarType) and (
                gql_type.coerce_output is None
                or gql_type.coerce_input is None
                or gql_type.parse_literal is None
            ):
                errors.append(
                    f"Scalar < {type_name} > " f"is missing an implementation"
                )
        return errors

    def _validate_enum_values_are_unique(self) -> List[str]:
        """
        Validates that enum values are unique for each enum types.
        :return: a list of errors
        :rtype: List[str]
        """
        errors = []
        for type_name, gql_type in self.type_definitions.items():
            if isinstance(gql_type, GraphQLEnumType):
                for non_unique_value in _value_uniqueness(
                    [str(x.value) for x in gql_type.values]
                ):
                    errors.append(
                        f"Enum < {type_name} > is invalid, Value < {non_unique_value} > is not unique"
                    )
        return errors

    def _validate_arguments_have_valid_type(self) -> List[str]:
        """
        Validates that argument definitions of fields and directives refer to
        an input type.
        :return: a list of errors
        :rtype: List[str]
        """
        errors = []
        for gqltype in self.type_definitions.values():
            try:
                for field in gqltype.implemented_fields.values():
                    for arg in field.arguments.values():
                        errors.extend(
                            self._validate_type_is_an_input_types(
                                arg,
                                f"Argument < {arg.name} > of Field < {gqltype}.{field.name} >",
                            )
                        )
            except AttributeError:
                pass

        for directive in self._directive_definitions.values():
            for arg in directive.arguments.values():
                errors.extend(
                    self._validate_type_is_an_input_types(
                        arg,
                        f"Argument < {arg.name} > of Directive < {directive.name} >",
                    )
                )

        return errors

    def _validate_type_is_an_input_types(
        self, obj: "GraphQLType", message_prefix: str
    ) -> List[str]:
        """
        Validates that the object is a defined input types.
        :param obj: object to check
        :param message_prefix: prefix to append to the error message
        :type obj: GraphQLType
        :type message_prefix: str
        :return: a list of errors
        :rtype: List[str]
        """
        rtype = reduce_type(obj.gql_type)
        if not rtype in self._input_types:
            return [
                f"{message_prefix} is of type "
                f"< {rtype} > which is not a Scalar, "
                "an Enum or an InputObject"
            ]
        return []

    def _validate_input_type_composed_of_input_type(self) -> List[str]:
        """
        Validates that each input fields of defined input object types refer to
        an input type.
        :return: a list of errors
        :rtype: List[str]
        """
        errors = []
        for typename in self._input_types:
            gqltype = self.type_definitions[typename]
            if isinstance(gqltype, GraphQLInputObjectType):
                for field in gqltype.input_fields.values():
                    errors.extend(
                        self._validate_type_is_an_input_types(
                            field, f"Field < {typename}.{field.name} >"
                        )
                    )
        return errors

    def _validate_directive_implementation(self) -> List[str]:
        """
        Validates that defined directives provide a proper implementation.
        :return: a list of errors
        :rtype: List[str]
        """
        errors = []
        for directive in self._directive_definitions.values():
            for expected in _IMPLEMENTABLE_DIRECTIVE_FUNCTION_HOOKS:
                attr = getattr(directive.implementation, expected, None)
                if attr and not is_valid_coroutine(attr):
                    errors.append(
                        f"Directive {directive.name} Method "
                        f"{expected} is not awaitable."
                    )

            for expected in _IMPLEMENTABLE_DIRECTIVE_GENERATOR_HOOKS:
                attr = getattr(directive.implementation, expected, None)
                if attr and not is_valid_async_generator(attr):
                    errors.append(
                        f"Directive {directive.name} Method "
                        f"{expected} is not an Async Generator."
                    )

        return errors

    def _validate_enum_extensions(self) -> List[str]:
        errors = []

        for extension in [
            x
            for x in self.extensions
            if isinstance(x, GraphQLEnumTypeExtension)
        ]:

            extended = self.type_definitions.get(extension.name)
            ext_errors = _validate_extension(
                extended, extension.name, GraphQLEnumType, "ENUM"
            )
            errors.extend(ext_errors)
            if not ext_errors:
                values = [x.name for x in extended.values]
                for value in extension.values:
                    if value.name in values:
                        errors.append(
                            f"Can't add < {value.name} > Value "
                            f"to < {extension.name} > "
                            f"ENUM, cause value already exists."
                        )

                errors.extend(
                    _validate_extension_directives(extension, extended, "ENUM")
                )

        return errors

    def _validate_object_extensions(self) -> List[str]:
        errors = []

        for extension in [
            x
            for x in self.extensions
            if isinstance(x, GraphQLObjectTypeExtension)
        ]:

            extended = self.type_definitions.get(extension.name)
            ext_errors = _validate_extension(
                extended, extension.name, GraphQLObjectType, "TYPE"
            )
            errors.extend(ext_errors)
            if not ext_errors:
                for field in extension.fields:
                    if field in extended.implemented_fields:
                        errors.append(
                            f"Can't add Field < {field} > to "
                            f"TYPE < {extended.name} > "
                            f"cause field already exists."
                        )

                for interface in extension.interfaces:
                    if interface in extended.interfaces_names:
                        errors.append(
                            f"Can't add Interface < {interface} > "
                            f"to TYPE < {extended.name} > "
                            f"cause Interface already exists."
                        )

                errors.extend(
                    _validate_extension_directives(extension, extended, "TYPE")
                )

        return errors

    def _validate_union_extensions(self) -> List[str]:
        errors = []

        for extension in [
            x
            for x in self.extensions
            if isinstance(x, GraphQLUnionTypeExtension)
        ]:

            extended = self.type_definitions.get(extension.name)
            ext_errors = _validate_extension(
                extended, extension.name, GraphQLUnionType, "UNION"
            )
            errors.extend(ext_errors)
            if not ext_errors:
                for typ in extension.types:
                    if typ in extended.types:
                        errors.append(
                            f"Can't add PossibleType < {typ} > to "
                            f"UNION < {extended.name} > "
                            f"cause PossibleType already exists."
                        )

                errors.extend(
                    _validate_extension_directives(
                        extension, extended, "UNION"
                    )
                )

        return errors

    def _validate_input_object_extensions(self) -> List[str]:
        errors = []

        for extension in [
            x
            for x in self.extensions
            if isinstance(x, GraphQLInputObjectTypeExtension)
        ]:
            extended = self.type_definitions.get(extension.name)
            ext_errors = _validate_extension(
                extended, extension.name, GraphQLInputObjectType, "INPUT"
            )
            errors.extend(ext_errors)
            if not ext_errors:
                errors.extend(
                    _validate_extension_directives(
                        extension, extended, "INPUT"
                    )
                )

                for ifield in extension.input_fields:
                    if ifield in extended.input_fields:
                        errors.append(
                            f"Can't add Input Field < {ifield} > "
                            f"to Input Object < {extended.name} > "
                            f"cause it already exists"
                        )

        return errors

    def _validate_interface_extensions(self) -> List[str]:
        errors = []

        for extension in [
            x
            for x in self.extensions
            if isinstance(x, GraphQLInterfaceTypeExtension)
        ]:

            extended = self.type_definitions.get(extension.name)
            ext_errors = _validate_extension(
                extended, extension.name, GraphQLInterfaceType, "INTERFACE"
            )
            errors.extend(ext_errors)
            if not ext_errors:
                for field in extension.fields:
                    if field in extended.implemented_fields:
                        errors.append(
                            f"Can't add Field < {field} > to "
                            f"INTERFACE < {extended.name} > "
                            f"cause field already exists."
                        )

                errors.extend(
                    _validate_extension_directives(
                        extension, extended, "INTERFACE"
                    )
                )

        return errors

    def _validate_scalar_extensions(self) -> List[str]:
        errors = []

        for extension in [
            x
            for x in self.extensions
            if isinstance(x, GraphQLScalarTypeExtension)
        ]:

            extended = self.type_definitions.get(extension.name)
            ext_errors = _validate_extension(
                extended, extension.name, GraphQLScalarType, "SCALAR"
            )
            errors.extend(ext_errors)
            if not ext_errors:
                errors.extend(
                    _validate_extension_directives(
                        extension, extended, "SCALAR"
                    )
                )

        return errors

    def _validate_schema_extensions(self) -> List[str]:
        errors = []
        extended_operations = []
        for extension in [
            x for x in self.extensions if isinstance(x, GraphQLSchemaExtension)
        ]:
            for operation in extension.operations:
                op_type = getattr(self, f"{operation}_operation_name")
                if op_type in extended_operations:
                    errors.append(
                        f"Can't extend Schema "
                        f"Operation < {op_type} >"
                        f" multiple times"
                    )

                if self.has_type(op_type):
                    errors.append(
                        f"Can't extend Schema with "
                        f"Operation < {op_type} > "
                        f"cause type is already defined."
                    )
                else:
                    extended_operations.append(op_type)

            schema_directives = [x.name.value for x in self._schema_directives]
            for directive in extension.directives:
                if directive.name.value in schema_directives:
                    errors.append(
                        f"Can't add < {directive.name.value} > "
                        f"Directive to schema cause it's already there."
                    )

        return errors

    def _validate(self) -> bool:
        """
        Check that the given schema is valid.
        :return: a boolean which determines whether or not the schema is valid
        :rtype: bool
        """
        # TODO: Optimization: most validation functions iterate over
        # the schema types: it could be done in one loop.
        validators = [
            self._validate_schema_named_types,
            self._validate_object_follow_interfaces,
            self._validate_schema_root_types_exist,
            self._validate_non_empty_object,
            self._validate_union_is_acceptable,
            self._validate_all_scalars_have_implementations,
            self._validate_enum_values_are_unique,
            self._validate_arguments_have_valid_type,
            self._validate_input_type_composed_of_input_type,
            self._validate_directive_implementation,
            # TODO: Validate Field: default value must be of given type
            # TODO: Check all objects have resolvers (at least in parent)
        ]
        errors = []
        for validator in validators:
            errors.extend(validator())

        if errors:
            raise GraphQLSchemaError(
                message=_format_schema_error_message(errors)
            )
        return True

    def _validate_extensions(self) -> None:
        validators = [
            self._validate_enum_extensions,
            self._validate_input_object_extensions,
            self._validate_object_extensions,
            self._validate_interface_extensions,
            self._validate_scalar_extensions,
            self._validate_union_extensions,
            self._validate_schema_extensions,
        ]
        errors = []
        for validator in validators:
            errors.extend(validator())

        if errors:
            raise GraphQLSchemaError(
                message=_format_schema_error_message(errors)
            )

    async def _bake_types(
        self, custom_default_resolver: Optional[Callable] = None
    ) -> None:
        """
        Bakes types linked to the schema.
        :param custom_default_resolver: callable that will replace the builtin
        default_resolver (called as resolver for each UNDECORATED field)
        :type custom_default_resolver: Optional[Callable]
        """
        for scalar_definition in self._scalar_definitions.values():
            scalar_definition.bake(self)

        for type_definition in self.type_definitions.values():
            # Scalar types are already baked
            if not isinstance(type_definition, GraphQLScalarType):
                type_definition.bake(self)

        for directive_definition in self._directive_definitions.values():
            directive_definition.bake(self)

        for type_definition in self.type_definitions.values():
            if isinstance(
                type_definition,
                (GraphQLObjectType, GraphQLInterfaceType, GraphQLUnionType),
            ):
                await type_definition.bake_fields(
                    self, custom_default_resolver
                )
            elif isinstance(type_definition, GraphQLEnumType):
                await type_definition.bake_enum_values(self)
            elif isinstance(type_definition, GraphQLInputObjectType):
                await type_definition.bake_input_fields(self)

    def get_operation_root_type(
        self, operation: "OperationDefinitionNode"
    ) -> "GraphQLObjectType":
        """
        Extracts the root type of the operation from the schema.
        :param operation: AST operation definition node from which retrieve the
        root type
        :type operation: OperationDefinitionNode
        :return: the GraphQLObjectType instance related to the operation
        definition
        :rtype: GraphQLObjectType
        """
        try:
            return self._operation_types[operation.operation_type]
        except KeyError:
            raise graphql_error_from_nodes(
                "Schema is not configured for %ss." % operation.operation_type,
                nodes=operation,
            )

    def _bake_extensions(self):
        for extension in self.extensions:
            extension.bake(self)

    def bake_execute(self, func_query, func_subscription):
        directives = compute_directive_nodes(self, self._schema_directives)
        func_query = wraps_with_directives(
            directives, "on_schema_execution", func_query, is_resolver=True
        )

        func_subscription = wraps_with_directives(
            directives,
            "on_schema_subscription",
            func_subscription,
            is_async_generator=True,
        )

        return func_query, func_subscription

    async def bake(
        self,
        custom_default_resolver: Optional[Callable] = None,
        custom_default_type_resolver: Optional[Callable] = None,
        custom_default_arguments_coercer: Optional[Callable] = None,
    ) -> None:
        """
        Bake the final schema (it should not change after this) used for
        execution.
        :param custom_default_resolver: callable that will replace the builtin
        default_resolver
        :param custom_default_type_resolver: callable that will replace the
        tartiflette `default_type_resolver` (will be called on abstract types
        to deduct the type of a result)
        :param custom_default_arguments_coercer: callable that will replace the
        tartiflette `default_arguments_coercer`
        :type custom_default_resolver: Optional[Callable]
        :type custom_default_type_resolver: Optional[Callable]
        :type custom_default_arguments_coercer: Optional[Callable]
        """
        self.default_type_resolver = (
            custom_default_type_resolver or default_type_resolver
        )
        self.default_arguments_coercer = (
            custom_default_arguments_coercer or gather_arguments_coercer
        )
        self._inject_introspection_fields()

        self._validate_extensions()  # Validate this before bake
        # TODO maybe a pre_bake/post_bake thing

        try:
            self._bake_extensions()
        except Exception:  # pylint: disable=broad-except
            # Exceptions should be collected at validation time
            pass

        SchemaRegistry.bake_registered_objects(self)

        try:
            await self._bake_types(custom_default_resolver)
        except Exception:  # pylint: disable=broad-except
            # Exceptions should be collected at validation time
            pass

        self._validate()

        # Bake introspection attributes
        self._operation_types = {
            "query": self.type_definitions.get(self.query_operation_name),
            "mutation": self.type_definitions.get(
                self.mutation_operation_name
            ),
            "subscription": self.type_definitions.get(
                self.subscription_operation_name
            ),
        }
        self.queryType = self._operation_types["query"]
        self.mutationType = self._operation_types["mutation"]
        self.subscriptionType = self._operation_types["subscription"]
        self.directives = list(self._directive_definitions.values())

        for type_name, type_definition in self.type_definitions.items():
            if not type_name.startswith("__"):
                self.types.append(type_definition)