/*
 * Copyright 2024 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 server

import (
	"context"
	"fmt"
	"testing"
	"unsafe"

	"github.com/bytedance/gopkg/cloud/metainfo"

	"github.com/cloudwego/kitex/internal/mocks"
	"github.com/cloudwego/kitex/internal/test"
	"github.com/cloudwego/kitex/pkg/consts"
	"github.com/cloudwego/kitex/pkg/endpoint"
	"github.com/cloudwego/kitex/pkg/rpcinfo"
	"github.com/cloudwego/kitex/pkg/serviceinfo"
)

const serviceName = "MockService"

const methodName = "method"

type serviceInline interface {
	BuildServiceInlineInvokeChain() endpoint.Endpoint
}

func constructClientRPCInfo() rpcinfo.RPCInfo {
	rpcStats := rpcinfo.AsMutableRPCStats(rpcinfo.NewRPCStats())
	// Export read-only views to external users and keep a mapping for internal users.
	ri := rpcinfo.NewRPCInfo(
		rpcinfo.EmptyEndpointInfo(),
		rpcinfo.FromBasicInfo(&rpcinfo.EndpointBasicInfo{
			ServiceName: "client",
			Method:      methodName,
			Tags:        nil,
		}),
		rpcinfo.NewServerInvocation(),
		rpcinfo.AsMutableRPCConfig(rpcinfo.NewRPCConfig()).Clone().ImmutableView(),
		rpcStats.ImmutableView(),
	)
	if v, ok := ri.Invocation().(rpcinfo.InvocationSetter); ok {
		v.SetExtra(consts.SERVICE_INLINE_SERVICE_NAME, serviceName)
	}
	return ri
}

func newServiceInfo() *serviceinfo.ServiceInfo {
	methods := map[string]serviceinfo.MethodInfo{
		methodName: serviceinfo.NewMethodInfo(mockHandler, nil, nil, false),
	}

	svcInfo := &serviceinfo.ServiceInfo{
		ServiceName: serviceName,
		Methods:     methods,
		Extra: map[string]interface{}{
			"PackageName": "mock",
		},
	}
	return svcInfo
}

func mockHandler(ctx context.Context, handler, args, result interface{}) error {
	serverRPCInfo := rpcinfo.GetRPCInfo(ctx)
	if serverRPCInfo.Invocation().ServiceName() != serviceName {
		return fmt.Errorf("service name mismatch: expected service name, got %s", serviceName)
	}

	if ctx.Value(consts.CtxKeyMethod).(string) != methodName {
		return fmt.Errorf("method name mismatch: expected method name, got %s", methodName)
	}
	if v, ok := metainfo.GetValue(ctx, "Key2"); ok {
		if v != "Value2" {
			return fmt.Errorf("method value mismatch: expected Value2, got %s", v)
		}
	}

	if v, ok := metainfo.GetValue(ctx, "KeyTmp"); ok {
		if v != "ValueTmp" {
			return fmt.Errorf("metainfo value mismatch: expected ValueTmp, got %s", v)
		}
	}

	if v, ok := metainfo.GetPersistentValue(ctx, "KeyPersist"); ok {
		if v != "ValuePersist" {
			return fmt.Errorf("metainfo value mismatch: expected ValuePersist, got %s", v)
		}
	}

	ctx = metainfo.WithBackwardValues(ctx)
	if ok := metainfo.SendBackwardValue(ctx, "BackwardKey", "BackwardValue"); !ok {
		return fmt.Errorf("set metainfo backward fail")
	}

	if v, ok := ctx.Value(consts.SERVICE_INLINE_CUSTOM_CTX_KEY).(string); !ok || v != "custom_ctx_key" {
		return fmt.Errorf("custom ctx key mismatch: expected custom_ctx_key, got %s", v)
	}

	return nil
}

func TestServiceInline(t *testing.T) {
	svr := NewServer()
	err := svr.RegisterService(newServiceInfo(), mocks.MyServiceHandler())
	test.Assert(t, err == nil, err)

	if iface, ok := svr.(serviceInline); ok {
		cliCtx := context.Background()
		cliRPCInfo := constructClientRPCInfo()
		cliCtx = context.WithValue(cliCtx, consts.SERVICE_INLINE_RPCINFO_KEY, unsafe.Pointer(&cliRPCInfo))
		cliCtx = context.WithValue(cliCtx, consts.SERVICE_INLINE_CUSTOM_CTX_KEY, "custom_ctx_key")

		eps := iface.BuildServiceInlineInvokeChain()
		// metainfo check
		cliCtx = metainfo.WithBackwardValues(cliCtx)
		cliCtx = metainfo.WithValue(cliCtx, "KeyTmp", "ValueTmp")
		cliCtx = metainfo.WithPersistentValue(cliCtx, "KeyPersist", "ValuePersist")
		err = eps(cliCtx, nil, nil)
		test.Assert(t, err == nil, err)

		if val, ok := metainfo.GetBackwardValue(cliCtx, "BackwardKey"); !ok || val != "BackwardValue" {
			test.Assert(t, val == "BackwardValue", fmt.Errorf("backward info[%s] is not right, expect=%s, actual=%s", "BackwardKey", "BackwardValue", val))
		}
	}
}
