package federation

import (
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/99designs/gqlgen/codegen"
	"github.com/99designs/gqlgen/codegen/config"
	"github.com/99designs/gqlgen/plugin/federation/fieldset"
)

func TestNew(t *testing.T) {
	t.Parallel()

	t.Run("success_no_options", func(t *testing.T) {
		cfg := &config.Config{
			Federation: config.PackageConfig{
				Options: map[string]bool{},
			},
		}
		plugin, err := New(1, cfg)
		require.NoError(t, err)
		assert.NotNil(t, plugin)
	})

	t.Run("success_computed", func(t *testing.T) {
		cfg := &config.Config{
			Federation: config.PackageConfig{
				Version: 2,
				Options: map[string]bool{
					"computed_requires": true,
				},
			},
			CallArgumentDirectivesWithNull: true,
		}
		plugin, err := New(1, cfg)
		require.NoError(t, err)
		assert.NotNil(t, plugin)
	})

	t.Run("error_computed_verion_1", func(t *testing.T) {
		cfg := &config.Config{
			Federation: config.PackageConfig{
				Version: 1,
				Options: map[string]bool{
					"computed_requires": true,
				},
			},
			CallArgumentDirectivesWithNull: true,
		}
		plugin, err := New(1, cfg)
		require.Error(t, err)
		assert.Nil(t, plugin)
	})

	t.Run("error_computed_CallArgumentDirectivesWithNull_is_false", func(t *testing.T) {
		cfg := &config.Config{
			Federation: config.PackageConfig{
				Version: 2,
				Options: map[string]bool{
					"computed_requires": true,
				},
			},
			CallArgumentDirectivesWithNull: false,
		}
		plugin, err := New(1, cfg)
		require.Error(t, err)
		assert.Nil(t, plugin)
	})

	t.Run("error_both_explicit_and_computed_set", func(t *testing.T) {
		cfg := &config.Config{
			Federation: config.PackageConfig{
				Version: 2,
				Options: map[string]bool{
					"explicit_requires": true,
					"computed_requires": true,
				},
			},
		}

		plugin, err := New(1, cfg)
		require.Error(t, err)
		assert.Nil(t, plugin)
	})
}

