<?php

declare(strict_types=1);

namespace GraphQL\Tests\Utils;

use GraphQL\Error\Error;
use GraphQL\GraphQL;
use GraphQL\Language\AST\DocumentNode;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeList;
use GraphQL\Language\DirectiveLocation;
use GraphQL\Language\Parser;
use GraphQL\Language\Printer;
use GraphQL\Type\Definition\CustomScalarType;
use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\FieldArgument;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\ScalarType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\UnionType;
use GraphQL\Type\Schema;
use GraphQL\Utils\SchemaExtender;
use GraphQL\Utils\SchemaPrinter;
use PHPUnit\Framework\TestCase;
use function array_filter;
use function array_map;
use function array_merge;
use function array_values;
use function count;
use function implode;
use function in_array;
use function iterator_to_array;
use function preg_match;
use function preg_replace;
use function trim;
use const PHP_EOL;

class SchemaExtenderTest extends TestCase
{
    /** @var Schema */
    protected $testSchema;

    /** @var string[] */
    protected $testSchemaDefinitions;

    /** @var ObjectType */
    protected $FooType;

    /** @var Directive */
    protected $FooDirective;

    public function setUp()
    {
        parent::setUp();

        $SomeScalarType = new CustomScalarType([
            'name' => 'SomeScalar',
            'serialize' => static function ($x) {
                return $x;
            },
        ]);

        $SomeInterfaceType = new InterfaceType([
            'name' => 'SomeInterface',
            'fields' => static function () use (&$SomeInterfaceType) {
                return [
                    'name' => [ 'type' => Type::string()],
                    'some' => [ 'type' => $SomeInterfaceType],
                ];
            },
        ]);

        $FooType = new ObjectType([
            'name' => 'Foo',
            'interfaces' => [$SomeInterfaceType],
            'fields' => static function () use ($SomeInterfaceType, &$FooType) {
                return [
                    'name' => [ 'type' => Type::string() ],
                    'some' => [ 'type' => $SomeInterfaceType ],
                    'tree' => [ 'type' => Type::nonNull(Type::listOf($FooType))],
                ];
            },
        ]);

        $BarType = new ObjectType([
            'name' => 'Bar',
            'interfaces' => [$SomeInterfaceType],
            'fields' => static function () use ($SomeInterfaceType, $FooType) : array {
                return [
                    'name' => [ 'type' => Type::string() ],
                    'some' => [ 'type' => $SomeInterfaceType ],
                    'foo' => [ 'type' => $FooType ],
                ];
            },
        ]);

        $BizType = new ObjectType([
            'name' => 'Biz',
            'fields' => static function () : array {
                return [
                    'fizz' => [ 'type' => Type::string() ],
                ];
            },
        ]);

        $SomeUnionType = new UnionType([
            'name' => 'SomeUnion',
            'types' => [$FooType, $BizType],
        ]);

        $SomeEnumType = new EnumType([
            'name' => 'SomeEnum',
            'values' => [
                'ONE' => [ 'value' => 1 ],
                'TWO' => [ 'value' => 2 ],
            ],
        ]);

        $SomeInputType = new InputObjectType([
            'name' => 'SomeInput',
            'fields' => static function () : array {
                return [
                    'fooArg' => [ 'type' => Type::string() ],
                ];
            },
        ]);

        $FooDirective = new Directive([
            'name' => 'foo',
            'args' => [
                new FieldArgument([
                    'name' => 'input',
                    'type' => $SomeInputType,
                ]),
            ],
            'locations' => [
                DirectiveLocation::SCHEMA,
                DirectiveLocation::SCALAR,
                DirectiveLocation::OBJECT,
                DirectiveLocation::FIELD_DEFINITION,
                DirectiveLocation::ARGUMENT_DEFINITION,
                DirectiveLocation::IFACE,
                DirectiveLocation::UNION,
                DirectiveLocation::ENUM,
                DirectiveLocation::ENUM_VALUE,
                DirectiveLocation::INPUT_OBJECT,
                DirectiveLocation::INPUT_FIELD_DEFINITION,
            ],
        ]);

        $this->testSchema = new Schema([
            'query' => new ObjectType([
                'name' => 'Query',
                'fields' => static function () use ($FooType, $SomeScalarType, $SomeUnionType, $SomeEnumType, $SomeInterfaceType, $SomeInputType) : array {
                    return [
                        'foo' => [ 'type' => $FooType ],
                        'someScalar' => [ 'type' => $SomeScalarType ],
                        'someUnion' => [ 'type' => $SomeUnionType ],
                        'someEnum' => [ 'type' => $SomeEnumType ],
                        'someInterface' => [
                            'args' => [
                                'id' => [
                                    'type' => Type::nonNull(Type::ID()),
                                ],
                            ],
                            'type' => $SomeInterfaceType,
                        ],
                        'someInput' => [
                            'args' => [ 'input' => [ 'type' => $SomeInputType ] ],
                            'type' => Type::string(),
                        ],
                    ];
                },
            ]),
            'types' => [$FooType, $BarType],
            'directives' => array_merge(GraphQL::getStandardDirectives(), [$FooDirective]),
        ]);

        $testSchemaAst = Parser::parse(SchemaPrinter::doPrint($this->testSchema));

        $this->testSchemaDefinitions = array_map(static function ($node) {
            return Printer::doPrint($node);
        }, iterator_to_array($testSchemaAst->definitions->getIterator()));

        $this->FooDirective = $FooDirective;
        $this->FooType      = $FooType;
    }

    protected function dedent(string $str) : string
    {
        $trimmedStr = trim($str, "\n");
        $trimmedStr = preg_replace('/[ \t]*$/', '', $trimmedStr);

        preg_match('/^[ \t]*/', $trimmedStr, $indentMatch);
        $indent = $indentMatch[0];
        return preg_replace('/^' . $indent . '/m', '', $trimmedStr);
    }

    /**
     * @param mixed[]|null $options
     */
    protected function extendTestSchema(string $sdl, ?array $options = null) : Schema
    {
        $originalPrint  = SchemaPrinter::doPrint($this->testSchema);
        $ast            = Parser::parse($sdl);
        $extendedSchema = SchemaExtender::extend($this->testSchema, $ast, $options);
        self::assertEquals(SchemaPrinter::doPrint($this->testSchema), $originalPrint);
        return $extendedSchema;
    }

