package schema_test

import (
	"fmt"
	"strings"
	"testing"

	"github.com/graph-gophers/graphql-go/ast"
	"github.com/graph-gophers/graphql-go/internal/schema"
)

func TestParse(t *testing.T) {
	for _, test := range []struct {
		name                  string
		sdl                   string
		useStringDescriptions bool
		validateError         func(err error) error
		validateSchema        func(s *ast.Schema) error
	}{
		{
			name: "Parses interface definition",
			sdl:  "interface Greeting { message: String! }",
			validateSchema: func(s *ast.Schema) error {
				const typeName = "Greeting"
				typ, ok := s.Types[typeName].(*ast.InterfaceTypeDefinition)
				if !ok {
					return fmt.Errorf("interface %q not found", typeName)
				}
				if want, have := 1, len(typ.Fields); want != have {
					return fmt.Errorf("invalid number of fields: want %d, have %d", want, have)
				}
				const fieldName = "message"
				if typ.Fields[0].Name != fieldName {
					return fmt.Errorf("field %q not found", fieldName)
				}
				return nil
			},
		},
		{
			name: "Parses implementing type without providing required fields",
			sdl: `
			interface Greeting { 
				message: String!
			}
			type Welcome implements Greeting {
				id: ID!
			}`,
			validateError: func(err error) error {
				if err == nil {
					return fmt.Errorf("want error, have <nil>")
				}
				if want, have := `graphql: interface "Greeting" expects field "message" but "Welcome" does not provide it`, err.Error(); want != have {
					return fmt.Errorf("unexpected error: want %q, have %q", want, have)
				}
				return nil
			},
		},
		{
			name: "Parses type with description string",
			sdl: `
			"Single line description."
			type Type { 
				field: String
			}`,
			useStringDescriptions: true,
			validateSchema: func(s *ast.Schema) error {
				const typeName = "Type"
				typ, ok := s.Types[typeName].(*ast.ObjectTypeDefinition)
				if !ok {
					return fmt.Errorf("type %q not found", typeName)
				}
				if want, have := "Single line description.", typ.Description(); want != have {
					return fmt.Errorf("invalid description: want %q, have %q", want, have)
				}
				return nil
			},
		},
		{
			name: "Parses type with simple multi-line 'BlockString' description",
			sdl: `
			"""
			Multi-line description.
			"""
			type Type {
				field: String
			}`,
			useStringDescriptions: true,
			validateSchema: func(s *ast.Schema) error {
				const typeName = "Type"
				typ, ok := s.Types[typeName].(*ast.ObjectTypeDefinition)
				if !ok {
					return fmt.Errorf("type %q not found", typeName)
				}
				if want, have := "Multi-line description.", typ.Description(); want != have {
					return fmt.Errorf("invalid description: want %q, have %q", want, have)
				}
				return nil
			},
		},
		{
			name: "Parses type with empty multi-line 'BlockString' description",
			sdl: `
			"""
			"""
			type Type {
				field: String
			}`,
			useStringDescriptions: true,
			validateSchema: func(s *ast.Schema) error {
				const typeName = "Type"
				typ, ok := s.Types[typeName].(*ast.ObjectTypeDefinition)
				if !ok {
					return fmt.Errorf("type %q not found", typeName)
				}
				if want, have := "", typ.Description(); want != have {
					return fmt.Errorf("invalid description: want %q, have %q", want, have)
				}
				return nil
			},
		},
		{
			name: "Parses type with multi-line 'BlockString' description",
			sdl: `
			"""
			First line of the description.

			Second line of the description.

				query {
					code {
						example
					}
				}

			Notes:

			 * First note
			 * Second note
			"""
			type Type {
				field: String
			}`,
			useStringDescriptions: true,
			validateSchema: func(s *ast.Schema) error {
				const typeName = "Type"
				typ, ok := s.Types[typeName].(*ast.ObjectTypeDefinition)
				if !ok {
					return fmt.Errorf("type %q not found", typeName)
				}
				want := "First line of the description.\n\nSecond line of the description.\n\n\tquery {\n\t\tcode {\n\t\t\texample\n\t\t}\n\t}\n\nNotes:\n\n * First note\n * Second note"
				if have := typ.Description(); want != have {
					return fmt.Errorf("invalid description: want %q, have %q", want, have)
				}
				return nil
			},
		},
		{
			name: "Parses type with un-indented multi-line 'BlockString' description",
			sdl: `
			"""
First line of the description.

Second line of the description.
			"""
			type Type {
				field: String
			}`,
			useStringDescriptions: true,
			validateSchema: func(s *ast.Schema) error {
				const typeName = "Type"
				typ, ok := s.Types[typeName].(*ast.ObjectTypeDefinition)
				if !ok {
					return fmt.Errorf("type %q not found", typeName)
				}
				want := "First line of the description.\n\nSecond line of the description."
				if have := typ.Description(); want != have {
					return fmt.Errorf("invalid description: want %q, have %q", want, have)
				}
				return nil
			},
		},
		{
			name: "Parses type with space-indented multi-line 'BlockString' description",
			sdl: `
            """
            First line of the description.

            Second line of the description.

                query {
                    code {
                        example
                    }
                }
            """
            type Type {
                field: String
            }`,
			useStringDescriptions: true,
			validateSchema: func(s *ast.Schema) error {
				const typeName = "Type"
				typ, ok := s.Types[typeName].(*ast.ObjectTypeDefinition)
				if !ok {
					return fmt.Errorf("type %q not found", typeName)
				}
				want := "First line of the description.\n\nSecond line of the description.\n\n    query {\n        code {\n            example\n        }\n    }"
				if have := typ.Description(); want != have {
					return fmt.Errorf("invalid description: want %q, have %q", want, have)
				}
				return nil
			},
		},
		{
			name: "Parses type with multi-line 'BlockString' description and ignores comments",
			sdl: `
			"""
			Multi-line description with ignored comments.
			"""
			# This comment should be ignored.
			type Type {
				field: String
			}`,
			useStringDescriptions: true,
			validateSchema: func(s *ast.Schema) error {
				const typeName = "Type"
				typ, ok := s.Types[typeName].(*ast.ObjectTypeDefinition)
				if !ok {
					return fmt.Errorf("type %q not found", typeName)
				}
				if want, have := "Multi-line description with ignored comments.", typ.Description(); want != have {
					return fmt.Errorf("invalid description: want %q, have %q", want, have)
				}
				return nil
			},
		},
		{
			name: "Parses type invalid syntax",
			sdl: `
			type U = T
			`,
			validateError: func(err error) error {
				msg := `graphql: syntax error: unexpected "=", expecting "{" (line 2, column 11)`
				if err == nil || err.Error() != msg {
					return fmt.Errorf("expected error %q, but got %q", msg, err)
				}
				return nil
			},
		},
		{
			name: "Description is correctly parsed for non-described types",
			sdl: `
			"Some description."
			scalar MyInt 
			type Type { 
				field: String
			}`,
			useStringDescriptions: true,
			validateSchema: func(s *ast.Schema) error {
				typ, ok := s.Types["Type"]
				if !ok {
					return fmt.Errorf("type %q not found", "Type")
				}
				if want, have := "", typ.Description(); want != have {
					return fmt.Errorf("description does not match: want %q, have %q ", want, have)
				}
				return nil
			},
		},
		{
			name: "Multi-line comment is correctly parsed",
			sdl: `
			# Multi-line
			# comment.
			" This description should be ignored. "
			scalar MyInt 
			type Type { 
				field: String
			}`,
			validateSchema: func(s *ast.Schema) error {
				typ, ok := s.Types["MyInt"]
				if !ok {
					return fmt.Errorf("scalar %q not found", "MyInt")
				}
				if want, have := "Multi-line\ncomment.", typ.Description(); want != have {
					return fmt.Errorf("description does not match: want %q, have %q ", want, have)
				}
				typ, ok = s.Types["Type"]
				if !ok {
					return fmt.Errorf("type %q not found", "Type")
				}
				if want, have := "", typ.Description(); want != have {
					return fmt.Errorf("description does not match: want %q, have %q ", want, have)
				}
				return nil
			},
		},
		{
			name: "Default Root schema",
			sdl: `
			type Query {
				hello: String!
			}
			type Mutation {
				concat(a: String!, b: String!): String!
			}
			`,
			validateSchema: func(s *ast.Schema) error {
				typq, ok := s.Types["Query"].(*ast.ObjectTypeDefinition)
				if !ok {
					return fmt.Errorf("type %q not found", "Query")
				}
				helloField := typq.Fields.Get("hello")
				if helloField == nil {
					return fmt.Errorf("field %q not found", "hello")
				}
				if helloField.Type.String() != "String!" {
					return fmt.Errorf("field %q has an invalid type: %q", "hello", helloField.Type.String())
				}

				typm, ok := s.Types["Mutation"].(*ast.ObjectTypeDefinition)
				if !ok {
					return fmt.Errorf("type %q not found", "Mutation")
				}
				concatField := typm.Fields.Get("concat")
				if concatField == nil {
					return fmt.Errorf("field %q not found", "concat")
				}
				if concatField.Type.String() != "String!" {
					return fmt.Errorf("field %q has an invalid type: %q", "concat", concatField.Type.String())
				}
				if len(concatField.Arguments) != 2 || concatField.Arguments[0] == nil || concatField.Arguments[1] == nil || concatField.Arguments[0].Type.String() != "String!" || concatField.Arguments[1].Type.String() != "String!" {
					return fmt.Errorf("field %q has an invalid args: %+v", "concat", concatField.Arguments)
				}
				return nil
			},
		},
		{
			name: "Extend type",
			sdl: `
			type Query {
				hello: String!
			}

			extend type Query {
				world: String!
			}`,
			validateSchema: func(s *ast.Schema) error {
				typ, ok := s.Types["Query"].(*ast.ObjectTypeDefinition)
				if !ok {
					return fmt.Errorf("type %q not found", "Query")
				}

				helloField := typ.Fields.Get("hello")
				if helloField == nil {
					return fmt.Errorf("field %q not found", "hello")
				}
				if helloField.Type.String() != "String!" {
					return fmt.Errorf("field %q has an invalid type: %q", "hello", helloField.Type.String())
				}

				worldField := typ.Fields.Get("world")
				if worldField == nil {
					return fmt.Errorf("field %q not found", "world")
				}
				if worldField.Type.String() != "String!" {
					return fmt.Errorf("field %q has an invalid type: %q", "world", worldField.Type.String())
				}
				return nil
			},
		},
		{
			name: "Extend schema",
			sdl: `
			schema {
				query: Query
			}
			type Query {
				hello: String!
			}
			extend schema {
				mutation: Mutation
			}
			type Mutation {
				concat(a: String!, b: String!): String!
			}
			`,
			validateSchema: func(s *ast.Schema) error {
				typq, ok := s.Types["Query"].(*ast.ObjectTypeDefinition)
				if !ok {
					return fmt.Errorf("type %q not found", "Query")
				}
				helloField := typq.Fields.Get("hello")
				if helloField == nil {
					return fmt.Errorf("field %q not found", "hello")
				}
				if helloField.Type.String() != "String!" {
					return fmt.Errorf("field %q has an invalid type: %q", "hello", helloField.Type.String())
				}

				typm, ok := s.Types["Mutation"].(*ast.ObjectTypeDefinition)
				if !ok {
					return fmt.Errorf("type %q not found", "Mutation")
				}
				concatField := typm.Fields.Get("concat")
				if concatField == nil {
					return fmt.Errorf("field %q not found", "concat")
				}
				if concatField.Type.String() != "String!" {
					return fmt.Errorf("field %q has an invalid type: %q", "concat", concatField.Type.String())
				}
				if len(concatField.Arguments) != 2 || concatField.Arguments[0] == nil || concatField.Arguments[1] == nil || concatField.Arguments[0].Type.String() != "String!" || concatField.Arguments[1].Type.String() != "String!" {
					return fmt.Errorf("field %q has an invalid args: %+v", "concat", concatField.Arguments)
				}
				return nil
			},
		},
		{
			name: "Extend type with interface implementation",
			sdl: `
			interface Named {
				name: String!
			}
			type Product {
				id: ID!
			}
			extend type Product implements Named {
				name: String!
			}`,
			validateSchema: func(s *ast.Schema) error {
				typ, ok := s.Types["Product"].(*ast.ObjectTypeDefinition)
				if !ok {
					return fmt.Errorf("type %q not found", "Product")
				}
				idField := typ.Fields.Get("id")
				if idField == nil {
					return fmt.Errorf("field %q not found", "id")
				}
				if idField.Type.String() != "ID!" {
					return fmt.Errorf("field %q has an invalid type: %q", "id", idField.Type.String())
				}
				nameField := typ.Fields.Get("name")
				if nameField == nil {
					return fmt.Errorf("field %q not found", "name")
				}
				if nameField.Type.String() != "String!" {
					return fmt.Errorf("field %q has an invalid type: %q", "name", nameField.Type.String())
				}

				ifc, ok := s.Types["Named"].(*ast.InterfaceTypeDefinition)
				if !ok {
					return fmt.Errorf("type %q not found", "Named")
				}
				nameField = ifc.Fields.Get("name")
				if nameField == nil {
					return fmt.Errorf("field %q not found", "name")
				}
				if nameField.Type.String() != "String!" {
					return fmt.Errorf("field %q has an invalid type: %q", "name", nameField.Type.String())
				}
				return nil
			},
		},
		{
			name: "Extend union type",
			sdl: `
			type Named {
				name: String!
			}
			type Numbered {
				num: Int!
			}
			union Item = Named | Numbered
			type Coloured {
				Colour: String!
			}
			extend union Item = Coloured
			`,
			validateSchema: func(s *ast.Schema) error {
				typ, ok := s.Types["Item"].(*ast.Union)
				if !ok {
					return fmt.Errorf("type %q not found", "Item")
				}
				if len(typ.UnionMemberTypes) != 3 {
					return fmt.Errorf("Expected 3 possible types, but instead got %d types", len(typ.UnionMemberTypes))
				}
				posible := map[string]struct{}{
					"Coloured": {},
					"Named":    {},
					"Numbered": {},
				}
				for _, pt := range typ.UnionMemberTypes {
					if _, ok := posible[pt.Name]; !ok {
						return fmt.Errorf("Unexpected possible type %q", pt.Name)
					}
				}
				return nil
			},
		},
		{
			name: "Extend enum type",
			sdl: `
			enum Currencies{
				AUD
				USD
				EUR
			}
			extend enum Currencies {
				BGN
				GBP
			}
			`,
			validateSchema: func(s *ast.Schema) error {
				typ, ok := s.Types["Currencies"].(*ast.EnumTypeDefinition)
				if !ok {
					return fmt.Errorf("enum %q not found", "Currencies")
				}
				if len(typ.EnumValuesDefinition) != 5 {
					return fmt.Errorf("Expected 5 enum values, but instead got %d types", len(typ.EnumValuesDefinition))
				}
				posible := map[string]struct{}{
					"AUD": {},
					"USD": {},
					"EUR": {},
					"BGN": {},
					"GBP": {},
				}
				for _, v := range typ.EnumValuesDefinition {
					if _, ok := posible[v.EnumValue]; !ok {
						return fmt.Errorf("Unexpected enum value %q", v.EnumValue)
					}
				}
				return nil
			},
		},
		{
			name: "Extend incompatible type",
			sdl: `
			type Query {
				hello: String!
			}

			extend interface Query {
				name: String!
			}`,
			validateError: func(err error) error {
				msg := `trying to extend type "OBJECT" with type "INTERFACE"`
				if err == nil || err.Error() != msg {
					return fmt.Errorf("expected error %q, but got %q", msg, err)
				}
				return nil
			},
		},
		{
			name: "Extend type already implements an interface",
			sdl: `
			interface Named {
				name: String!
			}
			type Product implements Named {
				id: ID!
				name: String!
			}
			extend type Product implements Named {
			}`,
			validateError: func(err error) error {
				msg := `interface "Named" implemented in the extension is already implemented in "Product"`
				if err == nil || err.Error() != msg {
					return fmt.Errorf("expected error %q, but got %q", msg, err)
				}
				return nil
			},
		},
		{
			name: "Extend union already contains type",
			sdl: `
			type Named {
				name: String!
			}
			type Numbered {
				num: Int!
			}
			union Item = Named | Numbered
			type Coloured {
				Colour: String!
			}
			extend union Item = Coloured | Named
			`,
			validateError: func(err error) error {
				msg := `union type "Named" already declared in "Item"`
				if err == nil || err.Error() != msg {
					return fmt.Errorf("expected error %q, but got %q", msg, err)
				}
				return nil
			},
		},
		{
			name: "Extend union contains type",
			sdl: `
			type Named {
				name: String!
			}
			type Numbered {
				num: Int!
			}
			union Item = Named | Numbered

			type Coloured {
				Colour: String!
			}
			
			extend union Item = Coloured
			`,
			validateSchema: func(s *ast.Schema) error {
				typ, ok := s.Types["Item"].(*ast.Union)
				if !ok {
					return fmt.Errorf("type %q not found", "Item")
				}
				if len(typ.UnionMemberTypes) != 3 {
					return fmt.Errorf("Expected 3 possible types, but instead got %d types", len(typ.UnionMemberTypes))
				}
				posible := map[string]struct{}{
					"Coloured": {},
					"Named":    {},
					"Numbered": {},
				}
				for _, pt := range typ.UnionMemberTypes {
					if _, ok := posible[pt.Name]; !ok {
						return fmt.Errorf("Unexpected possible type %q", pt.Name)
					}
				}
				return nil
			},
		},
		{
			name: "Extend input",
			sdl: `
			input Product {
				id: ID!
				name: String!
			}
			extend input Product {
				category: Category!
				tags: [String!]! = ["sale", "shoes"]
			}
			input Category {
				id: ID!
				name: String!
			}
			`,
			validateSchema: func(s *ast.Schema) error {
				typ, ok := s.Types["Product"].(*ast.InputObject)
				if !ok {
					return fmt.Errorf("type %q not found", "Product")
				}
				if len(typ.Values) != 4 {
					return fmt.Errorf("Expected 4 fields, but instead got %d types", len(typ.Values))
				}
				posible := map[string]struct{}{
					"id":       {},
					"name":     {},
					"category": {},
					"tags":     {},
				}
				for _, pt := range typ.Values {
					if _, ok := posible[pt.Name.Name]; !ok {
						return fmt.Errorf("Unexpected possible type %q", pt.Name)
					}
				}
				categoryField := typ.Values.Get("category")
				if categoryField == nil {
					return fmt.Errorf("field %q not found", "category")
				}
				if categoryField.Type.String() != "Category!" {
					return fmt.Errorf("expected type %q, but got %q", "Category!", categoryField.Type.String())
				}
				if categoryField.Type.Kind() != "NON_NULL" {
					return fmt.Errorf("expected kind %q, but got %q", "NON_NULL", categoryField.Type.Kind())
				}
				return nil
			},
		},
		{
			name: "Extend enum value already exists",
			sdl: `
			enum Currencies{
				AUD
				USD
				EUR
			}
			extend enum Currencies {
				AUD
			}`,
			validateError: func(err error) error {
				msg := `enum value "AUD" already declared in "Currencies"`
				if err == nil || err.Error() != msg {
					return fmt.Errorf("expected error %q, but got %q", msg, err)
				}
				return nil
			},
		},
		{
			name: "Extend input field already exists",
			sdl: `
			input Product{
				name: String!
			}
			extend input Product {
				name: String!
			}`,
			validateError: func(err error) error {
				msg := `extended field {"name" {'\x06' '\x05'}} already exists`
				if err == nil || err.Error() != msg {
					return fmt.Errorf("expected error %q, but got %q", msg, err)
				}
				return nil
			},
		},
		{
			name: "Extend field already exists",
			sdl: `
			interface Named {
				name: String!
			}
			type Product implements Named {
				id: ID!
				name: String!
			}
			extend type Product {
				name: String!
			}`,
			validateError: func(err error) error {
				msg := `extended field "name" already exists`
				if err == nil || err.Error() != msg {
					return fmt.Errorf("expected error %q, but got %q", msg, err)
				}
				return nil
			},
		},
		{
			name: "Extend interface type",
			sdl: `
			interface Product {
				id: ID!
				name: String!
			}
			extend interface Product {
				category: String!
			}
			`,
			validateSchema: func(s *ast.Schema) error {
				typ, ok := s.Types["Product"].(*ast.InterfaceTypeDefinition)
				if !ok {
					return fmt.Errorf("type %q not found", "Product")
				}
				if len(typ.Fields) != 3 {
					return fmt.Errorf("Expected 3 fields, but instead got %d types", len(typ.Fields))
				}
				fields := map[string]struct{}{
					"id":       {},
					"name":     {},
					"category": {},
				}
				for _, f := range typ.Fields {
					if _, ok := fields[f.Name]; !ok {
						return fmt.Errorf("Unexpected field %q", f.Name)
					}
				}
				return nil
			},
		},
		{
			name: "Extend unknown type",
			sdl: `
			extend type User {
				name: String!
			}
			`,
			validateError: func(err error) error {
				msg := `trying to extend unknown type "User"`
				if err == nil || err.Error() != msg {
					return fmt.Errorf("expected error %q, but got %q", msg, err)
				}
				return nil
			},
		},
		{
			name: "Extend invalid syntax",
			sdl: `
			extend invalid Node {
				id: ID!
			}
			`,
			validateError: func(err error) error {
				msg := `graphql: syntax error: unexpected "invalid", expecting "schema", "type", "enum", "interface", "union" or "input" (line 2, column 19)`
				if err == nil || err.Error() != msg {
					return fmt.Errorf("expected error %q, but got %q", msg, err)
				}
				return nil
			},
		},
		{
			name: "Parses directives",
			sdl: `
			directive @objectdirective on OBJECT
			directive @fielddirective on FIELD_DEFINITION
			directive @enumdirective on ENUM
			directive @uniondirective on UNION
			directive @directive on SCALAR
				| OBJECT
				| FIELD_DEFINITION
				| ARGUMENT_DEFINITION
				| INTERFACE
				| UNION
				| ENUM
				| ENUM_VALUE
				| INPUT_OBJECT
				| INPUT_FIELD_DEFINITION
			directive @repeatabledirective repeatable on SCALAR

			interface NamedEntity @directive { name: String }

			scalar Time @directive

			type Photo @objectdirective {
				id: ID! @deprecated @fielddirective
			}

			type Person implements NamedEntity @objectdirective {
				name: String
			}

			enum Direction @enumdirective {
				NORTH @deprecated
				EAST
				SOUTH
				WEST
			}

			union Union @uniondirective = Photo | Person

			scalar Mass @repeatabledirective @repeatabledirective
			`,
			validateSchema: func(s *ast.Schema) error {
				namedEntityDirectives := s.Types["NamedEntity"].(*ast.InterfaceTypeDefinition).Directives
				if len(namedEntityDirectives) != 1 || namedEntityDirectives[0].Name.Name != "directive" {
					return fmt.Errorf("missing directive on NamedEntity interface, expected @directive but got %v", namedEntityDirectives)
				}

				timeDirectives := s.Types["Time"].(*ast.ScalarTypeDefinition).Directives
				if len(timeDirectives) != 1 || timeDirectives[0].Name.Name != "directive" {
					return fmt.Errorf("missing directive on Time scalar, expected @directive but got %v", timeDirectives)
				}

				photo := s.Types["Photo"].(*ast.ObjectTypeDefinition)
				photoDirectives := photo.Directives
				if len(photoDirectives) != 1 || photoDirectives[0].Name.Name != "objectdirective" {
					return fmt.Errorf("missing directive on Time scalar, expected @objectdirective but got %v", photoDirectives)
				}
				if len(photo.Fields.Get("id").Directives) != 2 {
					return fmt.Errorf("expected Photo.id to have 2 directives but got %v", photoDirectives)
				}

				directionDirectives := s.Types["Direction"].(*ast.EnumTypeDefinition).Directives
				if len(directionDirectives) != 1 || directionDirectives[0].Name.Name != "enumdirective" {
					return fmt.Errorf("missing directive on Direction enum, expected @enumdirective but got %v", directionDirectives)
				}

				unionDirectives := s.Types["Union"].(*ast.Union).Directives
				if len(unionDirectives) != 1 || unionDirectives[0].Name.Name != "uniondirective" {
					return fmt.Errorf("missing directive on Union union, expected @uniondirective but got %v", unionDirectives)
				}

				massDirectives := s.Types["Mass"].(*ast.ScalarTypeDefinition).Directives
				if len(massDirectives) != 2 || massDirectives[0].Name.Name != "repeatabledirective" || massDirectives[1].Name.Name != "repeatabledirective" {
					return fmt.Errorf("missing directive on Repeatable scalar, expected @repeatabledirective @repeatabledirective but got %v", massDirectives)
				}
				return nil
			},
		},
		{
			name: "Sets Directive.Repeatable if `repeatable` keyword is given",
			sdl: `
			directive @nonrepeatabledirective on SCALAR
			directive @repeatabledirective repeatable on SCALAR
			`,
			validateSchema: func(s *ast.Schema) error {
				if dir := s.Directives["nonrepeatabledirective"]; dir.Repeatable {
					return fmt.Errorf("did not expect directive to be repeatable: %v", dir)
				}
				if dir := s.Directives["repeatabledirective"]; !dir.Repeatable {
					return fmt.Errorf("expected directive to be repeatable: %v", dir)
				}
				return nil
			},
		},
		{
			name: "Directive definition does not allow double-`repeatable`",
			sdl: `
			directive @mydirective repeatable repeatable SCALAR
			scalar MyScalar @mydirective
			`,
			validateError: func(err error) error {
				msg := `graphql: syntax error: unexpected "repeatable", expecting "on" (line 2, column 38)`
				if err == nil || err.Error() != msg {
					return fmt.Errorf("expected error %q, but got %q", msg, err)
				}
				return nil
			},
		},
		{
			name: "Directive definition does not allow double-`on` instead of `repeatable on`",
			sdl: `
			directive @mydirective on on SCALAR
			scalar MyScalar @mydirective
			`,
			validateError: func(err error) error {
				prefix := `graphql: syntax error: "on" is not a legal directive location`
				if err == nil || !strings.HasPrefix(err.Error(), prefix) {
					return fmt.Errorf("expected error starting with %q, but got %q", prefix, err)
				}
				return nil
			},
		},
		{
			name: "Disallow repeat of a directive if it is not `repeatable`",
			sdl: `
			directive @nonrepeatabledirective on FIELD_DEFINITION
			type Foo {
				bar: String @nonrepeatabledirective @nonrepeatabledirective
			}
			`,
			validateError: func(err error) error {
				prefix := `graphql: non repeatable directive "nonrepeatabledirective" can not be repeated. Consider adding "repeatable"`
				if err == nil || !strings.HasPrefix(err.Error(), prefix) {
					return fmt.Errorf("expected error starting with %q, but got %q", prefix, err)
				}
				return nil
			},
		},
		{
			name: "Decorating scalar with an undeclared directive should return an error",
			sdl: `
			scalar S @undeclareddirective
			`,
			validateError: func(err error) error {
				prefix := `graphql: directive "undeclareddirective" not found`
				if err == nil || !strings.HasPrefix(err.Error(), prefix) {
					return fmt.Errorf("expected error starting with %q, but got %q", prefix, err)
				}
				return nil
			},
		},
		{
			name: "Decorating argument with an undeclared directive should return an error",
			sdl: `
			type Query {
				hello(name: String! @undeclareddirective): String!
			}
			`,
			validateError: func(err error) error {
				prefix := `graphql: directive "undeclareddirective" not found`
				if err == nil || !strings.HasPrefix(err.Error(), prefix) {
					return fmt.Errorf("expected error starting with %q, but got %q", prefix, err)
				}
				return nil
			},
		},
		{
			name: "Decorating input object with an undeclared directive should return an error",
			sdl: `
			input InputObject @undeclareddirective{field: String!}
			`,
			validateError: func(err error) error {
				prefix := `graphql: directive "undeclareddirective" not found`
				if err == nil || !strings.HasPrefix(err.Error(), prefix) {
					return fmt.Errorf("expected error starting with %q, but got %q", prefix, err)
				}
				return nil
			},
		},
		{
			name: "Decorating interface with an undeclared directive should return an error",
			sdl: `
			interface I @undeclareddirective {field: String!}
			`,
			validateError: func(err error) error {
				prefix := `graphql: directive "undeclareddirective" not found`
				if err == nil || !strings.HasPrefix(err.Error(), prefix) {
					return fmt.Errorf("expected error starting with %q, but got %q", prefix, err)
				}
				return nil
			},
		},
	} {
		t.Run(test.name, func(t *testing.T) {
			s, err := schema.ParseSchema(test.sdl, test.useStringDescriptions)
			if err != nil {
				if test.validateError == nil {
					t.Fatal(err)
				}
				if err2 := test.validateError(err); err2 != nil {
					t.Fatal(err2)
				}
			}
			if test.validateSchema != nil {
				if err := test.validateSchema(s); err != nil {
					t.Fatal(err)
				}
			}
		})
	}
}

