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

package test

import (
	"fmt"
	"testing"

	"github.com/google/uuid"

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

	"github.com/weaviate/weaviate/client/batch"

	"github.com/go-openapi/strfmt"
	"github.com/weaviate/weaviate/client/objects"
	clschema "github.com/weaviate/weaviate/client/schema"
	"github.com/weaviate/weaviate/entities/models"
	"github.com/weaviate/weaviate/entities/schema"
	"github.com/weaviate/weaviate/test/helper"
	testhelper "github.com/weaviate/weaviate/test/helper"
)

const (
	beaconStart = "weaviate://localhost/"
	pathStart   = "/v1/objects/"
)

func TestRefsWithTenantWithoutToClass(t *testing.T) {
	refToClassName := "ReferenceTo"
	refFromClassName := "ReferenceFrom"

	toParam := clschema.NewSchemaObjectsCreateParams().WithObjectClass(
		&models.Class{Class: refToClassName, MultiTenancyConfig: &models.MultiTenancyConfig{Enabled: true}},
	)
	respTo, err := helper.Client(t).Schema.SchemaObjectsCreate(toParam, nil)
	helper.AssertRequestOk(t, respTo, err, nil)

	fromParam := clschema.NewSchemaObjectsCreateParams().WithObjectClass(
		&models.Class{
			Class:              refFromClassName,
			MultiTenancyConfig: &models.MultiTenancyConfig{Enabled: true},
			Properties: []*models.Property{
				{
					DataType: []string{refToClassName},
					Name:     "ref",
				},
			},
		},
	)
	respFrom, err := helper.Client(t).Schema.SchemaObjectsCreate(fromParam, nil)
	helper.AssertRequestOk(t, respFrom, err, nil)

	defer deleteObjectClass(t, refToClassName)
	defer deleteObjectClass(t, refFromClassName)

	tenant := "tenant"
	tenants := make([]*models.Tenant, 1)
	for i := range tenants {
		tenants[i] = &models.Tenant{Name: tenant}
	}
	helper.CreateTenants(t, refToClassName, tenants)
	helper.CreateTenants(t, refFromClassName, tenants)

	refToId := strfmt.UUID(uuid.New().String())
	assertCreateObjectWithID(t, refToClassName, tenant, refToId, map[string]interface{}{})

	refFromId1 := strfmt.UUID(uuid.New().String())
	assertCreateObjectWithID(t, refFromClassName, tenant, refFromId1, map[string]interface{}{})

	// add reference between objects without to class name
	postRefParams := objects.NewObjectsClassReferencesCreateParams().
		WithID(refFromId1).
		WithPropertyName("ref").WithClassName(refFromClassName).
		WithBody(&models.SingleRef{
			Beacon: strfmt.URI(fmt.Sprintf(beaconStart+"%s", refToId.String())),
		}).WithTenant(&tenant)
	postRefResponse, err := helper.Client(t).Objects.ObjectsClassReferencesCreate(postRefParams, nil)
	helper.AssertRequestOk(t, postRefResponse, err, nil)

	// add reference from batch
	refFromId2 := strfmt.UUID(uuid.New().String())
	assertCreateObjectWithID(t, refFromClassName, tenant, refFromId2, map[string]interface{}{})

	// add refs without toClass
	batchRefs := []*models.BatchReference{
		{From: strfmt.URI(beaconStart + "ReferenceFrom/" + refFromId2 + "/ref"), To: strfmt.URI(beaconStart + refToId), Tenant: tenant},
	}
	postRefBatchParams := batch.NewBatchReferencesCreateParams().WithBody(batchRefs)
	postRefBatchResponse, err := helper.Client(t).Batch.BatchReferencesCreate(postRefBatchParams, nil)
	helper.AssertRequestOk(t, postRefBatchResponse, err, nil)
	require.Nil(t, postRefBatchResponse.Payload[0].Result.Errors)
}

