<?php
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\FindBreakingChanges;
use PHPUnit\Framework\TestCase;

class FindBreakingChangesTest extends TestCase
{
    private $queryType;

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

    //DESCRIBE: findBreakingChanges

    /**
     * @it should detect if a type was removed or not
     */
    public function testShouldDetectIfTypeWasRemovedOrNot()
    {
        $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' => FindBreakingChanges::BREAKING_CHANGE_TYPE_REMOVED,
                'description' => 'Type1 was removed.'
            ]
        ];

        $this->assertEquals(
            $expected,
            FindBreakingChanges::findRemovedTypes($oldSchema, $newSchema)
        );

        $this->assertEquals([], FindBreakingChanges::findRemovedTypes($oldSchema, $oldSchema));
    }

    /**
     * @it should detect if a type changed its type
     */
    public function testShouldDetectIfATypeChangedItsType()
    {
        $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' => FindBreakingChanges::BREAKING_CHANGE_TYPE_CHANGED_KIND,
                'description' => 'Type1 changed from an Interface type to a Union type.'
            ]
        ];

        $this->assertEquals(
            $expected,
            FindBreakingChanges::findTypesThatChangedKind($oldSchema, $newSchema)
        );
    }

    /**
     * @it should detect if a field on a type was deleted or changed type
     */
    public function testShouldDetectIfAFieldOnATypeWasDeletedOrChangedType()
    {
        $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' => FindBreakingChanges::BREAKING_CHANGE_FIELD_REMOVED,
                'description' => 'Type1.field2 was removed.',
            ],
            [
                'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED_KIND,
                'description' => 'Type1.field3 changed type from String to Boolean.',
            ],
            [
                'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED_KIND,
                'description' => 'Type1.field4 changed type from TypeA to TypeB.',
            ],
            [
                'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED_KIND,
                'description' => 'Type1.field6 changed type from String to [String].',
            ],
            [
                'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED_KIND,
                'description' => 'Type1.field7 changed type from [String] to String.',
            ],
            [
                'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED_KIND,
                'description' => 'Type1.field9 changed type from Int! to Int.',
            ],
            [
                'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED_KIND,
                'description' => 'Type1.field10 changed type from [Int]! to [Int].',
            ],
            [
                'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED_KIND,
                'description' => 'Type1.field11 changed type from Int to [Int]!.',
            ],
            [
                'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED_KIND,
                'description' => 'Type1.field13 changed type from [Int!] to [Int].',
            ],
            [
                'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED_KIND,
                'description' => 'Type1.field14 changed type from [Int] to [[Int]].',
            ],
            [
                'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED_KIND,
                'description' => 'Type1.field15 changed type from [[Int]] to [Int].',
            ],
            [
                'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED_KIND,
                'description' => 'Type1.field16 changed type from Int! to [Int]!.',
            ],
            [
                'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED_KIND,
                'description' => 'Type1.field18 changed type from [[Int!]!] to [[Int!]].',
            ],
        ];

        $this->assertEquals($expectedFieldChanges, FindBreakingChanges::findFieldsThatChangedTypeOnObjectOrInterfaceTypes($oldSchema, $newSchema));
    }

    /**
     * @it should detect if fields on input types changed kind or were removed
     */
    public function testShouldDetectIfFieldsOnInputTypesChangedKindOrWereRemoved()
    {
        $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' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED_KIND,
                'description' => 'InputType1.field1 changed type from String to Int.',
            ],
            [
                'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_REMOVED,
                'description' => 'InputType1.field2 was removed.',
            ],
            [
                'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED_KIND,
                'description' => 'InputType1.field3 changed type from [String] to String.',
            ],
            [
                'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED_KIND,
                'description' => 'InputType1.field5 changed type from String to String!.',
            ],
            [
                'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED_KIND,
                'description' => 'InputType1.field6 changed type from [Int] to [Int]!.',
            ],
            [
                'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED_KIND,
                'description' => 'InputType1.field8 changed type from Int to [Int]!.',
            ],
            [
                'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED_KIND,
                'description' => 'InputType1.field9 changed type from [Int] to [Int!].',
            ],
            [
                'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED_KIND,
                'description' => 'InputType1.field11 changed type from [Int] to [[Int]].',
            ],
            [
                'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED_KIND,
                'description' => 'InputType1.field12 changed type from [[Int]] to [Int].',
            ],
            [
                'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED_KIND,
                'description' => 'InputType1.field13 changed type from Int! to [Int]!.',
            ],
            [
                'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED_KIND,
                'description' => 'InputType1.field15 changed type from [[Int]!] to [[Int!]!].',
            ],
        ];

        $this->assertEquals($expectedFieldChanges, FindBreakingChanges::findFieldsThatChangedTypeOnInputObjectTypes($oldSchema, $newSchema)['breakingChanges']);
    }

    /**
     * @it should detect if a non-null field is added to an input type
     */
    public function testShouldDetectIfANonNullFieldIsAddedToAnInputType()
    {
        $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' => FindBreakingChanges::BREAKING_CHANGE_NON_NULL_INPUT_FIELD_ADDED,
                'description' => 'A non-null field requiredField on input type InputType1 was added.'
            ],
        ];

        $this->assertEquals(
            $expected,
            FindBreakingChanges::findFieldsThatChangedTypeOnInputObjectTypes($oldSchema, $newSchema)['breakingChanges']
        );
    }

    /**
     * @it should detect if a type was removed from a union type
     */
    public function testShouldRetectIfATypeWasRemovedFromAUnionType()
    {
        $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' => FindBreakingChanges::BREAKING_CHANGE_TYPE_REMOVED_FROM_UNION,
                'description' => 'Type2 was removed from union type UnionType1.'
            ]
        ];

        $this->assertEquals(
            $expected,
            FindBreakingChanges::findTypesRemovedFromUnions($oldSchema, $newSchema)
        );
    }

    /**
     * @it should detect if a value was removed from an enum type
     */
    public function testShouldDetectIfAValueWasRemovedFromAnEnumType()
    {
        $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' => FindBreakingChanges::BREAKING_CHANGE_VALUE_REMOVED_FROM_ENUM,
                'description' => 'VALUE1 was removed from enum type EnumType1.'
            ]
        ];

        $this->assertEquals(
            $expected,
            FindBreakingChanges::findValuesRemovedFromEnums($oldSchema, $newSchema)
        );
    }

    /**
     * @it should detect if a field argument was removed
     */
    public function testShouldDetectIfAFieldArgumentWasRemoved()
    {
        $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' => FindBreakingChanges::BREAKING_CHANGE_ARG_REMOVED,
                'description' => 'Type1.field1 arg name was removed',
            ],
            [
                'type' => FindBreakingChanges::BREAKING_CHANGE_ARG_REMOVED,
                'description' => 'Interface1.field1 arg arg1 was removed',
            ],
            [
                'type' => FindBreakingChanges::BREAKING_CHANGE_ARG_REMOVED,
                'description' => 'Interface1.field1 arg objectArg was removed',
            ]
        ];

        $this->assertEquals($expectedChanges, FindBreakingChanges::findArgChanges($oldSchema, $newSchema)['breakingChanges']);
    }

    /**
     * @it should detect if a field argument has changed type
     */
    public function testShouldDetectIfAFieldArgumentHasChangedType()
    {
        $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' => FindBreakingChanges::BREAKING_CHANGE_ARG_CHANGED_KIND,
                'description' => 'Type1.field1 arg arg1 has changed type from String to Int',
            ],
            [
                'type' => FindBreakingChanges::BREAKING_CHANGE_ARG_CHANGED_KIND,
                'description' => 'Type1.field1 arg arg2 has changed type from String to [String]'
            ],
            [
                'type' => FindBreakingChanges::BREAKING_CHANGE_ARG_CHANGED_KIND,
                'description' => 'Type1.field1 arg arg3 has changed type from [String] to String',
            ],
            [
                'type' => FindBreakingChanges::BREAKING_CHANGE_ARG_CHANGED_KIND,
                'description' => 'Type1.field1 arg arg4 has changed type from String to String!',
            ],
            [
                'type' => FindBreakingChanges::BREAKING_CHANGE_ARG_CHANGED_KIND,
                'description' => 'Type1.field1 arg arg5 has changed type from String! to Int',
            ],
            [
                'type' => FindBreakingChanges::BREAKING_CHANGE_ARG_CHANGED_KIND,
                'description' => 'Type1.field1 arg arg6 has changed type from String! to Int!',
            ],
            [
                'type' => FindBreakingChanges::BREAKING_CHANGE_ARG_CHANGED_KIND,
                'description' => 'Type1.field1 arg arg8 has changed type from Int to [Int]!',
            ],
            [
                'type' => FindBreakingChanges::BREAKING_CHANGE_ARG_CHANGED_KIND,
                'description' => 'Type1.field1 arg arg9 has changed type from [Int] to [Int!]',
            ],
            [
                'type' => FindBreakingChanges::BREAKING_CHANGE_ARG_CHANGED_KIND,
                'description' => 'Type1.field1 arg arg11 has changed type from [Int] to [[Int]]',
            ],
            [
                'type' => FindBreakingChanges::BREAKING_CHANGE_ARG_CHANGED_KIND,
                'description' => 'Type1.field1 arg arg12 has changed type from [[Int]] to [Int]',
            ],
            [
                'type' => FindBreakingChanges::BREAKING_CHANGE_ARG_CHANGED_KIND,
                'description' => 'Type1.field1 arg arg13 has changed type from Int! to [Int]!',
            ],
            [
                'type' => FindBreakingChanges::BREAKING_CHANGE_ARG_CHANGED_KIND,
                'description' => 'Type1.field1 arg arg15 has changed type from [[Int]!] to [[Int!]!]',
            ],
        ];

        $this->assertEquals($expectedChanges, FindBreakingChanges::findArgChanges($oldSchema, $newSchema)['breakingChanges']);
    }

    /**
     * @it should detect if a non-null field argument was added
     */
    public function testShouldDetectIfANonNullFieldArgumentWasAdded()
    {
        $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' => FindBreakingChanges::BREAKING_CHANGE_NON_NULL_ARG_ADDED,
                'description' => 'A non-null arg newRequiredArg on Type1.field1 was added'
            ]
        ];

        $this->assertEquals(
            $expected,
            FindBreakingChanges::findArgChanges($oldSchema, $newSchema)['breakingChanges']);
    }

    /**
     * @it should not flag args with the same type signature as breaking
     */
    public function testShouldNotFlagArgsWithTheSameTypeSignatureAsBreaking()
    {
        $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],
        ]);

        $this->assertEquals([], FindBreakingChanges::findArgChanges($oldSchema, $newSchema)['breakingChanges']);
    }

    /**
     * @it should consider args that move away from NonNull as non-breaking
     */
    public function testShouldConsiderArgsThatMoveAwayFromNonNullAsNonBreaking()
    {
        $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],
        ]);

        $this->assertEquals([], FindBreakingChanges::findArgChanges($oldSchema, $newSchema)['breakingChanges']);
    }

    /**
     * @it should detect interfaces removed from types
     */
    public function testShouldDetectInterfacesRemovedFromTypes()
    {
        $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' => FindBreakingChanges::BREAKING_CHANGE_INTERFACE_REMOVED_FROM_OBJECT,
                'description' => 'Type1 no longer implements interface Interface1.'
            ],
        ];

        $this->assertEquals(
            $expected,
            FindBreakingChanges::findInterfacesRemovedFromObjectTypes($oldSchema, $newSchema));
    }

    /**
     * @it should detect all breaking changes
     */
    public function testShouldDetectAllBreakingChanges()
    {
        $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' => FindBreakingChanges::BREAKING_CHANGE_TYPE_REMOVED,
                'description' => 'TypeThatGetsRemoved was removed.',
            ],
            [
                'type' => FindBreakingChanges::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' => FindBreakingChanges::BREAKING_CHANGE_TYPE_CHANGED_KIND,
                'description' => 'TypeThatChangesType changed from an Object type to an Interface type.',
            ],
            [
                'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_REMOVED,
                'description' => 'TypeThatHasBreakingFieldChanges.field1 was removed.',
            ],
            [
                'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED_KIND,
                'description' => 'TypeThatHasBreakingFieldChanges.field2 changed type from String to Boolean.',
            ],
            [
                'type' => FindBreakingChanges::BREAKING_CHANGE_TYPE_REMOVED_FROM_UNION,
                'description' => 'TypeInUnion2 was removed from union type UnionTypeThatLosesAType.',
            ],
            [
                'type' => FindBreakingChanges::BREAKING_CHANGE_VALUE_REMOVED_FROM_ENUM,
                'description' => 'VALUE0 was removed from enum type EnumTypeThatLosesAValue.',
            ],
            [
                'type' => FindBreakingChanges::BREAKING_CHANGE_ARG_CHANGED_KIND,
                'description' => 'ArgThatChanges.field1 arg id has changed type from Int to String',
            ],
            [
                'type' => FindBreakingChanges::BREAKING_CHANGE_INTERFACE_REMOVED_FROM_OBJECT,
                'description' => 'TypeThatLosesInterface1 no longer implements interface Interface1.',
            ],
            [
                'type' => FindBreakingChanges::BREAKING_CHANGE_DIRECTIVE_REMOVED,
                'description' => 'skip was removed',
            ],
            [
                'type' => FindBreakingChanges::BREAKING_CHANGE_DIRECTIVE_ARG_REMOVED,
                'description' => 'arg1 was removed from DirectiveThatRemovesArg',
            ],
            [
                'type' => FindBreakingChanges::BREAKING_CHANGE_NON_NULL_DIRECTIVE_ARG_ADDED,
                'description' => 'A non-null arg arg1 on directive NonNullDirectiveAdded was added',
            ],
            [
                'type' => FindBreakingChanges::BREAKING_CHANGE_DIRECTIVE_LOCATION_REMOVED,
                'description' => 'QUERY was removed from Directive Name',
            ]
        ];

        $this->assertEquals($expectedBreakingChanges, FindBreakingChanges::findBreakingChanges($oldSchema, $newSchema));
    }

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

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

        $includeDirective = Directive::includeDirective();

        $expectedBreakingChanges = [
            [
                'type' => FindBreakingChanges::BREAKING_CHANGE_DIRECTIVE_REMOVED,
                'description' => "{$includeDirective->name} was removed",
            ]
        ];

        $this->assertEquals($expectedBreakingChanges, FindBreakingChanges::findRemovedDirectives($oldSchema, $newSchema));
    }

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

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

        $deprecatedDirective = Directive::deprecatedDirective();

        $expectedBreakingChanges = [
            [
                'type' => FindBreakingChanges::BREAKING_CHANGE_DIRECTIVE_REMOVED,
                'description' => "{$deprecatedDirective->name} was removed",
            ]
        ];

        $this->assertEquals($expectedBreakingChanges, FindBreakingChanges::findRemovedDirectives($oldSchema, $newSchema));
    }

    /**
     * @it should detect if a directive argument was removed
     */
    public function testShouldDetectIfADirectiveArgumentWasRemoved()
    {
        $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' => FindBreakingChanges::BREAKING_CHANGE_DIRECTIVE_ARG_REMOVED,
                'description' => "arg1 was removed from DirectiveWithArg",
            ]
        ];

        $this->assertEquals($expectedBreakingChanges, FindBreakingChanges::findRemovedDirectiveArgs($oldSchema, $newSchema));
    }

    /**
     * @it should detect if a non-nullable directive argument was added
     */
    public function testShouldDetectIfANonNullableDirectiveArgumentWasAdded()
    {
        $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' => FindBreakingChanges::BREAKING_CHANGE_NON_NULL_DIRECTIVE_ARG_ADDED,
                'description' => "A non-null arg arg1 on directive DirectiveName was added",
            ]
        ];

        $this->assertEquals($expectedBreakingChanges, FindBreakingChanges::findAddedNonNullDirectiveArgs($oldSchema, $newSchema));
    }

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

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

        $this->assertEquals([DirectiveLocation::QUERY], FindBreakingChanges::findRemovedLocationsForDirective($d1, $d2));
    }

    /**
     * @it should detect locations removed directives within a schema
     */
    public function testShouldDetectLocationsRemovedDirectiveWithinASchema()
    {
        $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' => FindBreakingChanges::BREAKING_CHANGE_DIRECTIVE_LOCATION_REMOVED,
                'description' => "QUERY was removed from Directive Name",
            ]
        ];

        $this->assertEquals($expectedBreakingChanges, FindBreakingChanges::findRemovedDirectiveLocations($oldSchema, $newSchema));
    }

    // DESCRIBE: findDangerousChanges
    // DESCRIBE: findArgChanges

    /**
     * @it should detect if an argument's defaultValue has changed
     */
    public function testShouldDetectIfAnArgumentsDefaultValueHasChanged()
    {
        $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' => FindBreakingChanges::DANGEROUS_CHANGE_ARG_DEFAULT_VALUE_CHANGED,
                'description' => 'Type1.field1 arg name has changed defaultValue'
            ]
        ];

        $this->assertEquals(
            $expected,
            FindBreakingChanges::findArgChanges($oldSchema, $newSchema)['dangerousChanges']
        );
    }

    /**
     * @it should detect if a value was added to an enum type
     */
    public function testShouldDetectIfAValueWasAddedToAnEnumType()
    {
        $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' => FindBreakingChanges::DANGEROUS_CHANGE_VALUE_ADDED_TO_ENUM,
                'description' => 'VALUE2 was added to enum type EnumType1.'
            ]
        ];

        $this->assertEquals(
            $expected,
            FindBreakingChanges::findValuesAddedToEnums($oldSchema, $newSchema)
        );
    }

    /**
     * @it should detect interfaces added to types
     */
    public function testShouldDetectInterfacesAddedToTypes()
    {
        $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' => FindBreakingChanges::DANGEROUS_CHANGE_INTERFACE_ADDED_TO_OBJECT,
                'description' => 'Interface1 added to interfaces implemented by Type1.'
            ]
        ];

        $this->assertEquals(
            $expected,
            FindBreakingChanges::findInterfacesAddedToObjectTypes($oldSchema, $newSchema)
        );
    }

    /**
     * @it should detect if a type was added to a union type
     */
    public function testShouldDetectIfATypeWasAddedToAUnionType()
    {
        $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' => FindBreakingChanges::DANGEROUS_CHANGE_TYPE_ADDED_TO_UNION,
                'description' => 'Type2 was added to union type UnionType1.'
            ]
        ];

        $this->assertEquals(
            $expected,
            FindBreakingChanges::findTypesAddedToUnions($oldSchema, $newSchema)
        );
    }

    /**
     * @it should detect if a nullable field was added to an input
     */
    public function testShouldDetectIfANullableFieldWasAddedToAnInput()
    {
        $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' => FindBreakingChanges::DANGEROUS_CHANGE_NULLABLE_INPUT_FIELD_ADDED
            ],
        ];

        $this->assertEquals($expectedFieldChanges, FindBreakingChanges::findFieldsThatChangedTypeOnInputObjectTypes($oldSchema, $newSchema)['dangerousChanges']);
    }

    /**
     * @it should find all dangerous changes
     */
    public function testShouldFindAllDangerousChanges()
    {
        $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' => FindBreakingChanges::DANGEROUS_CHANGE_ARG_DEFAULT_VALUE_CHANGED
            ],
            [
                'description' => 'VALUE2 was added to enum type EnumType1.',
                'type' => FindBreakingChanges::DANGEROUS_CHANGE_VALUE_ADDED_TO_ENUM
            ],
            [
                'type' => FindBreakingChanges::DANGEROUS_CHANGE_INTERFACE_ADDED_TO_OBJECT,
                'description' => 'Interface1 added to interfaces implemented by TypeThatGainsInterface1.',
            ],
            [
                'type' => FindBreakingChanges::DANGEROUS_CHANGE_TYPE_ADDED_TO_UNION,
                'description' => 'TypeInUnion2 was added to union type UnionTypeThatGainsAType.',
            ]
        ];

        $this->assertEquals($expectedDangerousChanges, FindBreakingChanges::findDangerousChanges($oldSchema, $newSchema));
    }

    /**
     * @it should detect if a nullable field argument was added
     */
    public function testShouldDetectIfANullableFieldArgumentWasAdded()
    {
        $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' => FindBreakingChanges::DANGEROUS_CHANGE_NULLABLE_ARG_ADDED
            ],
        ];

        $this->assertEquals($expectedFieldChanges, FindBreakingChanges::findArgChanges($oldSchema, $newSchema)['dangerousChanges']);
    }
}