    protected function printTestSchemaChanges(Schema $extendedSchema) : string
    {
        $ast              = Parser::parse(SchemaPrinter::doPrint($extendedSchema));
        $ast->definitions = array_values(array_filter(
            $ast->definitions instanceof NodeList ? iterator_to_array($ast->definitions->getIterator()) : $ast->definitions,
            function (Node $node) : bool {
                return ! in_array(Printer::doPrint($node), $this->testSchemaDefinitions);
            }
        ));

        return Printer::doPrint($ast);
    }

    /**
     * @see it('returns the original schema when there are no type definitions')
     */
    public function testReturnsTheOriginalSchemaWhenThereAreNoTypeDefinitions()
    {
        $extendedSchema = $this->extendTestSchema('{ field }');
        self::assertEquals($extendedSchema, $this->testSchema);
    }

    /**
     * @see it('extends without altering original schema')
     */
    public function testExtendsWithoutAlteringOriginalSchema()
    {
        $extendedSchema = $this->extendTestSchema('
            extend type Query {
                newField: String
            }');
        self::assertNotEquals($extendedSchema, $this->testSchema);
        self::assertContains('newField', SchemaPrinter::doPrint($extendedSchema));
        self::assertNotContains('newField', SchemaPrinter::doPrint($this->testSchema));
    }

    /**
     * @see it('can be used for limited execution')
     */
    public function testCanBeUsedForLimitedExecution()
    {
        $extendedSchema = $this->extendTestSchema('
          extend type Query {
            newField: String
          }
        ');

        $result = GraphQL::executeQuery($extendedSchema, '{ newField }', ['newField' => 123]);

        self::assertEquals($result->toArray(), [
            'data' => ['newField' => '123'],
        ]);
    }

    /**
     * @see it('can describe the extended fields')
     */
    public function testCanDescribeTheExtendedFields()
    {
        $extendedSchema = $this->extendTestSchema('
            extend type Query {
                "New field description."
                newField: String
            }
        ');

        self::assertEquals(
            $extendedSchema->getQueryType()->getField('newField')->description,
            'New field description.'
        );
    }

    /**
     * @see it('can describe the extended fields with legacy comments')
     */
    public function testCanDescribeTheExtendedFieldsWithLegacyComments()
    {
        $extendedSchema = $this->extendTestSchema('
            extend type Query {
                # New field description.
                newField: String
            }
        ', ['commentDescriptions' => true]);

        self::assertEquals(
            $extendedSchema->getQueryType()->getField('newField')->description,
            'New field description.'
        );
    }

    /**
     * @see it('describes extended fields with strings when present')
     */
    public function testDescribesExtendedFieldsWithStringsWhenPresent()
    {
        $extendedSchema = $this->extendTestSchema('
            extend type Query {
                # New field description.
                "Actually use this description."
                newField: String
            }
        ', ['commentDescriptions' => true ]);

        self::assertEquals(
            $extendedSchema->getQueryType()->getField('newField')->description,
            'Actually use this description.'
        );
    }

    /**
     * @see it('extends objects by adding new fields')
     */
    public function testExtendsObjectsByAddingNewFields()
    {
        $extendedSchema = $this->extendTestSchema(
            '
            extend type Foo {
                newField: String
            }
        '
        );

        self::assertEquals(
            $this->printTestSchemaChanges($extendedSchema),
            $this->dedent('
              type Foo implements SomeInterface {
                name: String
                some: SomeInterface
                tree: [Foo]!
                newField: String
              }
            ')
        );

        $fooType  = $extendedSchema->getType('Foo');
        $fooField = $extendedSchema->getQueryType()->getField('foo');
        self::assertEquals($fooField->getType(), $fooType);
    }

    /**
     * @see it('extends enums by adding new values')
     */
    public function testExtendsEnumsByAddingNewValues()
    {
        $extendedSchema = $this->extendTestSchema('
          extend enum SomeEnum {
            NEW_ENUM
          }
        ');

        self::assertEquals(
            $this->printTestSchemaChanges($extendedSchema),
            $this->dedent('
              enum SomeEnum {
                ONE
                TWO
                NEW_ENUM
              }
          ')
        );

        $someEnumType = $extendedSchema->getType('SomeEnum');
        $enumField    = $extendedSchema->getQueryType()->getField('someEnum');
        self::assertEquals($enumField->getType(), $someEnumType);
    }

    /**
     * @see it('extends unions by adding new types')
     */
    public function testExtendsUnionsByAddingNewTypes()
    {
        $extendedSchema = $this->extendTestSchema('
          extend union SomeUnion = Bar
        ');
        self::assertEquals(
            $this->printTestSchemaChanges($extendedSchema),
            $this->dedent('
              union SomeUnion = Foo | Biz | Bar
            ')
        );

        $someUnionType = $extendedSchema->getType('SomeUnion');
        $unionField    = $extendedSchema->getQueryType()->getField('someUnion');
        self::assertEquals($unionField->getType(), $someUnionType);
    }

    /**
     * @see it('allows extension of union by adding itself')
     */
    public function testAllowsExtensionOfUnionByAddingItself()
    {
        $extendedSchema = $this->extendTestSchema('
          extend union SomeUnion = SomeUnion
        ');

        $errors = $extendedSchema->validate();
        self::assertGreaterThan(0, count($errors));

        self::assertEquals(
            $this->printTestSchemaChanges($extendedSchema),
            $this->dedent('
                union SomeUnion = Foo | Biz | SomeUnion
            ')
        );
    }

    /**
     * @see it('extends inputs by adding new fields')
     */
    public function testExtendsInputsByAddingNewFields()
    {
        $extendedSchema = $this->extendTestSchema('
          extend input SomeInput {
            newField: String
          }
        ');

        self::assertEquals(
            $this->printTestSchemaChanges($extendedSchema),
            $this->dedent('
              input SomeInput {
                fooArg: String
                newField: String
              }
            ')
        );

        $someInputType = $extendedSchema->getType('SomeInput');
        $inputField    = $extendedSchema->getQueryType()->getField('someInput');
        self::assertEquals($inputField->args[0]->getType(), $someInputType);

        $fooDirective = $extendedSchema->getDirective('foo');
        self::assertEquals($fooDirective->args[0]->getType(), $someInputType);
    }

    /**
     * @see it('extends scalars by adding new directives')
     */
    public function testExtendsScalarsByAddingNewDirectives()
    {
        $extendedSchema = $this->extendTestSchema('
          extend scalar SomeScalar @foo
        ');

        $someScalar = $extendedSchema->getType('SomeScalar');
        self::assertCount(1, $someScalar->extensionASTNodes);
        self::assertEquals(
            Printer::doPrint($someScalar->extensionASTNodes[0]),
            'extend scalar SomeScalar @foo'
        );
    }

    /**
     * @see it('correctly assign AST nodes to new and extended types')
     */
    public function testCorrectlyAssignASTNodesToNewAndExtendedTypes()
    {
        $extendedSchema = $this->extendTestSchema('
              extend type Query {
                newField(testArg: TestInput): TestEnum
              }
              extend scalar SomeScalar @foo
              extend enum SomeEnum {
                NEW_VALUE
              }
              extend union SomeUnion = Bar
              extend input SomeInput {
                newField: String
              }
              extend interface SomeInterface {
                newField: String
              }
              enum TestEnum {
                TEST_VALUE
              }
              input TestInput {
                testInputField: TestEnum
              }
            ');

        $ast = Parser::parse('
            extend type Query {
                oneMoreNewField: TestUnion
            }
            extend scalar SomeScalar @test
            extend enum SomeEnum {
                ONE_MORE_NEW_VALUE
            }
            extend union SomeUnion = TestType
            extend input SomeInput {
                oneMoreNewField: String
            }
            extend interface SomeInterface {
                oneMoreNewField: String
            }
            union TestUnion = TestType
            interface TestInterface {
                interfaceField: String
            }
            type TestType implements TestInterface {
                interfaceField: String
            }
            directive @test(arg: Int) on FIELD | SCALAR
        ');

        $extendedTwiceSchema = SchemaExtender::extend($extendedSchema, $ast);
        $query               = $extendedTwiceSchema->getQueryType();
        /** @var ScalarType $someScalar */
        $someScalar = $extendedTwiceSchema->getType('SomeScalar');
        /** @var EnumType $someEnum */
        $someEnum = $extendedTwiceSchema->getType('SomeEnum');
        /** @var UnionType $someUnion */
        $someUnion = $extendedTwiceSchema->getType('SomeUnion');
        /** @var InputObjectType $someInput */
        $someInput = $extendedTwiceSchema->getType('SomeInput');
        /** @var InterfaceType $someInterface */
        $someInterface = $extendedTwiceSchema->getType('SomeInterface');

        /** @var InputObjectType $testInput */
        $testInput = $extendedTwiceSchema->getType('TestInput');
        /** @var EnumType $testEnum */
        $testEnum = $extendedTwiceSchema->getType('TestEnum');
        /** @var UnionType $testUnion */
        $testUnion = $extendedTwiceSchema->getType('TestUnion');
        /** @var InterfaceType $testInterface */
        $testInterface = $extendedTwiceSchema->getType('TestInterface');
        /** @var ObjectType $testType */
        $testType = $extendedTwiceSchema->getType('TestType');
        /** @var Directive $testDirective */
        $testDirective = $extendedTwiceSchema->getDirective('test');

        self::assertCount(2, $query->extensionASTNodes);
        self::assertCount(2, $someScalar->extensionASTNodes);
        self::assertCount(2, $someEnum->extensionASTNodes);
        self::assertCount(2, $someUnion->extensionASTNodes);
        self::assertCount(2, $someInput->extensionASTNodes);
        self::assertCount(2, $someInterface->extensionASTNodes);

        self::assertCount(0, $testType->extensionASTNodes ?? []);
        self::assertCount(0, $testEnum->extensionASTNodes ?? []);
        self::assertCount(0, $testUnion->extensionASTNodes ?? []);
        self::assertCount(0, $testInput->extensionASTNodes ?? []);
        self::assertCount(0, $testInterface->extensionASTNodes ?? []);

        $restoredExtensionAST = new DocumentNode([
            'definitions' => array_merge(
                $query->extensionASTNodes,
                $someScalar->extensionASTNodes,
                $someEnum->extensionASTNodes,
                $someUnion->extensionASTNodes,
                $someInput->extensionASTNodes,
                $someInterface->extensionASTNodes,
                [
                    $testInput->astNode,
                    $testEnum->astNode,
                    $testUnion->astNode,
                    $testInterface->astNode,
                    $testType->astNode,
                    $testDirective->astNode,
                ]
            ),
        ]);

        self::assertEquals(
            SchemaPrinter::doPrint(SchemaExtender::extend($this->testSchema, $restoredExtensionAST)),
            SchemaPrinter::doPrint($extendedTwiceSchema)
        );

        $newField = $query->getField('newField');

        self::assertEquals(Printer::doPrint($newField->astNode), 'newField(testArg: TestInput): TestEnum');
        self::assertEquals(Printer::doPrint($newField->args[0]->astNode), 'testArg: TestInput');
        self::assertEquals(Printer::doPrint($query->getField('oneMoreNewField')->astNode), 'oneMoreNewField: TestUnion');
        self::assertEquals(Printer::doPrint($someEnum->getValue('NEW_VALUE')->astNode), 'NEW_VALUE');
        self::assertEquals(Printer::doPrint($someEnum->getValue('ONE_MORE_NEW_VALUE')->astNode), 'ONE_MORE_NEW_VALUE');
        self::assertEquals(Printer::doPrint($someInput->getField('newField')->astNode), 'newField: String');
        self::assertEquals(Printer::doPrint($someInput->getField('oneMoreNewField')->astNode), 'oneMoreNewField: String');
        self::assertEquals(Printer::doPrint($someInterface->getField('newField')->astNode), 'newField: String');
        self::assertEquals(Printer::doPrint($someInterface->getField('oneMoreNewField')->astNode), 'oneMoreNewField: String');
        self::assertEquals(Printer::doPrint($testInput->getField('testInputField')->astNode), 'testInputField: TestEnum');
        self::assertEquals(Printer::doPrint($testEnum->getValue('TEST_VALUE')->astNode), 'TEST_VALUE');
        self::assertEquals(Printer::doPrint($testInterface->getField('interfaceField')->astNode), 'interfaceField: String');
        self::assertEquals(Printer::doPrint($testType->getField('interfaceField')->astNode), 'interfaceField: String');
        self::assertEquals(Printer::doPrint($testDirective->args[0]->astNode), 'arg: Int');
    }

    /**
     * @see it('builds types with deprecated fields/values')
     */
    public function testBuildsTypesWithDeprecatedFieldsOrValues()
    {
        $extendedSchema = $this->extendTestSchema('
            type TypeWithDeprecatedField {
                newDeprecatedField: String @deprecated(reason: "not used anymore")
            }
            enum EnumWithDeprecatedValue {
                DEPRECATED @deprecated(reason: "do not use")
            }
        ');

        /** @var ObjectType $typeWithDeprecatedField */
        $typeWithDeprecatedField = $extendedSchema->getType('TypeWithDeprecatedField');
        $deprecatedFieldDef      = $typeWithDeprecatedField->getField('newDeprecatedField');

        self::assertEquals(true, $deprecatedFieldDef->isDeprecated());
        self::assertEquals('not used anymore', $deprecatedFieldDef->deprecationReason);

        /** @var EnumType $enumWithDeprecatedValue */
        $enumWithDeprecatedValue = $extendedSchema->getType('EnumWithDeprecatedValue');
        $deprecatedEnumDef       = $enumWithDeprecatedValue->getValue('DEPRECATED');

        self::assertEquals(true, $deprecatedEnumDef->isDeprecated());
        self::assertEquals('do not use', $deprecatedEnumDef->deprecationReason);
    }

    /**
     * @see it('extends objects with deprecated fields')
     */
    public function testExtendsObjectsWithDeprecatedFields()
    {
        $extendedSchema = $this->extendTestSchema('
          extend type Foo {
            deprecatedField: String @deprecated(reason: "not used anymore")
          }
        ');
        /** @var ObjectType $fooType */
        $fooType            = $extendedSchema->getType('Foo');
        $deprecatedFieldDef = $fooType->getField('deprecatedField');

        self::assertTrue($deprecatedFieldDef->isDeprecated());
        self::assertEquals('not used anymore', $deprecatedFieldDef->deprecationReason);
    }

    /**
     * @see it('extends enums with deprecated values')
     */
    public function testExtendsEnumsWithDeprecatedValues()
    {
        $extendedSchema = $this->extendTestSchema('
          extend enum SomeEnum {
            DEPRECATED @deprecated(reason: "do not use")
          }
        ');

        /** @var EnumType $someEnumType */
        $someEnumType      = $extendedSchema->getType('SomeEnum');
        $deprecatedEnumDef = $someEnumType->getValue('DEPRECATED');

        self::assertTrue($deprecatedEnumDef->isDeprecated());
        self::assertEquals('do not use', $deprecatedEnumDef->deprecationReason);
    }

    /**
     * @see it('adds new unused object type')
     */
    public function testAddsNewUnusedObjectType()
    {
        $extendedSchema = $this->extendTestSchema('
          type Unused {
            someField: String
          }
        ');
        self::assertNotEquals($this->testSchema, $extendedSchema);
        self::assertEquals(
            $this->dedent('
                type Unused {
                  someField: String
                }
            '),
            $this->printTestSchemaChanges($extendedSchema)
        );
    }

    /**
     * @see it('adds new unused enum type')
     */
    public function testAddsNewUnusedEnumType()
    {
        $extendedSchema = $this->extendTestSchema('
          enum UnusedEnum {
            SOME
          }
        ');
        self::assertNotEquals($extendedSchema, $this->testSchema);
        self::assertEquals(
            $this->dedent('
                enum UnusedEnum {
                  SOME
                }
            '),
            $this->printTestSchemaChanges($extendedSchema)
        );
    }

    /**
     * @see it('adds new unused input object type')
     */
    public function testAddsNewUnusedInputObjectType()
    {
        $extendedSchema = $this->extendTestSchema('
          input UnusedInput {
            someInput: String
          }
        ');

        self::assertNotEquals($extendedSchema, $this->testSchema);
        self::assertEquals(
            $this->dedent('
              input UnusedInput {
                someInput: String
              }
            '),
            $this->printTestSchemaChanges($extendedSchema)
        );
    }

    /**
     * @see it('adds new union using new object type')
     */
    public function testAddsNewUnionUsingNewObjectType()
    {
        $extendedSchema = $this->extendTestSchema('
          type DummyUnionMember {
            someField: String
          }

          union UnusedUnion = DummyUnionMember
        ');

        self::assertNotEquals($extendedSchema, $this->testSchema);
        self::assertEquals(
            $this->dedent('
              type DummyUnionMember {
                someField: String
              }

              union UnusedUnion = DummyUnionMember
            '),
            $this->printTestSchemaChanges($extendedSchema)
        );
    }

    /**
     * @see it('extends objects by adding new fields with arguments')
     */
    public function testExtendsObjectsByAddingNewFieldsWithArguments()
    {
        $extendedSchema = $this->extendTestSchema('
          extend type Foo {
            newField(arg1: String, arg2: NewInputObj!): String
          }

          input NewInputObj {
            field1: Int
            field2: [Float]
            field3: String!
          }
        ');

        self::assertEquals(
            $this->dedent('
                type Foo implements SomeInterface {
                  name: String
                  some: SomeInterface
                  tree: [Foo]!
                  newField(arg1: String, arg2: NewInputObj!): String
                }

                input NewInputObj {
                  field1: Int
                  field2: [Float]
                  field3: String!
                }
            '),
            $this->printTestSchemaChanges($extendedSchema)
        );
    }

    /**
     * @see it('extends objects by adding new fields with existing types')
     */
    public function testExtendsObjectsByAddingNewFieldsWithExistingTypes()
    {
        $extendedSchema = $this->extendTestSchema('
          extend type Foo {
            newField(arg1: SomeEnum!): SomeEnum
          }
        ');

        self::assertEquals(
            $this->dedent('
                type Foo implements SomeInterface {
                  name: String
                  some: SomeInterface
                  tree: [Foo]!
                  newField(arg1: SomeEnum!): SomeEnum
                }
            '),
            $this->printTestSchemaChanges($extendedSchema)
        );
    }

    /**
     * @see it('extends objects by adding implemented interfaces')
     */
    public function testExtendsObjectsByAddingImplementedInterfaces()
    {
        $extendedSchema = $this->extendTestSchema('
          extend type Biz implements SomeInterface {
            name: String
            some: SomeInterface
          }
        ');

        self::assertEquals(
            $this->dedent('
                type Biz implements SomeInterface {
                  fizz: String
                  name: String
                  some: SomeInterface
                }
            '),
            $this->printTestSchemaChanges($extendedSchema)
        );
    }

    /**
     * @see it('extends objects by including new types')
     */
    public function testExtendsObjectsByIncludingNewTypes()
    {
        $extendedSchema = $this->extendTestSchema('
          extend type Foo {
            newObject: NewObject
            newInterface: NewInterface
            newUnion: NewUnion
            newScalar: NewScalar
            newEnum: NewEnum
            newTree: [Foo]!
          }

          type NewObject implements NewInterface {
            baz: String
          }

          type NewOtherObject {
            fizz: Int
          }

          interface NewInterface {
            baz: String
          }

          union NewUnion = NewObject | NewOtherObject

          scalar NewScalar

          enum NewEnum {
            OPTION_A
            OPTION_B
          }
        ');

        self::assertEquals(
            $this->dedent('
                type Foo implements SomeInterface {
                  name: String
                  some: SomeInterface
                  tree: [Foo]!
                  newObject: NewObject
                  newInterface: NewInterface
                  newUnion: NewUnion
                  newScalar: NewScalar
                  newEnum: NewEnum
                  newTree: [Foo]!
                }

                enum NewEnum {
                  OPTION_A
                  OPTION_B
                }

                interface NewInterface {
                  baz: String
                }

                type NewObject implements NewInterface {
                  baz: String
                }

                type NewOtherObject {
                  fizz: Int
                }

                scalar NewScalar

                union NewUnion = NewObject | NewOtherObject
            '),
            $this->printTestSchemaChanges($extendedSchema)
        );
    }

    /**
     * @see it('extends objects by adding implemented new interfaces')
     */
    public function testExtendsObjectsByAddingImplementedNewInterfaces()
    {
        $extendedSchema = $this->extendTestSchema('
          extend type Foo implements NewInterface {
            baz: String
          }

          interface NewInterface {
            baz: String
          }
        ');

        self::assertEquals(
            $this->dedent('
                type Foo implements SomeInterface & NewInterface {
                  name: String
                  some: SomeInterface
                  tree: [Foo]!
                  baz: String
                }

                interface NewInterface {
                  baz: String
                }
            '),
            $this->printTestSchemaChanges($extendedSchema)
        );
    }

    /**
     * @see it('extends different types multiple times')
     */
    public function testExtendsDifferentTypesMultipleTimes()
    {
        $extendedSchema = $this->extendTestSchema('
          extend type Biz implements NewInterface {
            buzz: String
          }

          extend type Biz implements SomeInterface {
            name: String
            some: SomeInterface
            newFieldA: Int
          }

          extend type Biz {
            newFieldA: Int
            newFieldB: Float
          }

          interface NewInterface {
            buzz: String
          }

          extend enum SomeEnum {
            THREE
          }

          extend enum SomeEnum {
            FOUR
          }

          extend union SomeUnion = Boo

          extend union SomeUnion = Joo

          type Boo {
            fieldA: String
          }

          type Joo {
            fieldB: String
          }

          extend input SomeInput {
            fieldA: String
          }

          extend input SomeInput {
            fieldB: String
          }
        ');

        self::assertEquals(
            $this->dedent('
                type Biz implements NewInterface & SomeInterface {
                  fizz: String
                  buzz: String
                  name: String
                  some: SomeInterface
                  newFieldA: Int
                  newFieldB: Float
                }

                type Boo {
                  fieldA: String
                }

                type Joo {
                  fieldB: String
                }

                interface NewInterface {
                  buzz: String
                }

                enum SomeEnum {
                  ONE
                  TWO
                  THREE
                  FOUR
                }

                input SomeInput {
                  fooArg: String
                  fieldA: String
                  fieldB: String
                }

                union SomeUnion = Foo | Biz | Boo | Joo
            '),
            $this->printTestSchemaChanges($extendedSchema)
        );
    }

    /**
     * @see it('extends interfaces by adding new fields')
     */
    public function testExtendsInterfacesByAddingNewFields()
    {
        $extendedSchema = $this->extendTestSchema('
          extend interface SomeInterface {
            newField: String
          }

          extend type Bar {
            newField: String
          }

          extend type Foo {
            newField: String
          }
        ');

        self::assertEquals(
            $this->dedent('
                type Bar implements SomeInterface {
                  name: String
                  some: SomeInterface
                  foo: Foo
                  newField: String
                }

                type Foo implements SomeInterface {
                  name: String
                  some: SomeInterface
                  tree: [Foo]!
                  newField: String
                }

                interface SomeInterface {
                  name: String
                  some: SomeInterface
                  newField: String
                }
            '),
            $this->printTestSchemaChanges($extendedSchema)
        );
    }

    /**
     * @see it('allows extension of interface with missing Object fields')
     */
    public function testAllowsExtensionOfInterfaceWithMissingObjectFields()
    {
        $extendedSchema = $this->extendTestSchema('
          extend interface SomeInterface {
            newField: String
          }
        ');

        $errors = $extendedSchema->validate();
        self::assertGreaterThan(0, $errors);

        self::assertEquals(
            $this->dedent('
                interface SomeInterface {
                  name: String
                  some: SomeInterface
                  newField: String
                }
            '),
            $this->printTestSchemaChanges($extendedSchema)
        );
    }

    /**
     * @see it('extends interfaces multiple times')
     */
    public function testExtendsInterfacesMultipleTimes()
    {
        $extendedSchema = $this->extendTestSchema('
          extend interface SomeInterface {
            newFieldA: Int
          }
          extend interface SomeInterface {
            newFieldB(test: Boolean): String
          }
        ');

        self::assertEquals(
            $this->dedent('
                interface SomeInterface {
                  name: String
                  some: SomeInterface
                  newFieldA: Int
                  newFieldB(test: Boolean): String
                }
            '),
            $this->printTestSchemaChanges($extendedSchema)
        );
    }

    /**
     * @see it('may extend mutations and subscriptions')
     */
    public function testMayExtendMutationsAndSubscriptions()
    {
        $mutationSchema = new Schema([
            'query' => new ObjectType([
                'name' => 'Query',
                'fields' => static function () {
                    return [ 'queryField' => [ 'type' => Type::string() ] ];
                },
            ]),
            'mutation' => new ObjectType([
                'name' => 'Mutation',
                'fields' => static function () {
                    return [ 'mutationField' => ['type' => Type::string() ] ];
                },
            ]),
            'subscription' => new ObjectType([
                'name' => 'Subscription',
                'fields' => static function () {
                    return ['subscriptionField' => ['type' => Type::string()]];
                },
            ]),
        ]);

        $ast = Parser::parse('
            extend type Query {
              newQueryField: Int
            }

            extend type Mutation {
              newMutationField: Int
            }

            extend type Subscription {
              newSubscriptionField: Int
            }
        ');

        $originalPrint  = SchemaPrinter::doPrint($mutationSchema);
        $extendedSchema = SchemaExtender::extend($mutationSchema, $ast);
        self::assertNotEquals($mutationSchema, $extendedSchema);
        self::assertEquals(SchemaPrinter::doPrint($mutationSchema), $originalPrint);
        self::assertEquals(SchemaPrinter::doPrint($extendedSchema), $this->dedent('
            type Mutation {
              mutationField: String
              newMutationField: Int
            }

            type Query {
              queryField: String
              newQueryField: Int
            }

            type Subscription {
              subscriptionField: String
              newSubscriptionField: Int
            }
        '));
    }

    /**
     * @see it('may extend directives with new simple directive')
     */
    public function testMayExtendDirectivesWithNewSimpleDirective()
    {
        $extendedSchema = $this->extendTestSchema('
          directive @neat on QUERY
        ');

        $newDirective = $extendedSchema->getDirective('neat');
        self::assertEquals($newDirective->name, 'neat');
        self::assertContains('QUERY', $newDirective->locations);
    }

    /**
     * @see it('sets correct description when extending with a new directive')
     */
    public function testSetsCorrectDescriptionWhenExtendingWithANewDirective()
    {
        $extendedSchema = $this->extendTestSchema('
          """
          new directive
          """
          directive @new on QUERY
        ');

        $newDirective = $extendedSchema->getDirective('new');
        self::assertEquals('new directive', $newDirective->description);
    }

    /**
     * @see it('sets correct description using legacy comments')
     */
    public function testSetsCorrectDescriptionUsingLegacyComments()
    {
        $extendedSchema = $this->extendTestSchema(
            '
          # new directive
          directive @new on QUERY
        ',
            [ 'commentDescriptions' => true ]
        );

        $newDirective = $extendedSchema->getDirective('new');
        self::assertEquals('new directive', $newDirective->description);
    }


    /**
     * @see it('may extend directives with new complex directive')
     */
    public function testMayExtendDirectivesWithNewComplexDirective()
    {
        $extendedSchema = $this->extendTestSchema('
          directive @profile(enable: Boolean! tag: String) on QUERY | FIELD
        ');

        $extendedDirective = $extendedSchema->getDirective('profile');
        self::assertContains('QUERY', $extendedDirective->locations);
        self::assertContains('FIELD', $extendedDirective->locations);

        $args = $extendedDirective->args;
        self::assertCount(2, $args);

        $arg0 = $args[0];
        $arg1 = $args[1];
        /** @var NonNull $arg0Type */
        $arg0Type = $arg0->getType();

        self::assertEquals('enable', $arg0->name);
        self::assertTrue($arg0Type instanceof NonNull);
        self::assertTrue($arg0Type->getWrappedType() instanceof ScalarType);

        self::assertEquals('tag', $arg1->name);
        self::assertTrue($arg1->getType() instanceof ScalarType);
    }

    /**
     * @see it('Rejects invalid SDL')
     */
    public function testRejectsInvalidSDL()
    {
        $sdl = '
            extend schema @unknown
        ';

        try {
            $this->extendTestSchema($sdl);
            self::fail();
        } catch (Error $error) {
            self::assertEquals('Unknown directive "unknown".', $error->getMessage());
        }
    }

    /**
     * @see it('Allows to disable SDL validation')
     */
    public function testAllowsToDisableSDLValidation()
    {
        $sdl = '
          extend schema @unknown
        ';

        $this->extendTestSchema($sdl, [ 'assumeValid' => true ]);
        $this->extendTestSchema($sdl, [ 'assumeValidSDL' => true ]);
    }

    /**
     * @see it('does not allow replacing a default directive')
     */
    public function testDoesNotAllowReplacingADefaultDirective()
    {
        $sdl = '
          directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD
        ';

        try {
            $this->extendTestSchema($sdl);
            self::fail();
        } catch (Error $error) {
            self::assertEquals('Directive "include" already exists in the schema. It cannot be redefined.', $error->getMessage());
        }
    }

    /**
     * @see it('does not allow replacing a custom directive')
     */
    public function testDoesNotAllowReplacingACustomDirective()
    {
        $extendedSchema = $this->extendTestSchema('
          directive @meow(if: Boolean!) on FIELD | FRAGMENT_SPREAD
        ');

        $replacementAST = Parser::parse('
            directive @meow(if: Boolean!) on FIELD | QUERY
        ');

        try {
            SchemaExtender::extend($extendedSchema, $replacementAST);
            self::fail();
        } catch (Error $error) {
            self::assertEquals('Directive "meow" already exists in the schema. It cannot be redefined.', $error->getMessage());
        }
    }

    /**
     * @see it('does not allow replacing an existing type')
     */
    public function testDoesNotAllowReplacingAnExistingType()
    {
        $existingTypeError = static function ($type) {
            return 'Type "' . $type . '" already exists in the schema. It cannot also be defined in this type definition.';
        };

        $typeSDL = '
            type Bar
        ';

        try {
            $this->extendTestSchema($typeSDL);
            self::fail();
        } catch (Error $error) {
            self::assertEquals($existingTypeError('Bar'), $error->getMessage());
        }

        $scalarSDL = '
          scalar SomeScalar
        ';

        try {
            $this->extendTestSchema($scalarSDL);
            self::fail();
        } catch (Error $error) {
            self::assertEquals($existingTypeError('SomeScalar'), $error->getMessage());
        }

        $interfaceSDL = '
          interface SomeInterface
        ';

        try {
            $this->extendTestSchema($interfaceSDL);
            self::fail();
        } catch (Error $error) {
            self::assertEquals($existingTypeError('SomeInterface'), $error->getMessage());
        }

        $enumSDL = '
          enum SomeEnum
        ';

        try {
            $this->extendTestSchema($enumSDL);
            self::fail();
        } catch (Error $error) {
            self::assertEquals($existingTypeError('SomeEnum'), $error->getMessage());
        }

        $unionSDL = '
          union SomeUnion
        ';

        try {
            $this->extendTestSchema($unionSDL);
            self::fail();
        } catch (Error $error) {
            self::assertEquals($existingTypeError('SomeUnion'), $error->getMessage());
        }

        $inputSDL = '
          input SomeInput
        ';

        try {
            $this->extendTestSchema($inputSDL);
            self::fail();
        } catch (Error $error) {
            self::assertEquals($existingTypeError('SomeInput'), $error->getMessage());
        }
    }

    /**
     * @see it('does not allow replacing an existing field')
     */
    public function testDoesNotAllowReplacingAnExistingField()
    {
        $existingFieldError = static function (string $type, string $field) {
            return 'Field "' . $type . '.' . $field . '" already exists in the schema. It cannot also be defined in this type extension.';
        };

        $typeSDL = '
          extend type Bar {
            foo: Foo
          }
        ';

        try {
            $this->extendTestSchema($typeSDL);
            self::fail();
        } catch (Error $error) {
            self::assertEquals($existingFieldError('Bar', 'foo'), $error->getMessage());
        }

        $interfaceSDL = '
          extend interface SomeInterface {
            some: Foo
          }
        ';

        try {
            $this->extendTestSchema($interfaceSDL);
            self::fail();
        } catch (Error  $error) {
            self::assertEquals($existingFieldError('SomeInterface', 'some'), $error->getMessage());
        }

        $inputSDL = '
          extend input SomeInput {
            fooArg: String
          }
        ';

        try {
            $this->extendTestSchema($inputSDL);
            self::fail();
        } catch (Error $error) {
            self::assertEquals($existingFieldError('SomeInput', 'fooArg'), $error->getMessage());
        }
    }

    /**
     * @see it('does not allow replacing an existing enum value')
     */
    public function testDoesNotAllowReplacingAnExistingEnumValue()
    {
        $sdl = '
          extend enum SomeEnum {
            ONE
          }
        ';

        try {
            $this->extendTestSchema($sdl);
            self::fail();
        } catch (Error $error) {
            self::assertEquals('Enum value "SomeEnum.ONE" already exists in the schema. It cannot also be defined in this type extension.', $error->getMessage());
        }
    }

    /**
     * @see it('does not allow referencing an unknown type')
     */
    public function testDoesNotAllowReferencingAnUnknownType()
    {
        $unknownTypeError = 'Unknown type: "Quix". Ensure that this type exists either in the original schema, or is added in a type definition.';

        $typeSDL = '
          extend type Bar {
            quix: Quix
          }
        ';

        try {
            $this->extendTestSchema($typeSDL);
            self::fail();
        } catch (Error $error) {
            self::assertEquals($unknownTypeError, $error->getMessage());
        }

        $interfaceSDL = '
          extend interface SomeInterface {
            quix: Quix
          }
        ';

        try {
            $this->extendTestSchema($interfaceSDL);
            self::fail();
        } catch (Error $error) {
            self::assertEquals($unknownTypeError, $error->getMessage());
        }

        $unionSDL = '
          extend union SomeUnion = Quix
        ';

        try {
            $this->extendTestSchema($unionSDL);
            self::fail();
        } catch (Error $error) {
            self::assertEquals($unknownTypeError, $error->getMessage());
        }

        $inputSDL = '
          extend input SomeInput {
            quix: Quix
          }
        ';

        try {
            $this->extendTestSchema($inputSDL);
            self::fail();
        } catch (Error $error) {
            self::assertEquals($unknownTypeError, $error->getMessage());
        }
    }

    /**
     * @see it('does not allow extending an unknown type')
     */
    public function testDoesNotAllowExtendingAnUnknownType()
    {
        $sdls = [
            'extend scalar UnknownType @foo',
            'extend type UnknownType @foo',
            'extend interface UnknownType @foo',
            'extend enum UnknownType @foo',
            'extend union UnknownType @foo',
            'extend input UnknownType @foo',
        ];

        foreach ($sdls as $sdl) {
            try {
                $this->extendTestSchema($sdl);
                self::fail();
            } catch (Error $error) {
                self::assertEquals('Cannot extend type "UnknownType" because it does not exist in the existing schema.', $error->getMessage());
            }
        }
    }

    /**
     * @see it('maintains configuration of the original schema object')
     */
    public function testMaintainsConfigurationOfTheOriginalSchemaObject()
    {
        $this->markTestSkipped('allowedLegacyNames currently not supported');

        $testSchemaWithLegacyNames = new Schema(
            [
                'query' => new ObjectType([
                    'name' => 'Query',
                    'fields' => static function () {
                        return ['id' => ['type' => Type::ID()]];
                    },
                ]),
            ]/*,
            [ 'allowedLegacyNames' => ['__badName'] ]
            */
        );

        $ast    = Parser::parse('
            extend type Query {
                __badName: String
            }
        ');
        $schema = SchemaExtender::extend($testSchemaWithLegacyNames, $ast);
        self::assertEquals(['__badName'], $schema->__allowedLegacyNames);
    }

    /**
     * @see it('adds to the configuration of the original schema object')
     */
    public function testAddsToTheConfigurationOfTheOriginalSchemaObject()
    {
        $this->markTestSkipped('allowedLegacyNames currently not supported');

        $testSchemaWithLegacyNames = new Schema(
            [
                'query' => new ObjectType([
                    'name' => 'Query',
                    'fields' => static function () {
                        return ['__badName' => ['type' => Type::string()]];
                    },
                ]),
            ]/*,
            ['allowedLegacyNames' => ['__badName']]
            */
        );

        $ast = Parser::parse('
          extend type Query {
            __anotherBadName: String
          }
        ');

        $schema = SchemaExtender::extend($testSchemaWithLegacyNames, $ast, [
            'allowedLegacyNames' => ['__anotherBadName'],
        ]);

        self::assertEquals(['__badName', '__anotherBadName'], $schema->__allowedLegacyNames);
    }

    /**
     * @see it('does not allow extending a mismatch type')
     */
    public function testDoesNotAllowExtendingAMismatchType()
    {
        $typeSDL = '
          extend type SomeInterface @foo
        ';

        try {
            $this->extendTestSchema($typeSDL);
            self::fail();
        } catch (Error $error) {
            self::assertEquals('Cannot extend non-object type "SomeInterface".', $error->getMessage());
        }

        $interfaceSDL = '
          extend interface Foo @foo
        ';

        try {
            $this->extendTestSchema($interfaceSDL);
            self::fail();
        } catch (Error $error) {
            self::assertEquals('Cannot extend non-interface type "Foo".', $error->getMessage());
        }

        $enumSDL = '
          extend enum Foo @foo
        ';

        try {
            $this->extendTestSchema($enumSDL);
            self::fail();
        } catch (Error $error) {
            self::assertEquals('Cannot extend non-enum type "Foo".', $error->getMessage());
        }

        $unionSDL = '
          extend union Foo @foo
        ';

        try {
            $this->extendTestSchema($unionSDL);
            self::fail();
        } catch (Error $error) {
            self::assertEquals('Cannot extend non-union type "Foo".', $error->getMessage());
        }

        $inputSDL = '
          extend input Foo @foo
        ';

        try {
            $this->extendTestSchema($inputSDL);
            self::fail();
        } catch (Error $error) {
            self::assertEquals('Cannot extend non-input object type "Foo".', $error->getMessage());
        }
    }

    /**
     * @see it('does not automatically include common root type names')
     */
    public function testDoesNotAutomaticallyIncludeCommonRootTypeNames()
    {
        $schema = $this->extendTestSchema('
            type Mutation {
              doSomething: String
            }
        ');

        self::assertNull($schema->getMutationType());
    }

    /**
     * @see it('adds schema definition missing in the original schema')
     */
    public function testAddsSchemaDefinitionMissingInTheOriginalSchema()
    {
        $schema = new Schema([
            'directives' => [$this->FooDirective],
            'types' => [$this->FooType],
        ]);

        self::assertNull($schema->getQueryType());

        $ast = Parser::parse('
            schema @foo {
              query: Foo
            }
        ');

        $schema    = SchemaExtender::extend($schema, $ast);
        $queryType = $schema->getQueryType();

        self::assertEquals($queryType->name, 'Foo');
    }


    /**
     * @see it('adds new root types via schema extension')
     */
    public function testAddsNewRootTypesViaSchemaExtension()
    {
        $schema = $this->extendTestSchema('
            extend schema {
              mutation: Mutation
            }
            type Mutation {
              doSomething: String
            }
        ');

        $mutationType = $schema->getMutationType();
        self::assertEquals($mutationType->name, 'Mutation');
    }

    /**
     * @see it('adds multiple new root types via schema extension')
     */
    public function testAddsMultipleNewRootTypesViaSchemaExtension()
    {
        $schema           = $this->extendTestSchema('
            extend schema {
              mutation: Mutation
              subscription: Subscription
            }
            type Mutation {
              doSomething: String
            }
            type Subscription {
              hearSomething: String
            }
        ');
        $mutationType     = $schema->getMutationType();
        $subscriptionType = $schema->getSubscriptionType();

        self::assertEquals('Mutation', $mutationType->name);
        self::assertEquals('Subscription', $subscriptionType->name);
    }

    /**
     * @see it('applies multiple schema extensions')
     */
    public function testAppliesMultipleSchemaExtensions()
    {
        $schema = $this->extendTestSchema('
            extend schema {
              mutation: Mutation
            }
            extend schema {
              subscription: Subscription
            }
            type Mutation {
              doSomething: String
            }
            type Subscription {
              hearSomething: String
            }
        ');

        $mutationType     = $schema->getMutationType();
        $subscriptionType = $schema->getSubscriptionType();

        self::assertEquals('Mutation', $mutationType->name);
        self::assertEquals('Subscription', $subscriptionType->name);
    }

    /**
     * @see it('schema extension AST are available from schema object')
     */
    public function testSchemaExtensionASTAreAvailableFromSchemaObject()
    {
        $schema = $this->extendTestSchema('
            extend schema {
              mutation: Mutation
            }
            extend schema {
              subscription: Subscription
            }
            type Mutation {
              doSomething: String
            }
            type Subscription {
              hearSomething: String
            }
        ');

        $ast    = Parser::parse('
            extend schema @foo
        ');
        $schema = SchemaExtender::extend($schema, $ast);

        $nodes = $schema->extensionASTNodes;
        self::assertEquals(
            $this->dedent('
                extend schema {
                  mutation: Mutation
                }

                extend schema {
                  subscription: Subscription
                }

                extend schema @foo
            '),
            implode(
                PHP_EOL,
                array_map(static function ($node) {
                    return Printer::doPrint($node) . PHP_EOL;
                }, $nodes)
            )
        );
    }

    /**
     * @see it('does not allow redefining an existing root type')
     */
    public function testDoesNotAllowRedefiningAnExistingRootType()
    {
        $sdl = '
            extend schema {
                query: SomeType
            }

            type SomeType {
                seeSomething: String
            }
        ';

        try {
            $this->extendTestSchema($sdl);
            self::fail();
        } catch (Error $error) {
            self::assertEquals('Must provide only one query type in schema.', $error->getMessage());
        }
    }


    /**
     * @see it('does not allow defining a root operation type twice')
     */
    public function testDoesNotAllowDefiningARootOperationTypeTwice()
    {
        $sdl = '
            extend schema {
              mutation: Mutation
            }
            extend schema {
              mutation: Mutation
            }
            type Mutation {
              doSomething: String
            }
        ';

        try {
            $this->extendTestSchema($sdl);
            self::fail();
        } catch (Error $error) {
            self::assertEquals('Must provide only one mutation type in schema.', $error->getMessage());
        }
    }

    /**
     * @see it('does not allow defining a root operation type with different types')
     */
    public function testDoesNotAllowDefiningARootOperationTypeWithDifferentTypes()
    {
        $sdl = '
            extend schema {
              mutation: Mutation
            }
            extend schema {
              mutation: SomethingElse
            }
            type Mutation {
              doSomething: String
            }
            type SomethingElse {
              doSomethingElse: String
            }
        ';

        try {
            $this->extendTestSchema($sdl);
            self::fail();
        } catch (Error $error) {
            self::assertEquals('Must provide only one mutation type in schema.', $error->getMessage());
        }
    }
}