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

package objects

import (
	"context"
	"strings"
	"testing"

	"github.com/go-openapi/strfmt"
	"github.com/google/uuid"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/sirupsen/logrus/hooks/test"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"

	"github.com/weaviate/weaviate/entities/models"
	"github.com/weaviate/weaviate/entities/schema"
	"github.com/weaviate/weaviate/entities/vectorindex/hnsw"
	"github.com/weaviate/weaviate/usecases/auth/authorization/mocks"
	"github.com/weaviate/weaviate/usecases/config"
	"github.com/weaviate/weaviate/usecases/config/runtime"
)

func Test_Add_Object_WithNoVectorizerModule(t *testing.T) {
	var (
		vectorRepo      *fakeVectorRepo
		modulesProvider *fakeModulesProvider
		manager         *Manager
		schemaManager   *fakeSchemaManager
		authorizer      *mocks.FakeAuthorizer
	)

	sch := schema.Schema{
		Objects: &models.Schema{
			Classes: []*models.Class{
				{
					Class:             "Foo",
					Vectorizer:        config.VectorizerModuleNone,
					VectorIndexConfig: hnsw.UserConfig{},
				},
				{
					Class:      "FooSkipped",
					Vectorizer: config.VectorizerModuleNone,
					VectorIndexConfig: hnsw.UserConfig{
						Skip: true,
					},
				},
			},
		},
	}

	resetAutoSchema := func(autoSchemaEnabled bool) {
		vectorRepo = &fakeVectorRepo{}
		vectorRepo.On("PutObject", mock.Anything, mock.Anything).Return(nil).Once()
		schemaManager = &fakeSchemaManager{
			GetSchemaResponse: sch,
		}
		cfg := &config.WeaviateConfig{
			Config: config.Config{
				AutoSchema: config.AutoSchema{
					Enabled:       runtime.NewDynamicValue(autoSchemaEnabled),
					DefaultString: schema.DataTypeText.String(),
				},
			},
		}
		authorizer = mocks.NewMockAuthorizer()
		logger, _ := test.NewNullLogger()

		modulesProvider = getFakeModulesProvider()
		metrics := &fakeMetrics{}
		manager = NewManager(schemaManager, cfg, logger, authorizer,
			vectorRepo, modulesProvider, metrics, nil,
			NewAutoSchemaManager(schemaManager, vectorRepo, cfg, authorizer, logger, prometheus.NewPedanticRegistry()))
	}

	reset := func() {
		resetAutoSchema(false)
	}

	t.Run("without an id set", func(t *testing.T) {
		reset()

		ctx := context.Background()
		class := &models.Object{
			Vector: []float32{0.1, 0.2, 0.3},
			Class:  "Foo",
		}

		modulesProvider.On("UpdateVector", mock.Anything, mock.AnythingOfType(FindObjectFn)).
			Return(nil, nil)

		res, err := manager.AddObject(ctx, nil, class, nil)
		require.Nil(t, err)
		uuidDuringCreation := vectorRepo.Mock.Calls[0].Arguments.Get(0).(*models.Object).ID

		assert.Len(t, uuidDuringCreation, 36, "check that a uuid was assigned")
		assert.Equal(t, uuidDuringCreation, res.ID, "check that connector add ID and user response match")
	})

	t.Run("with an explicit (correct) ID set", func(t *testing.T) {
		reset()

		ctx := context.Background()
		id := strfmt.UUID("5a1cd361-1e0d-42ae-bd52-ee09cb5f31cc")
		object := &models.Object{
			Vector: []float32{0.1, 0.2, 0.3},
			ID:     id,
			Class:  "Foo",
		}
		vectorRepo.On("Exists", "Foo", id).Return(false, nil).Once()
		modulesProvider.On("UpdateVector", mock.Anything, mock.AnythingOfType(FindObjectFn)).
			Return(nil, nil)

		res, err := manager.AddObject(ctx, nil, object, nil)
		require.Nil(t, err)
		uuidDuringCreation := vectorRepo.Mock.Calls[1].Arguments.Get(0).(*models.Object).ID

		assert.Equal(t, id, uuidDuringCreation, "check that a uuid is the user specified one")
		assert.Equal(t, res.ID, uuidDuringCreation, "check that connector add ID and user response match")
	})

	t.Run("with an explicit (correct) uppercase id set", func(t *testing.T) {
		reset()

		ctx := context.Background()
		id := strfmt.UUID("4A334D0B-6347-40A0-A5AE-339677B20EDE")
		lowered := strfmt.UUID(strings.ToLower(id.String()))
		object := &models.Object{
			ID:     id,
			Class:  "Foo",
			Vector: []float32{0.1, 0.2, 0.3},
		}
		vectorRepo.On("Exists", "Foo", lowered).Return(false, nil).Once()
		modulesProvider.On("UpdateVector", mock.Anything, mock.AnythingOfType(FindObjectFn)).
			Return(nil, nil)

		res, err := manager.AddObject(ctx, nil, object, nil)
		require.Nil(t, err)
		assert.Equal(t, res.ID, lowered, "check that id was lowered and added")
	})

	t.Run("with an explicit (correct) ID set and a property that doesn't exist", func(t *testing.T) {
		resetAutoSchema(true)

		ctx := context.Background()
		id := strfmt.UUID("5aaad361-1e0d-42ae-bb52-ee09cb5f31cc")
		object := &models.Object{
			Vector: []float32{0.1, 0.2, 0.3},
			ID:     id,
			Class:  "Foo",
			Properties: map[string]interface{}{
				"newProperty": "string value",
			},
		}
		vectorRepo.On("Exists", "Foo", id).Return(false, nil).Once()
		modulesProvider.On("UpdateVector", mock.Anything, mock.AnythingOfType(FindObjectFn)).
			Return(nil, nil)

		res, err := manager.AddObject(ctx, nil, object, nil)
		require.Nil(t, err)
		uuidDuringCreation := vectorRepo.Mock.Calls[1].Arguments.Get(0).(*models.Object).ID

		assert.Equal(t, id, uuidDuringCreation, "check that a uuid is the user specified one")
		assert.Equal(t, res.ID, uuidDuringCreation, "check that connector add ID and user response match")
	})

	t.Run("with a uuid that's already taken", func(t *testing.T) {
		reset()

		ctx := context.Background()
		id := strfmt.UUID("5a1cd361-1e0d-42ae-bd52-ee09cb5f31cc")
		class := &models.Object{
			ID:    id,
			Class: "Foo",
		}

		vectorRepo.On("Exists", "Foo", id).Return(true, nil).Once()
		modulesProvider.On("UpdateVector", mock.Anything, mock.AnythingOfType(FindObjectFn)).
			Return(nil, nil)

		_, err := manager.AddObject(ctx, nil, class, nil)
		assert.Equal(t, NewErrInvalidUserInput("id '%s' already exists", id), err)
	})

	t.Run("with a uuid that's malformed", func(t *testing.T) {
		reset()

		ctx := context.Background()
		id := strfmt.UUID("5a1cd361-1e0d-4fooooooo2ae-bd52-ee09cb5f31cc")
		class := &models.Object{
			ID:    id,
			Class: "Foo",
		}

		vectorRepo.On("Exists", "Foo", id).Return(false, nil).Once()

		_, err := manager.AddObject(ctx, nil, class, nil)
		assert.Equal(t, NewErrInvalidUserInput("invalid object: invalid UUID length: %d", len(id)), err)
	})

	t.Run("without a vector", func(t *testing.T) {
		// Note that this was an invalid case before v1.10 which added this
		// functionality, as part of
		// https://github.com/weaviate/weaviate/issues/1800
		reset()

		ctx := context.Background()
		class := &models.Object{
			Class: "Foo",
		}

		modulesProvider.On("UpdateVector", mock.Anything, mock.AnythingOfType(FindObjectFn)).
			Return(nil, nil)

		_, err := manager.AddObject(ctx, nil, class, nil)
		assert.Nil(t, err)
	})

	t.Run("resolve alias before rbac check", func(t *testing.T) {
		// This test is to make sure alias is resolved to correct
		// collection before doing RBAC check on original class during add object.

		reset()
		ctx := context.Background()
		alias := "FooAlias"
		class := "Foo"

		obj := &models.Object{
			Class: alias, // via alias
		}

		schemaManager.resolveAliasTo = class
		modulesProvider.On("UpdateVector", mock.Anything, mock.AnythingOfType(FindObjectFn)).
			Return(nil, nil)

		_, err := manager.AddObject(ctx, nil, obj, nil)
		require.NoError(t, err)
		assert.Len(t, authorizer.Calls(), 1)
		assert.Contains(t, authorizer.Calls()[0].Resources[0], class) // make sure rbac is called with "resolved class" name
	})

	t.Run("without a vector, but indexing skipped", func(t *testing.T) {
		reset()

		ctx := context.Background()
		class := &models.Object{
			Class: "FooSkipped",
		}

		modulesProvider.On("UpdateVector", mock.Anything, mock.AnythingOfType(FindObjectFn)).
			Return(nil, nil)

		_, err := manager.AddObject(ctx, nil, class, nil)
		assert.Nil(t, err)
	})
}

