// Licensed to the LF AI & Data foundation under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package accesslog

import (
	"context"
	"net"
	"strings"
	"testing"

	"github.com/stretchr/testify/suite"
	"go.opentelemetry.io/otel"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/metadata"
	"google.golang.org/grpc/peer"
	"google.golang.org/grpc/status"

	"github.com/milvus-io/milvus-proto/go-api/v2/milvuspb"
	"github.com/milvus-io/milvus/internal/proxy/accesslog/info"
	"github.com/milvus-io/milvus/pkg/tracer"
	"github.com/milvus-io/milvus/pkg/util"
	"github.com/milvus-io/milvus/pkg/util/crypto"
	"github.com/milvus-io/milvus/pkg/util/merr"
	"github.com/milvus-io/milvus/pkg/util/typeutil"
)

type LogFormatterSuite struct {
	suite.Suite

	serverinfo *grpc.UnaryServerInfo
	reqs       []interface{}
	resps      []interface{}
	errs       []error
	username   string
	traceID    string
	ctx        context.Context
}

func (s *LogFormatterSuite) SetupSuite() {
	s.username = "test-user"
	s.ctx = peer.NewContext(
		context.Background(),
		&peer.Peer{
			Addr: &net.IPAddr{
				IP:   net.IPv4(0, 0, 0, 0),
				Zone: "test",
			},
		})

	md := metadata.Pairs(util.HeaderAuthorize, crypto.Base64Encode("mockUser:mockPass"))
	s.ctx = metadata.NewIncomingContext(s.ctx, md)
	s.traceID = "test-trace"
	s.serverinfo = &grpc.UnaryServerInfo{
		FullMethod: "test",
	}
	s.reqs = []interface{}{
		&milvuspb.QueryRequest{
			DbName:         "test-db",
			CollectionName: "test-collection",
			PartitionNames: []string{"test-partition-1", "test-partition-2"},
		},
		&milvuspb.SearchRequest{
			DbName:         "test-db",
			CollectionName: "test-collection",
			PartitionNames: []string{"test-partition-1", "test-partition-2"},
		},
		&milvuspb.InsertRequest{
			DbName:         "test-db",
			CollectionName: "test-collection",
			PartitionName:  "test-partition-1",
		},
	}

	s.resps = []interface{}{
		&milvuspb.QueryResults{
			Status: merr.Status(nil),
		},
		&milvuspb.MutationResult{
			Status: merr.Status(merr.ErrCollectionNotFound),
		},
		merr.Status(nil),
	}

	s.errs = []error{nil, nil, status.Errorf(codes.Unavailable, "")}
}

func (s *LogFormatterSuite) TestFormatNames() {
	fmt := "{$database_name}: $collection_name: $partition_name"
	formatter := NewFormatter(fmt)

	for _, req := range s.reqs {
		i := info.NewGrpcAccessInfo(s.ctx, s.serverinfo, req)
		fs := formatter.Format(i)
		s.False(strings.Contains(fs, info.Unknown))
	}

	i := info.NewGrpcAccessInfo(s.ctx, s.serverinfo, nil)
	fs := formatter.Format(i)
	s.True(strings.Contains(fs, info.Unknown))
}

func (s *LogFormatterSuite) TestFormatTime() {
	fmt := "$time_now: $time_start: $time_end: $time_cost: $time_now"
	formatter := NewFormatter(fmt)

	for id, req := range s.reqs {
		i := info.NewGrpcAccessInfo(s.ctx, s.serverinfo, req)
		fs := formatter.Format(i)
		s.True(strings.Contains(fs, info.Unknown))
		i.UpdateCtx(s.ctx)
		i.SetResult(s.resps[id], s.errs[id])
		fs = formatter.Format(i)
		s.False(strings.Contains(fs, info.Unknown))
	}
}

func (s *LogFormatterSuite) TestFormatUserInfo() {
	fmt := "$user_name: $user_addr"
	formatter := NewFormatter(fmt)

	for _, req := range s.reqs {
		i := info.NewGrpcAccessInfo(s.ctx, s.serverinfo, req)
		fs := formatter.Format(i)
		s.False(strings.Contains(fs, info.Unknown))
	}

	// test unknown
	i := info.NewGrpcAccessInfo(context.Background(), &grpc.UnaryServerInfo{}, nil)
	fs := formatter.Format(i)
	s.True(strings.Contains(fs, info.Unknown))
}

func (s *LogFormatterSuite) TestFormatMethodInfo() {
	fmt := "$method_name: $method_status $trace_id"
	formatter := NewFormatter(fmt)

	metaContext := metadata.AppendToOutgoingContext(s.ctx, info.ClientRequestIDKey, s.traceID)
	for _, req := range s.reqs {
		i := info.NewGrpcAccessInfo(metaContext, s.serverinfo, req)
		fs := formatter.Format(i)
		s.True(strings.Contains(fs, s.traceID))
	}

	tracer.Init()
	traceContext, traceSpan := otel.Tracer(typeutil.ProxyRole).Start(s.ctx, "test")
	trueTraceID := traceSpan.SpanContext().TraceID().String()
	for _, req := range s.reqs {
		i := info.NewGrpcAccessInfo(traceContext, s.serverinfo, req)
		fs := formatter.Format(i)
		s.True(strings.Contains(fs, trueTraceID))
	}
}

func (s *LogFormatterSuite) TestFormatMethodResult() {
	fmt := "$method_name: $method_status $response_size $error_code $error_msg"
	formatter := NewFormatter(fmt)

	for id, req := range s.reqs {
		i := info.NewGrpcAccessInfo(s.ctx, s.serverinfo, req)
		fs := formatter.Format(i)
		s.True(strings.Contains(fs, info.Unknown))

		i.SetResult(s.resps[id], s.errs[id])
		fs = formatter.Format(i)
		s.False(strings.Contains(fs, info.Unknown))
	}
}

func (s *LogFormatterSuite) TestParseConfigKeyFailed() {
	configKey := ".testf.invalidSub"
	_, _, err := parseConfigKey(configKey)
	s.Error(err)
}

func TestLogFormatter(t *testing.T) {
	suite.Run(t, new(LogFormatterSuite))
}