func TestRefsWithoutToClass(t *testing.T) {
	params := clschema.NewSchemaObjectsCreateParams().WithObjectClass(&models.Class{Class: "ReferenceTo"})
	resp, err := helper.Client(t).Schema.SchemaObjectsCreate(params, nil)
	helper.AssertRequestOk(t, resp, err, nil)
	refToClassName := "ReferenceTo"
	refFromClassName := "ReferenceFrom"
	otherClassMT := "Other"

	paramsMT := clschema.NewSchemaObjectsCreateParams().WithObjectClass(
		&models.Class{
			Class:              otherClassMT,
			MultiTenancyConfig: &models.MultiTenancyConfig{Enabled: true},
			Properties: []*models.Property{
				{
					DataType: []string{refToClassName},
					Name:     "ref",
				},
			},
		},
	)
	respMT, err := helper.Client(t).Schema.SchemaObjectsCreate(paramsMT, nil)
	helper.AssertRequestOk(t, respMT, err, nil)

	tenant := "tenant"
	tenants := make([]*models.Tenant, 1)
	for i := range tenants {
		tenants[i] = &models.Tenant{Name: tenant}
	}
	helper.CreateTenants(t, otherClassMT, tenants)

	refFromClass := &models.Class{
		Class: refFromClassName,
		Properties: []*models.Property{
			{
				DataType: []string{refToClassName},
				Name:     "ref",
			},
		},
	}
	params2 := clschema.NewSchemaObjectsCreateParams().WithObjectClass(refFromClass)
	resp2, err := helper.Client(t).Schema.SchemaObjectsCreate(params2, nil)
	helper.AssertRequestOk(t, resp2, err, nil)

	defer deleteObjectClass(t, refToClassName)
	defer deleteObjectClass(t, refFromClassName)
	defer deleteObjectClass(t, otherClassMT)

	refToId := assertCreateObject(t, refToClassName, map[string]interface{}{})
	assertGetObjectWithClass(t, refToId, refToClassName)
	assertCreateObjectWithID(t, otherClassMT, tenant, refToId, map[string]interface{}{})
	refFromId := assertCreateObject(t, refFromClassName, map[string]interface{}{})
	assertGetObjectWithClass(t, refFromId, refFromClassName)

	postRefParams := objects.NewObjectsClassReferencesCreateParams().
		WithID(refFromId).
		WithPropertyName("ref").WithClassName(refFromClass.Class).
		WithBody(&models.SingleRef{
			Beacon: strfmt.URI(fmt.Sprintf(beaconStart+"%s", refToId.String())),
		})
	postRefResponse, err := helper.Client(t).Objects.ObjectsClassReferencesCreate(postRefParams, nil)
	helper.AssertRequestOk(t, postRefResponse, err, nil)

	// validate that ref was create for the correct class
	objWithRef := func() interface{} {
		obj := assertGetObjectWithClass(t, refFromId, refFromClassName)
		return obj.Properties
	}
	testhelper.AssertEventuallyEqual(t, map[string]interface{}{
		"ref": []interface{}{
			map[string]interface{}{
				"beacon": fmt.Sprintf(beaconStart+"%s/%s", refToClassName, refToId.String()),
				"href":   fmt.Sprintf(pathStart+"%s/%s", refToClassName, refToId.String()),
			},
		},
	}, objWithRef)

	// update prop with multiple references
	updateRefParams := objects.NewObjectsClassReferencesPutParams().
		WithID(refFromId).
		WithPropertyName("ref").WithClassName(refFromClass.Class).
		WithBody(models.MultipleRef{
			{Beacon: strfmt.URI(fmt.Sprintf(beaconStart+"%s", refToId.String()))},
			{Beacon: strfmt.URI(fmt.Sprintf(beaconStart+"%s/%s", refToClassName, refToId.String()))},
		})
	updateRefResponse, err := helper.Client(t).Objects.ObjectsClassReferencesPut(updateRefParams, nil)
	helper.AssertRequestOk(t, updateRefResponse, err, nil)

	objWithTwoRef := func() interface{} {
		obj := assertGetObjectWithClass(t, refFromId, refFromClassName)
		return obj.Properties
	}
	testhelper.AssertEventuallyEqual(t, map[string]interface{}{
		"ref": []interface{}{
			map[string]interface{}{
				"beacon": fmt.Sprintf(beaconStart+"%s/%s", refToClassName, refToId.String()),
				"href":   fmt.Sprintf(pathStart+"%s/%s", refToClassName, refToId.String()),
			},
			map[string]interface{}{
				"beacon": fmt.Sprintf(beaconStart+"%s/%s", refToClassName, refToId.String()),
				"href":   fmt.Sprintf(pathStart+"%s/%s", refToClassName, refToId.String()),
			},
		},
	}, objWithTwoRef)

	// delete reference without class
	deleteRefParams := objects.NewObjectsClassReferencesDeleteParams().
		WithID(refFromId).
		WithPropertyName("ref").WithClassName(refFromClass.Class).
		WithBody(&models.SingleRef{
			Beacon: strfmt.URI(fmt.Sprintf(beaconStart+"%s", refToId.String())),
		})
	deleteRefResponse, err := helper.Client(t).Objects.ObjectsClassReferencesDelete(deleteRefParams, nil)
	helper.AssertRequestOk(t, deleteRefResponse, err, nil)
	objWithoutRef := func() interface{} {
		obj := assertGetObjectWithClass(t, refFromId, refFromClassName)
		return obj.Properties
	}
	testhelper.AssertEventuallyEqual(t, map[string]interface{}{
		"ref": []interface{}{},
	}, objWithoutRef)
}

