package errs

import (
	"context"
	"errors"
	"fmt"
	"testing"

	"github.com/smartystreets/goconvey/convey"
	"github.com/tidbcloud/pkgv2/bizerr"
	"go.buf.build/protocolbuffers/go/linuxgit/tidbcloud/base"
	"google.golang.org/grpc"
)

func TestGetBaseResp(t *testing.T) {
	type respStruct struct {
		BaseResp *base.BaseResp
	}

	convey.Convey("test get base resp", t, func() {
		convey.Convey("nil", func() {
			convey.So(GetBaseResp(nil), convey.ShouldBeNil)
		})

		convey.Convey("base resp is nil", func() {
			resp := respStruct{nil}
			extractedResp := GetBaseResp(&resp)

			convey.So(extractedResp.ErrCode, convey.ShouldEqual, 0)
			convey.So(extractedResp.ErrMsg, convey.ShouldEqual, "")
		})

		convey.Convey("base resp is not nil", func() {
			resp := respStruct{&base.BaseResp{ErrCode: 1, ErrMsg: "hello"}}
			extractedResp := GetBaseResp(&resp)

			convey.So(extractedResp.ErrCode, convey.ShouldEqual, 1)
			convey.So(extractedResp.ErrMsg, convey.ShouldEqual, "hello")
		})
	})
}

func TestIsBizError(t *testing.T) {
	convey.Convey("test is bizerror", t, func() {
		convey.So(isBizErr(errors.New("hello world")), convey.ShouldBeFalse)
		convey.So(isBizErr(bizerr.New(1, "hello")), convey.ShouldBeTrue)
	})
}

type ReqWithBaseReq struct {
	BaseReq *base.BaseReq
	Foo     string `protobuf:"bytes,2,opt,name=foo,proto3" json:"foo,omitempty"`
}

type RespWithBaseResp struct {
	BaseResp *base.BaseResp
	Foo      string `protobuf:"bytes,2,opt,name=foo,proto3" json:"foo,omitempty"`
}