func TestInterfaceImplementsInterface(t *testing.T) {
	for _, tt := range []struct {
		name                  string
		sdl                   string
		useStringDescriptions bool
		validateError         func(err error) error
		validateSchema        func(s *ast.Schema) error
	}{
		{
			name: "Parses interface implementing other interface",
			sdl: `
			interface Foo {
				field: String!
			}
			interface Bar implements Foo {
				field: String!
			}
			`,
			validateSchema: func(s *ast.Schema) error {
				const implementedInterfaceName = "Bar"
				typ, ok := s.Types[implementedInterfaceName].(*ast.InterfaceTypeDefinition)
				if !ok {
					return fmt.Errorf("interface %q not found", implementedInterfaceName)
				}
				if len(typ.Fields) != 1 {
					return fmt.Errorf("invalid number of fields: want %d, have %d", 1, len(typ.Fields))
				}
				const fieldName = "field"

				if typ.Fields[0].Name != fieldName {
					return fmt.Errorf("field %q not found", fieldName)
				}

				if len(typ.Interfaces) != 1 {
					return fmt.Errorf("invalid number of implementing interfaces found on %q: want %d, have %d", implementedInterfaceName, 1, len(typ.Interfaces))
				}

				const implementingInterfaceName = "Foo"
				if typ.Interfaces[0].Name != implementingInterfaceName {
					return fmt.Errorf("interface %q not found", implementingInterfaceName)
				}

				return nil
			},
		},
		{
			name: "Parses interface transitively implementing an interface that implements an interface",
			sdl: `
			interface Foo {
				field: String!
			}
			interface Bar implements Foo {
				field: String!
			}
			interface Baz implements Bar & Foo {
				field: String!
			}
			`,
			validateSchema: func(s *ast.Schema) error {
				const implementedInterfaceName = "Baz"
				typ, ok := s.Types[implementedInterfaceName].(*ast.InterfaceTypeDefinition)
				if !ok {
					return fmt.Errorf("interface %q not found", implementedInterfaceName)
				}
				if len(typ.Fields) != 1 {
					return fmt.Errorf("invalid number of fields: want %d, have %d", 1, len(typ.Fields))
				}
				const fieldName = "field"

				if typ.Fields[0].Name != fieldName {
					return fmt.Errorf("field %q not found", fieldName)
				}

				if len(typ.Interfaces) != 2 {
					return fmt.Errorf("invalid number of implementing interfaces found on %q: want %d, have %d", implementedInterfaceName, 2, len(typ.Interfaces))
				}

				const firstImplementingInterfaceName = "Bar"
				if typ.Interfaces[0].Name != firstImplementingInterfaceName {
					return fmt.Errorf("first interface %q not found", firstImplementingInterfaceName)
				}

				const secondImplementingInterfaceName = "Foo"
				if typ.Interfaces[1].Name != secondImplementingInterfaceName {
					return fmt.Errorf("second interface %q not found", secondImplementingInterfaceName)
				}

				return nil
			},
		},
		{
			name: "Transitively implemented interfaces must also be defined on an implementing type or interface",
			sdl: `
			interface A {
				message: String!
			}
			interface B implements A {
				message: String!
				name: String!
			}
			interface C implements B {
				message: String!
				name: String!
				hug: Boolean!
			}
			`,
			validateError: func(err error) error {
				msg := `graphql: interface "C" must explicitly implement transitive interface "A"`
				if err == nil || err.Error() != msg {
					return fmt.Errorf("expected error %q, but got %q", msg, err)
				}
				return nil
			},
		},
		{
			name: "Unions can be defined with a leading pipe",
			sdl: `
			type Named {
				name: String!
			}
			type Numbered {
				num: Int!
			}
			union Item1 =
				| Named
				| Numbered
			union Item2 = | Named | Numbered
			`,
			validateSchema: func(s *ast.Schema) error {
				for _, itemName := range []string{"Item1", "Item2"} {
					typ, ok := s.Types[itemName].(*ast.Union)
					if !ok {
						return fmt.Errorf("type %q not found", "Item")
					}
					if len(typ.UnionMemberTypes) != 2 {
						return fmt.Errorf("Expected 2 possible types, but instead got %d types", len(typ.UnionMemberTypes))
					}
					posible := map[string]struct{}{
						"Named":    {},
						"Numbered": {},
					}
					for _, pt := range typ.UnionMemberTypes {
						if _, ok := posible[pt.Name]; !ok {
							return fmt.Errorf("Unexpected possible type %q", pt.Name)
						}
					}
				}
				return nil
			},
		},
		{
			name: "Accepts object implementing interface with additional optional argument",
			sdl: `
			interface Node {
				name(format: String!): String!
			}
			type User implements Node {
				name(format: String!, locale: String): String!
			}
			`,
			validateSchema: func(s *ast.Schema) error {
				return nil
			},
		},
		{
			name: "Rejects object implementing interface missing required interface argument",
			sdl: `
			interface Node {
				name(format: String!): String!
			}
			type User implements Node {
				name: String!
			}
			`,
			validateError: func(err error) error {
				if err == nil {
					return fmt.Errorf("want error, have <nil>")
				}
				if want, have := `expects argument "format"`, err.Error(); !strings.Contains(have, want) {
					return fmt.Errorf("unexpected error: want %q to contain %q", have, want)
				}
				return nil
			},
		},
		{
			name: "Rejects object implementing interface with incompatible argument type",
			sdl: `
			interface Node {
				name(format: String!): String!
			}
			type User implements Node {
				name(format: Int!): String!
			}
			`,
			validateError: func(err error) error {
				if err == nil {
					return fmt.Errorf("want error, have <nil>")
				}
				if want, have := `argument "format" has type "String!"`, err.Error(); !strings.Contains(have, want) {
					return fmt.Errorf("unexpected error: want %q to contain %q", have, want)
				}
				if want, have := `defines type "Int!"`, err.Error(); !strings.Contains(have, want) {
					return fmt.Errorf("unexpected error: want %q to contain %q", have, want)
				}
				return nil
			},
		},
		{
			name: "Rejects object implementing interface with additional required argument",
			sdl: `
			interface Node {
				name(format: String!): String!
			}
			type User implements Node {
				name(format: String!, locale: String!): String!
			}
			`,
			validateError: func(err error) error {
				if err == nil {
					return fmt.Errorf("want error, have <nil>")
				}
				if want, have := `additional argument "locale"`, err.Error(); !strings.Contains(have, want) {
					return fmt.Errorf("unexpected error: want %q to contain %q", have, want)
				}
				if want, have := `must not be required`, err.Error(); !strings.Contains(have, want) {
					return fmt.Errorf("unexpected error: want %q to contain %q", have, want)
				}
				return nil
			},
		},
		{
			name: "Rejects interface implementing interface with additional required argument",
			sdl: `
			interface Node {
				name(format: String!): String!
			}
			interface Entity implements Node {
				name(format: String!, locale: String!): String!
			}
			`,
			validateError: func(err error) error {
				if err == nil {
					return fmt.Errorf("want error, have <nil>")
				}
				if want, have := `implementing interface "Entity"`, err.Error(); !strings.Contains(have, want) {
					return fmt.Errorf("unexpected error: want %q to contain %q", have, want)
				}
				if want, have := `must not be required`, err.Error(); !strings.Contains(have, want) {
					return fmt.Errorf("unexpected error: want %q to contain %q", have, want)
				}
				return nil
			},
		},
		{
			name: "Parses valid OneOf input type with nullable fields",
			sdl: `
			input FindUserInput @oneOf {
				id: ID
				email: String
			}
			type Query {
				findUser(by: FindUserInput): String
			}
			`,
			validateSchema: func(s *ast.Schema) error {
				typ, ok := s.Types["FindUserInput"].(*ast.InputObject)
				if !ok {
					return fmt.Errorf("type %q not found", "FindUserInput")
				}
				if len(typ.Values) != 2 {
					return fmt.Errorf("expected 2 fields, got %d", len(typ.Values))
				}
				if typ.Directives.Get("oneOf") == nil {
					return fmt.Errorf("expected @oneOf directive on FindUserInput")
				}
				return nil
			},
		},
		{
			name: "Rejects OneOf input type with non-nullable field",
			sdl: `
			input FindUserInput @oneOf {
				id: ID!
				email: String
			}
			type Query {
				findUser(by: FindUserInput): String
			}
			`,
			validateError: func(err error) error {
				if err == nil {
					return fmt.Errorf("want error, have <nil>")
				}
				if want, have := `OneOf input field FindUserInput.id must be nullable`, err.Error(); !strings.Contains(have, want) {
					return fmt.Errorf("unexpected error: want %q to contain %q", have, want)
				}
				return nil
			},
		},
		{
			name: "Rejects OneOf input type with field default value",
			sdl: `
			input FindUserInput @oneOf {
				id: ID
				priority: Int = 1
			}
			type Query {
				findUser(by: FindUserInput): String
			}
			`,
			validateError: func(err error) error {
				if err == nil {
					return fmt.Errorf("want error, have <nil>")
				}
				if want, have := `OneOf input field FindUserInput.priority cannot have a default value`, err.Error(); !strings.Contains(have, want) {
					return fmt.Errorf("unexpected error: want %q to contain %q", have, want)
				}
				return nil
			},
		},
		{
			name: "Rejects @oneOf directive on non-input type",
			sdl: `
			type Query @oneOf {
				field: String
			}
			`,
			validateError: func(err error) error {
				if err == nil {
					return fmt.Errorf("want error, have <nil>")
				}
				if strings.Contains(err.Error(), "@oneOf") && strings.Contains(err.Error(), "INPUT_OBJECT") {
					return nil
				}
				return fmt.Errorf("unexpected error: want error about @oneOf only on INPUT_OBJECT, have %q", err.Error())
			},
		},
		{
			name: "Accepts object implementing interface where both field and interface field are deprecated",
			sdl: `
			interface Node {
				id: ID! @deprecated(reason: "use newId")
			}
			type User implements Node {
				id: ID! @deprecated(reason: "use newId")
			}
			`,
			validateSchema: func(s *ast.Schema) error {
				return nil
			},
		},
		{
			name: "Accepts object implementing interface where interface field is deprecated and implementing field is not",
			sdl: `
			interface Node {
				id: ID! @deprecated(reason: "old")
			}
			type User implements Node {
				id: ID!
			}
			`,
			validateSchema: func(s *ast.Schema) error {
				return nil
			},
		},
		{
			name: "Rejects object implementing interface where interface field is not deprecated but implementing field is",
			sdl: `
			interface Node {
				id: ID!
			}
			type User implements Node {
				id: ID! @deprecated(reason: "use newId")
			}
			`,
			validateError: func(err error) error {
				msg := `graphql: interface "Node" field "id" is not deprecated but implementing type "User" marks it as deprecated`
				if err == nil || err.Error() != msg {
					return fmt.Errorf("expected error %q, but got %q", msg, err)
				}
				return nil
			},
		},
		{
			name: "Rejects interface implementing interface where interface field is not deprecated but implementing interface field is",
			sdl: `
			interface Node {
				id: ID!
			}
			interface Entity implements Node {
				id: ID! @deprecated(reason: "use newId")
			}
			`,
			validateError: func(err error) error {
				msg := `graphql: interface "Node" field "id" is not deprecated but implementing interface "Entity" marks it as deprecated`
				if err == nil || err.Error() != msg {
					return fmt.Errorf("expected error %q, but got %q", msg, err)
				}
				return nil
			},
		},
	} {
		t.Run(tt.name, func(t *testing.T) {
			s, err := schema.ParseSchema(tt.sdl, tt.useStringDescriptions)
			if err != nil {
				if tt.validateError == nil {
					t.Fatal(err)
				}
				if err2 := tt.validateError(err); err2 != nil {
					t.Fatal(err2)
				}
			}
			if tt.validateSchema != nil {
				if err2 := tt.validateSchema(s); err2 != nil {
					t.Fatal(err2)
				}
			}
		})
	}
}
