<?php

declare(strict_types=1);

namespace GraphQL\Tests\Utils;

use GraphQL\Language\DirectiveLocation;
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\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\UnionType;
use GraphQL\Type\Schema;
use GraphQL\Utils\BreakingChangesFinder;
use PHPUnit\Framework\TestCase;
use function sprintf;

class BreakingChangesFinderTest extends TestCase
{
    /** @var ObjectType */
    private $queryType;

    public function setUp()
    {
        $this->queryType = new ObjectType([
            'name'   => 'Query',
            'fields' => [
                'field1' => [
                    'type' => Type::string(),
                ],
            ],
        ]);
    }

    //DESCRIBE: findBreakingChanges

    /**
     * @see it('should detect if a type was removed or not')
     */
    public function testShouldDetectIfTypeWasRemovedOrNot() : void
    {
        $type1     = new ObjectType([
            'name'   => 'Type1',
            'fields' => [
                'field1' => ['type' => Type::string()],
            ],
        ]);
        $type2     = new ObjectType([
            'name'   => 'Type2',
            'fields' => [
                'field1' => ['type' => Type::string()],
            ],
        ]);
        $oldSchema = new Schema([
            'query' => $this->queryType,
            'types' => [$type1, $type2],
        ]);
        $newSchema = new Schema([
            'query' => $this->queryType,
            'types' => [$type2],
        ]);

        $expected = [
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_TYPE_REMOVED,
                'description' => 'Type1 was removed.',
            ],
        ];

        self::assertEquals(
            $expected,
            BreakingChangesFinder::findRemovedTypes($oldSchema, $newSchema)
        );