func TestRefsMultiTarget(t *testing.T) {
	refToClassName := "ReferenceTo"
	refFromClassName := "ReferenceFrom"
	defer deleteObjectClass(t, refToClassName)
	defer deleteObjectClass(t, refFromClassName)

	params := clschema.NewSchemaObjectsCreateParams().WithObjectClass(&models.Class{Class: refToClassName})
	resp, err := helper.Client(t).Schema.SchemaObjectsCreate(params, nil)
	helper.AssertRequestOk(t, resp, err, nil)

	refFromClass := &models.Class{
		Class: refFromClassName,
		Properties: []*models.Property{
			{
				DataType: []string{refToClassName, refFromClassName},
				Name:     "ref",
			},
		},
	}
	params2 := clschema.NewSchemaObjectsCreateParams().WithObjectClass(refFromClass)
	resp2, err := helper.Client(t).Schema.SchemaObjectsCreate(params2, nil)
	helper.AssertRequestOk(t, resp2, err, nil)

	refToId := assertCreateObject(t, refToClassName, map[string]interface{}{})
	assertGetObjectEventually(t, refToId)
	refFromId := assertCreateObject(t, refFromClassName, map[string]interface{}{})
	assertGetObjectEventually(t, refFromId)

	cases := []struct {
		classRef string
		id       string
	}{
		{classRef: "", id: refToId.String()},
		{classRef: refToClassName + "/", id: refToId.String()},
		{classRef: refFromClassName + "/", id: refFromId.String()},
	}
	for _, tt := range cases {
		postRefParams := objects.NewObjectsClassReferencesCreateParams().
			WithID(refFromId).
			WithPropertyName("ref").WithClassName(refFromClass.Class).
			WithBody(&models.SingleRef{
				Beacon: strfmt.URI(fmt.Sprintf(beaconStart+"%s%s", tt.classRef, tt.id)),
			})
		postRefResponse, err := helper.Client(t).Objects.ObjectsClassReferencesCreate(postRefParams, nil)
		helper.AssertRequestOk(t, postRefResponse, err, nil)

		// validate that ref was create for the correct class
		objWithRef := func() interface{} {
			obj := assertGetObjectWithClass(t, refFromId, refFromClassName)
			return obj.Properties
		}
		testhelper.AssertEventuallyEqual(t, map[string]interface{}{
			"ref": []interface{}{
				map[string]interface{}{
					"beacon": fmt.Sprintf(beaconStart+"%s%s", tt.classRef, tt.id),
					"href":   fmt.Sprintf(pathStart+"%s%s", tt.classRef, tt.id),
				},
			},
		}, objWithRef)

		// delete refs
		updateRefParams := objects.NewObjectsClassReferencesPutParams().
			WithID(refFromId).
			WithPropertyName("ref").WithClassName(refFromClass.Class).
			WithBody(models.MultipleRef{})
		updateRefResponse, err := helper.Client(t).Objects.ObjectsClassReferencesPut(updateRefParams, nil)
		helper.AssertRequestOk(t, updateRefResponse, err, nil)
	}
}

func TestBatchRefsMultiTarget(t *testing.T) {
	refToClassName := "ReferenceTo"
	refFromClassName := "ReferenceFrom"
	defer deleteObjectClass(t, refToClassName)
	defer deleteObjectClass(t, refFromClassName)

	params := clschema.NewSchemaObjectsCreateParams().WithObjectClass(&models.Class{Class: refToClassName})
	resp, err := helper.Client(t).Schema.SchemaObjectsCreate(params, nil)
	helper.AssertRequestOk(t, resp, err, nil)

	refFromClass := &models.Class{
		Class: refFromClassName,
		Properties: []*models.Property{
			{
				DataType: []string{refToClassName, refFromClassName},
				Name:     "ref",
			},
		},
	}
	params2 := clschema.NewSchemaObjectsCreateParams().WithObjectClass(refFromClass)
	resp2, err := helper.Client(t).Schema.SchemaObjectsCreate(params2, nil)
	helper.AssertRequestOk(t, resp2, err, nil)

	uuidsTo := make([]strfmt.UUID, 10)
	uuidsFrom := make([]strfmt.UUID, 10)
	for i := 0; i < 10; i++ {
		uuidsTo[i] = assertCreateObject(t, refToClassName, map[string]interface{}{})
		assertGetObjectEventually(t, uuidsTo[i])
		uuidsFrom[i] = assertCreateObject(t, refFromClassName, map[string]interface{}{})
		assertGetObjectEventually(t, uuidsFrom[i])
	}

	// add refs without toClass
	var batchRefs []*models.BatchReference
	for i := range uuidsFrom[:2] {
		from := beaconStart + "ReferenceFrom/" + uuidsFrom[i] + "/ref"
		to := beaconStart + uuidsTo[i]
		batchRefs = append(batchRefs, &models.BatchReference{From: strfmt.URI(from), To: strfmt.URI(to)})
	}

	// add refs with toClass target 1
	for i := range uuidsFrom[2:5] {
		j := i + 2
		from := beaconStart + "ReferenceFrom/" + uuidsFrom[j] + "/ref"
		to := beaconStart + "ReferenceTo/" + uuidsTo[j]
		batchRefs = append(batchRefs, &models.BatchReference{From: strfmt.URI(from), To: strfmt.URI(to)})
	}

	// add refs with toClass target 2
	for i := range uuidsFrom[5:] {
		j := i + 5
		from := beaconStart + "ReferenceFrom/" + uuidsFrom[j] + "/ref"
		to := beaconStart + "ReferenceFrom/" + uuidsTo[j]
		batchRefs = append(batchRefs, &models.BatchReference{From: strfmt.URI(from), To: strfmt.URI(to)})
	}

	postRefParams := batch.NewBatchReferencesCreateParams().WithBody(batchRefs)
	postRefResponse, err := helper.Client(t).Batch.BatchReferencesCreate(postRefParams, nil)
	helper.AssertRequestOk(t, postRefResponse, err, nil)

	// no autodetect for multi-target
	for i := range uuidsFrom[:2] {
		objWithRef := func() interface{} {
			obj := assertGetObjectWithClass(t, uuidsFrom[i], refFromClassName)
			return obj.Properties
		}
		testhelper.AssertEventuallyEqual(t, map[string]interface{}{
			"ref": []interface{}{
				map[string]interface{}{
					"beacon": fmt.Sprintf(beaconStart+"%s", uuidsTo[i].String()),
					"href":   fmt.Sprintf(pathStart+"%s", uuidsTo[i].String()),
				},
			},
		}, objWithRef)
	}

	// refs for target 1
	for i := range uuidsFrom[2:5] {
		j := i + 2
		objWithRef := func() interface{} {
			obj := assertGetObjectWithClass(t, uuidsFrom[j], refFromClassName)
			return obj.Properties
		}
		testhelper.AssertEventuallyEqual(t, map[string]interface{}{
			"ref": []interface{}{
				map[string]interface{}{
					"beacon": fmt.Sprintf(beaconStart+"%s/%s", refToClassName, uuidsTo[j].String()),
					"href":   fmt.Sprintf(pathStart+"%s/%s", refToClassName, uuidsTo[j].String()),
				},
			},
		}, objWithRef)
	}

	// refs for target 2
	for i := range uuidsFrom[5:] {
		j := i + 5
		objWithRef := func() interface{} {
			obj := assertGetObjectWithClass(t, uuidsFrom[j], refFromClassName)
			return obj.Properties
		}
		testhelper.AssertEventuallyEqual(t, map[string]interface{}{
			"ref": []interface{}{
				map[string]interface{}{
					"beacon": fmt.Sprintf(beaconStart+"%s/%s", refFromClassName, uuidsTo[j].String()),
					"href":   fmt.Sprintf(pathStart+"%s/%s", refFromClassName, uuidsTo[j].String()),
				},
			},
		}, objWithRef)
	}
}

