package testcases

import (
	"context"
	"fmt"
	"strconv"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/milvus-io/milvus/client/v3/entity"
	client "github.com/milvus-io/milvus/client/v3/milvusclient"
	"github.com/milvus-io/milvus/pkg/v3/mlog"
	"github.com/milvus-io/milvus/tests/go_client/base"
	"github.com/milvus-io/milvus/tests/go_client/common"
	hp "github.com/milvus-io/milvus/tests/go_client/testcases/helper"
)

// teardownTest
func teardownTest(t *testing.T) func(t *testing.T) {
	mlog.Info(context.TODO(), "setup test func")
	return func(t *testing.T) {
		mlog.Info(context.TODO(), "teardown func drop all non-default db")
		// drop all db
		ctx := hp.CreateContext(t, time.Second*common.DefaultTimeout)
		mc := hp.CreateDefaultMilvusClient(ctx, t)
		dbs, _ := mc.ListDatabase(ctx, client.NewListDatabaseOption())
		for _, db := range dbs {
			if db != common.DefaultDb {
				_ = mc.UseDatabase(ctx, client.NewUseDatabaseOption(db))
				collections, _ := mc.ListCollections(ctx, client.NewListCollectionOption())
				for _, coll := range collections {
					_ = mc.DropCollection(ctx, client.NewDropCollectionOption(coll))
				}
				_ = mc.DropDatabase(ctx, client.NewDropDatabaseOption(db))
			}
		}
	}
}

func TestDatabase(t *testing.T) {
	teardownSuite := teardownTest(t)
	defer teardownSuite(t)

	ctx := hp.CreateContext(t, time.Second*common.DefaultTimeout)
	clientDefault := hp.CreateMilvusClient(ctx, t, hp.GetDefaultClientConfig())

	// create db1
	dbName1 := common.GenRandomString("db1", 4)
	err := clientDefault.CreateDatabase(ctx, client.NewCreateDatabaseOption(dbName1))
	common.CheckErr(t, err, true)

	// list db and verify db1 in dbs
	dbs, errList := clientDefault.ListDatabase(ctx, client.NewListDatabaseOption())
	common.CheckErr(t, errList, true)
	require.Containsf(t, dbs, dbName1, fmt.Sprintf("%s db not in dbs: %v", dbName1, dbs))

	// new client with db1
	clientDB1 := hp.CreateMilvusClient(ctx, t, &client.ClientConfig{Address: hp.GetAddr(), DBName: dbName1})

	// create collections -> verify collections contains
	_, db1Col1 := hp.CollPrepare.CreateCollection(ctx, t, clientDB1, hp.NewCreateCollectionParams(hp.Int64Vec), hp.TNewFieldsOption(), hp.TNewSchemaOption())
	_, db1Col2 := hp.CollPrepare.CreateCollection(ctx, t, clientDB1, hp.NewCreateCollectionParams(hp.Int64Vec), hp.TNewFieldsOption(), hp.TNewSchemaOption())
	collections, errListCollections := clientDB1.ListCollections(ctx, client.NewListCollectionOption())
	common.CheckErr(t, errListCollections, true)
	require.Containsf(t, collections, db1Col1.CollectionName, fmt.Sprintf("The collection %s not in: %v", db1Col1.CollectionName, collections))
	require.Containsf(t, collections, db1Col2.CollectionName, fmt.Sprintf("The collection %s not in: %v", db1Col2.CollectionName, collections))

	// create db2
	dbName2 := common.GenRandomString("db2", 4)
	err = clientDefault.CreateDatabase(ctx, client.NewCreateDatabaseOption(dbName2))
	common.CheckErr(t, err, true)
	dbs, err = clientDefault.ListDatabase(ctx, client.NewListDatabaseOption())
	common.CheckErr(t, err, true)
	require.Containsf(t, dbs, dbName2, fmt.Sprintf("%s db not in dbs: %v", dbName2, dbs))

	// using db2 -> create collection -> drop collection
	err = clientDefault.UseDatabase(ctx, client.NewUseDatabaseOption(dbName2))
	common.CheckErr(t, err, true)
	_, db2Col1 := hp.CollPrepare.CreateCollection(ctx, t, clientDefault, hp.NewCreateCollectionParams(hp.Int64Vec), hp.TNewFieldsOption(), hp.TNewSchemaOption())
	err = clientDefault.DropCollection(ctx, client.NewDropCollectionOption(db2Col1.CollectionName))
	common.CheckErr(t, err, true)

	// using empty db -> drop db2
	clientDefault.UseDatabase(ctx, client.NewUseDatabaseOption(""))
	err = clientDefault.DropDatabase(ctx, client.NewDropDatabaseOption(dbName2))
	common.CheckErr(t, err, true)

	// list db and verify db drop success
	dbs, err = clientDefault.ListDatabase(ctx, client.NewListDatabaseOption())
	common.CheckErr(t, err, true)
	require.NotContains(t, dbs, dbName2)

	// drop db1 which has some collections
	err = clientDB1.DropDatabase(ctx, client.NewDropDatabaseOption(dbName1))
	common.CheckErr(t, err, false, "must drop all collections before drop database")

	// drop all db1's collections -> drop db1
	clientDB1.UseDatabase(ctx, client.NewUseDatabaseOption(dbName1))
	err = clientDB1.DropCollection(ctx, client.NewDropCollectionOption(db1Col1.CollectionName))
	common.CheckErr(t, err, true)

	err = clientDB1.DropCollection(ctx, client.NewDropCollectionOption(db1Col2.CollectionName))
	common.CheckErr(t, err, true)

	err = clientDB1.DropDatabase(ctx, client.NewDropDatabaseOption(dbName1))
	common.CheckErr(t, err, true)

	// drop default db
	err = clientDefault.DropDatabase(ctx, client.NewDropDatabaseOption(common.DefaultDb))
	common.CheckErr(t, err, false, "can not drop default database")

	dbs, err = clientDefault.ListDatabase(ctx, client.NewListDatabaseOption())
	common.CheckErr(t, err, true)
	require.Containsf(t, dbs, common.DefaultDb, fmt.Sprintf("The db %s not in: %v", common.DefaultDb, dbs))
}