func TestWithEntities(t *testing.T) {
	f, cfg := load(t, "testdata/allthethings/gqlgen.yml")

	require.Equal(t, []string{"ExternalExtension", "Hello", "MoreNesting", "MultiHelloMultiKey", "NestedKey", "VeryNestedKey", "World"}, cfg.Schema.Types["_Entity"].Types)

	require.Len(t, cfg.Schema.Types["Entity"].Fields, 8)

	require.Equal(t, "findExternalExtensionByUpc", cfg.Schema.Types["Entity"].Fields[0].Name)
	require.Equal(t, "findHelloByName", cfg.Schema.Types["Entity"].Fields[1].Name)
	// missing on purpose: all @external fields:
	// require.Equal(t, "findMoreNestingByID", cfg.Schema.Types["Entity"].Fields[2].Name)
	require.Equal(t, "findManyMultiHelloMultiKeyByNames", cfg.Schema.Types["Entity"].Fields[2].Name)
	require.Equal(t, "findManyMultiHelloMultiKeyByKey2s", cfg.Schema.Types["Entity"].Fields[3].Name)
	require.Equal(t, "findNestedKeyByIDAndHelloName", cfg.Schema.Types["Entity"].Fields[4].Name)
	require.Equal(t, "findVeryNestedKeyByIDAndHelloNameAndWorldFooAndWorldBarAndMoreWorldFoo", cfg.Schema.Types["Entity"].Fields[5].Name)
	require.Equal(t, "findWorldByFoo", cfg.Schema.Types["Entity"].Fields[6].Name)
	require.Equal(t, "findWorldByBar", cfg.Schema.Types["Entity"].Fields[7].Name)

	require.NoError(t, f.MutateConfig(cfg))

	require.Len(t, f.Entities, 7)

	require.Equal(t, "ExternalExtension", f.Entities[0].Name)
	require.Len(t, f.Entities[0].Resolvers, 1)
	require.Len(t, f.Entities[0].Resolvers[0].KeyFields, 1)
	require.Equal(t, "upc", f.Entities[0].Resolvers[0].KeyFields[0].Definition.Name)
	require.Equal(t, "String", f.Entities[0].Resolvers[0].KeyFields[0].Definition.Type.Name())

	require.Equal(t, "Hello", f.Entities[1].Name)
	require.Len(t, f.Entities[1].Resolvers, 1)
	require.Len(t, f.Entities[1].Resolvers[0].KeyFields, 1)
	require.Equal(t, "name", f.Entities[1].Resolvers[0].KeyFields[0].Definition.Name)
	require.Equal(t, "String", f.Entities[1].Resolvers[0].KeyFields[0].Definition.Type.Name())

	require.Equal(t, "MoreNesting", f.Entities[2].Name)
	require.Empty(t, f.Entities[2].Resolvers)

	require.Equal(t, "MultiHelloMultiKey", f.Entities[3].Name)
	require.Len(t, f.Entities[3].Resolvers, 2)
	require.Len(t, f.Entities[3].Resolvers[0].KeyFields, 1)
	require.Len(t, f.Entities[3].Resolvers[1].KeyFields, 1)
	require.Equal(t, "name", f.Entities[3].Resolvers[0].KeyFields[0].Definition.Name)
	require.Equal(t, "String", f.Entities[3].Resolvers[0].KeyFields[0].Definition.Type.Name())
	require.Equal(t, "key2", f.Entities[3].Resolvers[1].KeyFields[0].Definition.Name)
	require.Equal(t, "String", f.Entities[3].Resolvers[1].KeyFields[0].Definition.Type.Name())

	require.Equal(t, "NestedKey", f.Entities[4].Name)
	require.Len(t, f.Entities[4].Resolvers, 1)
	require.Len(t, f.Entities[4].Resolvers[0].KeyFields, 2)
	require.Equal(t, "id", f.Entities[4].Resolvers[0].KeyFields[0].Definition.Name)
	require.Equal(t, "String", f.Entities[4].Resolvers[0].KeyFields[0].Definition.Type.Name())
	require.Equal(t, "helloName", f.Entities[4].Resolvers[0].KeyFields[1].Definition.Name)
	require.Equal(t, "String", f.Entities[4].Resolvers[0].KeyFields[1].Definition.Type.Name())

	require.Equal(t, "VeryNestedKey", f.Entities[5].Name)
	require.Len(t, f.Entities[5].Resolvers, 1)
	require.Len(t, f.Entities[5].Resolvers[0].KeyFields, 5)
	require.Equal(t, "id", f.Entities[5].Resolvers[0].KeyFields[0].Definition.Name)
	require.Equal(t, "String", f.Entities[5].Resolvers[0].KeyFields[0].Definition.Type.Name())
	require.Equal(t, "helloName", f.Entities[5].Resolvers[0].KeyFields[1].Definition.Name)
	require.Equal(t, "String", f.Entities[5].Resolvers[0].KeyFields[1].Definition.Type.Name())
	require.Equal(t, "worldFoo", f.Entities[5].Resolvers[0].KeyFields[2].Definition.Name)
	require.Equal(t, "String", f.Entities[5].Resolvers[0].KeyFields[2].Definition.Type.Name())
	require.Equal(t, "worldBar", f.Entities[5].Resolvers[0].KeyFields[3].Definition.Name)
	require.Equal(t, "Int", f.Entities[5].Resolvers[0].KeyFields[3].Definition.Type.Name())
	require.Equal(t, "moreWorldFoo", f.Entities[5].Resolvers[0].KeyFields[4].Definition.Name)
	require.Equal(t, "String", f.Entities[5].Resolvers[0].KeyFields[4].Definition.Type.Name())

	require.Len(t, f.Entities[5].Requires, 2)
	require.Equal(t, "id", f.Entities[5].Requires[0].Name)
	require.Equal(t, "helloSecondary", f.Entities[5].Requires[1].Name)

	require.Equal(t, "World", f.Entities[6].Name)
	require.Len(t, f.Entities[6].Resolvers, 2)
	require.Len(t, f.Entities[6].Resolvers[0].KeyFields, 1)
	require.Equal(t, "foo", f.Entities[6].Resolvers[0].KeyFields[0].Definition.Name)
	require.Equal(t, "String", f.Entities[6].Resolvers[0].KeyFields[0].Definition.Type.Name())
	require.Len(t, f.Entities[6].Resolvers[1].KeyFields, 1)
	require.Equal(t, "bar", f.Entities[6].Resolvers[1].KeyFields[0].Definition.Name)
	require.Equal(t, "Int", f.Entities[6].Resolvers[1].KeyFields[0].Definition.Type.Name())
}

