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

package replication

import (
	"encoding/json"
	"fmt"
	"strings"
	"testing"

	"github.com/go-openapi/strfmt"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/weaviate/weaviate/client/nodes"
	"github.com/weaviate/weaviate/client/objects"
	"github.com/weaviate/weaviate/entities/filters"
	"github.com/weaviate/weaviate/entities/models"
	"github.com/weaviate/weaviate/entities/verbosity"
	"github.com/weaviate/weaviate/test/helper"
	graphqlhelper "github.com/weaviate/weaviate/test/helper/graphql"
	"github.com/weaviate/weaviate/usecases/replica"
)

func getClass(t *testing.T, host, class string) *models.Class {
	t.Helper()
	helper.SetupClient(host)
	return helper.GetClass(t, class)
}

func updateClass(t *testing.T, host string, class *models.Class) {
	t.Helper()
	helper.SetupClient(host)
	helper.UpdateClass(t, class)
}

func createObjectCL(t *testing.T, host string, obj *models.Object, cl replica.ConsistencyLevel) error {
	t.Helper()
	helper.SetupClient(host)
	return helper.CreateObjectCL(t, obj, cl)
}

func createObjects(t *testing.T, host string, batch []*models.Object) {
	t.Helper()
	helper.SetupClient(host)
	helper.CreateObjectsBatch(t, batch)
}

func createObjectsCL(t *testing.T, host string, batch []*models.Object, cl replica.ConsistencyLevel) {
	helper.SetupClient(host)
	helper.CreateObjectsBatchCL(t, batch, cl)
}

func createTenantObjects(t *testing.T, host string, batch []*models.Object) {
	t.Helper()
	helper.SetupClient(host)
	helper.CreateObjectsBatch(t, batch)
}

func getObject(t *testing.T, host, class string, id strfmt.UUID, withVec bool) (*models.Object, error) {
	t.Helper()
	helper.SetupClient(host)
	var include string
	if withVec {
		include = "vector"
	}
	return helper.GetObject(t, class, id, include)
}

func getTenantObject(t *testing.T, host, class string, id strfmt.UUID, tenant string) (*models.Object, error) {
	t.Helper()
	helper.SetupClient(host)
	return helper.TenantObject(t, class, id, tenant)
}

func objectExistsCL(t *testing.T, host, class string, id strfmt.UUID, cl replica.ConsistencyLevel) (bool, error) {
	t.Helper()
	helper.SetupClient(host)
	return helper.ObjectExistsCL(t, class, id, cl)
}

func getObjectCL(t *testing.T, host, class string, id strfmt.UUID, cl replica.ConsistencyLevel) (*models.Object, error) {
	t.Helper()
	helper.SetupClient(host)
	return helper.GetObjectCL(t, class, id, cl)
}

func getObjectFromNode(t *testing.T, host, class string, id strfmt.UUID, nodename string) (*models.Object, error) {
	t.Helper()
	helper.SetupClient(host)
	return helper.GetObjectFromNode(t, class, id, nodename)
}

func getTenantObjectFromNode(t *testing.T, host, class string, id strfmt.UUID, nodename, tenant string) (*models.Object, error) {
	t.Helper()
	helper.SetupClient(host)
	return helper.GetTenantObjectFromNode(t, class, id, nodename, tenant)
}

func patchObject(t *testing.T, host string, patch *models.Object) {
	t.Helper()
	helper.SetupClient(host)
	helper.PatchObject(t, patch)
}

func updateObjectCL(t *testing.T, host string, obj *models.Object, cl replica.ConsistencyLevel) error {
	t.Helper()
	helper.SetupClient(host)
	return helper.UpdateObjectCL(t, obj, cl)
}

func addReferences(t *testing.T, host string, refs []*models.BatchReference) {
	t.Helper()
	helper.SetupClient(host)
	resp, err := helper.AddReferences(t, refs)
	helper.CheckReferencesBatchResponse(t, resp, err)
}

func addTenantReferences(t *testing.T, host string, refs []*models.BatchReference) {
	t.Helper()
	helper.SetupClient(host)
	resp, err := helper.AddReferences(t, refs)
	helper.CheckReferencesBatchResponse(t, resp, err)
}

func deleteObject(t *testing.T, host, class string, id strfmt.UUID, cl replica.ConsistencyLevel) {
	t.Helper()
	helper.SetupClient(host)

	_, err := helper.GetObject(t, class, id)
	require.Nil(t, err)

	helper.DeleteObjectCL(t, class, id, cl)

	_, err = helper.GetObject(t, class, id)
	require.Equal(t, &objects.ObjectsClassGetNotFound{}, err)
}

func deleteTenantObject(t *testing.T, host, class string, id strfmt.UUID, tenant string, cl replica.ConsistencyLevel) {
	helper.SetupClient(host)
	helper.DeleteTenantObject(t, class, id, tenant, cl)

	_, err := helper.TenantObject(t, class, id, tenant)
	assert.Equal(t, &objects.ObjectsClassGetNotFound{}, err)
}