// test create with invalid db name
func TestCreateDb(t *testing.T) {
	teardownSuite := teardownTest(t)
	defer teardownSuite(t)

	// create db
	ctx := hp.CreateContext(t, time.Second*common.DefaultTimeout)
	mc := hp.CreateDefaultMilvusClient(ctx, t)
	dbName := common.GenRandomString("db", 4)
	err := mc.CreateDatabase(ctx, client.NewCreateDatabaseOption(dbName))
	common.CheckErr(t, err, true)

	// create existed db
	err = mc.CreateDatabase(ctx, client.NewCreateDatabaseOption(dbName))
	common.CheckErr(t, err, false, fmt.Sprintf("database already exist: %s", dbName))

	// create default db
	err = mc.CreateDatabase(ctx, client.NewCreateDatabaseOption(common.DefaultDb))
	common.CheckErr(t, err, false, fmt.Sprintf("database already exist: %s", common.DefaultDb))

	emptyErr := mc.CreateDatabase(ctx, client.NewCreateDatabaseOption(""))
	common.CheckErr(t, emptyErr, false, "database name couldn't be empty")
}

// test drop db
func TestDropDb(t *testing.T) {
	teardownSuite := teardownTest(t)
	defer teardownSuite(t)

	// create collection in default db
	listCollOpt := client.NewListCollectionOption()
	ctx := hp.CreateContext(t, time.Second*common.DefaultTimeout)
	mc := hp.CreateDefaultMilvusClient(ctx, t)
	_, defCol := hp.CollPrepare.CreateCollection(ctx, t, mc, hp.NewCreateCollectionParams(hp.Int64Vec), hp.TNewFieldsOption(), hp.TNewSchemaOption())
	collections, _ := mc.ListCollections(ctx, listCollOpt)
	require.Contains(t, collections, defCol.CollectionName)

	// create db
	dbName := common.GenRandomString("db", 4)
	err := mc.CreateDatabase(ctx, client.NewCreateDatabaseOption(dbName))
	common.CheckErr(t, err, true)

	// using db and drop the db
	err = mc.UseDatabase(ctx, client.NewUseDatabaseOption(dbName))
	common.CheckErr(t, err, true)
	err = mc.DropDatabase(ctx, client.NewDropDatabaseOption(dbName))
	common.CheckErr(t, err, true)

	// verify current db
	_, err = mc.ListCollections(ctx, listCollOpt)
	common.CheckErr(t, err, false, fmt.Sprintf("database not found[database=%s]", dbName))

	// using default db and verify collections
	err = mc.UseDatabase(ctx, client.NewUseDatabaseOption(common.DefaultDb))
	common.CheckErr(t, err, true)
	collections, _ = mc.ListCollections(ctx, listCollOpt)
	require.Contains(t, collections, defCol.CollectionName)

	// drop not existed db
	err = mc.DropDatabase(ctx, client.NewDropDatabaseOption(common.GenRandomString("db", 4)))
	common.CheckErr(t, err, true)

	// drop empty db
	err = mc.DropDatabase(ctx, client.NewDropDatabaseOption(""))
	common.CheckErr(t, err, false, "database name couldn't be empty")

	// drop default db
	err = mc.DropDatabase(ctx, client.NewDropDatabaseOption(common.DefaultDb))
	common.CheckErr(t, err, false, "can not drop default database")
}