func TestBatchRefsWithoutFromAndToClass(t *testing.T) {
	refToClassName := "ReferenceTo"
	refFromClassName := "ReferenceFrom"

	params := clschema.NewSchemaObjectsCreateParams().WithObjectClass(&models.Class{Class: refToClassName})
	resp, err := helper.Client(t).Schema.SchemaObjectsCreate(params, nil)
	helper.AssertRequestOk(t, resp, err, nil)

	refFromClass := &models.Class{
		Class: refFromClassName,
		Properties: []*models.Property{
			{
				DataType: []string{refToClassName},
				Name:     "ref",
			},
		},
	}
	params2 := clschema.NewSchemaObjectsCreateParams().WithObjectClass(refFromClass)
	resp2, err := helper.Client(t).Schema.SchemaObjectsCreate(params2, nil)
	helper.AssertRequestOk(t, resp2, err, nil)

	defer deleteObjectClass(t, refToClassName)
	defer deleteObjectClass(t, refFromClassName)

	uuidsTo := make([]strfmt.UUID, 10)
	uuidsFrom := make([]strfmt.UUID, 10)
	for i := 0; i < 10; i++ {
		uuidsTo[i] = assertCreateObject(t, refToClassName, map[string]interface{}{})
		assertGetObjectWithClass(t, uuidsTo[i], refToClassName)

		uuidsFrom[i] = assertCreateObject(t, refFromClassName, map[string]interface{}{})
		assertGetObjectWithClass(t, uuidsFrom[i], refFromClassName)
	}

	// cannot do from urls without class
	var batchRefs []*models.BatchReference
	for i := range uuidsFrom {
		from := beaconStart + uuidsFrom[i] + "/ref"
		to := beaconStart + uuidsTo[i]
		batchRefs = append(batchRefs, &models.BatchReference{From: strfmt.URI(from), To: strfmt.URI(to)})
	}

	postRefParams := batch.NewBatchReferencesCreateParams().WithBody(batchRefs)
	resp3, err := helper.Client(t).Batch.BatchReferencesCreate(postRefParams, nil)
	require.Nil(t, err)
	require.NotNil(t, resp3)
	for i := range resp3.Payload {
		require.NotNil(t, resp3.Payload[i].Result.Errors)
	}
}

func TestBatchRefWithErrors(t *testing.T) {
	refToClassName := "ReferenceTo"
	refFromClassName := "ReferenceFrom"

	params := clschema.NewSchemaObjectsCreateParams().WithObjectClass(&models.Class{Class: refToClassName})
	resp, err := helper.Client(t).Schema.SchemaObjectsCreate(params, nil)
	helper.AssertRequestOk(t, resp, err, nil)

	refFromClass := &models.Class{
		Class: refFromClassName,
		Properties: []*models.Property{
			{
				DataType: []string{refToClassName},
				Name:     "ref",
			},
		},
	}
	params2 := clschema.NewSchemaObjectsCreateParams().WithObjectClass(refFromClass)
	resp2, err := helper.Client(t).Schema.SchemaObjectsCreate(params2, nil)
	helper.AssertRequestOk(t, resp2, err, nil)

	defer deleteObjectClass(t, refToClassName)
	defer deleteObjectClass(t, refFromClassName)

	uuidsTo := make([]strfmt.UUID, 2)
	uuidsFrom := make([]strfmt.UUID, 2)
	for i := 0; i < 2; i++ {
		uuidsTo[i] = assertCreateObject(t, refToClassName, map[string]interface{}{})
		assertGetObjectWithClass(t, uuidsTo[i], refToClassName)

		uuidsFrom[i] = assertCreateObject(t, refFromClassName, map[string]interface{}{})
		assertGetObjectWithClass(t, uuidsFrom[i], refFromClassName)
	}

	var batchRefs []*models.BatchReference
	for i := range uuidsFrom {
		from := beaconStart + "ReferenceFrom/" + uuidsFrom[i] + "/ref"
		to := beaconStart + uuidsTo[i]
		batchRefs = append(batchRefs, &models.BatchReference{From: strfmt.URI(from), To: strfmt.URI(to)})
	}

	// append one entry with a non-existent class
	batchRefs = append(batchRefs, &models.BatchReference{From: strfmt.URI(beaconStart + "DoesNotExist/" + uuidsFrom[0] + "/ref"), To: strfmt.URI(beaconStart + uuidsTo[0])})

	// append one entry with a non-existent property for existing class
	batchRefs = append(batchRefs, &models.BatchReference{From: strfmt.URI(beaconStart + "ReferenceFrom/" + uuidsFrom[0] + "/doesNotExist"), To: strfmt.URI(beaconStart + uuidsTo[0])})

	postRefParams := batch.NewBatchReferencesCreateParams().WithBody(batchRefs)
	postRefResponse, err := helper.Client(t).Batch.BatchReferencesCreate(postRefParams, nil)
	helper.AssertRequestOk(t, postRefResponse, err, nil)

	require.NotNil(t, postRefResponse.Payload[2].Result.Errors)
	require.Contains(t, postRefResponse.Payload[2].Result.Errors.Error[0].Message, "source class \"DoesNotExist\" not found in schema")

	require.NotNil(t, postRefResponse.Payload[3].Result.Errors)
	require.Contains(t, postRefResponse.Payload[3].Result.Errors.Error[0].Message, "property doesNotExist does not exist for class ReferenceFrom")
}