func TestNoEntities(t *testing.T) {
	f, cfg := load(t, "testdata/entities/nokey.yml")

	err := f.MutateConfig(cfg)
	require.NoError(t, err)
	require.Empty(t, f.Entities)
}

func TestUnusedInterfaceKeyDirective(t *testing.T) {
	f, cfg := load(t, "testdata/interfaces/unused_key.yml")

	err := f.MutateConfig(cfg)
	require.NoError(t, err)
	require.Empty(t, f.Entities)
}

func TestInterfaceKeyDirective(t *testing.T) {
	f, cfg := load(t, "testdata/interfaces/key.yml")

	err := f.MutateConfig(cfg)
	require.NoError(t, err)
	require.Len(t, cfg.Schema.Types["_Entity"].Types, 1)
	require.Len(t, f.Entities, 2)
}

func TestInterfaceExtendsDirective(t *testing.T) {
	require.Panics(t, func() {
		load(t, "testdata/interfaces/extends.yml")
	})
}

func TestEntityInterfaces(t *testing.T) {
	f, cfg := load(t, "testdata/entityinterfaces/interface.yml")
	err := f.MutateConfig(cfg)

	require.NoError(t, err)
	require.Len(t, f.Entities[0].Resolvers, 1)
	require.Equal(t, "Hello", f.Entities[0].Name)
	require.NotEmpty(t, f.Entities[1].Resolvers)
}

func TestCodeGeneration(t *testing.T) {
	f, cfg := load(t, "testdata/allthethings/gqlgen.yml")

	require.Len(t, cfg.Schema.Types["_Entity"].Types, 7)
	require.Len(t, f.Entities, 7)

	require.NoError(t, f.MutateConfig(cfg))

	data, err := codegen.BuildData(cfg)
	require.NoError(t, err)
	require.NoError(t, f.GenerateCode(data))
}

func TestCodeGenerationFederation2(t *testing.T) {
	f, cfg := load(t, "testdata/federation2/federation2.yml")
	err := f.MutateConfig(cfg)

	require.NoError(t, err)
	require.Equal(t, "ExternalExtension", f.Entities[0].Name)
	require.Len(t, f.Entities[0].Resolvers, 1)
	require.Equal(t, "Hello", f.Entities[1].Name)
	require.Empty(t, f.Entities[1].Resolvers)
	require.Equal(t, "World", f.Entities[2].Name)
	require.Empty(t, f.Entities[2].Resolvers)

	data, err := codegen.BuildData(cfg)
	require.NoError(t, err)
	require.NoError(t, f.GenerateCode(data))
}

// This test is to ensure that the input arguments are not
// changed when cfg.OmitSliceElementPointers is false OR true
func TestMultiWithOmitSliceElemPointersCfg(t *testing.T) {
	staticRepsString := "reps: [HelloByNamesInput]!"
	t.Run("OmitSliceElementPointers true", func(t *testing.T) {
		f, cfg := load(t, "testdata/multi/multi.yml")
		cfg.OmitSliceElementPointers = true
		err := f.MutateConfig(cfg)
		require.NoError(t, err)
		require.Len(t, cfg.Schema.Types["_Entity"].Types, 1)
		require.Len(t, f.Entities, 1)

		entityGraphqlGenerated := false
		for _, source := range cfg.Sources {
			if source.Name != "federation/entity.graphql" {
				continue
			}
			entityGraphqlGenerated = true
			require.Contains(t, source.Input, staticRepsString)
		}
		require.True(t, entityGraphqlGenerated)
	})

	t.Run("OmitSliceElementPointers false", func(t *testing.T) {
		f, cfg := load(t, "testdata/multi/multi.yml")
		cfg.OmitSliceElementPointers = false
		err := f.MutateConfig(cfg)
		require.NoError(t, err)
		require.Len(t, cfg.Schema.Types["_Entity"].Types, 1)
		require.Len(t, f.Entities, 1)

		entityGraphqlGenerated := false
		for _, source := range cfg.Sources {
			if source.Name != "federation/entity.graphql" {
				continue
			}
			entityGraphqlGenerated = true
			require.Contains(t, source.Input, staticRepsString)
		}
		require.True(t, entityGraphqlGenerated)
	})
}