func Test_Add_Object_Uses_Max_SchemaVersion_For_Write_With_AutoTenant(t *testing.T) {
	const autoSchemaVersion uint64 = 41
	const tenantVersion uint64 = 42

	sch := schema.Schema{Objects: &models.Schema{Classes: []*models.Class{{
		Class: "FooMT", Vectorizer: config.VectorizerModuleNone, VectorIndexConfig: hnsw.UserConfig{},
		MultiTenancyConfig: &models.MultiTenancyConfig{Enabled: true, AutoTenantCreation: true},
	}}}}

	vectorRepo := &fakeVectorRepo{}
	vectorRepo.On("PutObject", mock.Anything, mock.Anything).Return(nil).Once()
	schemaManager := &fakeSchemaManager{GetSchemaResponse: sch, AddTenantsSchemaVersion: tenantVersion, AutoSchemaVersion: autoSchemaVersion}
	cfg := &config.WeaviateConfig{Config: config.Config{AutoSchema: config.AutoSchema{Enabled: runtime.NewDynamicValue(true)}}}
	authorizer := mocks.NewMockAuthorizer()
	logger, _ := test.NewNullLogger()
	modulesProvider := getFakeModulesProvider()
	metrics := &fakeMetrics{}
	manager := NewManager(schemaManager, cfg, logger, authorizer, vectorRepo, modulesProvider, metrics, nil,
		NewAutoSchemaManager(schemaManager, vectorRepo, cfg, authorizer, logger, prometheus.NewPedanticRegistry()))

	modulesProvider.On("UpdateVector", mock.Anything, mock.AnythingOfType(FindObjectFn)).Return(nil, nil)

	_, err := manager.AddObject(context.Background(), nil, &models.Object{Class: "FooMT", Tenant: "t1"}, nil)
	require.NoError(t, err)

	assert.Equal(t, tenantVersion, vectorRepo.CapturedSchemaVersion)
}