func TestBatchRefsWithoutToClass(t *testing.T) {
	refToClassName := "ReferenceTo"
	refFromClassName := "ReferenceFrom"
	otherClassMT := "Other"

	// other class has multi-tenancy enabled to make sure that problems trigger an error
	paramsMT := clschema.NewSchemaObjectsCreateParams().WithObjectClass(
		&models.Class{Class: otherClassMT, MultiTenancyConfig: &models.MultiTenancyConfig{Enabled: true}},
	)
	respMT, err := helper.Client(t).Schema.SchemaObjectsCreate(paramsMT, nil)
	helper.AssertRequestOk(t, respMT, err, nil)

	tenant := "tenant"
	tenants := make([]*models.Tenant, 1)
	for i := range tenants {
		tenants[i] = &models.Tenant{Name: tenant}
	}
	helper.CreateTenants(t, otherClassMT, tenants)

	params := clschema.NewSchemaObjectsCreateParams().WithObjectClass(&models.Class{Class: refToClassName})
	resp, err := helper.Client(t).Schema.SchemaObjectsCreate(params, nil)
	helper.AssertRequestOk(t, resp, err, nil)

	refFromClass := &models.Class{
		Class: refFromClassName,
		Properties: []*models.Property{
			{
				DataType: []string{refToClassName},
				Name:     "ref",
			},
		},
	}
	params2 := clschema.NewSchemaObjectsCreateParams().WithObjectClass(refFromClass)
	resp2, err := helper.Client(t).Schema.SchemaObjectsCreate(params2, nil)
	helper.AssertRequestOk(t, resp2, err, nil)

	defer deleteObjectClass(t, refToClassName)
	defer deleteObjectClass(t, refFromClassName)
	defer deleteObjectClass(t, otherClassMT)

	uuidsTo := make([]strfmt.UUID, 10)
	uuidsFrom := make([]strfmt.UUID, 10)
	for i := 0; i < 10; i++ {
		uuidsTo[i] = assertCreateObject(t, refToClassName, map[string]interface{}{})
		assertGetObjectWithClass(t, uuidsTo[i], refToClassName)

		// create object with same id in MT class
		assertCreateObjectWithID(t, otherClassMT, tenant, uuidsTo[i], map[string]interface{}{})

		uuidsFrom[i] = assertCreateObject(t, refFromClassName, map[string]interface{}{})
		assertGetObjectWithClass(t, uuidsFrom[i], refFromClassName)
	}

	var batchRefs []*models.BatchReference
	for i := range uuidsFrom {
		from := beaconStart + "ReferenceFrom/" + uuidsFrom[i] + "/ref"
		to := beaconStart + uuidsTo[i]
		batchRefs = append(batchRefs, &models.BatchReference{From: strfmt.URI(from), To: strfmt.URI(to)})
	}

	postRefParams := batch.NewBatchReferencesCreateParams().WithBody(batchRefs)
	postRefResponse, err := helper.Client(t).Batch.BatchReferencesCreate(postRefParams, nil)
	helper.AssertRequestOk(t, postRefResponse, err, nil)

	for i := range uuidsFrom {
		// validate that ref was create for the correct class
		objWithRef := func() interface{} {
			obj := assertGetObjectWithClass(t, uuidsFrom[i], refFromClassName)
			return obj.Properties
		}
		testhelper.AssertEventuallyEqual(t, map[string]interface{}{
			"ref": []interface{}{
				map[string]interface{}{
					"beacon": fmt.Sprintf(beaconStart+"%s/%s", refToClassName, uuidsTo[i].String()),
					"href":   fmt.Sprintf(pathStart+"%s/%s", refToClassName, uuidsTo[i].String()),
				},
			},
		}, objWithRef)
	}
}