// test using db
func TestUsingDb(t *testing.T) {
	teardownSuite := teardownTest(t)
	defer teardownSuite(t)

	// create collection in default db
	listCollOpt := client.NewListCollectionOption()
	ctx := hp.CreateContext(t, time.Second*common.DefaultTimeout)
	mc := hp.CreateDefaultMilvusClient(ctx, t)

	_, col := hp.CollPrepare.CreateCollection(ctx, t, mc, hp.NewCreateCollectionParams(hp.Int64Vec), hp.TNewFieldsOption(), hp.TNewSchemaOption())
	// collName := createDefaultCollection(ctx, t, mc, true, common.DefaultShards)
	collections, _ := mc.ListCollections(ctx, listCollOpt)
	require.Contains(t, collections, col.CollectionName)

	// using not existed db
	dbName := common.GenRandomString("db", 4)
	err := mc.UseDatabase(ctx, client.NewUseDatabaseOption(dbName))
	common.CheckErr(t, err, false, fmt.Sprintf("database not found[database=%s]", dbName))

	// using empty db
	err = mc.UseDatabase(ctx, client.NewUseDatabaseOption(""))
	common.CheckErr(t, err, true)
	collections, _ = mc.ListCollections(ctx, listCollOpt)
	require.Contains(t, collections, col.CollectionName)

	// using current db
	err = mc.UseDatabase(ctx, client.NewUseDatabaseOption(common.DefaultDb))
	common.CheckErr(t, err, true)
	collections, _ = mc.ListCollections(ctx, listCollOpt)
	require.Contains(t, collections, col.CollectionName)
}