        self::assertEquals([], BreakingChangesFinder::findRemovedTypes($oldSchema, $oldSchema));
    }

    /**
     * @see it('should detect if a type changed its type')
     */
    public function testShouldDetectIfATypeChangedItsType() : void
    {
        $objectType = new ObjectType([
            'name'   => 'ObjectType',
            'fields' => [
                'field1' => ['type' => Type::string()],
            ],
        ]);

        $interfaceType = new InterfaceType([
            'name'   => 'Type1',
            'fields' => [
                'field1' => ['type' => Type::string()],
            ],
        ]);

        $unionType = new UnionType([
            'name'  => 'Type1',
            'types' => [$objectType],
        ]);

        $oldSchema = new Schema([
            'query' => $this->queryType,
            'types' => [$interfaceType],
        ]);

        $newSchema = new Schema([
            'query' => $this->queryType,
            'types' => [$unionType],
        ]);

        $expected = [
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_TYPE_CHANGED_KIND,
                'description' => 'Type1 changed from an Interface type to a Union type.',
            ],
        ];

        self::assertEquals(
            $expected,
            BreakingChangesFinder::findTypesThatChangedKind($oldSchema, $newSchema)
        );
    }

    /**
     * @see it('should detect if a field on a type was deleted or changed type')
     */
    public function testShouldDetectIfAFieldOnATypeWasDeletedOrChangedType() : void
    {
        $typeA = new ObjectType([
            'name'   => 'TypeA',
            'fields' => [
                'field1' => ['type' => Type::string()],
            ],
        ]);
        // logically equivalent to TypeA; findBreakingFieldChanges shouldn't
        // treat this as different than TypeA
        $typeA2   = new ObjectType([
            'name'   => 'TypeA',
            'fields' => [
                'field1' => ['type' => Type::string()],
            ],
        ]);
        $typeB    = new ObjectType([
            'name'   => 'TypeB',
            'fields' => [
                'field1' => ['type' => Type::string()],
            ],
        ]);
        $oldType1 = new InterfaceType([
            'name'   => 'Type1',
            'fields' => [
                'field1'  => ['type' => $typeA],
                'field2'  => ['type' => Type::string()],
                'field3'  => ['type' => Type::string()],
                'field4'  => ['type' => $typeA],
                'field6'  => ['type' => Type::string()],
                'field7'  => ['type' => Type::listOf(Type::string())],
                'field8'  => ['type' => Type::int()],
                'field9'  => ['type' => Type::nonNull(Type::int())],
                'field10' => ['type' => Type::nonNull(Type::listOf(Type::int()))],
                'field11' => ['type' => Type::int()],
                'field12' => ['type' => Type::listOf(Type::int())],
                'field13' => ['type' => Type::listOf(Type::nonNull(Type::int()))],
                'field14' => ['type' => Type::listOf(Type::int())],
                'field15' => ['type' => Type::listOf(Type::listOf(Type::int()))],
                'field16' => ['type' => Type::nonNull(Type::int())],
                'field17' => ['type' => Type::listOf(Type::int())],
                'field18' => [
                    'type' => Type::listOf(Type::nonNull(
                        Type::listOf(Type::nonNull(Type::int()))
                    )),
                ],
            ],
        ]);
        $newType1 = new InterfaceType([
            'name'   => 'Type1',
            'fields' => [
                'field1'  => ['type' => $typeA2],
                'field3'  => ['type' => Type::boolean()],
                'field4'  => ['type' => $typeB],
                'field5'  => ['type' => Type::string()],
                'field6'  => ['type' => Type::listOf(Type::string())],
                'field7'  => ['type' => Type::string()],
                'field8'  => ['type' => Type::nonNull(Type::int())],
                'field9'  => ['type' => Type::int()],
                'field10' => ['type' => Type::listOf(Type::int())],
                'field11' => ['type' => Type::nonNull(Type::listOf(Type::int()))],
                'field12' => ['type' => Type::listOf(Type::nonNull(Type::int()))],
                'field13' => ['type' => Type::listOf(Type::int())],
                'field14' => ['type' => Type::listOf(Type::listOf(Type::int()))],
                'field15' => ['type' => Type::listOf(Type::int())],
                'field16' => ['type' => Type::nonNull(Type::listOf(Type::int()))],
                'field17' => ['type' => Type::nonNull(Type::listOf(Type::int()))],
                'field18' => [
                    'type' => Type::listOf(
                        Type::listOf(Type::nonNull(Type::int()))
                    ),
                ],
            ],
        ]);

        $oldSchema = new Schema([
            'query' => $this->queryType,
            'types' => [$oldType1],
        ]);

        $newSchema = new Schema([
            'query' => $this->queryType,
            'types' => [$newType1],
        ]);

        $expectedFieldChanges = [
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_FIELD_REMOVED,
                'description' => 'Type1.field2 was removed.',
            ],
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_FIELD_CHANGED_KIND,
                'description' => 'Type1.field3 changed type from String to Boolean.',
            ],
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_FIELD_CHANGED_KIND,
                'description' => 'Type1.field4 changed type from TypeA to TypeB.',
            ],
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_FIELD_CHANGED_KIND,
                'description' => 'Type1.field6 changed type from String to [String].',
            ],
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_FIELD_CHANGED_KIND,
                'description' => 'Type1.field7 changed type from [String] to String.',
            ],
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_FIELD_CHANGED_KIND,
                'description' => 'Type1.field9 changed type from Int! to Int.',
            ],
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_FIELD_CHANGED_KIND,
                'description' => 'Type1.field10 changed type from [Int]! to [Int].',
            ],
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_FIELD_CHANGED_KIND,
                'description' => 'Type1.field11 changed type from Int to [Int]!.',
            ],
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_FIELD_CHANGED_KIND,
                'description' => 'Type1.field13 changed type from [Int!] to [Int].',
            ],
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_FIELD_CHANGED_KIND,
                'description' => 'Type1.field14 changed type from [Int] to [[Int]].',
            ],
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_FIELD_CHANGED_KIND,
                'description' => 'Type1.field15 changed type from [[Int]] to [Int].',
            ],
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_FIELD_CHANGED_KIND,
                'description' => 'Type1.field16 changed type from Int! to [Int]!.',
            ],
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_FIELD_CHANGED_KIND,
                'description' => 'Type1.field18 changed type from [[Int!]!] to [[Int!]].',
            ],
        ];

        self::assertEquals(
            $expectedFieldChanges,
            BreakingChangesFinder::findFieldsThatChangedTypeOnObjectOrInterfaceTypes($oldSchema, $newSchema)
        );
    }

    /**
     * @see it('should detect if fields on input types changed kind or were removed')
     */
    public function testShouldDetectIfFieldsOnInputTypesChangedKindOrWereRemoved() : void
    {
        $oldInputType = new InputObjectType([
            'name'   => 'InputType1',
            'fields' => [
                'field1'  => [
                    'type' => Type::string(),
                ],
                'field2'  => [
                    'type' => Type::boolean(),
                ],
                'field3'  => [
                    'type' => Type::listOf(Type::string()),
                ],
                'field4'  => [
                    'type' => Type::nonNull(Type::string()),
                ],
                'field5'  => [
                    'type' => Type::string(),
                ],
                'field6'  => [
                    'type' => Type::listOf(Type::int()),
                ],
                'field7'  => [
                    'type' => Type::nonNull(Type::listOf(Type::int())),
                ],
                'field8'  => [
                    'type' => Type::int(),
                ],
                'field9'  => [
                    'type' => Type::listOf(Type::int()),
                ],
                'field10' => [
                    'type' => Type::listOf(Type::nonNull(Type::int())),
                ],
                'field11' => [
                    'type' => Type::listOf(Type::int()),
                ],
                'field12' => [
                    'type' => Type::listOf(Type::listOf(Type::int())),
                ],
                'field13' => [
                    'type' => Type::nonNull(Type::int()),
                ],
                'field14' => [
                    'type' => Type::listOf(Type::nonNull(Type::listOf(Type::int()))),
                ],
                'field15' => [
                    'type' => Type::listOf(Type::nonNull(Type::listOf(Type::int()))),
                ],
            ],
        ]);

        $newInputType = new InputObjectType([
            'name'   => 'InputType1',
            'fields' => [
                'field1'  => [
                    'type' => Type::int(),
                ],
                'field3'  => [
                    'type' => Type::string(),
                ],
                'field4'  => [
                    'type' => Type::string(),
                ],
                'field5'  => [
                    'type' => Type::nonNull(Type::string()),
                ],
                'field6'  => [
                    'type' => Type::nonNull(Type::listOf(Type::int())),
                ],
                'field7'  => [
                    'type' => Type::listOf(Type::int()),
                ],
                'field8'  => [
                    'type' => Type::nonNull(Type::listOf(Type::int())),
                ],
                'field9'  => [
                    'type' => Type::listOf(Type::nonNull(Type::int())),
                ],
                'field10' => [
                    'type' => Type::listOf(Type::int()),
                ],
                'field11' => [
                    'type' => Type::listOf(Type::listOf(Type::int())),
                ],
                'field12' => [
                    'type' => Type::listOf(Type::int()),
                ],
                'field13' => [
                    'type' => Type::nonNull(Type::listOf(Type::int())),
                ],
                'field14' => [
                    'type' => Type::listOf(Type::listOf(Type::int())),
                ],
                'field15' => [
                    'type' => Type::listOf(Type::nonNull(Type::listOf(Type::nonNull(Type::int())))),
                ],
            ],
        ]);

        $oldSchema = new Schema([
            'query' => $this->queryType,
            'types' => [$oldInputType],
        ]);

        $newSchema = new Schema([
            'query' => $this->queryType,
            'types' => [$newInputType],
        ]);

        $expectedFieldChanges = [
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_FIELD_CHANGED_KIND,
                'description' => 'InputType1.field1 changed type from String to Int.',
            ],
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_FIELD_REMOVED,
                'description' => 'InputType1.field2 was removed.',
            ],
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_FIELD_CHANGED_KIND,
                'description' => 'InputType1.field3 changed type from [String] to String.',
            ],
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_FIELD_CHANGED_KIND,
                'description' => 'InputType1.field5 changed type from String to String!.',
            ],
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_FIELD_CHANGED_KIND,
                'description' => 'InputType1.field6 changed type from [Int] to [Int]!.',
            ],
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_FIELD_CHANGED_KIND,
                'description' => 'InputType1.field8 changed type from Int to [Int]!.',
            ],
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_FIELD_CHANGED_KIND,
                'description' => 'InputType1.field9 changed type from [Int] to [Int!].',
            ],
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_FIELD_CHANGED_KIND,
                'description' => 'InputType1.field11 changed type from [Int] to [[Int]].',
            ],
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_FIELD_CHANGED_KIND,
                'description' => 'InputType1.field12 changed type from [[Int]] to [Int].',
            ],
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_FIELD_CHANGED_KIND,
                'description' => 'InputType1.field13 changed type from Int! to [Int]!.',
            ],
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_FIELD_CHANGED_KIND,
                'description' => 'InputType1.field15 changed type from [[Int]!] to [[Int!]!].',
            ],
        ];

        self::assertEquals(
            $expectedFieldChanges,
            BreakingChangesFinder::findFieldsThatChangedTypeOnInputObjectTypes(
                $oldSchema,
                $newSchema
            )['breakingChanges']
        );
    }

    /**
     * @see it('should detect if a non-null field is added to an input type')
     */
    public function testShouldDetectIfANonNullFieldIsAddedToAnInputType() : void
    {
        $oldInputType = new InputObjectType([
            'name'   => 'InputType1',
            'fields' => [
                'field1' => Type::string(),
            ],
        ]);

        $newInputType = new InputObjectType([
            'name'   => 'InputType1',
            'fields' => [
                'field1'        => Type::string(),
                'requiredField' => Type::nonNull(Type::int()),
                'optionalField' => Type::boolean(),
            ],
        ]);

        $oldSchema = new Schema([
            'query' => $this->queryType,
            'types' => [$oldInputType],
        ]);

        $newSchema = new Schema([
            'query' => $this->queryType,
            'types' => [$newInputType],
        ]);

        $expected = [
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_NON_NULL_INPUT_FIELD_ADDED,
                'description' => 'A non-null field requiredField on input type InputType1 was added.',
            ],
        ];

        self::assertEquals(
            $expected,
            BreakingChangesFinder::findFieldsThatChangedTypeOnInputObjectTypes(
                $oldSchema,
                $newSchema
            )['breakingChanges']
        );
    }

    /**
     * @see it('should detect if a type was removed from a union type')
     */
    public function testShouldRetectIfATypeWasRemovedFromAUnionType() : void
    {
        $type1 = new ObjectType([
            'name'   => 'Type1',
            'fields' => [
                'field1' => Type::string(),
            ],
        ]);
        // logially equivalent to type1; findTypesRemovedFromUnions should not
        // treat this as different than type1
        $type1a = new ObjectType([
            'name'   => 'Type1',
            'fields' => [
                'field1' => Type::string(),
            ],
        ]);
        $type2  = new ObjectType([
            'name'   => 'Type2',
            'fields' => [
                'field1' => Type::string(),
            ],
        ]);
        $type3  = new ObjectType([
            'name'   => 'Type3',
            'fields' => [
                'field1' => Type::string(),
            ],
        ]);

        $oldUnionType = new UnionType([
            'name'  => 'UnionType1',
            'types' => [$type1, $type2],
        ]);
        $newUnionType = new UnionType([
            'name'  => 'UnionType1',
            'types' => [$type1a, $type3],
        ]);

        $oldSchema = new Schema([
            'query' => $this->queryType,
            'types' => [$oldUnionType],
        ]);
        $newSchema = new Schema([
            'query' => $this->queryType,
            'types' => [$newUnionType],
        ]);

        $expected = [
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_TYPE_REMOVED_FROM_UNION,
                'description' => 'Type2 was removed from union type UnionType1.',
            ],
        ];

        self::assertEquals(
            $expected,
            BreakingChangesFinder::findTypesRemovedFromUnions($oldSchema, $newSchema)
        );
    }

    /**
     * @see it('should detect if a value was removed from an enum type')
     */
    public function testShouldDetectIfAValueWasRemovedFromAnEnumType() : void
    {
        $oldEnumType = new EnumType([
            'name'   => 'EnumType1',
            'values' => [
                'VALUE0' => 0,
                'VALUE1' => 1,
                'VALUE2' => 2,
            ],
        ]);
        $newEnumType = new EnumType([
            'name'   => 'EnumType1',
            'values' => [
                'VALUE0' => 0,
                'VALUE2' => 1,
                'VALUE3' => 2,
            ],
        ]);

        $oldSchema = new Schema([
            'query' => $this->queryType,
            'types' => [$oldEnumType],
        ]);

        $newSchema = new Schema([
            'query' => $this->queryType,
            'types' => [$newEnumType],
        ]);

        $expected = [
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_VALUE_REMOVED_FROM_ENUM,
                'description' => 'VALUE1 was removed from enum type EnumType1.',
            ],
        ];

        self::assertEquals(
            $expected,
            BreakingChangesFinder::findValuesRemovedFromEnums($oldSchema, $newSchema)
        );
    }

    /**
     * @see it('should detect if a field argument was removed')
     */
    public function testShouldDetectIfAFieldArgumentWasRemoved() : void
    {
        $oldType = new ObjectType([
            'name'   => 'Type1',
            'fields' => [
                'field1' => [
                    'type' => Type::string(),
                    'args' => [
                        'name' => Type::string(),
                    ],
                ],
            ],
        ]);

        $inputType = new InputObjectType([
            'name'   => 'InputType1',
            'fields' => [
                'field1' => Type::string(),
            ],
        ]);

        $oldInterfaceType = new InterfaceType([
            'name'   => 'Interface1',
            'fields' => [
                'field1' => [
                    'type' => Type::string(),
                    'args' => [
                        'arg1'      => Type::boolean(),
                        'objectArg' => $inputType,
                    ],
                ],
            ],
        ]);

        $newType = new ObjectType([
            'name'   => 'Type1',
            'fields' => [
                'field1' => [
                    'type' => Type::string(),
                    'args' => [],
                ],
            ],
        ]);

        $newInterfaceType = new InterfaceType([
            'name'   => 'Interface1',
            'fields' => [
                'field1' => Type::string(),
            ],
        ]);

        $oldSchema = new Schema([
            'query' => $this->queryType,
            'types' => [$oldType, $oldInterfaceType],
        ]);

        $newSchema = new Schema([
            'query' => $this->queryType,
            'types' => [$newType, $newInterfaceType],
        ]);

        $expectedChanges = [
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_ARG_REMOVED,
                'description' => 'Type1.field1 arg name was removed',
            ],
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_ARG_REMOVED,
                'description' => 'Interface1.field1 arg arg1 was removed',
            ],
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_ARG_REMOVED,
                'description' => 'Interface1.field1 arg objectArg was removed',
            ],
        ];

        self::assertEquals(
            $expectedChanges,
            BreakingChangesFinder::findArgChanges($oldSchema, $newSchema)['breakingChanges']
        );
    }

    /**
     * @see it('should detect if a field argument has changed type')
     */
    public function testShouldDetectIfAFieldArgumentHasChangedType() : void
    {
        $oldType = new ObjectType([
            'name'   => 'Type1',
            'fields' => [
                'field1' => [
                    'type' => Type::string(),
                    'args' => [
                        'arg1'  => Type::string(),
                        'arg2'  => Type::string(),
                        'arg3'  => Type::listOf(Type::string()),
                        'arg4'  => Type::string(),
                        'arg5'  => Type::nonNull(Type::string()),
                        'arg6'  => Type::nonNull(Type::string()),
                        'arg7'  => Type::nonNull(Type::listOf(Type::int())),
                        'arg8'  => Type::int(),
                        'arg9'  => Type::listOf(Type::int()),
                        'arg10' => Type::listOf(Type::nonNull(Type::int())),
                        'arg11' => Type::listOf(Type::int()),
                        'arg12' => Type::listOf(Type::listOf(Type::int())),
                        'arg13' => Type::nonNull(Type::int()),
                        'arg14' => Type::listOf(Type::nonNull(Type::listOf(Type::int()))),
                        'arg15' => Type::listOf(Type::nonNull(Type::listOf(Type::int()))),
                    ],
                ],
            ],
        ]);

        $newType = new ObjectType([
            'name'   => 'Type1',
            'fields' => [
                'field1' => [
                    'type' => Type::string(),
                    'args' => [
                        'arg1'  => Type::int(),
                        'arg2'  => Type::listOf(Type::string()),
                        'arg3'  => Type::string(),
                        'arg4'  => Type::nonNull(Type::string()),
                        'arg5'  => Type::int(),
                        'arg6'  => Type::nonNull(Type::int()),
                        'arg7'  => Type::listOf(Type::int()),
                        'arg8'  => Type::nonNull(Type::listOf(Type::int())),
                        'arg9'  => Type::listOf(Type::nonNull(Type::int())),
                        'arg10' => Type::listOf(Type::int()),
                        'arg11' => Type::listOf(Type::listOf(Type::int())),
                        'arg12' => Type::listOf(Type::int()),
                        'arg13' => Type::nonNull(Type::listOf(Type::int())),
                        'arg14' => Type::listOf(Type::listOf(Type::int())),
                        'arg15' => Type::listOf(Type::nonNull(Type::listOf(Type::nonNull(Type::int())))),
                    ],
                ],
            ],
        ]);

        $oldSchema = new Schema([
            'query' => $this->queryType,
            'types' => [$oldType],
        ]);

        $newSchema = new Schema([
            'query' => $this->queryType,
            'types' => [$newType],
        ]);

        $expectedChanges = [
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_ARG_CHANGED_KIND,
                'description' => 'Type1.field1 arg arg1 has changed type from String to Int',
            ],
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_ARG_CHANGED_KIND,
                'description' => 'Type1.field1 arg arg2 has changed type from String to [String]',
            ],
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_ARG_CHANGED_KIND,
                'description' => 'Type1.field1 arg arg3 has changed type from [String] to String',
            ],
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_ARG_CHANGED_KIND,
                'description' => 'Type1.field1 arg arg4 has changed type from String to String!',
            ],
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_ARG_CHANGED_KIND,
                'description' => 'Type1.field1 arg arg5 has changed type from String! to Int',
            ],
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_ARG_CHANGED_KIND,
                'description' => 'Type1.field1 arg arg6 has changed type from String! to Int!',
            ],
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_ARG_CHANGED_KIND,
                'description' => 'Type1.field1 arg arg8 has changed type from Int to [Int]!',
            ],
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_ARG_CHANGED_KIND,
                'description' => 'Type1.field1 arg arg9 has changed type from [Int] to [Int!]',
            ],
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_ARG_CHANGED_KIND,
                'description' => 'Type1.field1 arg arg11 has changed type from [Int] to [[Int]]',
            ],
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_ARG_CHANGED_KIND,
                'description' => 'Type1.field1 arg arg12 has changed type from [[Int]] to [Int]',
            ],
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_ARG_CHANGED_KIND,
                'description' => 'Type1.field1 arg arg13 has changed type from Int! to [Int]!',
            ],
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_ARG_CHANGED_KIND,
                'description' => 'Type1.field1 arg arg15 has changed type from [[Int]!] to [[Int!]!]',
            ],
        ];

        self::assertEquals(
            $expectedChanges,
            BreakingChangesFinder::findArgChanges($oldSchema, $newSchema)['breakingChanges']
        );
    }

    /**
     * @see it('should detect if a non-null field argument was added')
     */
    public function testShouldDetectIfANonNullFieldArgumentWasAdded() : void
    {
        $oldType   = new ObjectType([
            'name'   => 'Type1',
            'fields' => [
                'field1' => [
                    'type' => Type::string(),
                    'args' => [
                        'arg1' => Type::string(),
                    ],
                ],
            ],
        ]);
        $newType   = new ObjectType([
            'name'   => 'Type1',
            'fields' => [
                'field1' => [
                    'type' => Type::string(),
                    'args' => [
                        'arg1'           => Type::string(),
                        'newRequiredArg' => Type::nonNull(Type::string()),
                        'newOptionalArg' => Type::int(),
                    ],
                ],
            ],
        ]);
        $oldSchema = new Schema([
            'query' => $this->queryType,
            'types' => [$oldType],
        ]);
        $newSchema = new Schema([
            'query' => $this->queryType,
            'types' => [$newType],
        ]);

        $expected = [
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_NON_NULL_ARG_ADDED,
                'description' => 'A non-null arg newRequiredArg on Type1.field1 was added',
            ],
        ];

        self::assertEquals(
            $expected,
            BreakingChangesFinder::findArgChanges($oldSchema, $newSchema)['breakingChanges']
        );
    }

    /**
     * @see it('should not flag args with the same type signature as breaking')
     */
    public function testShouldNotFlagArgsWithTheSameTypeSignatureAsBreaking() : void
    {
        $inputType1a = new InputObjectType([
            'name'   => 'InputType1',
            'fields' => [
                'field1' => Type::string(),
            ],
        ]);

        $inputType1b = new InputObjectType([
            'name'   => 'InputType1',
            'fields' => [
                'field1' => Type::string(),
            ],
        ]);

        $oldType = new ObjectType([
            'name'   => 'Type1',
            'fields' => [
                'field1' => [
                    'type' => Type::int(),
                    'args' => [
                        'arg1' => Type::nonNull(Type::int()),
                        'arg2' => $inputType1a,
                    ],
                ],
            ],
        ]);

        $newType = new ObjectType([
            'name'   => 'Type1',
            'fields' => [
                'field1' => [
                    'type' => Type::int(),
                    'args' => [
                        'arg1' => Type::nonNull(Type::int()),
                        'arg2' => $inputType1b,
                    ],
                ],
            ],
        ]);

        $oldSchema = new Schema([
            'query' => $this->queryType,
            'types' => [$oldType],
        ]);
        $newSchema = new Schema([
            'query' => $this->queryType,
            'types' => [$newType],
        ]);

        self::assertEquals([], BreakingChangesFinder::findArgChanges($oldSchema, $newSchema)['breakingChanges']);
    }

    /**
     * @see it('should consider args that move away from NonNull as non-breaking')
     */
    public function testShouldConsiderArgsThatMoveAwayFromNonNullAsNonBreaking() : void
    {
        $oldType = new ObjectType([
            'name'   => 'Type1',
            'fields' => [
                'field1' => [
                    'type' => Type::string(),
                    'args' => [
                        'arg1' => Type::nonNull(Type::string()),
                    ],
                ],
            ],
        ]);
        $newType = new ObjectType([
            'name'   => 'Type1',
            'fields' => [
                'field1' => [
                    'type' => Type::string(),
                    'args' => [
                        'arg1' => Type::string(),
                    ],
                ],
            ],
        ]);

        $oldSchema = new Schema([
            'query' => $this->queryType,
            'types' => [$oldType],
        ]);
        $newSchema = new Schema([
            'query' => $this->queryType,
            'types' => [$newType],
        ]);

        self::assertEquals([], BreakingChangesFinder::findArgChanges($oldSchema, $newSchema)['breakingChanges']);
    }

    /**
     * @see it('should detect interfaces removed from types')
     */
    public function testShouldDetectInterfacesRemovedFromTypes() : void
    {
        $interface1 = new InterfaceType([
            'name'   => 'Interface1',
            'fields' => [
                'field1' => Type::string(),
            ],
        ]);
        $oldType    = new ObjectType([
            'name'       => 'Type1',
            'interfaces' => [$interface1],
            'fields'     => [
                'field1' => Type::string(),
            ],
        ]);
        $newType    = new ObjectType([
            'name'   => 'Type1',
            'fields' => [
                'field1' => Type::string(),
            ],
        ]);

        $oldSchema = new Schema([
            'query' => $this->queryType,
            'types' => [$oldType],
        ]);
        $newSchema = new Schema([
            'query' => $this->queryType,
            'types' => [$newType],
        ]);

        $expected = [
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_INTERFACE_REMOVED_FROM_OBJECT,
                'description' => 'Type1 no longer implements interface Interface1.',
            ],
        ];

        self::assertEquals(
            $expected,
            BreakingChangesFinder::findInterfacesRemovedFromObjectTypes($oldSchema, $newSchema)
        );
    }

    /**
     * @see it('should detect all breaking changes')
     */
    public function testShouldDetectAllBreakingChanges() : void
    {
        $typeThatGetsRemoved = new ObjectType([
            'name'   => 'TypeThatGetsRemoved',
            'fields' => [
                'field1' => Type::string(),
            ],
        ]);

        $argThatChanges = new ObjectType([
            'name'   => 'ArgThatChanges',
            'fields' => [
                'field1' => [
                    'type' => Type::string(),
                    'args' => [
                        'id' => Type::int(),
                    ],
                ],
            ],
        ]);

        $argChanged = new ObjectType([
            'name'   => 'ArgThatChanges',
            'fields' => [
                'field1' => [
                    'type' => Type::string(),
                    'args' => [
                        'id' => Type::string(),
                    ],
                ],
            ],
        ]);

        $typeThatChangesTypeOld = new ObjectType([
            'name'   => 'TypeThatChangesType',
            'fields' => [
                'field1' => Type::string(),
            ],
        ]);

        $typeThatChangesTypeNew = new InterfaceType([
            'name'   => 'TypeThatChangesType',
            'fields' => [
                'field1' => Type::string(),
            ],
        ]);

        $typeThatHasBreakingFieldChangesOld = new InterfaceType([
            'name'   => 'TypeThatHasBreakingFieldChanges',
            'fields' => [
                'field1' => Type::string(),
                'field2' => Type::string(),
            ],
        ]);

        $typeThatHasBreakingFieldChangesNew = new InterfaceType([
            'name'   => 'TypeThatHasBreakingFieldChanges',
            'fields' => [
                'field2' => Type::boolean(),
            ],
        ]);

        $typeInUnion1 = new ObjectType([
            'name'   => 'TypeInUnion1',
            'fields' => [
                'field1' => Type::string(),
            ],
        ]);

        $typeInUnion2 = new ObjectType([
            'name'   => 'TypeInUnion2',
            'fields' => [
                'field1' => Type::string(),
            ],
        ]);

        $unionTypeThatLosesATypeOld = new UnionType([
            'name'  => 'UnionTypeThatLosesAType',
            'types' => [$typeInUnion1, $typeInUnion2],
        ]);

        $unionTypeThatLosesATypeNew = new UnionType([
            'name'  => 'UnionTypeThatLosesAType',
            'types' => [$typeInUnion1],
        ]);

        $enumTypeThatLosesAValueOld = new EnumType([
            'name'   => 'EnumTypeThatLosesAValue',
            'values' => [
                'VALUE0' => 0,
                'VALUE1' => 1,
                'VALUE2' => 2,
            ],
        ]);

        $enumTypeThatLosesAValueNew = new EnumType([
            'name'   => 'EnumTypeThatLosesAValue',
            'values' => [
                'VALUE1' => 1,
                'VALUE2' => 2,
            ],
        ]);

        $interface1 = new InterfaceType([
            'name'   => 'Interface1',
            'fields' => [
                'field1' => Type::string(),
            ],
        ]);

        $typeThatLosesInterfaceOld = new ObjectType([
            'name'       => 'TypeThatLosesInterface1',
            'interfaces' => [$interface1],
            'fields'     => [
                'field1' => Type::string(),
            ],
        ]);

        $typeThatLosesInterfaceNew = new ObjectType([
            'name'   => 'TypeThatLosesInterface1',
            'fields' => [
                'field1' => Type::string(),
            ],
        ]);

        $directiveThatIsRemoved      = Directive::skipDirective();
        $directiveThatRemovesArgOld  = new Directive([
            'name'      => 'DirectiveThatRemovesArg',
            'locations' => [DirectiveLocation::FIELD_DEFINITION],
            'args'      => FieldArgument::createMap([
                'arg1' => ['name' => 'arg1'],
            ]),
        ]);
        $directiveThatRemovesArgNew  = new Directive([
            'name'      => 'DirectiveThatRemovesArg',
            'locations' => [DirectiveLocation::FIELD_DEFINITION],
        ]);
        $nonNullDirectiveAddedOld    = new Directive([
            'name'      => 'NonNullDirectiveAdded',
            'locations' => [DirectiveLocation::FIELD_DEFINITION],
        ]);
        $nonNullDirectiveAddedNew    = new Directive([
            'name'      => 'NonNullDirectiveAdded',
            'locations' => [DirectiveLocation::FIELD_DEFINITION],
            'args'      => FieldArgument::createMap([
                'arg1' => [
                    'name' => 'arg1',
                    'type' => Type::nonNull(Type::boolean()),
                ],
            ]),
        ]);
        $directiveRemovedLocationOld = new Directive([
            'name'      => 'Directive Name',
            'locations' => [DirectiveLocation::FIELD_DEFINITION, DirectiveLocation::QUERY],
        ]);
        $directiveRemovedLocationNew = new Directive([
            'name'      => 'Directive Name',
            'locations' => [DirectiveLocation::FIELD_DEFINITION],
        ]);

        $oldSchema = new Schema([
            'query'      => $this->queryType,
            'types'      => [
                $typeThatGetsRemoved,
                $typeThatChangesTypeOld,
                $typeThatHasBreakingFieldChangesOld,
                $unionTypeThatLosesATypeOld,
                $enumTypeThatLosesAValueOld,
                $argThatChanges,
                $typeThatLosesInterfaceOld,
            ],
            'directives' => [
                $directiveThatIsRemoved,
                $directiveThatRemovesArgOld,
                $nonNullDirectiveAddedOld,
                $directiveRemovedLocationOld,
            ],
        ]);

        $newSchema = new Schema([
            'query'      => $this->queryType,
            'types'      => [
                $typeThatChangesTypeNew,
                $typeThatHasBreakingFieldChangesNew,
                $unionTypeThatLosesATypeNew,
                $enumTypeThatLosesAValueNew,
                $argChanged,
                $typeThatLosesInterfaceNew,
                $interface1,
            ],
            'directives' => [
                $directiveThatRemovesArgNew,
                $nonNullDirectiveAddedNew,
                $directiveRemovedLocationNew,
            ],
        ]);

        $expectedBreakingChanges = [
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_TYPE_REMOVED,
                'description' => 'TypeThatGetsRemoved was removed.',
            ],
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_TYPE_REMOVED,
                'description' => 'TypeInUnion2 was removed.',
            ],
            /* This is reported in the js version because builtin sclar types are added on demand
               and not like here always
             [
                'type' => FindBreakingChanges::BREAKING_CHANGE_TYPE_REMOVED,
                'description' => 'Int was removed.'
            ],*/
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_TYPE_CHANGED_KIND,
                'description' => 'TypeThatChangesType changed from an Object type to an Interface type.',
            ],
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_FIELD_REMOVED,
                'description' => 'TypeThatHasBreakingFieldChanges.field1 was removed.',
            ],
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_FIELD_CHANGED_KIND,
                'description' => 'TypeThatHasBreakingFieldChanges.field2 changed type from String to Boolean.',
            ],
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_TYPE_REMOVED_FROM_UNION,
                'description' => 'TypeInUnion2 was removed from union type UnionTypeThatLosesAType.',
            ],
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_VALUE_REMOVED_FROM_ENUM,
                'description' => 'VALUE0 was removed from enum type EnumTypeThatLosesAValue.',
            ],
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_ARG_CHANGED_KIND,
                'description' => 'ArgThatChanges.field1 arg id has changed type from Int to String',
            ],
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_INTERFACE_REMOVED_FROM_OBJECT,
                'description' => 'TypeThatLosesInterface1 no longer implements interface Interface1.',
            ],
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_DIRECTIVE_REMOVED,
                'description' => 'skip was removed',
            ],
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_DIRECTIVE_ARG_REMOVED,
                'description' => 'arg1 was removed from DirectiveThatRemovesArg',
            ],
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_NON_NULL_DIRECTIVE_ARG_ADDED,
                'description' => 'A non-null arg arg1 on directive NonNullDirectiveAdded was added',
            ],
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_DIRECTIVE_LOCATION_REMOVED,
                'description' => 'QUERY was removed from Directive Name',
            ],
        ];

        self::assertEquals(
            $expectedBreakingChanges,
            BreakingChangesFinder::findBreakingChanges($oldSchema, $newSchema)
        );
    }

    /**
     * @see it('should detect if a directive was explicitly removed')
     */
    public function testShouldDetectIfADirectiveWasExplicitlyRemoved() : void
    {
        $oldSchema = new Schema([
            'directives' => [Directive::skipDirective(), Directive::includeDirective()],
        ]);

        $newSchema = new Schema([
            'directives' => [Directive::skipDirective()],
        ]);

        $includeDirective = Directive::includeDirective();

        $expectedBreakingChanges = [
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_DIRECTIVE_REMOVED,
                'description' => sprintf('%s was removed', $includeDirective->name),
            ],
        ];

        self::assertEquals(
            $expectedBreakingChanges,
            BreakingChangesFinder::findRemovedDirectives($oldSchema, $newSchema)
        );
    }

    /**
     * @see it('should detect if a directive was implicitly removed')
     */
    public function testShouldDetectIfADirectiveWasImplicitlyRemoved() : void
    {
        $oldSchema = new Schema([]);

        $newSchema = new Schema([
            'directives' => [Directive::skipDirective(), Directive::includeDirective()],
        ]);

        $deprecatedDirective = Directive::deprecatedDirective();

        $expectedBreakingChanges = [
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_DIRECTIVE_REMOVED,
                'description' => sprintf('%s was removed', $deprecatedDirective->name),
            ],
        ];

        self::assertEquals(
            $expectedBreakingChanges,
            BreakingChangesFinder::findRemovedDirectives($oldSchema, $newSchema)
        );
    }

    /**
     * @see it('should detect if a directive argument was removed')
     */
    public function testShouldDetectIfADirectiveArgumentWasRemoved() : void
    {
        $oldSchema = new Schema([
            'directives' => [
                new Directive([
                    'name'      => 'DirectiveWithArg',
                    'locations' => [DirectiveLocation::FIELD_DEFINITION],
                    'args'      => FieldArgument::createMap([
                        'arg1' => ['name' => 'arg1'],
                    ]),
                ]),
            ],
        ]);

        $newSchema = new Schema([
            'directives' => [
                new Directive([
                    'name'      => 'DirectiveWithArg',
                    'locations' => [DirectiveLocation::FIELD_DEFINITION],
                ]),
            ],
        ]);

        $expectedBreakingChanges = [
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_DIRECTIVE_ARG_REMOVED,
                'description' => 'arg1 was removed from DirectiveWithArg',
            ],
        ];

        self::assertEquals(
            $expectedBreakingChanges,
            BreakingChangesFinder::findRemovedDirectiveArgs($oldSchema, $newSchema)
        );
    }

    /**
     * @see it('should detect if a non-nullable directive argument was added')
     */
    public function testShouldDetectIfANonNullableDirectiveArgumentWasAdded() : void
    {
        $oldSchema = new Schema([
            'directives' => [
                new Directive([
                    'name'      => 'DirectiveName',
                    'locations' => [DirectiveLocation::FIELD_DEFINITION],
                ]),
            ],
        ]);

        $newSchema = new Schema([
            'directives' => [
                new Directive([
                    'name'      => 'DirectiveName',
                    'locations' => [DirectiveLocation::FIELD_DEFINITION],
                    'args'      => FieldArgument::createMap([
                        'arg1' => [
                            'name' => 'arg1',
                            'type' => Type::nonNull(Type::boolean()),
                        ],
                    ]),
                ]),
            ],
        ]);

        $expectedBreakingChanges = [
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_NON_NULL_DIRECTIVE_ARG_ADDED,
                'description' => 'A non-null arg arg1 on directive DirectiveName was added',
            ],
        ];

        self::assertEquals(
            $expectedBreakingChanges,
            BreakingChangesFinder::findAddedNonNullDirectiveArgs($oldSchema, $newSchema)
        );
    }

    /**
     * @see it('should detect locations removed from a directive')
     */
    public function testShouldDetectLocationsRemovedFromADirective() : void
    {
        $d1 = new Directive([
            'name'      => 'Directive Name',
            'locations' => [DirectiveLocation::FIELD_DEFINITION, DirectiveLocation::QUERY],
        ]);

        $d2 = new Directive([
            'name'      => 'Directive Name',
            'locations' => [DirectiveLocation::FIELD_DEFINITION],
        ]);

        self::assertEquals(
            [DirectiveLocation::QUERY],
            BreakingChangesFinder::findRemovedLocationsForDirective($d1, $d2)
        );
    }

    /**
     * @see it('should detect locations removed directives within a schema')
     */
    public function testShouldDetectLocationsRemovedDirectiveWithinASchema() : void
    {
        $oldSchema = new Schema([
            'directives' => [
                new Directive([
                    'name'      => 'Directive Name',
                    'locations' => [
                        DirectiveLocation::FIELD_DEFINITION,
                        DirectiveLocation::QUERY,
                    ],
                ]),
            ],
        ]);

        $newSchema = new Schema([
            'directives' => [
                new Directive([
                    'name'      => 'Directive Name',
                    'locations' => [DirectiveLocation::FIELD_DEFINITION],
                ]),
            ],
        ]);

        $expectedBreakingChanges = [
            [
                'type'        => BreakingChangesFinder::BREAKING_CHANGE_DIRECTIVE_LOCATION_REMOVED,
                'description' => 'QUERY was removed from Directive Name',
            ],
        ];

        self::assertEquals(
            $expectedBreakingChanges,
            BreakingChangesFinder::findRemovedDirectiveLocations($oldSchema, $newSchema)
        );
    }

    // DESCRIBE: findDangerousChanges
    // DESCRIBE: findArgChanges

    /**
     * @see it('should detect if an argument's defaultValue has changed')
     */
    public function testShouldDetectIfAnArgumentsDefaultValueHasChanged() : void
    {
        $oldType = new ObjectType([
            'name'   => 'Type1',
            'fields' => [
                'field1' => [
                    'type' => Type::string(),
                    'args' => [
                        'name' => [
                            'type'         => Type::string(),
                            'defaultValue' => 'test',
                        ],
                    ],
                ],
            ],
        ]);

        $newType = new ObjectType([
            'name'   => 'Type1',
            'fields' => [
                'field1' => [
                    'type' => Type::string(),
                    'args' => [
                        'name' => [
                            'type'         => Type::string(),
                            'defaultValue' => 'Test',
                        ],
                    ],
                ],
            ],
        ]);

        $oldSchema = new Schema([
            'query' => $this->queryType,
            'types' => [$oldType],
        ]);

        $newSchema = new Schema([
            'query' => $this->queryType,
            'types' => [$newType],
        ]);

        $expected = [
            [
                'type'        => BreakingChangesFinder::DANGEROUS_CHANGE_ARG_DEFAULT_VALUE_CHANGED,
                'description' => 'Type1.field1 arg name has changed defaultValue',
            ],
        ];

        self::assertEquals(
            $expected,
            BreakingChangesFinder::findArgChanges($oldSchema, $newSchema)['dangerousChanges']
        );
    }

    /**
     * @see it('should detect if a value was added to an enum type')
     */
    public function testShouldDetectIfAValueWasAddedToAnEnumType() : void
    {
        $oldEnumType = new EnumType([
            'name'   => 'EnumType1',
            'values' => [
                'VALUE0' => 0,
                'VALUE1' => 1,
            ],
        ]);
        $newEnumType = new EnumType([
            'name'   => 'EnumType1',
            'values' => [
                'VALUE0' => 0,
                'VALUE1' => 1,
                'VALUE2' => 2,
            ],
        ]);

        $oldSchema = new Schema([
            'query' => $this->queryType,
            'types' => [$oldEnumType],
        ]);

        $newSchema = new Schema([
            'query' => $this->queryType,
            'types' => [$newEnumType],
        ]);

        $expected = [
            [
                'type'        => BreakingChangesFinder::DANGEROUS_CHANGE_VALUE_ADDED_TO_ENUM,
                'description' => 'VALUE2 was added to enum type EnumType1.',
            ],
        ];

        self::assertEquals(
            $expected,
            BreakingChangesFinder::findValuesAddedToEnums($oldSchema, $newSchema)
        );
    }

    /**
     * @see it('should detect interfaces added to types')
     */
    public function testShouldDetectInterfacesAddedToTypes() : void
    {
        $interface1 = new InterfaceType([
            'name'   => 'Interface1',
            'fields' => [
                'field1' => Type::string(),
            ],
        ]);
        $oldType    = new ObjectType([
            'name'   => 'Type1',
            'fields' => [
                'field1' => Type::string(),
            ],
        ]);

        $newType = new ObjectType([
            'name'       => 'Type1',
            'interfaces' => [$interface1],
            'fields'     => [
                'field1' => Type::string(),
            ],
        ]);

        $oldSchema = new Schema([
            'query' => $this->queryType,
            'types' => [$oldType],
        ]);

        $newSchema = new Schema([
            'query' => $this->queryType,
            'types' => [$newType],
        ]);

        $expected = [
            [
                'type'        => BreakingChangesFinder::DANGEROUS_CHANGE_INTERFACE_ADDED_TO_OBJECT,
                'description' => 'Interface1 added to interfaces implemented by Type1.',
            ],
        ];

        self::assertEquals(
            $expected,
            BreakingChangesFinder::findInterfacesAddedToObjectTypes($oldSchema, $newSchema)
        );
    }

    /**
     * @see it('should detect if a type was added to a union type')
     */
    public function testShouldDetectIfATypeWasAddedToAUnionType() : void
    {
        $type1 = new ObjectType([
            'name'   => 'Type1',
            'fields' => [
                'field1' => Type::string(),
            ],
        ]);
        // logially equivalent to type1; findTypesRemovedFromUnions should not
        //treat this as different than type1
        $type1a = new ObjectType([
            'name'   => 'Type1',
            'fields' => [
                'field1' => Type::string(),
            ],
        ]);
        $type2  = new ObjectType([
            'name'   => 'Type2',
            'fields' => [
                'field1' => Type::string(),
            ],
        ]);

        $oldUnionType = new UnionType([
            'name'  => 'UnionType1',
            'types' => [$type1],
        ]);
        $newUnionType = new UnionType([
            'name'  => 'UnionType1',
            'types' => [$type1a, $type2],
        ]);

        $oldSchema = new Schema([
            'query' => $this->queryType,
            'types' => [$oldUnionType],
        ]);

        $newSchema = new Schema([
            'query' => $this->queryType,
            'types' => [$newUnionType],
        ]);

        $expected = [
            [
                'type'        => BreakingChangesFinder::DANGEROUS_CHANGE_TYPE_ADDED_TO_UNION,
                'description' => 'Type2 was added to union type UnionType1.',
            ],
        ];

        self::assertEquals(
            $expected,
            BreakingChangesFinder::findTypesAddedToUnions($oldSchema, $newSchema)
        );
    }

    /**
     * @see it('should detect if a nullable field was added to an input')
     */
    public function testShouldDetectIfANullableFieldWasAddedToAnInput() : void
    {
        $oldInputType = new InputObjectType([
            'name'   => 'InputType1',
            'fields' => [
                'field1' => [
                    'type' => Type::string(),
                ],
            ],
        ]);
        $newInputType = new InputObjectType([
            'name'   => 'InputType1',
            'fields' => [
                'field1' => [
                    'type' => Type::string(),
                ],
                'field2' => [
                    'type' => Type::int(),
                ],
            ],
        ]);

        $oldSchema = new Schema([
            'query' => $this->queryType,
            'types' => [$oldInputType],
        ]);

        $newSchema = new Schema([
            'query' => $this->queryType,
            'types' => [$newInputType],
        ]);

        $expectedFieldChanges = [
            [
                'description' => 'A nullable field field2 on input type InputType1 was added.',
                'type'        => BreakingChangesFinder::DANGEROUS_CHANGE_NULLABLE_INPUT_FIELD_ADDED,
            ],
        ];

        self::assertEquals(
            $expectedFieldChanges,
            BreakingChangesFinder::findFieldsThatChangedTypeOnInputObjectTypes(
                $oldSchema,
                $newSchema
            )['dangerousChanges']
        );
    }

    /**
     * @see it('should find all dangerous changes')
     */
    public function testShouldFindAllDangerousChanges() : void
    {
        $enumThatGainsAValueOld = new EnumType([
            'name'   => 'EnumType1',
            'values' => [
                'VALUE0' => 0,
                'VALUE1' => 1,
            ],
        ]);
        $enumThatGainsAValueNew = new EnumType([
            'name'   => 'EnumType1',
            'values' => [
                'VALUE0' => 0,
                'VALUE1' => 1,
                'VALUE2' => 2,
            ],
        ]);

        $oldType = new ObjectType([
            'name'   => 'Type1',
            'fields' => [
                'field1' => [
                    'type' => Type::string(),
                    'args' => [
                        'name' => [
                            'type'         => Type::string(),
                            'defaultValue' => 'test',
                        ],
                    ],
                ],
            ],
        ]);

        $typeInUnion1               = new ObjectType([
            'name'   => 'TypeInUnion1',
            'fields' => [
                'field1' => Type::string(),
            ],
        ]);
        $typeInUnion2               = new ObjectType([
            'name'   => 'TypeInUnion2',
            'fields' => [
                'field1' => Type::string(),
            ],
        ]);
        $unionTypeThatGainsATypeOld = new UnionType([
            'name'  => 'UnionTypeThatGainsAType',
            'types' => [$typeInUnion1],
        ]);
        $unionTypeThatGainsATypeNew = new UnionType([
            'name'  => 'UnionTypeThatGainsAType',
            'types' => [$typeInUnion1, $typeInUnion2],
        ]);

        $newType = new ObjectType([
            'name'   => 'Type1',
            'fields' => [
                'field1' => [
                    'type' => Type::string(),
                    'args' => [
                        'name' => [
                            'type'         => Type::string(),
                            'defaultValue' => 'Test',
                        ],
                    ],
                ],
            ],
        ]);

        $interface1 = new InterfaceType([
            'name'   => 'Interface1',
            'fields' => [
                'field1' => Type::string(),
            ],
        ]);

        $typeThatGainsInterfaceOld = new ObjectType([
            'name'   => 'TypeThatGainsInterface1',
            'fields' => [
                'field1' => Type::string(),
            ],
        ]);

        $typeThatGainsInterfaceNew = new ObjectType([
            'name'       => 'TypeThatGainsInterface1',
            'interfaces' => [$interface1],
            'fields'     => [
                'field1' => Type::string(),
            ],
        ]);

        $oldSchema = new Schema([
            'query' => $this->queryType,
            'types' => [
                $oldType,
                $enumThatGainsAValueOld,
                $typeThatGainsInterfaceOld,
                $unionTypeThatGainsATypeOld,
            ],
        ]);

        $newSchema = new Schema([
            'query' => $this->queryType,
            'types' => [
                $newType,
                $enumThatGainsAValueNew,
                $typeThatGainsInterfaceNew,
                $unionTypeThatGainsATypeNew,
            ],
        ]);

        $expectedDangerousChanges = [
            [
                'description' => 'Type1.field1 arg name has changed defaultValue',
                'type'        => BreakingChangesFinder::DANGEROUS_CHANGE_ARG_DEFAULT_VALUE_CHANGED,
            ],
            [
                'description' => 'VALUE2 was added to enum type EnumType1.',
                'type'        => BreakingChangesFinder::DANGEROUS_CHANGE_VALUE_ADDED_TO_ENUM,
            ],
            [
                'type'        => BreakingChangesFinder::DANGEROUS_CHANGE_INTERFACE_ADDED_TO_OBJECT,
                'description' => 'Interface1 added to interfaces implemented by TypeThatGainsInterface1.',
            ],
            [
                'type'        => BreakingChangesFinder::DANGEROUS_CHANGE_TYPE_ADDED_TO_UNION,
                'description' => 'TypeInUnion2 was added to union type UnionTypeThatGainsAType.',
            ],
        ];

        self::assertEquals(
            $expectedDangerousChanges,
            BreakingChangesFinder::findDangerousChanges($oldSchema, $newSchema)
        );
    }

    /**
     * @see it('should detect if a nullable field argument was added')
     */
    public function testShouldDetectIfANullableFieldArgumentWasAdded() : void
    {
        $oldType = new ObjectType([
            'name'   => 'Type1',
            'fields' => [
                'field1' => [
                    'type' => Type::string(),
                    'args' => [
                        'arg1' => [
                            'type' => Type::string(),
                        ],
                    ],
                ],
            ],
        ]);

        $newType = new ObjectType([
            'name'   => 'Type1',
            'fields' => [
                'field1' => [
                    'type' => Type::string(),
                    'args' => [
                        'arg1' => [
                            'type' => Type::string(),
                        ],
                        'arg2' => [
                            'type' => Type::string(),
                        ],
                    ],
                ],
            ],
        ]);

        $oldSchema = new Schema([
            'query' => $this->queryType,
            'types' => [$oldType],
        ]);

        $newSchema = new Schema([
            'query' => $this->queryType,
            'types' => [$newType],
        ]);

        $expectedFieldChanges = [
            [
                'description' => 'A nullable arg arg2 on Type1.field1 was added',
                'type'        => BreakingChangesFinder::DANGEROUS_CHANGE_NULLABLE_ARG_ADDED,
            ],
        ];

        self::assertEquals(
            $expectedFieldChanges,
            BreakingChangesFinder::findArgChanges($oldSchema, $newSchema)['dangerousChanges']
        );
    }
}