func TestObjectBatchToClassDetection(t *testing.T) {
	// uses same code path as normal object add
	refToClassName := "ReferenceTo"
	refFromClassName := "ReferenceFrom"
	defer deleteObjectClass(t, refToClassName)
	defer deleteObjectClass(t, refFromClassName)

	params := clschema.NewSchemaObjectsCreateParams().WithObjectClass(&models.Class{Class: refToClassName})
	resp, err := helper.Client(t).Schema.SchemaObjectsCreate(params, nil)
	helper.AssertRequestOk(t, resp, err, nil)

	refFromClass := &models.Class{
		Class: refFromClassName,
		Properties: []*models.Property{
			{
				DataType: []string{refToClassName},
				Name:     "ref",
			},
		},
	}
	params2 := clschema.NewSchemaObjectsCreateParams().WithObjectClass(refFromClass)
	resp2, err := helper.Client(t).Schema.SchemaObjectsCreate(params2, nil)
	helper.AssertRequestOk(t, resp2, err, nil)

	refs := make([]interface{}, 10)
	uuidsTo := make([]strfmt.UUID, 10)

	for i := 0; i < 10; i++ {
		uuidTo := assertCreateObject(t, refToClassName, map[string]interface{}{})
		uuidsTo[i] = uuidTo
		assertGetObjectEventually(t, uuidTo)
		refs[i] = map[string]interface{}{
			"beacon": beaconStart + uuidTo,
		}
	}

	fromBatch := make([]*models.Object, 10)
	for i := 0; i < 10; i++ {
		fromBatch[i] = &models.Object{
			Class: refFromClassName,
			ID:    strfmt.UUID(uuid.New().String()),
			Properties: map[string]interface{}{
				"ref": refs[i : i+1],
			},
		}
	}
	paramsBatch := batch.NewBatchObjectsCreateParams().WithBody(
		batch.BatchObjectsCreateBody{
			Objects: fromBatch,
		},
	)
	res, err := helper.Client(t).Batch.BatchObjectsCreate(paramsBatch, nil)
	require.Nil(t, err)
	for _, elem := range res.Payload {
		assert.Nil(t, elem.Result.Errors)
	}

	for i := range fromBatch {
		// validate that ref was create for the correct class
		objWithRef := func() interface{} {
			obj := assertGetObjectWithClass(t, fromBatch[i].ID, refFromClassName)
			return obj.Properties
		}
		testhelper.AssertEventuallyEqual(t, map[string]interface{}{
			"ref": []interface{}{
				map[string]interface{}{
					"beacon": fmt.Sprintf(beaconStart+"%s/%s", refToClassName, uuidsTo[i].String()),
					"href":   fmt.Sprintf(pathStart+"%s/%s", refToClassName, uuidsTo[i].String()),
				},
			},
		}, objWithRef)
	}
}

func TestObjectCrefWithoutToClass(t *testing.T) {
	refToClassName := "ReferenceTo"
	refFromClassName := "ReferenceFrom"
	otherClassMT := "Other"

	// other class has multi-tenancy enabled to make sure that problems trigger an error
	paramsMT := clschema.NewSchemaObjectsCreateParams().WithObjectClass(
		&models.Class{Class: otherClassMT, MultiTenancyConfig: &models.MultiTenancyConfig{Enabled: true}},
	)
	respMT, err := helper.Client(t).Schema.SchemaObjectsCreate(paramsMT, nil)
	helper.AssertRequestOk(t, respMT, err, nil)

	tenant := "tenant"
	tenants := make([]*models.Tenant, 1)
	for i := range tenants {
		tenants[i] = &models.Tenant{Name: tenant}
	}
	helper.CreateTenants(t, otherClassMT, tenants)

	params := clschema.NewSchemaObjectsCreateParams().WithObjectClass(&models.Class{Class: refToClassName})
	resp, err := helper.Client(t).Schema.SchemaObjectsCreate(params, nil)
	helper.AssertRequestOk(t, resp, err, nil)

	refFromClass := &models.Class{
		Class: refFromClassName,
		Properties: []*models.Property{
			{
				DataType: []string{refToClassName},
				Name:     "ref",
			},
		},
	}
	params2 := clschema.NewSchemaObjectsCreateParams().WithObjectClass(refFromClass)
	resp2, err := helper.Client(t).Schema.SchemaObjectsCreate(params2, nil)
	helper.AssertRequestOk(t, resp2, err, nil)

	defer deleteObjectClass(t, refToClassName)
	defer deleteObjectClass(t, refFromClassName)
	defer deleteObjectClass(t, otherClassMT)

	refs := make([]interface{}, 10)
	uuids := make([]strfmt.UUID, 10)
	for i := 0; i < 10; i++ {
		uuidTo := assertCreateObject(t, refToClassName, map[string]interface{}{})
		assertGetObjectWithClass(t, uuidTo, refToClassName)

		// create object with same id in MT class
		assertCreateObjectWithID(t, otherClassMT, tenant, uuidTo, map[string]interface{}{})

		refs[i] = map[string]interface{}{
			"beacon": beaconStart + uuidTo,
		}
		uuids[i] = uuidTo
	}

	uuidFrom := assertCreateObject(t, refFromClassName, map[string]interface{}{"ref": refs})
	assertGetObjectWithClass(t, uuidFrom, refFromClassName)

	objWithRef := assertGetObjectWithClass(t, uuidFrom, refFromClassName)
	assert.NotNil(t, objWithRef.Properties)
	refsReturned := objWithRef.Properties.(map[string]interface{})["ref"].([]interface{})
	for i := range refsReturned {
		require.Equal(t, refsReturned[i].(map[string]interface{})["beacon"], string(beaconStart+"ReferenceTo/"+uuids[i]))
	}
}