func TestHandlesRequiresArgumentCorrectlyIfNoSpace(t *testing.T) {
	requiresFieldSet := fieldset.New("foo bar baz(limit:4)", nil)
	assert.Len(t, requiresFieldSet, 3)
	assert.Equal(t, "Foo", requiresFieldSet[0].ToGo())
	assert.Equal(t, "Bar", requiresFieldSet[1].ToGo())
	assert.Equal(t, "Baz(limit:4)", requiresFieldSet[2].ToGo())
}

func TestHandlesArgumentGeneration(t *testing.T) {
	e := &Entity{
		Name:      "",
		Def:       nil,
		Resolvers: nil,
		Requires:  nil,
	}

	raw := "foo bar baz(limit:4)"
	requiresFieldSet := fieldset.New(raw, nil)
	for _, field := range requiresFieldSet {
		e.Requires = append(e.Requires, &Requires{
			Name:  field.ToGoPrivate(),
			Field: field,
		})
	}
}

func TestInjectSourceLate(t *testing.T) {
	_, cfg := load(t, "testdata/allthethings/gqlgen.yml")
	entityGraphqlGenerated := false
	for _, source := range cfg.Sources {
		if source.Name != "federation/entity.graphql" {
			continue
		}
		entityGraphqlGenerated = true
		require.Contains(t, source.Input, "union _Entity")
		require.Contains(t, source.Input, "type _Service {")
		require.Contains(t, source.Input, "extend type Query {")
		require.Contains(t, source.Input, "_entities(representations: [_Any!]!): [_Entity]!")
		require.Contains(t, source.Input, "_service: _Service!")
	}
	require.True(t, entityGraphqlGenerated)

	_, cfg = load(t, "testdata/entities/nokey.yml")
	entityGraphqlGenerated = false
	for _, source := range cfg.Sources {
		if source.Name != "federation/entity.graphql" {
			continue
		}
		entityGraphqlGenerated = true
		require.NotContains(t, source.Input, "union _Entity")
		require.Contains(t, source.Input, "type _Service {")
		require.Contains(t, source.Input, "extend type Query {")
		require.NotContains(t, source.Input, "_entities(representations: [_Any!]!): [_Entity]!")
		require.Contains(t, source.Input, "_service: _Service!")
	}
	require.True(t, entityGraphqlGenerated)

	_, cfg = load(t, "testdata/schema/customquerytype.yml")
	for _, source := range cfg.Sources {
		if source.Name != "federation/entity.graphql" {
			continue
		}
		require.Contains(t, source.Input, "extend type CustomQuery {")
	}
}

func load(t *testing.T, name string) (*Federation, *config.Config) {
	t.Helper()

	cfg, err := config.LoadConfig(name)
	require.NoError(t, err)

	if cfg.Federation.Version == 0 {
		cfg.Federation.Version = 1
	}

	f := &Federation{version: cfg.Federation.Version}
	s, err := f.InjectSourcesEarly()
	require.NoError(t, err)
	cfg.Sources = append(cfg.Sources, s...)
	require.NoError(t, cfg.LoadSchema())

	l, err := f.InjectSourcesLate(cfg.Schema)
	require.NoError(t, err)
	if l != nil {
		cfg.Sources = append(cfg.Sources, l...)
	}
	require.NoError(t, cfg.LoadSchema())

	require.NoError(t, cfg.Init())
	return f, cfg
}