func TestUnaryServerInterceptor(t *testing.T) {
	convey.Convey("TestUnaryServerInterceptor", t, func() {
		ctx := context.Background()
		req := &ReqWithBaseReq{}
		resp := 1
		fn := UnaryServerInterceptor()
		handler := grpc.UnaryHandler(func(ctx context.Context, req interface{}) (interface{}, error) {
			return resp, nil
		})

		convey.Convey("normal case", func() {
			r1, err := fn(ctx, req, &grpc.UnaryServerInfo{}, handler)
			convey.So(err, convey.ShouldBeNil)
			convey.So(r1, convey.ShouldEqual, resp)
		})
		convey.Convey("should set code and msg if err is BizError and resp is not nil", func() {
			handler = grpc.UnaryHandler(func(ctx context.Context, req interface{}) (interface{}, error) {
				return &RespWithBaseResp{}, bizerr.New(bizerr.ErrCodeParams, "this is message")
			})

			r1, err := fn(ctx, req, &grpc.UnaryServerInfo{}, handler)
			convey.So(err, convey.ShouldBeNil)
			convey.So(r1.(*RespWithBaseResp).BaseResp.ErrCode, convey.ShouldEqual, bizerr.ErrCodeParams)
			convey.So(r1.(*RespWithBaseResp).BaseResp.ErrMsg, convey.ShouldEqual, "this is message")
		})
		convey.Convey("[tierr]should set code and msg if err is BizError and resp is not nil", func() {
			handler = grpc.UnaryHandler(func(ctx context.Context, req interface{}) (interface{}, error) {
				return &RespWithBaseResp{}, NewBadRequest("this is message", nil)
			})

			r1, err := fn(ctx, req, &grpc.UnaryServerInfo{}, handler)
			convey.So(err, convey.ShouldBeNil)
			convey.So(r1.(*RespWithBaseResp).BaseResp.ErrCode, convey.ShouldEqual, bizerr.ErrCodeParams)
			convey.So(r1.(*RespWithBaseResp).BaseResp.ErrCode, convey.ShouldEqual, ErrCodeBadRequest)
			convey.So(r1.(*RespWithBaseResp).BaseResp.ErrMsg, convey.ShouldEqual, "this is message")
		})
		convey.Convey("should do nothing if err is BizError but resp is  nil", func() {
			handler = grpc.UnaryHandler(func(ctx context.Context, req interface{}) (interface{}, error) {
				return nil, bizerr.New(bizerr.ErrCodeParams, "this is message")
			})

			r1, err := fn(ctx, req, &grpc.UnaryServerInfo{}, handler)
			convey.So(err, convey.ShouldNotBeNil)
			convey.So(r1, convey.ShouldBeNil)
		})
		convey.Convey("[tierr]should do nothing if err is BizError but resp is  nil", func() {
			handler = grpc.UnaryHandler(func(ctx context.Context, req interface{}) (interface{}, error) {
				return nil, NewBadRequest("this is message", nil)
			})

			r1, err := fn(ctx, req, &grpc.UnaryServerInfo{}, handler)
			convey.So(err, convey.ShouldNotBeNil)
			convey.So(r1, convey.ShouldBeNil)
		})
		convey.Convey("should set code and msg after redact if err is BizError with exception level", func() {
			handler = grpc.UnaryHandler(func(ctx context.Context, req interface{}) (interface{}, error) {
				return &RespWithBaseResp{}, bizerr.New(bizerr.ErrCodeDB, "this is message").Escalate()
			})

			r1, err := fn(ctx, req, &grpc.UnaryServerInfo{}, handler)
			convey.So(err, convey.ShouldBeNil)
			convey.So(r1.(*RespWithBaseResp).BaseResp.ErrCode, convey.ShouldEqual, bizerr.ErrCodeDB+100000000) // TODO(chenfei): convertxx is required to expose
			convey.So(r1.(*RespWithBaseResp).BaseResp.ErrMsg, convey.ShouldEqual, "Internal Error")
		})
		convey.Convey("[tierr]should set code and msg after redact if err is BizError with exception level", func() {
			handler = grpc.UnaryHandler(func(ctx context.Context, req interface{}) (interface{}, error) {
				return &RespWithBaseResp{}, NewInternalDB("this is message", nil)
			})

			r1, err := fn(ctx, req, &grpc.UnaryServerInfo{}, handler)
			convey.So(err, convey.ShouldBeNil)
			convey.So(r1.(*RespWithBaseResp).BaseResp.ErrCode, convey.ShouldEqual, ErrCodeDB)
			convey.So(r1.(*RespWithBaseResp).BaseResp.ErrMsg, convey.ShouldEqual, "Internal Error")
		})
		convey.Convey("[tierr]should set code and msg after redact if err is wrapped tierr", func() {
			handler = grpc.UnaryHandler(func(ctx context.Context, req interface{}) (interface{}, error) {
				return &RespWithBaseResp{}, fmt.Errorf("hello world: %w", NewInternalDB("this is message", nil))
			})

			r1, err := fn(ctx, req, &grpc.UnaryServerInfo{}, handler)
			convey.So(err, convey.ShouldBeNil)
			convey.So(r1.(*RespWithBaseResp).BaseResp.ErrCode, convey.ShouldEqual, ErrCodeDB)
			convey.So(r1.(*RespWithBaseResp).BaseResp.ErrMsg, convey.ShouldEqual, "Internal Error")
		})
		convey.Convey("should set base_resp if base_resp is nil", func() {
			handler = grpc.UnaryHandler(func(ctx context.Context, req interface{}) (interface{}, error) {
				return &RespWithBaseResp{}, nil
			})
			r1, err := fn(ctx, req, &grpc.UnaryServerInfo{}, handler)
			convey.So(err, convey.ShouldBeNil)
			convey.So(r1.(*RespWithBaseResp).BaseResp, convey.ShouldNotBeNil)
		})
		convey.Convey("should do nothing if err is error", func() {
			handler = grpc.UnaryHandler(func(ctx context.Context, req interface{}) (interface{}, error) {
				return &RespWithBaseResp{}, fmt.Errorf("abc")
			})

			r1, err := fn(ctx, req, &grpc.UnaryServerInfo{}, handler)
			convey.So(err, convey.ShouldNotBeNil)
			convey.So(r1.(*RespWithBaseResp).BaseResp, convey.ShouldNotBeNil)
		})
		convey.Convey("a normal resp without base_resp", func() {
			type NormalResp struct{}

			handler = grpc.UnaryHandler(func(ctx context.Context, req interface{}) (interface{}, error) {
				return &NormalResp{}, nil
			})

			r1, err := fn(ctx, req, &grpc.UnaryServerInfo{}, handler)
			convey.So(err, convey.ShouldBeNil)
			convey.So(r1.(*NormalResp), convey.ShouldNotBeNil)
		})
		convey.Convey("a resp with base_resp set", func() {
			resp := &RespWithBaseResp{BaseResp: &base.BaseResp{
				Tags:    nil,
				ErrCode: 1,
				ErrMsg:  "gotcha!",
			}}
			handler = grpc.UnaryHandler(func(ctx context.Context, req interface{}) (interface{}, error) {
				return resp, nil
			})

			r1, err := fn(ctx, req, &grpc.UnaryServerInfo{}, handler)
			convey.So(err, convey.ShouldBeNil)
			convey.So(r1.(*RespWithBaseResp).BaseResp, convey.ShouldNotBeNil)
			convey.So(r1.(*RespWithBaseResp).BaseResp.ErrCode, convey.ShouldEqual, 1)
			convey.So(r1.(*RespWithBaseResp).BaseResp.ErrMsg, convey.ShouldEqual, "gotcha!")
			convey.So(r1, convey.ShouldResemble, resp)
		})
	})
}