func Test_Add_Object_WithExternalVectorizerModule(t *testing.T) {
	var (
		vectorRepo      *fakeVectorRepo
		modulesProvider *fakeModulesProvider
		manager         *Manager
	)

	schema := schema.Schema{
		Objects: &models.Schema{
			Classes: []*models.Class{
				{
					Class:             "Foo",
					Vectorizer:        config.VectorizerModuleText2VecContextionary,
					VectorIndexConfig: hnsw.UserConfig{},
				},
			},
		},
	}

	reset := func() {
		vectorRepo = &fakeVectorRepo{}
		vectorRepo.On("PutObject", mock.Anything, mock.Anything).Return(nil).Once()
		schemaManager := &fakeSchemaManager{
			GetSchemaResponse: schema,
		}
		cfg := &config.WeaviateConfig{}
		authorizer := mocks.NewMockAuthorizer()
		logger, _ := test.NewNullLogger()
		metrics := &fakeMetrics{}
		modulesProvider = getFakeModulesProvider()
		modulesProvider.On("UsingRef2Vec", mock.Anything).Return(false)
		manager = NewManager(schemaManager, cfg, logger, authorizer,
			vectorRepo, modulesProvider, metrics, nil,
			NewAutoSchemaManager(schemaManager, vectorRepo, cfg, authorizer, logger, prometheus.NewPedanticRegistry()))
	}

	t.Run("without an id set", func(t *testing.T) {
		reset()

		ctx := context.Background()
		object := &models.Object{
			Class: "Foo",
		}

		modulesProvider.On("UpdateVector", mock.Anything, mock.AnythingOfType(FindObjectFn)).
			Return(nil, nil)

		res, err := manager.AddObject(ctx, nil, object, nil)
		require.Nil(t, err)

		uuidDuringCreation := vectorRepo.Mock.Calls[0].Arguments.Get(0).(*models.Object).ID

		assert.Len(t, uuidDuringCreation, 36, "check that a uuid was assigned")
		assert.Equal(t, uuidDuringCreation, res.ID, "check that connector add ID and user response match")
	})

	t.Run("with an explicit (correct) ID set", func(t *testing.T) {
		reset()

		ctx := context.Background()
		id := strfmt.UUID("5a1cd361-1e0d-42ae-bd52-ee09cb5f31cc")
		object := &models.Object{
			ID:    id,
			Class: "Foo",
		}
		vectorRepo.On("Exists", "Foo", id).Return(false, nil).Once()
		modulesProvider.On("UpdateVector", mock.Anything, mock.AnythingOfType(FindObjectFn)).
			Return(nil, nil)

		res, err := manager.AddObject(ctx, nil, object, nil)
		uuidDuringCreation := vectorRepo.Mock.Calls[1].Arguments.Get(0).(*models.Object).ID

		assert.Nil(t, err)
		assert.Equal(t, id, uuidDuringCreation, "check that a uuid is the user specified one")
		assert.Equal(t, res.ID, uuidDuringCreation, "check that connector add ID and user response match")
	})

	t.Run("with a uuid that's already taken", func(t *testing.T) {
		reset()

		ctx := context.Background()
		id := strfmt.UUID("5a1cd361-1e0d-42ae-bd52-ee09cb5f31cc")
		object := &models.Object{
			ID:    id,
			Class: "Foo",
		}

		vectorRepo.On("Exists", "Foo", id).Return(true, nil).Once()
		modulesProvider.On("UpdateVector", mock.Anything, mock.AnythingOfType(FindObjectFn)).
			Return(nil, nil)

		_, err := manager.AddObject(ctx, nil, object, nil)
		assert.Equal(t, NewErrInvalidUserInput("id '%s' already exists", id), err)
	})

	t.Run("with a uuid that's malformed", func(t *testing.T) {
		reset()

		ctx := context.Background()
		id := strfmt.UUID("5a1cd361-1e0d-4f00000002ae-bd52-ee09cb5f31cc")
		object := &models.Object{
			ID:    id,
			Class: "Foo",
		}

		vectorRepo.On("Exists", "Foo", id).Return(false, nil).Once()
		modulesProvider.On("UpdateVector", mock.Anything, mock.AnythingOfType(FindObjectFn)).
			Return(nil, nil)

		_, err := manager.AddObject(ctx, nil, object, nil)
		assert.Equal(t, NewErrInvalidUserInput("invalid object: invalid UUID length: %d", len(id)), err)
	})
}