func TestClientWithDb(t *testing.T) {
	teardownSuite := teardownTest(t)
	defer teardownSuite(t)

	listCollOpt := client.NewListCollectionOption()
	ctx := hp.CreateContext(t, time.Second*common.DefaultTimeout)

	// connect with not existed db
	_, err := base.NewMilvusClient(ctx, &client.ClientConfig{Address: hp.GetAddr(), DBName: "dbName"})
	common.CheckErr(t, err, false, "database not found")

	// connect default db -> create a collection in default db
	mcDefault, errDefault := base.NewMilvusClient(ctx, &client.ClientConfig{
		Address: hp.GetAddr(),
		// DBName:  common.DefaultDb,
	})
	common.CheckErr(t, errDefault, true)
	_, defCol1 := hp.CollPrepare.CreateCollection(ctx, t, mcDefault, hp.NewCreateCollectionParams(hp.Int64Vec), hp.TNewFieldsOption(), hp.TNewSchemaOption())
	defCollections, _ := mcDefault.ListCollections(ctx, listCollOpt)
	require.Contains(t, defCollections, defCol1.CollectionName)
	mlog.Debug(context.TODO(), "default db collections:", mlog.Any("default collections", defCollections))

	// create a db and create collection in db
	dbName := common.GenRandomString("db", 5)
	err = mcDefault.CreateDatabase(ctx, client.NewCreateDatabaseOption(dbName))
	common.CheckErr(t, err, true)

	// and connect with db
	mcDb, err := base.NewMilvusClient(ctx, &client.ClientConfig{
		Address: hp.GetAddr(),
		DBName:  dbName,
	})
	common.CheckErr(t, err, true)
	_, dbCol1 := hp.CollPrepare.CreateCollection(ctx, t, mcDb, hp.NewCreateCollectionParams(hp.Int64Vec), hp.TNewFieldsOption(), hp.TNewSchemaOption())

	dbCollections, _ := mcDb.ListCollections(ctx, listCollOpt)
	mlog.Debug(context.TODO(), "db collections:", mlog.Any("db collections", dbCollections))
	require.Containsf(t, dbCollections, dbCol1.CollectionName, fmt.Sprintf("The collection %s not in: %v", dbCol1.CollectionName, dbCollections))

	// using default db and collection not in
	_ = mcDb.UseDatabase(ctx, client.NewUseDatabaseOption(common.DefaultDb))
	defCollections, _ = mcDb.ListCollections(ctx, listCollOpt)
	require.NotContains(t, defCollections, dbCol1.CollectionName)

	// connect empty db (actually default db)
	mcEmpty, err := base.NewMilvusClient(ctx, &client.ClientConfig{
		Address: hp.GetAddr(),
		DBName:  "",
	})
	common.CheckErr(t, err, true)
	defCollections, _ = mcEmpty.ListCollections(ctx, listCollOpt)
	require.Contains(t, defCollections, defCol1.CollectionName)
}

func TestDatabasePropertiesCollectionsNum(t *testing.T) {
	// create db with properties
	teardownSuite := teardownTest(t)
	defer teardownSuite(t)

	// create db
	ctx := hp.CreateContext(t, time.Second*common.DefaultTimeout)
	mc := hp.CreateDefaultMilvusClient(ctx, t)
	dbName := common.GenRandomString("db", 4)
	err := mc.CreateDatabase(ctx, client.NewCreateDatabaseOption(dbName))
	common.CheckErr(t, err, true)

	// alter database properties
	maxCollections := 2
	err = mc.AlterDatabaseProperties(ctx, client.NewAlterDatabasePropertiesOption(dbName).WithProperty(common.DatabaseMaxCollections, maxCollections))
	common.CheckErr(t, err, true)

	// describe database
	db, _ := mc.DescribeDatabase(ctx, client.NewDescribeDatabaseOption(dbName))
	require.Equal(t, map[string]string{common.DatabaseMaxCollections: strconv.Itoa(maxCollections)}, db.Properties)
	require.Equal(t, dbName, db.Name)

	// verify properties works
	mc.UseDatabase(ctx, client.NewUseDatabaseOption(dbName))
	var collections []string
	for i := 0; i < maxCollections; i++ {
		_, schema := hp.CollPrepare.CreateCollection(ctx, t, mc, hp.NewCreateCollectionParams(hp.Int64Vec), hp.TNewFieldsOption(), hp.TNewSchemaOption())
		collections = append(collections, schema.CollectionName)
	}
	fields := hp.FieldsFact.GenFieldsForCollection(hp.Int64Vec, hp.TNewFieldOptions())
	schema := hp.GenSchema(hp.TNewSchemaOption().TWithFields(fields))
	err = mc.CreateCollection(ctx, client.NewCreateCollectionOption(schema.CollectionName, schema))
	common.CheckErr(t, err, false, "exceeded the limit number of collections")

	// Other db are not restricted by this property
	mc.UseDatabase(ctx, client.NewUseDatabaseOption(common.DefaultDb))
	for i := 0; i < maxCollections+1; i++ {
		hp.CollPrepare.CreateCollection(ctx, t, mc, hp.NewCreateCollectionParams(hp.Int64Vec), hp.TNewFieldsOption(), hp.TNewSchemaOption())
	}

	// drop properties
	mc.UseDatabase(ctx, client.NewUseDatabaseOption(dbName))
	errDrop := mc.DropDatabaseProperties(ctx, client.NewDropDatabasePropertiesOption(dbName, common.DatabaseMaxCollections))
	common.CheckErr(t, errDrop, true)
	_, schema1 := hp.CollPrepare.CreateCollection(ctx, t, mc, hp.NewCreateCollectionParams(hp.Int64Vec), hp.TNewFieldsOption(), hp.TNewSchemaOption())
	collections = append(collections, schema1.CollectionName)

	// verify collection num
	collectionsList, _ := mc.ListCollections(ctx, client.NewListCollectionOption())
	require.Subset(t, collectionsList, collections)
	require.GreaterOrEqual(t, len(collectionsList), maxCollections)

	// describe database after drop properties
	db, _ = mc.DescribeDatabase(ctx, client.NewDescribeDatabaseOption(dbName))
	require.Equal(t, map[string]string{}, db.Properties)
	require.Equal(t, dbName, db.Name)
}