func deleteObjects(t *testing.T, host, class string, path []string, valueText string, cl replica.ConsistencyLevel) {
	t.Helper()
	helper.SetupClient(host)

	batchDelete := &models.BatchDelete{
		Match: &models.BatchDeleteMatch{
			Class: class,
			Where: &models.WhereFilter{
				Operator:  filters.OperatorLike.Name(),
				Path:      path,
				ValueText: &valueText,
			},
		},
	}
	helper.DeleteObjectsBatch(t, batchDelete, cl)

	resp := gqlGet(t, host, class, replica.All)
	assert.Empty(t, resp)
}

func deleteTenantObjects(t *testing.T, host, class string, path []string, valueText, tenant string, cl replica.ConsistencyLevel) {
	t.Helper()
	helper.SetupClient(host)

	batchDelete := &models.BatchDelete{
		Match: &models.BatchDeleteMatch{
			Class: class,
			Where: &models.WhereFilter{
				Operator:  filters.OperatorLike.Name(),
				Path:      path,
				ValueText: &valueText,
			},
		},
	}
	resp, err := helper.DeleteTenantObjectsBatchCL(t, batchDelete, tenant, cl)
	helper.AssertRequestOk(t, resp, err, nil)

	deleted := gqlTenantGet(t, host, class, replica.All, tenant)
	assert.Empty(t, deleted)
}

func gqlGet(t *testing.T, host, class string, cl replica.ConsistencyLevel, fields ...string) []interface{} {
	t.Helper()
	helper.SetupClient(host)

	if cl == "" {
		cl = replica.Quorum
	}

	q := fmt.Sprintf("{Get {%s (consistencyLevel: %s)", class, cl) + " {%s}}}"
	if len(fields) == 0 {
		fields = []string{"_additional{id isConsistent vector}"}
	}
	q = fmt.Sprintf(q, strings.Join(fields, " "))

	return gqlDo(t, class, q)
}

func gqlGetNearVec(t *testing.T, host, class string, vec []interface{}, cl replica.ConsistencyLevel, fields ...string) []interface{} {
	t.Helper()
	helper.SetupClient(host)

	if cl == "" {
		cl = replica.Quorum
	}

	q := fmt.Sprintf("{Get {%s (consistencyLevel: %s, nearVector: {vector: %s, certainty: 0.8})",
		class, cl, vec2String(vec)) + " {%s}}}"
	if len(fields) == 0 {
		fields = []string{"_additional{id isConsistent}"}
	}
	q = fmt.Sprintf(q, strings.Join(fields, " "))

	return gqlDo(t, class, q)
}

func gqlDo(t *testing.T, class, query string) []interface{} {
	t.Helper()
	resp := graphqlhelper.AssertGraphQL(t, helper.RootAuth, query)

	result := resp.Get("Get").Get(class)
	return result.Result.([]interface{})
}

func gqlTenantGet(t *testing.T, host, class string, cl replica.ConsistencyLevel,
	tenant string, fields ...string,
) []interface{} {
	t.Helper()
	helper.SetupClient(host)

	if cl == "" {
		cl = replica.Quorum
	}

	q := fmt.Sprintf("{Get {%s (tenant: %q, consistencyLevel: %s, limit: 1000)", class, tenant, cl) + " {%s}}}"
	if len(fields) == 0 {
		fields = []string{"_additional{id isConsistent}"}
	}
	q = fmt.Sprintf(q, strings.Join(fields, " "))

	resp := graphqlhelper.AssertGraphQL(t, helper.RootAuth, q)

	result := resp.Get("Get").Get(class)
	return result.Result.([]interface{})
}

func countObjects(t *testing.T, host, class string) int64 {
	helper.SetupClient(host)

	q := fmt.Sprintf(`{Aggregate{%s{meta{count}}}}`, class)

	resp := graphqlhelper.AssertGraphQL(t, helper.RootAuth, q)

	result := resp.Get("Aggregate").Get(class).AsSlice()
	require.Len(t, result, 1)
	meta := result[0].(map[string]interface{})["meta"].(map[string]interface{})
	count, err := meta["count"].(json.Number).Int64()
	require.Nil(t, err)
	return count
}

func countTenantObjects(t *testing.T, host, class string,
	tenant string,
) int64 {
	t.Helper()
	helper.SetupClient(host)

	q := fmt.Sprintf(`{Aggregate{%s(tenant: %q){meta{count}}}}`, class, tenant)

	resp := graphqlhelper.AssertGraphQL(t, helper.RootAuth, q)

	result := resp.Get("Aggregate").Get(class).AsSlice()
	require.Len(t, result, 1)
	meta := result[0].(map[string]interface{})["meta"].(map[string]interface{})
	count, err := meta["count"].(json.Number).Int64()
	require.Nil(t, err)
	return count
}

func getNodes(t *testing.T, host string) *models.NodesStatusResponse {
	t.Helper()
	helper.SetupClient(host)
	verbose := verbosity.OutputVerbose
	params := nodes.NewNodesGetParams().WithOutput(&verbose)
	resp, err := helper.Client(t).Nodes.NodesGet(params, nil)
	helper.AssertRequestOk(t, resp, err, nil)
	return resp.Payload
}

func vec2String(v []interface{}) (s string) {
	for _, n := range v {
		x := n.(json.Number)
		s = fmt.Sprintf("%s, %s", s, x.String())
	}
	s = strings.TrimLeft(s, ", ")
	return fmt.Sprintf("[%s]", s)
}