// This test suite is meant to prevent a regression on
// https://github.com/weaviate/weaviate/issues/868, hence it tries to
// reproduce the steps outlined in there as closely as possible
func Test_CREFWithCardinalityMany_UsingPatch(t *testing.T) {
	defer func() {
		// clean up so we can run this test multiple times in a row
		delCityParams := clschema.NewSchemaObjectsDeleteParams().WithClassName("ReferenceTestCity")
		dresp, err := helper.Client(t).Schema.SchemaObjectsDelete(delCityParams, nil)
		t.Logf("clean up - delete city \n%v\n %v", dresp, err)

		delPlaceParams := clschema.NewSchemaObjectsDeleteParams().WithClassName("ReferenceTestPlace")
		dresp, err = helper.Client(t).Schema.SchemaObjectsDelete(delPlaceParams, nil)
		t.Logf("clean up - delete place \n%v\n %v", dresp, err)
	}()

	t.Log("1. create ReferenceTestPlace class")
	placeClass := &models.Class{
		Class: "ReferenceTestPlace",
		Properties: []*models.Property{
			{
				DataType:     schema.DataTypeText.PropString(),
				Tokenization: models.PropertyTokenizationWhitespace,
				Name:         "name",
			},
		},
	}
	params := clschema.NewSchemaObjectsCreateParams().WithObjectClass(placeClass)
	resp, err := helper.Client(t).Schema.SchemaObjectsCreate(params, nil)
	helper.AssertRequestOk(t, resp, err, nil)

	t.Log("2. create ReferenceTestCity class with HasPlaces (many) cross-ref")
	cityClass := &models.Class{
		Class: "ReferenceTestCity",
		Properties: []*models.Property{
			{
				DataType:     schema.DataTypeText.PropString(),
				Tokenization: models.PropertyTokenizationWhitespace,
				Name:         "name",
			},
			{
				DataType: []string{"ReferenceTestPlace"},
				Name:     "HasPlaces",
			},
		},
	}
	params = clschema.NewSchemaObjectsCreateParams().WithObjectClass(cityClass)
	resp, err = helper.Client(t).Schema.SchemaObjectsCreate(params, nil)
	helper.AssertRequestOk(t, resp, err, nil)

	t.Log("3. add two places and save their IDs")
	place1ID := assertCreateObject(t, "ReferenceTestPlace", map[string]interface{}{
		"name": "Place 1",
	})
	place2ID := assertCreateObject(t, "ReferenceTestPlace", map[string]interface{}{
		"name": "Place 2",
	})
	assertGetObjectEventually(t, place1ID)
	assertGetObjectEventually(t, place2ID)

	t.Log("4. add one city")
	cityID := assertCreateObject(t, "ReferenceTestCity", map[string]interface{}{
		"name": "My City",
	})
	assertGetObjectEventually(t, cityID)

	t.Log("5. patch city to point to the first place")
	patchParams := objects.NewObjectsPatchParams().
		WithID(cityID).
		WithBody(&models.Object{
			Class: "ReferenceTestCity",
			Properties: map[string]interface{}{
				"hasPlaces": []interface{}{
					map[string]interface{}{
						"beacon": fmt.Sprintf("weaviate://localhost/%s", place1ID.String()),
					},
				},
			},
		})
	patchResp, err := helper.Client(t).Objects.ObjectsPatch(patchParams, nil)
	helper.AssertRequestOk(t, patchResp, err, nil)

	t.Log("6. verify first cross ref was added")

	actualThunk := func() interface{} {
		cityAfterFirstPatch := assertGetObject(t, cityID)
		return cityAfterFirstPatch.Properties
	}

	testhelper.AssertEventuallyEqual(t, map[string]interface{}{
		"name": "My City",
		"hasPlaces": []interface{}{
			map[string]interface{}{
				"beacon": fmt.Sprintf("weaviate://localhost/%s/%s", placeClass.Class, place1ID.String()),
				"href":   fmt.Sprintf("/v1/objects/%s/%s", placeClass.Class, place1ID.String()),
			},
		},
	}, actualThunk)

	t.Log("7. patch city to point to the second place")
	patchParams = objects.NewObjectsPatchParams().
		WithID(cityID).
		WithBody(&models.Object{
			Class: "ReferenceTestCity",
			Properties: map[string]interface{}{
				"hasPlaces": []interface{}{
					map[string]interface{}{
						"beacon": fmt.Sprintf("weaviate://localhost/%s", place2ID.String()),
					},
				},
			},
		})
	patchResp, err = helper.Client(t).Objects.ObjectsPatch(patchParams, nil)
	helper.AssertRequestOk(t, patchResp, err, nil)

	actualThunk = func() interface{} {
		city := assertGetObject(t, cityID)
		return city.Properties.(map[string]interface{})["hasPlaces"].([]interface{})
	}

	t.Log("9. verify both cross refs are present")
	expectedRefs := []interface{}{
		map[string]interface{}{
			"beacon": fmt.Sprintf("weaviate://localhost/%s/%s", placeClass.Class, place1ID.String()),
			"href":   fmt.Sprintf("/v1/objects/%s/%s", placeClass.Class, place1ID.String()),
		},
		map[string]interface{}{
			"beacon": fmt.Sprintf("weaviate://localhost/%s/%s", placeClass.Class, place2ID.String()),
			"href":   fmt.Sprintf("/v1/objects/%s/%s", placeClass.Class, place2ID.String()),
		},
	}

	testhelper.AssertEventuallyEqual(t, expectedRefs, actualThunk)
}