func TestDatabasePropertiesRgReplicas(t *testing.T) {
	// create db with properties
	teardownSuite := teardownTest(t)
	defer teardownSuite(t)

	// create db
	ctx := hp.CreateContext(t, time.Second*common.DefaultTimeout)
	mc := hp.CreateDefaultMilvusClient(ctx, t)
	dbName := common.GenRandomString("db", 4)
	err := mc.CreateDatabase(ctx, client.NewCreateDatabaseOption(dbName))
	common.CheckErr(t, err, true)

	rgName := common.GenRandomString("rg", 4)
	err = mc.CreateResourceGroup(ctx, client.NewCreateResourceGroupOption(rgName))
	common.CheckErr(t, err, true)
	t.Cleanup(func() {
		_ = mc.UpdateResourceGroup(ctx, client.NewUpdateResourceGroupOption(rgName, &entity.ResourceGroupConfig{
			Requests: entity.ResourceGroupLimit{NodeNum: 0},
			Limits:   entity.ResourceGroupLimit{NodeNum: 0},
		}))
		_ = mc.DropResourceGroup(ctx, client.NewDropResourceGroupOption(rgName))
	})
	require.Eventually(t, func() bool {
		_, err := mc.DescribeResourceGroup(ctx, client.NewDescribeResourceGroupOption(rgName))
		return err == nil
	}, 5*time.Second, 200*time.Millisecond)

	// alter database properties
	err = mc.AlterDatabaseProperties(ctx, client.NewAlterDatabasePropertiesOption(dbName).
		WithProperty(common.DatabaseResourceGroups, rgName).WithProperty(common.DatabaseReplicaNumber, 2))
	common.CheckErr(t, err, true)

	// describe database
	db, _ := mc.DescribeDatabase(ctx, client.NewDescribeDatabaseOption(dbName))
	require.Equal(t, map[string]string{common.DatabaseResourceGroups: rgName, common.DatabaseReplicaNumber: "2"}, db.Properties)

	mc.UseDatabase(ctx, client.NewUseDatabaseOption(dbName))
	prepare, schema := hp.CollPrepare.CreateCollection(ctx, t, mc, hp.NewCreateCollectionParams(hp.Int64Vec), hp.TNewFieldsOption(), hp.TNewSchemaOption().TWithEnableDynamicField(true))
	prepare.InsertData(ctx, t, mc, hp.NewInsertParams(schema), hp.TNewDataOption().TWithNb(1000))
	prepare.FlushData(ctx, t, mc, schema.CollectionName)
	prepare.CreateIndex(ctx, t, mc, hp.TNewIndexParams(schema))

	// When load does not specify parameters, rg and replica Properties take effect
	_, errLoad := mc.LoadCollection(ctx, client.NewLoadCollectionOption(schema.CollectionName))
	common.CheckErr(t, errLoad, false, "resource group not found", "service resource insufficient", "resource group node not enough")

	// actually load with default rg, rg1 not existed
	taskLoad, errLoad := mc.LoadCollection(ctx, client.NewLoadCollectionOption(schema.CollectionName).WithReplica(1))
	common.CheckErr(t, errLoad, true)
	errLoad = taskLoad.Await(ctx)
	common.CheckErr(t, errLoad, true)

	_, err = mc.Query(ctx, client.NewQueryOption(schema.CollectionName).WithLimit(10))
	common.CheckErr(t, err, true)
}