func Test_Add_Object_OverrideVectorizer(t *testing.T) {
	var (
		vectorRepo      *fakeVectorRepo
		modulesProvider *fakeModulesProvider
		manager         *Manager
	)

	schema := schema.Schema{
		Objects: &models.Schema{
			Classes: []*models.Class{
				{
					Class:             "FooOverride",
					Vectorizer:        config.VectorizerModuleText2VecContextionary,
					VectorIndexConfig: hnsw.UserConfig{},
				},
			},
		},
	}

	reset := func() {
		vectorRepo = &fakeVectorRepo{}
		vectorRepo.On("PutObject", mock.Anything, mock.Anything).Return(nil).Once()
		schemaManager := &fakeSchemaManager{
			GetSchemaResponse: schema,
		}
		cfg := &config.WeaviateConfig{}
		authorizer := mocks.NewMockAuthorizer()
		logger, _ := test.NewNullLogger()
		modulesProvider = getFakeModulesProvider()
		metrics := &fakeMetrics{}
		manager = NewManager(schemaManager, cfg, logger,
			authorizer, vectorRepo, modulesProvider, metrics, nil,
			NewAutoSchemaManager(schemaManager, vectorRepo, cfg, authorizer, logger, prometheus.NewPedanticRegistry()))
	}

	t.Run("overriding the vector by explicitly specifying it", func(t *testing.T) {
		reset()

		ctx := context.Background()
		object := &models.Object{
			Class:  "FooOverride",
			Vector: []float32{9, 9, 9},
		}

		modulesProvider.On("UpdateVector", mock.Anything, mock.AnythingOfType(FindObjectFn)).
			Return(object.Vector, nil)

		_, err := manager.AddObject(ctx, nil, object, nil)
		require.Nil(t, err)

		vec := vectorRepo.Mock.Calls[0].Arguments.Get(1).([]float32)

		assert.Equal(t, []float32{9, 9, 9}, vec, "check that vector was overridden")
	})
}