// This test suite is meant to prevent a regression on
// https://github.com/weaviate/weaviate/issues/868, hence it tries to
// reproduce the steps outlined in there as closely as possible
func Test_CREFWithCardinalityMany_UsingPostReference(t *testing.T) {
	defer func() {
		// clean up so we can run this test multiple times in a row
		delCityParams := clschema.NewSchemaObjectsDeleteParams().WithClassName("ReferenceTestCity")
		dresp, err := helper.Client(t).Schema.SchemaObjectsDelete(delCityParams, nil)
		t.Logf("clean up - delete city \n%v\n %v", dresp, err)

		delPlaceParams := clschema.NewSchemaObjectsDeleteParams().WithClassName("ReferenceTestPlace")
		dresp, err = helper.Client(t).Schema.SchemaObjectsDelete(delPlaceParams, nil)
		t.Logf("clean up - delete place \n%v\n %v", dresp, err)
	}()

	t.Log("1. create ReferenceTestPlace class")
	placeClass := &models.Class{
		Class: "ReferenceTestPlace",
		Properties: []*models.Property{
			{
				DataType:     schema.DataTypeText.PropString(),
				Tokenization: models.PropertyTokenizationWhitespace,
				Name:         "name",
			},
		},
	}
	params := clschema.NewSchemaObjectsCreateParams().WithObjectClass(placeClass)
	resp, err := helper.Client(t).Schema.SchemaObjectsCreate(params, nil)
	helper.AssertRequestOk(t, resp, err, nil)

	t.Log("2. create ReferenceTestCity class with HasPlaces (many) cross-ref")
	cityClass := &models.Class{
		Class: "ReferenceTestCity",
		Properties: []*models.Property{
			{
				DataType:     schema.DataTypeText.PropString(),
				Tokenization: models.PropertyTokenizationWhitespace,
				Name:         "name",
			},
			{
				DataType: []string{"ReferenceTestPlace"},
				Name:     "HasPlaces",
			},
		},
	}
	params = clschema.NewSchemaObjectsCreateParams().WithObjectClass(cityClass)
	resp, err = helper.Client(t).Schema.SchemaObjectsCreate(params, nil)
	helper.AssertRequestOk(t, resp, err, nil)

	t.Log("3. add two places and save their IDs")
	place1ID := assertCreateObject(t, "ReferenceTestPlace", map[string]interface{}{
		"name": "Place 1",
	})
	place2ID := assertCreateObject(t, "ReferenceTestPlace", map[string]interface{}{
		"name": "Place 2",
	})
	assertGetObjectEventually(t, place1ID)
	assertGetObjectEventually(t, place2ID)
	t.Logf("Place 1 ID: %s", place1ID)
	t.Logf("Place 2 ID: %s", place2ID)

	t.Log("4. add one city")
	cityID := assertCreateObject(t, "ReferenceTestCity", map[string]interface{}{
		"name": "My City",
	})
	assertGetObjectEventually(t, cityID)

	t.Log("5. POST /references/ for place 1")
	postRefParams := objects.NewObjectsReferencesCreateParams().
		WithID(cityID).
		WithPropertyName("hasPlaces").
		WithBody(&models.SingleRef{
			Beacon: strfmt.URI(fmt.Sprintf("weaviate://localhost/%s", place1ID.String())),
		})
	postRefResponse, err := helper.Client(t).Objects.ObjectsReferencesCreate(postRefParams, nil)
	helper.AssertRequestOk(t, postRefResponse, err, nil)

	actualThunk := func() interface{} {
		city := assertGetObject(t, cityID)
		return city.Properties
	}
	t.Log("7. verify first cross ref was added")
	testhelper.AssertEventuallyEqual(t, map[string]interface{}{
		"name": "My City",
		"hasPlaces": []interface{}{
			map[string]interface{}{
				"beacon": fmt.Sprintf("weaviate://localhost/%s/%s", placeClass.Class, place1ID.String()),
				"href":   fmt.Sprintf("/v1/objects/%s/%s", placeClass.Class, place1ID.String()),
			},
		},
	}, actualThunk)

	t.Log("8. POST /references/ for place 2")
	postRefParams = objects.NewObjectsReferencesCreateParams().
		WithID(cityID).
		WithPropertyName("hasPlaces").
		WithBody(&models.SingleRef{
			Beacon: strfmt.URI(fmt.Sprintf("weaviate://localhost/%s", place2ID.String())),
		})
	postRefResponse, err = helper.Client(t).Objects.ObjectsReferencesCreate(postRefParams, nil)
	helper.AssertRequestOk(t, postRefResponse, err, nil)

	t.Log("9. verify both cross refs are present")
	actualThunk = func() interface{} {
		city := assertGetObject(t, cityID)
		return city.Properties.(map[string]interface{})["hasPlaces"].([]interface{})
	}

	expectedRefs := []interface{}{
		map[string]interface{}{
			"beacon": fmt.Sprintf("weaviate://localhost/%s/%s", placeClass.Class, place1ID.String()),
			"href":   fmt.Sprintf("/v1/objects/%s/%s", placeClass.Class, place1ID.String()),
		},
		map[string]interface{}{
			"beacon": fmt.Sprintf("weaviate://localhost/%s/%s", placeClass.Class, place2ID.String()),
			"href":   fmt.Sprintf("/v1/objects/%s/%s", placeClass.Class, place2ID.String()),
		},
	}

	testhelper.AssertEventuallyEqual(t, expectedRefs, actualThunk)
}