func TestDatabasePropertyDeny(t *testing.T) {
	t.Skip("https://zilliz.atlassian.net/browse/VDC-7858")
	// create db with properties
	teardownSuite := teardownTest(t)
	defer teardownSuite(t)

	// create db and use db
	ctx := hp.CreateContext(t, time.Second*common.DefaultTimeout)
	mc := hp.CreateDefaultMilvusClient(ctx, t)
	dbName := common.GenRandomString("db", 4)
	err := mc.CreateDatabase(ctx, client.NewCreateDatabaseOption(dbName))
	common.CheckErr(t, err, true)

	// alter database properties and check
	err = mc.AlterDatabaseProperties(ctx, client.NewAlterDatabasePropertiesOption(dbName).
		WithProperty(common.DatabaseForceDenyWriting, true).
		WithProperty(common.DatabaseForceDenyReading, true))
	common.CheckErr(t, err, true)
	db, _ := mc.DescribeDatabase(ctx, client.NewDescribeDatabaseOption(dbName))
	require.Equal(t, map[string]string{common.DatabaseForceDenyWriting: "true", common.DatabaseForceDenyReading: "true"}, db.Properties)

	err = mc.UseDatabase(ctx, client.NewUseDatabaseOption(dbName))
	common.CheckErr(t, err, true)

	// prepare collection: create -> index -> load
	prepare, schema := hp.CollPrepare.CreateCollection(ctx, t, mc, hp.NewCreateCollectionParams(hp.Int64Vec), hp.TNewFieldsOption(), hp.TNewSchemaOption().TWithEnableDynamicField(true))
	prepare.CreateIndex(ctx, t, mc, hp.TNewIndexParams(schema))
	prepare.Load(ctx, t, mc, hp.NewLoadParams(schema.CollectionName))

	// reading
	_, err = mc.Query(ctx, client.NewQueryOption(schema.CollectionName).WithLimit(10))
	common.CheckErr(t, err, false, "access has been disabled by the administrator")

	// writing
	columnOps := hp.TNewColumnOptions()
	for _, fieldName := range hp.GetAllFieldsName(*schema) {
		columnOps = columnOps.WithColumnOption(fieldName, hp.TNewDataOption().TWithNb(10))
	}
	columns, _ := hp.GenColumnsBasedSchema(schema, columnOps)
	_, err = mc.Insert(ctx, client.NewColumnBasedInsertOption(schema.CollectionName, columns...))
	common.CheckErr(t, err, false, "access has been disabled by the administrator")
}

func TestDatabaseFakeProperties(t *testing.T) {
	// create db with properties
	teardownSuite := teardownTest(t)
	defer teardownSuite(t)

	// create db
	ctx := hp.CreateContext(t, time.Second*common.DefaultTimeout)
	mc := hp.CreateDefaultMilvusClient(ctx, t)
	dbName := common.GenRandomString("db", 4)
	err := mc.CreateDatabase(ctx, client.NewCreateDatabaseOption(dbName))
	common.CheckErr(t, err, true)

	// alter database with useless properties
	properties := map[string]any{
		"key_1": 1,
		"key2":  1.9,
		"key-3": true,
		"key.4": "a.b.c",
	}
	for key, value := range properties {
		err = mc.AlterDatabaseProperties(ctx, client.NewAlterDatabasePropertiesOption(dbName).WithProperty(key, value))
		common.CheckErr(t, err, true)
	}

	// describe database
	db, _ := mc.DescribeDatabase(ctx, client.NewDescribeDatabaseOption(dbName))
	require.EqualValues(t, map[string]string{"key_1": "1", "key2": "1.9", "key-3": "true", "key.4": "a.b.c"}, db.Properties)

	// drop database properties
	err = mc.DropDatabaseProperties(ctx, client.NewDropDatabasePropertiesOption(dbName, "aaa"))
	common.CheckErr(t, err, true)
}
