//                           _       _
// __      _____  __ ___   ___  __ _| |_ ___
// \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \
//  \ V  V /  __/ (_| |\ V /| | (_| | ||  __/
//   \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___|
//
//  Copyright © 2016 - 2024 Weaviate B.V. All rights reserved.
//
//  CONTACT: hello@weaviate.io
//

package local

import (
	"fmt"
	"runtime/debug"
	"testing"

	logrus "github.com/sirupsen/logrus/hooks/test"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/tailor-inc/graphql"
	"github.com/weaviate/weaviate/entities/models"
	"github.com/weaviate/weaviate/entities/schema"
	"github.com/weaviate/weaviate/usecases/config"
	usecaseModules "github.com/weaviate/weaviate/usecases/modules"
)

// These tests are component tests for the local package including all its
// subpackages, such as get, getmeta, etc.. However, they only assert that the
// graphql tree can be built under certain circumstances. This helps us to
// catch errors on edge cases like empty schemas, classes with empty
// properties, empty peer lists, peers with empty schemas, etc. However, we
// don't get any guarantee of whether the individual queries resolve
// correctly. For those cases we have unit tests in die individual subpackages
// (i.e. get, getmeta, aggregate, etc.).  Additionally we have (a few) e2e
// tests.

func TestBuild_GraphQLNetwork(t *testing.T) {
	tests := testCases{
		// This tests asserts that an action-only schema doesn't lead to errors.
		testCase{
			name: "with only objects locally",
			localSchema: schema.Schema{
				Objects: &models.Schema{
					Classes: []*models.Class{
						{
							Class: "BestLocalAction",
							Properties: []*models.Property{
								{
									DataType:     schema.DataTypeText.PropString(),
									Name:         "myStringProp",
									Tokenization: models.PropertyTokenizationWhitespace,
								},
							},
						},
					},
				},
			},
		},

		// This tests asserts that a things-only schema doesn't lead to errors.
		testCase{
			name: "with only objects locally",
			localSchema: schema.Schema{
				Objects: &models.Schema{
					Classes: []*models.Class{
						{
							Class: "BestLocalThing",
							Properties: []*models.Property{
								{
									DataType:     schema.DataTypeText.PropString(),
									Name:         "myStringProp",
									Tokenization: models.PropertyTokenizationWhitespace,
								},
							},
						},
					},
				},
			},
		},

		// // This tests asserts that a class without any properties doesn't lead to
		// // errors.
		testCase{
			name: "with things without properties locally",
			localSchema: schema.Schema{
				Objects: &models.Schema{
					Classes: []*models.Class{
						{
							Class:      "BestLocalThing",
							Properties: []*models.Property{},
						},
					},
				},
			},
		},

		testCase{
			name:        "without any peers",
			localSchema: validSchema(),
		},
	}

	tests.AssertNoError(t)
}

func TestBuild_RefProps(t *testing.T) {
	t.Run("expected error logs", func(t *testing.T) {
		tests := testCases{
			{
				name: "build class with nonexistent ref prop",
				localSchema: schema.Schema{
					Objects: &models.Schema{
						Classes: []*models.Class{
							{
								Class: "ThisClassExists",
								Properties: []*models.Property{
									{
										DataType: []string{"ThisClassDoesNotExist"},
										Name:     "ofNonexistentClass",
									},
								},
							},
						},
					},
				},
			},
		}

		expectedLogMsg := "ignoring ref prop \"ofNonexistentClass\" on class \"ThisClassExists\", " +
			"because it contains reference to nonexistent class [\"ThisClassDoesNotExist\"]"

		tests.AssertErrorLogs(t, expectedLogMsg)
	})

	t.Run("expected success", func(t *testing.T) {
		tests := testCases{
			{
				name: "build class with existing non-circular ref prop",
				localSchema: schema.Schema{
					Objects: &models.Schema{
						Classes: []*models.Class{
							{
								Class: "ThisClassExists",
								Properties: []*models.Property{
									{
										DataType: []string{"ThisClassAlsoExists"},
										Name:     "ofExistingClass",
									},
								},
							},
							{
								Class: "ThisClassAlsoExists",
								Properties: []*models.Property{
									{
										DataType:     schema.DataTypeText.PropString(),
										Name:         "stringProp",
										Tokenization: models.PropertyTokenizationWhitespace,
									},
								},
							},
						},
					},
				},
			},
			{
				name: "build class with existing circular ref prop",
				localSchema: schema.Schema{
					Objects: &models.Schema{
						Classes: []*models.Class{
							{
								Class: "ThisClassExists",
								Properties: []*models.Property{
									{
										DataType: []string{"ThisClassAlsoExists"},
										Name:     "ofExistingClass",
									},
								},
							},
							{
								Class: "ThisClassAlsoExists",
								Properties: []*models.Property{
									{
										DataType: []string{"ThisClassExists"},
										Name:     "ofExistingClass",
									},
								},
							},
						},
					},
				},
			},
		}

		tests.AssertNoError(t)
	})
}

type testCase struct {
	name        string
	localSchema schema.Schema
}

type testCases []testCase

func (tests testCases) AssertNoError(t *testing.T) {
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			logger, _ := logrus.NewNullLogger()
			modules := usecaseModules.NewProvider(logger)
			localSchema, err := Build(&test.localSchema, nil, config.Config{}, modules, nil)
			require.Nil(t, err, test.name)

			schemaObject := graphql.ObjectConfig{
				Name:        "WeaviateObj",
				Description: "Location of the root query",
				Fields:      localSchema,
			}

			func() {
				defer func() {
					if r := recover(); r != nil {
						err = fmt.Errorf("%v at %s", r, debug.Stack())
					}
				}()

				_, err = graphql.NewSchema(graphql.SchemaConfig{
					Query: graphql.NewObject(schemaObject),
				})
			}()

			assert.Nil(t, err, test.name)
		})
	}
}

// AssertErrorLogs still expects the test to pass without errors,
// but does expect the Build logger to contain errors messages
// from the GQL schema rebuilding thunk
func (tests testCases) AssertErrorLogs(t *testing.T, expectedMsg string) {
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			logger, logsHook := logrus.NewNullLogger()
			modules := usecaseModules.NewProvider(logger)
			localSchema, err := Build(&test.localSchema, logger, config.Config{}, modules, nil)
			require.Nil(t, err, test.name)

			schemaObject := graphql.ObjectConfig{
				Name:        "WeaviateObj",
				Description: "Location of the root query",
				Fields:      localSchema,
			}

			func() {
				defer func() {
					if r := recover(); r != nil {
						err = fmt.Errorf("%v at %s", r, debug.Stack())
					}
				}()

				_, err = graphql.NewSchema(graphql.SchemaConfig{
					Query: graphql.NewObject(schemaObject),
				})
			}()

			last := logsHook.LastEntry()
			assert.Contains(t, last.Message, expectedMsg)
			assert.Nil(t, err)
		})
	}
}

func validSchema() schema.Schema {
	return schema.Schema{
		Objects: &models.Schema{
			Classes: []*models.Class{
				{
					Class: "BestLocalThing",
					Properties: []*models.Property{
						{
							DataType:     schema.DataTypeText.PropString(),
							Name:         "myStringProp",
							Tokenization: models.PropertyTokenizationWhitespace,
						},
					},
				},
			},
		},
	}
}