func Test_AddObjectEmptyProperties(t *testing.T) {
	var (
		vectorRepo      *fakeVectorRepo
		modulesProvider *fakeModulesProvider
		manager         *Manager
	)
	schema := schema.Schema{
		Objects: &models.Schema{
			Classes: []*models.Class{
				{
					Class:             "TestClass",
					VectorIndexConfig: hnsw.UserConfig{},

					Properties: []*models.Property{
						{
							Name:         "strings",
							DataType:     schema.DataTypeTextArray.PropString(),
							Tokenization: models.PropertyTokenizationWhitespace,
						},
					},
				},
			},
		},
	}
	reset := func() {
		vectorRepo = &fakeVectorRepo{}
		vectorRepo.On("PutObject", mock.Anything, mock.Anything).Return(nil).Once()
		schemaManager := &fakeSchemaManager{
			GetSchemaResponse: schema,
		}
		cfg := &config.WeaviateConfig{}
		authorizer := mocks.NewMockAuthorizer()
		logger, _ := test.NewNullLogger()
		modulesProvider = getFakeModulesProvider()
		metrics := &fakeMetrics{}
		manager = NewManager(schemaManager, cfg, logger,
			authorizer, vectorRepo, modulesProvider, metrics, nil,
			NewAutoSchemaManager(schemaManager, vectorRepo, cfg, authorizer, logger, prometheus.NewPedanticRegistry()))
	}
	reset()
	ctx := context.Background()
	object := &models.Object{
		Class:  "TestClass",
		Vector: []float32{9, 9, 9},
	}
	assert.Nil(t, object.Properties)
	modulesProvider.On("UpdateVector", mock.Anything, mock.AnythingOfType(FindObjectFn)).
		Return(nil, nil)
	addedObject, err := manager.AddObject(ctx, nil, object, nil)
	assert.Nil(t, err)
	assert.NotNil(t, addedObject.Properties)
}

func Test_AddObjectWithUUIDProps(t *testing.T) {
	var (
		vectorRepo      *fakeVectorRepo
		modulesProvider *fakeModulesProvider
		manager         *Manager
	)
	schema := schema.Schema{
		Objects: &models.Schema{
			Classes: []*models.Class{
				{
					Class:             "TestClass",
					VectorIndexConfig: hnsw.UserConfig{},

					Properties: []*models.Property{
						{
							Name:     "my_id",
							DataType: []string{"uuid"},
						},
						{
							Name:     "my_idz",
							DataType: []string{"uuid[]"},
						},
					},
				},
			},
		},
	}
	reset := func() {
		vectorRepo = &fakeVectorRepo{}
		vectorRepo.On("PutObject", mock.Anything, mock.Anything).Return(nil).Once()
		schemaManager := &fakeSchemaManager{
			GetSchemaResponse: schema,
		}
		cfg := &config.WeaviateConfig{}
		authorizer := mocks.NewMockAuthorizer()
		logger, _ := test.NewNullLogger()
		modulesProvider = getFakeModulesProvider()
		metrics := &fakeMetrics{}
		manager = NewManager(schemaManager, cfg, logger,
			authorizer, vectorRepo, modulesProvider, metrics, nil,
			NewAutoSchemaManager(schemaManager, vectorRepo, cfg, authorizer, logger, prometheus.NewPedanticRegistry()),
		)
	}
	reset()
	ctx := context.Background()
	object := &models.Object{
		Class:  "TestClass",
		Vector: []float32{9, 9, 9},
		Properties: map[string]interface{}{
			"my_id":  "28bafa1e-7956-4c58-8a02-4499a9d15253",
			"my_idz": []any{"28bafa1e-7956-4c58-8a02-4499a9d15253"},
		},
	}
	modulesProvider.On("UpdateVector", mock.Anything, mock.AnythingOfType(FindObjectFn)).
		Return(nil, nil)
	addedObject, err := manager.AddObject(ctx, nil, object, nil)
	require.Nil(t, err)
	require.NotNil(t, addedObject.Properties)

	expectedID := uuid.MustParse("28bafa1e-7956-4c58-8a02-4499a9d15253")
	expectedIDz := []uuid.UUID{uuid.MustParse("28bafa1e-7956-4c58-8a02-4499a9d15253")}

	assert.Equal(t, expectedID, addedObject.Properties.(map[string]interface{})["my_id"])
	assert.Equal(t, expectedIDz, addedObject.Properties.(map[string]interface{})["my_idz"])
}
