<?php
namespace GraphQL\Tests\Utils;

use GraphQL\GraphQL;
use GraphQL\Language\AST\EnumTypeDefinitionNode;
use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
use GraphQL\Language\Parser;
use GraphQL\Language\Printer;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Utils\BuildSchema;
use GraphQL\Utils\SchemaPrinter;
use GraphQL\Type\Definition\Directive;

class BuildSchemaTest extends \PHPUnit_Framework_TestCase
{
    // Describe: Schema Builder

    private function cycleOutput($body, $options = [])
    {
        $ast = Parser::parse($body);
        $schema = BuildSchema::buildAST($ast, $options);
        return "\n" . SchemaPrinter::doPrint($schema, $options);
    }

    /**
     * @it can use built schema for limited execution
     */
    public function testUseBuiltSchemaForLimitedExecution()
    {
        $schema = BuildSchema::buildAST(Parser::parse('
            schema { query: Query }
            type Query {
                str: String
            }
        '));

        $result = GraphQL::executeQuery($schema, '{ str }', ['str' => 123]);
        $this->assertEquals(['str' => 123], $result->toArray(true)['data']);
    }

    /**
     * @it can build a schema directly from the source
     */
    public function testBuildSchemaDirectlyFromSource()
    {
        $schema = BuildSchema::build("
            schema { query: Query }
            type Query {
                add(x: Int, y: Int): Int
            }
        ");

        $result = GraphQL::executeQuery(
            $schema,
            '{ add(x: 34, y: 55) }',
            [
                'add' => function ($root, $args) {
                    return $args['x'] + $args['y'];
                }
            ]
        );
        $this->assertEquals(['data' => ['add' => 89]], $result->toArray(true));
    }

    /**
     * @it Simple Type
     */
    public function testSimpleType()
    {
        $body = '
schema {
  query: HelloScalars
}

type HelloScalars {
  str: String
  int: Int
  float: Float
  id: ID
  bool: Boolean
}
';
        $output = $this->cycleOutput($body);
        $this->assertEquals($output, $body);
    }

    /**
     * @it With directives
     */
    public function testWithDirectives()
    {
        $body = '
schema {
  query: Hello
}

directive @foo(arg: Int) on FIELD

type Hello {
  str: String
}
';
        $output = $this->cycleOutput($body);
        $this->assertEquals($output, $body);
    }

    /**
     * @it Supports descriptions
     */
    public function testSupportsDescriptions()
    {
      $body = '
schema {
  query: Hello
}

"""This is a directive"""
directive @foo(
  """It has an argument"""
  arg: Int
) on FIELD

"""With an enum"""
enum Color {
  RED

  """Not a creative color"""
  GREEN
  BLUE
}

"""What a great type"""
type Hello {
  """And a field to boot"""
  str: String
}
';

        $output = $this->cycleOutput($body);
        $this->assertEquals($body, $output);
    }

    /**
     * @it Supports descriptions
     */
    public function testSupportsOptionForCommentDescriptions()
    {
        $body = '
schema {
  query: Hello
}

# This is a directive
directive @foo(
  # It has an argument
  arg: Int
) on FIELD

# With an enum
enum Color {
  RED

  # Not a creative color
  GREEN
  BLUE
}

# What a great type
type Hello {
  # And a field to boot
  str: String
}
';
        $output = $this->cycleOutput($body, [ 'commentDescriptions' => true ]);
        $this->assertEquals($body, $output);
    }

    /**
     * @it Maintains @skip & @include
     */
    public function testMaintainsSkipAndInclude()
    {
        $body = '
schema {
  query: Hello
}

type Hello {
  str: String
}
';
        $schema = BuildSchema::buildAST(Parser::parse($body));
        $this->assertEquals(count($schema->getDirectives()), 3);
        $this->assertEquals($schema->getDirective('skip'), Directive::skipDirective());
        $this->assertEquals($schema->getDirective('include'), Directive::includeDirective());
        $this->assertEquals($schema->getDirective('deprecated'), Directive::deprecatedDirective());
    }

    /**
     * @it Overriding directives excludes specified
     */
    public function testOverridingDirectivesExcludesSpecified()
    {
        $body = '
schema {
  query: Hello
}

directive @skip on FIELD
directive @include on FIELD
directive @deprecated on FIELD_DEFINITION

type Hello {
  str: String
}
    ';
        $schema = BuildSchema::buildAST(Parser::parse($body));
        $this->assertEquals(count($schema->getDirectives()), 3);
        $this->assertNotEquals($schema->getDirective('skip'), Directive::skipDirective());
        $this->assertNotEquals($schema->getDirective('include'), Directive::includeDirective());
        $this->assertNotEquals($schema->getDirective('deprecated'), Directive::deprecatedDirective());
    }

    /**
     * @it Type modifiers
     */
    public function testTypeModifiers()
    {
        $body = '
schema {
  query: HelloScalars
}

type HelloScalars {
  nonNullStr: String!
  listOfStrs: [String]
  listOfNonNullStrs: [String!]
  nonNullListOfStrs: [String]!
  nonNullListOfNonNullStrs: [String!]!
}
';
        $output = $this->cycleOutput($body);
        $this->assertEquals($output, $body);
    }

    /**
     * @it Recursive type
     */
    public function testRecursiveType()
    {
        $body = '
schema {
  query: Recurse
}

type Recurse {
  str: String
  recurse: Recurse
}
';
        $output = $this->cycleOutput($body);
        $this->assertEquals($output, $body);
    }

    /**
     * @it Two types circular
     */
    public function testTwoTypesCircular()
    {
        $body = '
schema {
  query: TypeOne
}

type TypeOne {
  str: String
  typeTwo: TypeTwo
}

type TypeTwo {
  str: String
  typeOne: TypeOne
}
';
        $output = $this->cycleOutput($body);
        $this->assertEquals($output, $body);
    }

    /**
     * @it Single argument field
     */
    public function testSingleArgumentField()
    {
        $body = '
schema {
  query: Hello
}

type Hello {
  str(int: Int): String
  floatToStr(float: Float): String
  idToStr(id: ID): String
  booleanToStr(bool: Boolean): String
  strToStr(bool: String): String
}
';
        $output = $this->cycleOutput($body);
        $this->assertEquals($output, $body);
    }

    /**
     * @it Simple type with multiple arguments
     */
    public function testSimpleTypeWithMultipleArguments()
    {
        $body = '
schema {
  query: Hello
}

type Hello {
  str(int: Int, bool: Boolean): String
}
';
        $output = $this->cycleOutput($body);
        $this->assertEquals($output, $body);
    }

    /**
     * @it Simple type with interface
     */
    public function testSimpleTypeWithInterface()
    {
        $body = '
schema {
  query: Hello
}

type Hello implements WorldInterface {
  str: String
}

interface WorldInterface {
  str: String
}
';
        $output = $this->cycleOutput($body);
        $this->assertEquals($output, $body);
    }

    /**
     * @it Simple output enum
     */
    public function testSimpleOutputEnum()
    {
        $body = '
schema {
  query: OutputEnumRoot
}

enum Hello {
  WORLD
}

type OutputEnumRoot {
  hello: Hello
}
';
        $output = $this->cycleOutput($body);
        $this->assertEquals($output, $body);
    }

    /**
     * @it Multiple value enum
     */
    public function testMultipleValueEnum()
    {
        $body = '
schema {
  query: OutputEnumRoot
}

enum Hello {
  WO
  RLD
}

type OutputEnumRoot {
  hello: Hello
}
';
        $output = $this->cycleOutput($body);
        $this->assertEquals($output, $body);
    }

    /**
     * @it Simple Union
     */
    public function testSimpleUnion()
    {
        $body = '
schema {
  query: Root
}

union Hello = World

type Root {
  hello: Hello
}

type World {
  str: String
}
';
        $output = $this->cycleOutput($body);
        $this->assertEquals($output, $body);
    }

    /**
     * @it Multiple Union
     */
    public function testMultipleUnion()
    {
        $body = '
schema {
  query: Root
}

union Hello = WorldOne | WorldTwo

type Root {
  hello: Hello
}

type WorldOne {
  str: String
}

type WorldTwo {
  str: String
}
';
        $output = $this->cycleOutput($body);
        $this->assertEquals($output, $body);
    }

    /**
     * @it Specifying Union type using __typename
     */
    public function testSpecifyingUnionTypeUsingTypename()
    {
        $schema = BuildSchema::buildAST(Parser::parse('
            schema {
              query: Root
            }
            
            type Root {
              fruits: [Fruit]
            }
            
            union Fruit = Apple | Banana
            
            type Apple {
              color: String
            }
            
            type Banana {
              length: Int
            }
        '));
        $query = '
            {
              fruits {
                ... on Apple {
                  color
                }
                ... on Banana {
                  length
                }
              }
            }
        ';
        $root = [
            'fruits' => [
                [
                    'color' => 'green',
                    '__typename' => 'Apple',
                ],
                [
                    'length' => 5,
                    '__typename' => 'Banana',
                ]
            ]
        ];
        $expected = [
            'data' => [
                'fruits' => [
                    ['color' => 'green'],
                    ['length' => 5],
                ]
            ]
        ];

        $result = GraphQL::executeQuery($schema, $query, $root);
        $this->assertEquals($expected, $result->toArray(true));
    }

    /**
     * @it Specifying Interface type using __typename
     */
    public function testSpecifyingInterfaceUsingTypename()
    {
        $schema = BuildSchema::buildAST(Parser::parse('
            schema {
              query: Root
            }
            
            type Root {
              characters: [Character]
            }
            
            interface Character {
              name: String!
            }
            
            type Human implements Character {
              name: String!
              totalCredits: Int
            }
            
            type Droid implements Character {
              name: String!
              primaryFunction: String
            }
        '));
        $query = '
            {
              characters {
                name
                ... on Human {
                  totalCredits
                }
                ... on Droid {
                  primaryFunction
                }
              }
            }
        ';
        $root = [
            'characters' => [
                [
                    'name' => 'Han Solo',
                    'totalCredits' => 10,
                    '__typename' => 'Human',
                ],
                [
                    'name' => 'R2-D2',
                    'primaryFunction' => 'Astromech',
                    '__typename' => 'Droid',
                ]
            ]
        ];
        $expected = [
            'data' => [
                'characters' => [
                    ['name' => 'Han Solo', 'totalCredits' => 10],
                    ['name' => 'R2-D2', 'primaryFunction' => 'Astromech'],
                ]
            ]
        ];

        $result = GraphQL::executeQuery($schema, $query, $root);
        $this->assertEquals($expected, $result->toArray(true));
    }

    /**
     * @it CustomScalar
     */
    public function testCustomScalar()
    {
        $body = '
schema {
  query: Root
}

scalar CustomScalar

type Root {
  customScalar: CustomScalar
}
';
        $output = $this->cycleOutput($body);
        $this->assertEquals($output, $body);
    }

    /**
     * @it CustomScalar
     */
    public function testInputObject()
    {
        $body = '
schema {
  query: Root
}

input Input {
  int: Int
}

type Root {
  field(in: Input): String
}
';
        $output = $this->cycleOutput($body);
        $this->assertEquals($output, $body);
    }

    /**
     * @it Simple argument field with default
     */
    public function testSimpleArgumentFieldWithDefault()
    {
        $body = '
schema {
  query: Hello
}

type Hello {
  str(int: Int = 2): String
}
';
        $output = $this->cycleOutput($body);
        $this->assertEquals($output, $body);
    }

    /**
     * @it Custom scalar argument field with default
     */
    public function testCustomScalarArgumentFieldWithDefault()
    {
        $body = '
schema {
  query: Hello
}

scalar CustomScalar

type Hello {
  str(int: CustomScalar = 2): String
}
';
        $output = $this->cycleOutput($body);
        $this->assertEquals($output, $body);
    }

    /**
     * @it Simple type with mutation
     */
    public function testSimpleTypeWithMutation()
    {
        $body = '
schema {
  query: HelloScalars
  mutation: Mutation
}

type HelloScalars {
  str: String
  int: Int
  bool: Boolean
}

type Mutation {
  addHelloScalars(str: String, int: Int, bool: Boolean): HelloScalars
}
';
        $output = $this->cycleOutput($body);
        $this->assertEquals($output, $body);
    }

    /**
     * @it Simple type with subscription
     */
    public function testSimpleTypeWithSubscription()
    {
        $body = '
schema {
  query: HelloScalars
  subscription: Subscription
}

type HelloScalars {
  str: String
  int: Int
  bool: Boolean
}

type Subscription {
  subscribeHelloScalars(str: String, int: Int, bool: Boolean): HelloScalars
}
';
        $output = $this->cycleOutput($body);
        $this->assertEquals($output, $body);
    }

    /**
     * @it Unreferenced type implementing referenced interface
     */
    public function testUnreferencedTypeImplementingReferencedInterface()
    {
        $body = '
type Concrete implements Iface {
  key: String
}

interface Iface {
  key: String
}

type Query {
  iface: Iface
}
';
        $output = $this->cycleOutput($body);
        $this->assertEquals($output, $body);
    }

    /**
     * @it Unreferenced type implementing referenced union
     */
    public function testUnreferencedTypeImplementingReferencedUnion()
    {
        $body = '
type Concrete {
  key: String
}

type Query {
  union: Union
}

union Union = Concrete
';
        $output = $this->cycleOutput($body);
        $this->assertEquals($output, $body);
    }

    /**
     * @it Supports @deprecated
     */
    public function testSupportsDeprecated()
    {
        $body = '
enum MyEnum {
  VALUE
  OLD_VALUE @deprecated
  OTHER_VALUE @deprecated(reason: "Terrible reasons")
}

type Query {
  field1: String @deprecated
  field2: Int @deprecated(reason: "Because I said so")
  enum: MyEnum
}
';
        $output = $this->cycleOutput($body);
        $this->assertEquals($output, $body);

        $ast = Parser::parse($body);
        $schema = BuildSchema::buildAST($ast);

        /** @var EnumType $myEnum */
        $myEnum = $schema->getType('MyEnum');

        $value = $myEnum->getValue('VALUE');
        $this->assertFalse($value->isDeprecated());

        $oldValue = $myEnum->getValue('OLD_VALUE');
        $this->assertTrue($oldValue->isDeprecated());
        $this->assertEquals('No longer supported', $oldValue->deprecationReason);

        $otherValue = $myEnum->getValue('OTHER_VALUE');
        $this->assertTrue($otherValue->isDeprecated());
        $this->assertEquals('Terrible reasons', $otherValue->deprecationReason);

        $rootFields = $schema->getType('Query')->getFields();
        $this->assertEquals($rootFields['field1']->isDeprecated(), true);
        $this->assertEquals($rootFields['field1']->deprecationReason, 'No longer supported');

        $this->assertEquals($rootFields['field2']->isDeprecated(), true);
        $this->assertEquals($rootFields['field2']->deprecationReason, 'Because I said so');
    }

    /**
     * @it Correctly assign AST nodes
     */
    public function testCorrectlyAssignASTNodes()
    {

        $schema = BuildSchema::build('
      schema {
        query: Query
      }

      type Query {
        testField(testArg: TestInput): TestUnion
      }

      input TestInput {
        testInputField: TestEnum
      }

      enum TestEnum {
        TEST_VALUE
      }

      union TestUnion = TestType

      interface TestInterface {
        interfaceField: String
      }

      type TestType implements TestInterface {
        interfaceField: String
      }

      directive @test(arg: Int) on FIELD
    ');
        /** @var ObjectType $query */
        $query = $schema->getType('Query');
        $testInput = $schema->getType('TestInput');
        $testEnum = $schema->getType('TestEnum');
        $testUnion = $schema->getType('TestUnion');
        $testInterface = $schema->getType('TestInterface');
        $testType = $schema->getType('TestType');
        $testDirective = $schema->getDirective('test');

        $restoredIDL = SchemaPrinter::doPrint(BuildSchema::build(
            Printer::doPrint($schema->getAstNode()) . "\n" .
            Printer::doPrint($query->astNode) . "\n" .
            Printer::doPrint($testInput->astNode) . "\n" .
            Printer::doPrint($testEnum->astNode) . "\n" .
            Printer::doPrint($testUnion->astNode) . "\n" .
            Printer::doPrint($testInterface->astNode) . "\n" .
            Printer::doPrint($testType->astNode) . "\n" .
            Printer::doPrint($testDirective->astNode)
        ));

        $this->assertEquals($restoredIDL, SchemaPrinter::doPrint($schema));

        $testField = $query->getField('testField');
        $this->assertEquals('testField(testArg: TestInput): TestUnion', Printer::doPrint($testField->astNode));
        $this->assertEquals('testArg: TestInput', Printer::doPrint($testField->args[0]->astNode));
        $this->assertEquals('testInputField: TestEnum', Printer::doPrint($testInput->getField('testInputField')->astNode));
        $this->assertEquals('TEST_VALUE', Printer::doPrint($testEnum->getValue('TEST_VALUE')->astNode));
        $this->assertEquals('interfaceField: String', Printer::doPrint($testInterface->getField('interfaceField')->astNode));
        $this->assertEquals('interfaceField: String', Printer::doPrint($testType->getField('interfaceField')->astNode));
        $this->assertEquals('arg: Int', Printer::doPrint($testDirective->args[0]->astNode));
    }

    // Describe: Failures

    /**
     * @it Allows only a single schema definition
     */
    public function testAllowsOnlySingleSchemaDefinition()
    {
        $this->setExpectedException('GraphQL\Error\Error', 'Must provide only one schema definition.');
        $body = '
schema {
  query: Hello
}

schema {
  query: Hello
}

type Hello {
  bar: Bar
}
';
        $doc = Parser::parse($body);
        BuildSchema::buildAST($doc);
    }

    /**
     * @it Allows only a single query type
     */
    public function testAllowsOnlySingleQueryType()
    {
        $this->setExpectedException('GraphQL\Error\Error', 'Must provide only one query type in schema.');
        $body = '
schema {
  query: Hello
  query: Yellow
}

type Hello {
  bar: Bar
}

type Yellow {
  isColor: Boolean
}
';
        $doc = Parser::parse($body);
        BuildSchema::buildAST($doc);
    }

    /**
     * @it Allows only a single mutation type
     */
    public function testAllowsOnlySingleMutationType()
    {
        $this->setExpectedException('GraphQL\Error\Error', 'Must provide only one mutation type in schema.');
        $body = '
schema {
  query: Hello
  mutation: Hello
  mutation: Yellow
}

type Hello {
  bar: Bar
}

type Yellow {
  isColor: Boolean
}
';
        $doc = Parser::parse($body);
        BuildSchema::buildAST($doc);
    }

    /**
     * @it Allows only a single subscription type
     */
    public function testAllowsOnlySingleSubscriptionType()
    {
        $this->setExpectedException('GraphQL\Error\Error', 'Must provide only one subscription type in schema.');
        $body = '
schema {
  query: Hello
  subscription: Hello
  subscription: Yellow
}

type Hello {
  bar: Bar
}

type Yellow {
  isColor: Boolean
}
';
        $doc = Parser::parse($body);
        BuildSchema::buildAST($doc);
    }

    /**
     * @it Unknown type referenced
     */
    public function testUnknownTypeReferenced()
    {
        $this->setExpectedException('GraphQL\Error\Error', 'Type "Bar" not found in document.');
        $body = '
schema {
  query: Hello
}

type Hello {
  bar: Bar
}
';
        $doc = Parser::parse($body);
        $schema = BuildSchema::buildAST($doc);
        $schema->getTypeMap();
    }

    /**
     * @it Unknown type in interface list
     */
    public function testUnknownTypeInInterfaceList()
    {
        $this->setExpectedException('GraphQL\Error\Error', 'Type "Bar" not found in document.');
        $body = '
schema {
  query: Hello
}

type Hello implements Bar {
  field: String
}
';
        $doc = Parser::parse($body);
        $schema = BuildSchema::buildAST($doc);
        $schema->getTypeMap();
    }

    /**
     * @it Unknown type in union list
     */
    public function testUnknownTypeInUnionList()
    {
        $this->setExpectedException('GraphQL\Error\Error', 'Type "Bar" not found in document.');
        $body = '
schema {
  query: Hello
}

union TestUnion = Bar
type Hello { testUnion: TestUnion }
';
        $doc = Parser::parse($body);
        $schema = BuildSchema::buildAST($doc);
        $schema->getTypeMap();
    }

    /**
     * @it Unknown query type
     */
    public function testUnknownQueryType()
    {
        $this->setExpectedException('GraphQL\Error\Error', 'Specified query type "Wat" not found in document.');
        $body = '
schema {
  query: Wat
}

type Hello {
  str: String
}
';
        $doc = Parser::parse($body);
        BuildSchema::buildAST($doc);
    }

    /**
     * @it Unknown mutation type
     */
    public function testUnknownMutationType()
    {
        $this->setExpectedException('GraphQL\Error\Error', 'Specified mutation type "Wat" not found in document.');
        $body = '
schema {
  query: Hello
  mutation: Wat
}

type Hello {
  str: String
}
';
        $doc = Parser::parse($body);
        BuildSchema::buildAST($doc);
    }

    /**
     * @it Unknown subscription type
     */
    public function testUnknownSubscriptionType()
    {
        $this->setExpectedException('GraphQL\Error\Error', 'Specified subscription type "Awesome" not found in document.');
        $body = '
schema {
  query: Hello
  mutation: Wat
  subscription: Awesome
}

type Hello {
  str: String
}

type Wat {
  str: String
}
';
        $doc = Parser::parse($body);
        BuildSchema::buildAST($doc);
    }

    /**
     * @it Does not consider operation names
     */
    public function testDoesNotConsiderOperationNames()
    {
        $this->setExpectedException('GraphQL\Error\Error', 'Specified query type "Foo" not found in document.');
        $body = '
schema {
  query: Foo
}

query Foo { field }
';
        $doc = Parser::parse($body);
        BuildSchema::buildAST($doc);
    }

    /**
     * @it Does not consider fragment names
     */
    public function testDoesNotConsiderFragmentNames()
    {
        $this->setExpectedException('GraphQL\Error\Error', 'Specified query type "Foo" not found in document.');
        $body = '
schema {
  query: Foo
}

fragment Foo on Type { field }
';
        $doc = Parser::parse($body);
        BuildSchema::buildAST($doc);
    }

    /**
     * @it Forbids duplicate type definitions
     */
    public function testForbidsDuplicateTypeDefinitions()
    {
        $body = '
schema {
  query: Repeated
}

type Repeated {
  id: Int
}

type Repeated {
  id: String
}
';
        $doc = Parser::parse($body);

        $this->setExpectedException('GraphQL\Error\Error', 'Type "Repeated" was defined more than once.');
        BuildSchema::buildAST($doc);
    }
}