/*
 * Copyright 2021 CloudWeGo Authors
 *
 * Licensed 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 client

import (
	"context"
	"crypto/tls"
	"errors"
	"fmt"
	"reflect"
	"testing"
	"time"

	"github.com/golang/mock/gomock"

	"github.com/cloudwego/kitex/internal/client"
	igeneric "github.com/cloudwego/kitex/internal/generic"
	"github.com/cloudwego/kitex/internal/mocks"
	mocksloadbalance "github.com/cloudwego/kitex/internal/mocks/loadbalance"
	mocksnetpoll "github.com/cloudwego/kitex/internal/mocks/netpoll"
	mocksproxy "github.com/cloudwego/kitex/internal/mocks/proxy"
	mock_remote "github.com/cloudwego/kitex/internal/mocks/remote"
	"github.com/cloudwego/kitex/internal/mocks/rpc_info"
	"github.com/cloudwego/kitex/internal/test"
	"github.com/cloudwego/kitex/pkg/circuitbreak"
	"github.com/cloudwego/kitex/pkg/connpool"
	"github.com/cloudwego/kitex/pkg/diagnosis"
	"github.com/cloudwego/kitex/pkg/discovery"
	"github.com/cloudwego/kitex/pkg/endpoint"
	"github.com/cloudwego/kitex/pkg/generic"
	"github.com/cloudwego/kitex/pkg/http"
	"github.com/cloudwego/kitex/pkg/loadbalance"
	"github.com/cloudwego/kitex/pkg/proxy"
	"github.com/cloudwego/kitex/pkg/remote"
	"github.com/cloudwego/kitex/pkg/remote/trans/nphttp2/grpc"
	"github.com/cloudwego/kitex/pkg/retry"
	"github.com/cloudwego/kitex/pkg/rpcinfo"
	"github.com/cloudwego/kitex/pkg/rpcinfo/remoteinfo"
	"github.com/cloudwego/kitex/pkg/stats"
	"github.com/cloudwego/kitex/pkg/utils"
	"github.com/cloudwego/kitex/pkg/xds"
	"github.com/cloudwego/kitex/transport"
)

func TestRetryOptionDebugInfo(t *testing.T) {
	t.Run("FailurePolicy", func(t *testing.T) {
		fp := retry.NewFailurePolicy()
		fp.WithDDLStop()
		expectPolicyStr := "WithFailureRetry({StopPolicy:{MaxRetryTimes:2 MaxDurationMS:0 DisableChainStop:false DDLStop:true " +
			"CBPolicy:{ErrorRate:0.1}} BackOffPolicy:&{BackOffType:none CfgItems:map[]} RetrySameNode:false ShouldResultRetry:{ErrorRetry:false, RespRetry:false}})"
		opt := WithFailureRetry(fp)
		err := checkOneOptionDebugInfo(t, opt, expectPolicyStr)
		test.Assert(t, err == nil, err)

		fp.WithFixedBackOff(10)
		expectPolicyStr = "WithFailureRetry({StopPolicy:{MaxRetryTimes:2 MaxDurationMS:0 DisableChainStop:false DDLStop:true " +
			"CBPolicy:{ErrorRate:0.1}} BackOffPolicy:&{BackOffType:fixed CfgItems:map[fix_ms:10]} RetrySameNode:false ShouldResultRetry:{ErrorRetry:false, RespRetry:false}})"
		opt = WithFailureRetry(fp)
		err = checkOneOptionDebugInfo(t, opt, expectPolicyStr)
		test.Assert(t, err == nil, err)

		fp.WithRandomBackOff(10, 20)
		fp.DisableChainRetryStop()
		expectPolicyStr = "WithFailureRetry({StopPolicy:{MaxRetryTimes:2 MaxDurationMS:0 DisableChainStop:true DDLStop:true " +
			"CBPolicy:{ErrorRate:0.1}} BackOffPolicy:&{BackOffType:random CfgItems:map[max_ms:20 min_ms:10]} RetrySameNode:false ShouldResultRetry:{ErrorRetry:false, RespRetry:false}})"
		opt = WithFailureRetry(fp)
		err = checkOneOptionDebugInfo(t, opt, expectPolicyStr)
		test.Assert(t, err == nil, err)

		fp.WithRetrySameNode()
		expectPolicyStr = "WithFailureRetry({StopPolicy:{MaxRetryTimes:2 MaxDurationMS:0 DisableChainStop:true DDLStop:true " +
			"CBPolicy:{ErrorRate:0.1}} BackOffPolicy:&{BackOffType:random CfgItems:map[max_ms:20 min_ms:10]} RetrySameNode:true ShouldResultRetry:{ErrorRetry:false, RespRetry:false}})"
		opt = WithFailureRetry(fp)
		err = checkOneOptionDebugInfo(t, opt, expectPolicyStr)
		test.Assert(t, err == nil, err)

		fp.WithSpecifiedResultRetry(&retry.ShouldResultRetry{ErrorRetryWithCtx: func(ctx context.Context, err error, ri rpcinfo.RPCInfo) bool {
			return false
		}})
		expectPolicyStr = "WithFailureRetry({StopPolicy:{MaxRetryTimes:2 MaxDurationMS:0 DisableChainStop:true DDLStop:true " +
			"CBPolicy:{ErrorRate:0.1}} BackOffPolicy:&{BackOffType:random CfgItems:map[max_ms:20 min_ms:10]} RetrySameNode:true ShouldResultRetry:{ErrorRetry:true, RespRetry:false}})"
		opt = WithFailureRetry(fp)
		err = checkOneOptionDebugInfo(t, opt, expectPolicyStr)
		test.Assert(t, err == nil, err)
	})

	t.Run("FailurePolicy", func(t *testing.T) {
		bp := retry.NewBackupPolicy(20)
		expectPolicyStr := "WithBackupRequest({RetryDelayMS:20 StopPolicy:{MaxRetryTimes:1 MaxDurationMS:0 DisableChainStop:false " +
			"DDLStop:false CBPolicy:{ErrorRate:0.1}} RetrySameNode:false})"
		opt := WithBackupRequest(bp)
		err := checkOneOptionDebugInfo(t, opt, expectPolicyStr)
		test.Assert(t, err == nil, err)
	})

	t.Run("MixedPolicy", func(t *testing.T) {
		mp := retry.NewMixedPolicy(100)
		mp.WithDDLStop()
		expectPolicyStr := "WithMixedRetry({RetryDelayMS:100 StopPolicy:{MaxRetryTimes:1 MaxDurationMS:0 DisableChainStop:false " +
			"DDLStop:true CBPolicy:{ErrorRate:0.1}} BackOffPolicy:&{BackOffType:none CfgItems:map[]} RetrySameNode:false " +
			"ShouldResultRetry:{ErrorRetry:false, RespRetry:false}})"
		opt := WithMixedRetry(mp)
		err := checkOneOptionDebugInfo(t, opt, expectPolicyStr)
		test.Assert(t, err == nil, err)

		mp.WithSpecifiedResultRetry(&retry.ShouldResultRetry{ErrorRetryWithCtx: func(ctx context.Context, err error, ri rpcinfo.RPCInfo) bool {
			return false
		}})
		expectPolicyStr = "WithMixedRetry({RetryDelayMS:100 StopPolicy:{MaxRetryTimes:1 MaxDurationMS:0 DisableChainStop:false " +
			"DDLStop:true CBPolicy:{ErrorRate:0.1}} BackOffPolicy:&{BackOffType:none CfgItems:map[]} RetrySameNode:false " +
			"ShouldResultRetry:{ErrorRetry:true, RespRetry:false}})"
		opt = WithMixedRetry(mp)
		err = checkOneOptionDebugInfo(t, opt, expectPolicyStr)
		test.Assert(t, err == nil, err)
	})
}

func TestRetryOption(t *testing.T) {
	defer func() {
		err := recover()
		test.Assert(t, err != nil)
	}()
	fp := retry.NewFailurePolicy()
	var options []client.Option
	options = append(options, WithFailureRetry(fp))
	bp := retry.NewBackupPolicy(20)
	options = append(options, WithBackupRequest(bp))

	client.NewOptions(options)
	// both setup failure and backup retry, panic should happen
	test.Assert(t, false)
}

func TestTransportProtocolOption(t *testing.T) {
	opts := []client.Option{WithTransportProtocol(transport.GRPC)}
	options := client.NewOptions(opts)
	test.Assert(t, options.RemoteOpt.ConnPool != nil)
	test.Assert(t, options.RemoteOpt.CliHandlerFactory != nil)
	opts = []client.Option{WithTransportProtocol(transport.GRPCStreaming)}
	options = client.NewOptions(opts)
	test.Assert(t, options.RemoteOpt.GRPCStreamingConnPool != nil)
	test.Assert(t, options.RemoteOpt.GRPCStreamingCliHandlerFactory != nil)
	opts = []client.Option{WithTransportProtocol(transport.TTHeaderStreaming)}
	options = client.NewOptions(opts)
	test.Assert(t, options.RemoteOpt.TTHeaderStreamingCliHandlerFactory != nil)
}

func TestWithHostPorts(t *testing.T) {
	var options []client.Option
	options = append(options, WithHostPorts("127.0.0.1:8080"))
	opts := client.NewOptions(options)
	test.Assert(t, opts.Resolver.Name() == "127.0.0.1:8080")
	res, err := opts.Resolver.Resolve(context.Background(), "")
	test.Assert(t, err == nil)
	test.Assert(t, res.Instances[0].Address().String() == "127.0.0.1:8080")
}

func TestForwardProxy(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	fp := mocksproxy.NewMockForwardProxy(ctrl)
	fp.EXPECT().Configure(gomock.Any()).DoAndReturn(func(cfg *proxy.Config) error {
		test.Assert(t, cfg.Resolver == nil, cfg.Resolver)
		test.Assert(t, cfg.Balancer == nil, cfg.Balancer)
		return nil
	}).AnyTimes()
	fp.EXPECT().ResolveProxyInstance(gomock.Any()).DoAndReturn(func(ctx context.Context) error {
		ri := rpcinfo.GetRPCInfo(ctx)
		re := remoteinfo.AsRemoteInfo(ri.To())
		in := re.GetInstance()
		test.Assert(t, in == nil, in)
		re.SetInstance(instance505)
		return nil
	}).AnyTimes()

	var opts []Option
	opts = append(opts, WithTransHandlerFactory(newMockCliTransHandlerFactory(ctrl)))
	opts = append(opts, WithDialer(newDialer(ctrl)))
	opts = append(opts, WithDestService("destService"))
	opts = append(opts, WithProxy(fp))

	svcInfo := mocks.ServiceInfo()
	// Configure long pool and check if the pool is closed when initProxy
	// If the proxy does not configure pool, use the predefined pool
	mockLongPool := mock_remote.NewMockLongConnPool(ctrl)
	var closed bool
	mockLongPool.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(mocksnetpoll.NewMockConnection(ctrl), nil).Times(1)
	mockLongPool.EXPECT().Close().Do(func() {
		closed = true
	}).AnyTimes()
	mockLongPool.EXPECT().Put(gomock.Any()).Return(nil).AnyTimes()
	mockLongPool.EXPECT().Discard(gomock.Any()).Return(nil).AnyTimes()
	opts = append(opts, WithConnPool(mockLongPool))
	cli, err := NewClient(svcInfo, opts...)
	test.Assert(t, err == nil)
	test.Assert(t, !closed)
	mtd := mocks.MockMethod
	ctx := context.Background()
	req := new(MockTStruct)
	res := new(MockTStruct)

	err = cli.Call(ctx, mtd, req, res)
	test.Assert(t, err == nil, err)
}

func TestProxyWithResolver(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	resolver := resolver404(ctrl)
	fp := mocksproxy.NewMockForwardProxy(ctrl)
	fp.EXPECT().Configure(gomock.Any()).DoAndReturn(func(cfg *proxy.Config) error {
		test.Assert(t, cfg.Resolver == resolver)
		test.Assert(t, cfg.Balancer == nil, cfg.Balancer)
		return nil
	}).AnyTimes()
	fp.EXPECT().ResolveProxyInstance(gomock.Any()).DoAndReturn(func(ctx context.Context) error {
		ri := rpcinfo.GetRPCInfo(ctx)
		re := remoteinfo.AsRemoteInfo(ri.To())
		in := re.GetInstance()
		test.Assert(t, in == instance404[0])
		re.SetInstance(instance505)
		return nil
	}).AnyTimes()

	var opts []Option
	opts = append(opts, WithTransHandlerFactory(newMockCliTransHandlerFactory(ctrl)))
	opts = append(opts, WithDialer(newDialer(ctrl)))
	opts = append(opts, WithDestService("destService"))
	opts = append(opts, WithProxy(fp))
	opts = append(opts, WithResolver(resolver))

	svcInfo := mocks.ServiceInfo()
	cli, err := NewClient(svcInfo, opts...)
	test.Assert(t, err == nil)

	mtd := mocks.MockMethod
	ctx := context.Background()
	req := new(MockTStruct)
	res := new(MockTStruct)

	err = cli.Call(ctx, mtd, req, res)
	test.Assert(t, err == nil, err)
}

func TestProxyWithBalancer(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	lb := mocksloadbalance.NewMockLoadbalancer(ctrl)
	lb.EXPECT().GetPicker(gomock.Any()).DoAndReturn(func(entry discovery.Result) loadbalance.Picker {
		picker := mocksloadbalance.NewMockPicker(ctrl)
		picker.EXPECT().Next(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, request interface{}) discovery.Instance {
			if len(entry.Instances) > 0 {
				return entry.Instances[0]
			}
			return nil
		}).AnyTimes()
		return picker
	}).AnyTimes()
	fp := mocksproxy.NewMockForwardProxy(ctrl)
	fp.EXPECT().Configure(gomock.Any()).DoAndReturn(func(cfg *proxy.Config) error {
		test.Assert(t, cfg.Resolver == nil, cfg.Resolver)
		test.Assert(t, cfg.Balancer == lb)
		return nil
	}).AnyTimes()
	fp.EXPECT().ResolveProxyInstance(gomock.Any()).DoAndReturn(func(ctx context.Context) error {
		ri := rpcinfo.GetRPCInfo(ctx)
		re := remoteinfo.AsRemoteInfo(ri.To())
		in := re.GetInstance()
		test.Assert(t, in == nil)
		re.SetInstance(instance505)
		return nil
	}).AnyTimes()

	var opts []Option
	opts = append(opts, WithTransHandlerFactory(newMockCliTransHandlerFactory(ctrl)))
	opts = append(opts, WithDialer(newDialer(ctrl)))
	opts = append(opts, WithDestService("destService"))
	opts = append(opts, WithProxy(fp))
	opts = append(opts, WithLoadBalancer(lb))

	svcInfo := mocks.ServiceInfo()
	cli, err := NewClient(svcInfo, opts...)
	test.Assert(t, err == nil)

	mtd := mocks.MockMethod
	ctx := context.Background()
	req := new(MockTStruct)
	res := new(MockTStruct)

	err = cli.Call(ctx, mtd, req, res)
	test.Assert(t, err == nil)
}

func TestProxyWithResolverAndBalancer(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	lb := mocksloadbalance.NewMockLoadbalancer(ctrl)
	lb.EXPECT().GetPicker(gomock.Any()).DoAndReturn(func(entry discovery.Result) loadbalance.Picker {
		picker := mocksloadbalance.NewMockPicker(ctrl)
		picker.EXPECT().Next(gomock.Any(), gomock.Any()).Return(instance505).AnyTimes()
		return picker
	}).AnyTimes()
	lb.EXPECT().Name().Return("mock_load_balancer").AnyTimes()
	resolver := resolver404(ctrl)
	fp := mocksproxy.NewMockForwardProxy(ctrl)
	fp.EXPECT().Configure(gomock.Any()).DoAndReturn(func(cfg *proxy.Config) error {
		test.Assert(t, cfg.Resolver == resolver)
		test.Assert(t, cfg.Balancer == lb)
		return nil
	}).AnyTimes()
	fp.EXPECT().ResolveProxyInstance(gomock.Any()).DoAndReturn(func(ctx context.Context) error {
		ri := rpcinfo.GetRPCInfo(ctx)
		re := remoteinfo.AsRemoteInfo(ri.To())
		in := re.GetInstance()
		test.Assert(t, in == instance505)
		return nil
	}).AnyTimes()

	var opts []Option
	opts = append(opts, WithTransHandlerFactory(newMockCliTransHandlerFactory(ctrl)))
	opts = append(opts, WithDialer(newDialer(ctrl)))
	opts = append(opts, WithDestService("destService"))
	opts = append(opts, WithProxy(fp))
	opts = append(opts, WithResolver(resolver))
	opts = append(opts, WithLoadBalancer(lb))

	svcInfo := mocks.ServiceInfo()
	cli, err := NewClient(svcInfo, opts...)
	test.Assert(t, err == nil)

	mtd := mocks.MockMethod
	ctx := context.Background()
	req := new(MockTStruct)
	res := new(MockTStruct)

	err = cli.Call(ctx, mtd, req, res)
	test.Assert(t, err == nil)
}

func TestProxyWithConnPool(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	type mockLongConnPool struct {
		*mock_remote.MockLongConnPool
		*mock_remote.MockConnPoolReporter
	}

	fp := mocksproxy.NewMockForwardProxy(ctrl)
	fp.EXPECT().Configure(gomock.Any()).DoAndReturn(func(cfg *proxy.Config) error {
		cp := mock_remote.NewMockLongConnPool(ctrl)
		cp.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(mocksnetpoll.NewMockConnection(ctrl), nil).Times(1)
		cp.EXPECT().Put(gomock.Any()).Return(nil).AnyTimes()
		cp.EXPECT().Discard(gomock.Any()).Return(nil).AnyTimes()
		cp.EXPECT().Close().Return(nil).Times(1)
		rp := mock_remote.NewMockConnPoolReporter(ctrl)
		rp.EXPECT().EnableReporter().Times(1)
		cfg.Pool = &mockLongConnPool{cp, rp}
		return nil
	}).AnyTimes()
	fp.EXPECT().ResolveProxyInstance(gomock.Any()).Return(nil).AnyTimes()

	var opts []Option
	opts = append(opts, WithTransHandlerFactory(newMockCliTransHandlerFactory(ctrl)))
	opts = append(opts, WithDestService("destService"))
	opts = append(opts, WithProxy(fp))
	opts = append(opts, WithConnReporterEnabled())
	opts = append(opts, WithResolver(resolver404(ctrl)))
	// Preconfigured longConnPool should be closed because the proxy configured the pool
	mockLongPool := mock_remote.NewMockLongConnPool(ctrl)
	var closed bool
	mockLongPool.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(mocksnetpoll.NewMockConnection(ctrl), nil).Times(0)
	mockLongPool.EXPECT().Close().Do(func() {
		closed = true
	}).AnyTimes()
	mockLongPool.EXPECT().Put(gomock.Any()).Return(nil).AnyTimes()
	mockLongPool.EXPECT().Discard(gomock.Any()).Return(nil).AnyTimes()
	opts = append(opts, WithConnPool(mockLongPool))

	svcInfo := mocks.ServiceInfo()
	cli, err := NewClient(svcInfo, opts...)
	test.Assert(t, err == nil)
	test.Assert(t, closed) // should be true

	mtd := mocks.MockMethod
	ctx := context.Background()
	req := new(MockTStruct)
	res := new(MockTStruct)

	err = cli.Call(ctx, mtd, req, res)
	test.Assert(t, err == nil)

	err = cli.(interface{ Close() error }).Close()
	test.Assert(t, err == nil)
}

var (
	// mock middleware, do nothing
	md = func(next endpoint.Endpoint) endpoint.Endpoint {
		return func(ctx context.Context, req, resp interface{}) (err error) {
			return nil
		}
	}
	httpResolver = http.NewDefaultResolver()
)

type mockDiagnosis struct {
	probes map[diagnosis.ProbeName]diagnosis.ProbeFunc
}

func (m *mockDiagnosis) RegisterProbeFunc(name diagnosis.ProbeName, probeFunc diagnosis.ProbeFunc) {
	m.probes[name] = probeFunc
}

func (m *mockDiagnosis) ProbePairs() map[diagnosis.ProbeName]diagnosis.ProbeFunc {
	return m.probes
}

func TestWithInstanceMW(t *testing.T) {
	opts := client.NewOptions([]client.Option{WithInstanceMW(md)})
	test.Assert(t, len(opts.IMWBs) == 1, len(opts.IMWBs))
}

func TestWithHTTPResolver(t *testing.T) {
	opts := client.NewOptions([]client.Option{WithHTTPResolver(httpResolver)})
	test.DeepEqual(t, opts.HTTPResolver, httpResolver)
}

func TestShortConnection(t *testing.T) {
	opts := client.NewOptions([]client.Option{WithShortConnection()})
	test.Assert(t, opts.PoolCfg != nil, opts.PoolCfg)
}

func TestWithMuxConnection(t *testing.T) {
	connNum := 100
	opts := client.NewOptions([]client.Option{WithMuxConnection(connNum)})
	test.Assert(t, opts.RemoteOpt.ConnPool != nil)
	test.Assert(t, opts.RemoteOpt.CliHandlerFactory != nil)
	test.Assert(t, opts.Configs.TransportProtocol() == transport.TTHeaderFramed, opts.Configs.TransportProtocol())
}

func TestWithTimeoutProvider(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	mockTimeoutProvider := rpc_info.NewMockTimeoutProvider(ctrl)
	opts := client.NewOptions([]client.Option{WithTimeoutProvider(mockTimeoutProvider)})
	test.DeepEqual(t, opts.Timeouts, mockTimeoutProvider)
}

func TestWithStatsLevel(t *testing.T) {
	opts := client.NewOptions([]client.Option{WithStatsLevel(stats.LevelDisabled)})
	test.Assert(t, opts.StatsLevel != nil && *opts.StatsLevel == stats.LevelDisabled, opts.StatsLevel)
}

func TestWithCodec(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	mockCodec := mock_remote.NewMockCodec(ctrl)
	opts := client.NewOptions([]client.Option{WithCodec(mockCodec)})
	test.DeepEqual(t, opts.RemoteOpt.Codec, mockCodec)
}

func TestWithPayloadCodec(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	mockPayloadCodec := mock_remote.NewMockPayloadCodec(ctrl)
	opts := client.NewOptions([]client.Option{WithPayloadCodec(mockPayloadCodec)})
	test.DeepEqual(t, opts.RemoteOpt.PayloadCodec, mockPayloadCodec)
}

func TestWithConnReporterEnabled(t *testing.T) {
	opts := client.NewOptions([]client.Option{WithConnReporterEnabled()})
	test.Assert(t, opts.RemoteOpt.EnableConnPoolReporter)
}

func TestWithCircuitBreaker(t *testing.T) {
	opts := client.NewOptions([]client.Option{
		WithCircuitBreaker(circuitbreak.NewCBSuite(func(ri rpcinfo.RPCInfo) string { return "" })),
	})
	test.Assert(t, opts.CBSuite != nil)
}

const mockUint32Size uint32 = 0

func TestWithGRPCConnPoolSize(t *testing.T) {
	opts := client.NewOptions([]client.Option{WithGRPCConnPoolSize(mockUint32Size)})
	test.Assert(t, opts.GRPCConnPoolSize == mockUint32Size, opts.GRPCConnPoolSize)
}

func TestWithGRPCInitialWindowSize(t *testing.T) {
	opts := client.NewOptions([]client.Option{WithGRPCInitialWindowSize(mockUint32Size)})
	test.Assert(t, opts.GRPCConnectOpts.InitialWindowSize == mockUint32Size, opts.GRPCConnectOpts.InitialWindowSize)
}

func TestWithGRPCInitialConnWindowSize(t *testing.T) {
	opts := client.NewOptions([]client.Option{WithGRPCInitialConnWindowSize(mockUint32Size)})
	test.Assert(t, opts.GRPCConnectOpts.InitialConnWindowSize == mockUint32Size,
		opts.GRPCConnectOpts.InitialConnWindowSize)
}

func TestWithGRPCMaxHeaderListSize(t *testing.T) {
	opts := client.NewOptions([]client.Option{WithGRPCMaxHeaderListSize(mockUint32Size)})
	test.Assert(t,
		opts.GRPCConnectOpts.MaxHeaderListSize != nil &&
			*opts.GRPCConnectOpts.MaxHeaderListSize == mockUint32Size,
		opts.GRPCConnectOpts.MaxHeaderListSize)
}

func TestWithGRPCKeepaliveParams(t *testing.T) {
	opts := client.NewOptions(
		[]client.Option{
			WithGRPCKeepaliveParams(grpc.ClientKeepalive{
				Time:                5 * time.Second, // less than  grpc.KeepaliveMinPingTime(5s)
				Timeout:             20 * time.Second,
				PermitWithoutStream: true,
			}),
		})
	test.Assert(t, opts.GRPCConnectOpts.KeepaliveParams.Time == grpc.KeepaliveMinPingTime,
		opts.GRPCConnectOpts.KeepaliveParams.Time)
	test.Assert(t, opts.GRPCConnectOpts.KeepaliveParams.Timeout == 20*time.Second,
		opts.GRPCConnectOpts.KeepaliveParams.Timeout)
	test.Assert(t, opts.GRPCConnectOpts.KeepaliveParams.PermitWithoutStream)
}

func TestWithHTTPConnection(t *testing.T) {
	opts := client.NewOptions([]client.Option{WithHTTPConnection()})
	test.Assert(t, opts.RemoteOpt.CliHandlerFactory != nil)
}

func TestWithClientBasicInfo(t *testing.T) {
	mockEBI := &rpcinfo.EndpointBasicInfo{}
	opts := client.NewOptions([]client.Option{WithClientBasicInfo(mockEBI)})
	test.Assert(t, opts.Cli == mockEBI)
}

func TestWithDiagnosisService(t *testing.T) {
	mockDS := &mockDiagnosis{
		make(map[diagnosis.ProbeName]diagnosis.ProbeFunc),
	}
	opts := client.NewOptions([]client.Option{
		WithDiagnosisService(mockDS),
	})
	test.Assert(t, opts.DebugService == mockDS, opts.DebugService)
}

func mockACLRule(ctx context.Context, request interface{}) (reason error) {
	return nil
}

func TestWithACLRules(t *testing.T) {
	opts := client.NewOptions([]client.Option{WithACLRules(mockACLRule)})
	test.Assert(t, reflect.ValueOf(mockACLRule).Pointer() == reflect.ValueOf(opts.ACLRules[0]).Pointer())
}

func TestWithFirstMetaHandler(t *testing.T) {
	mockMetaHandler := &mock_remote.MockMetaHandler{}
	opts := client.NewOptions([]client.Option{WithFirstMetaHandler(mockMetaHandler)})
	test.DeepEqual(t, opts.MetaHandlers[0], mockMetaHandler)
}

func TestWithMetaHandler(t *testing.T) {
	mockMetaHandler := &mock_remote.MockMetaHandler{}
	opts := client.NewOptions([]client.Option{WithMetaHandler(mockMetaHandler)})
	test.DeepEqual(t, opts.MetaHandlers[1], mockMetaHandler)
}

func TestWithConnPool(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	mockPool := mock_remote.NewMockConnPool(ctrl)
	opts := client.NewOptions([]client.Option{WithConnPool(mockPool)})
	test.Assert(t, opts.RemoteOpt.ConnPool == mockPool)
}

func TestWithRetryContainer(t *testing.T) {
	mockRetryContainer := &retry.Container{}
	opts := client.NewOptions([]client.Option{WithRetryContainer(mockRetryContainer)})
	test.Assert(t, opts.UnaryOptions.RetryContainer == mockRetryContainer)
}

func TestWithGeneric(t *testing.T) {
	g := generic.BinaryThriftGeneric()
	opts := client.NewOptions([]client.Option{WithGeneric(g)})
	test.DeepEqual(t, opts.RemoteOpt.PayloadCodec, g.GetExtra(igeneric.BinaryThriftGenericV1PayloadCodecKey).(remote.PayloadCodec))
}

func TestWithCloseCallbacks(t *testing.T) {
	opts := client.NewOptions([]client.Option{WithCloseCallbacks(func() error { return nil })})
	test.Assert(t, len(opts.CloseCallbacks) > 0)
}

func TestWithErrorHandler(t *testing.T) {
	errHandler := func(context.Context, error) error { return nil }
	opts := client.NewOptions([]client.Option{WithErrorHandler(errHandler)})
	test.Assert(t, reflect.ValueOf(opts.ErrHandle).Pointer() == reflect.ValueOf(errHandler).Pointer())
}

func TestWithBoundHandler(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	mockInboundHandler := mock_remote.NewMockInboundHandler(ctrl)
	opts := client.NewOptions([]client.Option{WithBoundHandler(mockInboundHandler)})
	test.Assert(t, len(opts.RemoteOpt.Outbounds) == 0)
	test.Assert(t, len(opts.RemoteOpt.Inbounds) == 1)

	opts = client.NewOptions([]client.Option{WithBoundHandler(mockInboundHandler), WithBoundHandler(mockInboundHandler)})
	test.Assert(t, len(opts.RemoteOpt.Outbounds) == 0)
	test.Assert(t, len(opts.RemoteOpt.Inbounds) == 1)

	mockInboundHandler2 := mock_remote.NewMockInboundHandler(ctrl)
	opts = client.NewOptions([]client.Option{WithBoundHandler(mockInboundHandler), WithBoundHandler(mockInboundHandler2)})
	test.Assert(t, len(opts.RemoteOpt.Outbounds) == 0)
	test.Assert(t, len(opts.RemoteOpt.Inbounds) == 1)

	mockOutboundHandler := mock_remote.NewMockOutboundHandler(ctrl)
	opts = client.NewOptions([]client.Option{WithBoundHandler(mockOutboundHandler)})
	test.Assert(t, len(opts.RemoteOpt.Outbounds) == 1)
	test.Assert(t, len(opts.RemoteOpt.Inbounds) == 0)

	opts = client.NewOptions(
		[]client.Option{WithBoundHandler(mockOutboundHandler), WithBoundHandler(mockOutboundHandler)})
	test.Assert(t, len(opts.RemoteOpt.Outbounds) == 1)
	test.Assert(t, len(opts.RemoteOpt.Inbounds) == 0)

	mockOutboundHandler2 := mock_remote.NewMockOutboundHandler(ctrl)
	opts = client.NewOptions([]client.Option{
		WithBoundHandler(mockOutboundHandler), WithBoundHandler(mockOutboundHandler2),
	})
	test.Assert(t, len(opts.RemoteOpt.Outbounds) == 1)
	test.Assert(t, len(opts.RemoteOpt.Inbounds) == 0)

	mockDuplexBoundHandler := mock_remote.NewMockDuplexBoundHandler(ctrl)
	opts = client.NewOptions([]client.Option{WithBoundHandler(mockDuplexBoundHandler), WithBoundHandler(mockDuplexBoundHandler)})
	test.Assert(t, len(opts.RemoteOpt.Outbounds) == 1)
	test.Assert(t, len(opts.RemoteOpt.Inbounds) == 1)
}

// collection of options
type mockSuite struct{}

var (
	mockEndpointBasicInfo = &rpcinfo.EndpointBasicInfo{}
	mockDiagnosisService  = &mockDiagnosis{make(map[diagnosis.ProbeName]diagnosis.ProbeFunc)}
	mockRetryContainer    = &retry.Container{}
)

func (m *mockSuite) Options() []Option {
	return []Option{
		WithClientBasicInfo(mockEndpointBasicInfo),
		WithDiagnosisService(mockDiagnosisService),
		WithRetryContainer(mockRetryContainer),
	}
}

func TestWithSuite(t *testing.T) {
	var suite *mockSuite
	options := []client.Option{
		WithSuite(suite),
	}
	opts := client.NewOptions(options)
	test.Assert(t, opts.Cli == mockEndpointBasicInfo)
	test.Assert(t, reflect.DeepEqual(opts.DebugService, mockDiagnosisService))
	test.Assert(t, opts.UnaryOptions.RetryContainer == mockRetryContainer)
}

func TestWithLongConnectionOption(t *testing.T) {
	idleCfg := connpool.IdleConfig{}
	options := []client.Option{
		WithLongConnection(idleCfg),
	}
	opts := client.NewOptions(options)
	test.Assert(t, opts.PoolCfg.MaxIdleTimeout == 30*time.Second) // defaultMaxIdleTimeout
	test.Assert(t, opts.PoolCfg.MaxIdlePerAddress == 1)           // default
	test.Assert(t, opts.PoolCfg.MaxIdleGlobal == 1<<20)           // default
}

func TestWithWarmingUpOption(t *testing.T) {
	options := []client.Option{
		WithWarmingUp(mockWarmupOption),
	}
	opt := client.NewOptions(options)
	test.Assert(t, opt.WarmUpOption == mockWarmupOption)
}

func TestWithFramedTransport(t *testing.T) {
	options := []client.Option{
		WithFramedTransport(),
	}
	opt := client.NewOptions(options)
	test.Assert(t, opt.Configs.TransportProtocol() == transport.Framed)
}

func TestWithConnMetric(t *testing.T) {
	options := []client.Option{
		WithConnMetric(),
	}
	opt := client.NewOptions(options)
	test.Assert(t, opt.RemoteOpt.EnableConnPoolReporter == true)
}

func TestWithXDSSuite(t *testing.T) {
	// failed
	s := xds.ClientSuite{}
	options := []client.Option{
		WithXDSSuite(s),
	}
	opt := client.NewOptions(options)
	test.Assert(t, opt.XDSEnabled == false)
	test.Assert(t, opt.XDSRouterMiddleware == nil)
	test.Assert(t, opt.Resolver == nil)

	// succeed
	s = xds.ClientSuite{
		RouterMiddleware: endpoint.DummyMiddleware,
		Resolver:         discovery.SynthesizedResolver{},
	}
	options = []client.Option{
		WithXDSSuite(s),
	}
	opt = client.NewOptions(options)
	test.Assert(t, opt.XDSEnabled == true)
	test.Assert(t, opt.XDSRouterMiddleware != nil)
	test.Assert(t, opt.Resolver != nil)
}

func TestWithGRPCTLSConfig(t *testing.T) {
	cfg := &tls.Config{}
	opts := client.NewOptions([]client.Option{WithGRPCTLSConfig(cfg)})
	test.Assert(t, opts.GRPCConnectOpts != nil)
}

func TestTailOption(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	opt := Option{F: func(o *client.Options, di *utils.Slice) {
		if o.RemoteOpt.ConnPool == nil {
			panic("invalid Dialer: nil")
		}
		o.RemoteOpt.Dialer = newDialer(ctrl)
	}}
	opt = TailOption(opt)
	opts := client.NewOptions([]client.Option{opt, WithConnPool(mock_remote.NewMockLongConnPool(ctrl))})
	test.Assert(t, opts.RemoteOpt.Dialer != nil)
}

func checkOneOptionDebugInfo(t *testing.T, opt Option, expectStr string) error {
	o := &Options{}
	o.Apply([]Option{opt})
	if len(o.DebugInfo) != 1 {
		return errors.New("length of DebugInfo is unexpected")
	}
	if o.DebugInfo[0] != expectStr {
		return fmt.Errorf("DebugInfo not match with expect str:\n debugInfo=%s", o.DebugInfo[0])
	}
	return nil
}
