package proxy

import (
	"context"
	"testing"

	"github.com/stretchr/testify/assert"
	"google.golang.org/grpc"
	"google.golang.org/grpc/metadata"
	"google.golang.org/protobuf/encoding/prototext"
	"google.golang.org/protobuf/proto"

	"github.com/milvus-io/milvus-proto/go-api/v3/milvuspb"
	"github.com/milvus-io/milvus/pkg/v3/util"
)

func TestDatabaseInterceptor(t *testing.T) {
	ctx := context.Background()
	interceptor := DatabaseInterceptor()

	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
		return "", nil
	}

	t.Run("empty md", func(t *testing.T) {
		req := &milvuspb.CreateCollectionRequest{}
		_, err := interceptor(ctx, req, &grpc.UnaryServerInfo{}, handler)
		assert.NoError(t, err)
		assert.Equal(t, util.DefaultDBName, req.GetDbName())
	})

	t.Run("with invalid metadata", func(t *testing.T) {
		md := metadata.Pairs("xxx", "yyy")
		ctx = metadata.NewIncomingContext(ctx, md)
		req := &milvuspb.CreateCollectionRequest{}
		_, err := interceptor(ctx, req, &grpc.UnaryServerInfo{}, handler)
		assert.NoError(t, err)
		assert.Equal(t, util.DefaultDBName, req.GetDbName())
	})

	t.Run("empty req", func(t *testing.T) {
		md := metadata.Pairs("xxx", "yyy")
		ctx = metadata.NewIncomingContext(ctx, md)
		_, err := interceptor(ctx, "", &grpc.UnaryServerInfo{}, handler)
		assert.NoError(t, err)
	})

	t.Run("id-only describe keeps omitted database empty", func(t *testing.T) {
		md := metadata.Pairs(util.HeaderDBName, "db-from-header")
		ctx := metadata.NewIncomingContext(context.Background(), md)
		req := &milvuspb.DescribeCollectionRequest{CollectionID: 100}
		_, err := interceptor(ctx, req, &grpc.UnaryServerInfo{}, handler)
		assert.NoError(t, err)
		assert.Empty(t, req.GetDbName())
	})

	t.Run("name-based describe still gets database from metadata", func(t *testing.T) {
		md := metadata.Pairs(util.HeaderDBName, "db-from-header")
		ctx := metadata.NewIncomingContext(context.Background(), md)
		req := &milvuspb.DescribeCollectionRequest{CollectionName: "collection"}
		_, err := interceptor(ctx, req, &grpc.UnaryServerInfo{}, handler)
		assert.NoError(t, err)
		assert.Equal(t, "db-from-header", req.GetDbName())
	})

	t.Run("external snapshot requests get db from metadata", func(t *testing.T) {
		md := metadata.Pairs(util.HeaderDBName, "db")
		ctx := metadata.NewIncomingContext(context.Background(), md)
		testCases := []struct {
			name  string
			req   proto.Message
			dbFun func(proto.Message) string
		}{
			{
				name: "restore external snapshot",
				req:  &milvuspb.RestoreExternalSnapshotRequest{},
				dbFun: func(req proto.Message) string {
					return req.(*milvuspb.RestoreExternalSnapshotRequest).GetDbName()
				},
			},
			{
				name: "export snapshot",
				req:  &milvuspb.ExportSnapshotRequest{},
				dbFun: func(req proto.Message) string {
					return req.(*milvuspb.ExportSnapshotRequest).GetDbName()
				},
			},
		}

		for _, testCase := range testCases {
			t.Run(testCase.name, func(t *testing.T) {
				_, err := interceptor(ctx, testCase.req, &grpc.UnaryServerInfo{}, handler)
				assert.NoError(t, err)
				assert.Equal(t, "db", testCase.dbFun(testCase.req))
			})
		}
	})

	t.Run("test ok for all request", func(t *testing.T) {
		availableReqs := []proto.Message{
			&milvuspb.CreateCollectionRequest{},
			&milvuspb.DropCollectionRequest{},
			&milvuspb.TruncateCollectionRequest{},
			&milvuspb.HasCollectionRequest{},
			&milvuspb.LoadCollectionRequest{},
			&milvuspb.ReleaseCollectionRequest{},
			&milvuspb.DescribeCollectionRequest{},
			&milvuspb.BatchDescribeCollectionRequest{},
			&milvuspb.GetStatisticsRequest{},
			&milvuspb.GetCollectionStatisticsRequest{},
			&milvuspb.ShowCollectionsRequest{},
			&milvuspb.AlterCollectionRequest{},
			&milvuspb.AlterCollectionFieldRequest{},
			&milvuspb.AddCollectionFunctionRequest{},
			&milvuspb.DropCollectionFunctionRequest{},
			&milvuspb.AlterCollectionFunctionRequest{},
			&milvuspb.CreatePartitionRequest{},
			&milvuspb.DropPartitionRequest{},
			&milvuspb.HasPartitionRequest{},
			&milvuspb.LoadPartitionsRequest{},
			&milvuspb.ReleasePartitionsRequest{},
			&milvuspb.GetPartitionStatisticsRequest{},
			&milvuspb.ShowPartitionsRequest{},
			&milvuspb.GetLoadingProgressRequest{},
			&milvuspb.GetLoadStateRequest{},
			&milvuspb.CreateIndexRequest{},
			&milvuspb.DescribeIndexRequest{},
			&milvuspb.DropIndexRequest{},
			&milvuspb.AlterIndexRequest{},
			&milvuspb.GetIndexBuildProgressRequest{},
			&milvuspb.GetIndexStateRequest{},
			&milvuspb.InsertRequest{},
			&milvuspb.DeleteRequest{},
			&milvuspb.SearchRequest{},
			&milvuspb.HybridSearchRequest{},
			&milvuspb.FlushRequest{},
			&milvuspb.GetFlushStateRequest{},
			&milvuspb.QueryRequest{},
			&milvuspb.CreateAliasRequest{},
			&milvuspb.DropAliasRequest{},
			&milvuspb.AlterAliasRequest{},
			&milvuspb.ListAliasesRequest{},
			&milvuspb.DescribeAliasRequest{},
			&milvuspb.GetPersistentSegmentInfoRequest{},
			&milvuspb.GetQuerySegmentInfoRequest{},
			&milvuspb.LoadBalanceRequest{},
			&milvuspb.GetReplicasRequest{},
			&milvuspb.ImportRequest{},
			&milvuspb.RenameCollectionRequest{},
			&milvuspb.TransferReplicaRequest{},
			&milvuspb.ListImportTasksRequest{},
			&milvuspb.OperatePrivilegeRequest{Entity: &milvuspb.GrantEntity{}},
			&milvuspb.SelectGrantRequest{Entity: &milvuspb.GrantEntity{}},
			&milvuspb.ManualCompactionRequest{},
			&milvuspb.AddCollectionFieldRequest{},
			&milvuspb.AddCollectionStructFieldRequest{},
			&milvuspb.AlterCollectionSchemaRequest{},
			&milvuspb.RunAnalyzerRequest{},
			&milvuspb.RestoreExternalSnapshotRequest{},
			&milvuspb.ExportSnapshotRequest{},
			&milvuspb.RefreshExternalCollectionRequest{},
			&milvuspb.ListRefreshExternalCollectionJobsRequest{},
		}

		md := metadata.Pairs(util.HeaderDBName, "db")
		ctx = metadata.NewIncomingContext(ctx, md)
		for _, req := range availableReqs {
			before, err := proto.Marshal(req)
			assert.NoError(t, err)

			_, err = interceptor(ctx, req, &grpc.UnaryServerInfo{}, handler)
			assert.NoError(t, err)

			after, err := proto.Marshal(req)
			assert.NoError(t, err)

			assert.True(t, len(after) > len(before))
		}

		unavailableReqs := []proto.Message{
			&milvuspb.GetMetricsRequest{},
			&milvuspb.DummyRequest{},
			&milvuspb.CalcDistanceRequest{},
			&milvuspb.FlushAllRequest{},
			&milvuspb.GetCompactionStateRequest{},
			&milvuspb.GetCompactionPlansRequest{},
			&milvuspb.GetFlushAllStateRequest{},
			&milvuspb.GetImportStateRequest{},
		}

		for _, req := range unavailableReqs {
			before, err := proto.Marshal(req)
			assert.NoError(t, err)

			_, err = interceptor(ctx, req, &grpc.UnaryServerInfo{}, handler)
			assert.NoError(t, err)

			after, err := proto.Marshal(req)
			assert.NoError(t, err)

			if len(after) != len(before) {
				t.Errorf("req has been modified:%s", prototext.Format(req))
			}
		}
	})
}
