package bizerr

import (
	"net/http"
	"testing"

	. "github.com/smartystreets/goconvey/convey"
)

func TestNew(t *testing.T) {
	Convey("TestNew", t, func() {
		Convey("normal case", func() {
			err := New(ErrCodeParams, "message")
			be := err.(*BizErr)
			So(be.Code, ShouldEqual, ErrCodeParams)
			So(be.Message, ShouldEqual, "message")
			So(be.StackTrace, ShouldBeEmpty)
			//So(be.filename, ShouldEndWith, "/pkgv2/bizerr/error.go")
			//So(be.methodName, ShouldEndWith, "pkgv2/bizerr.New")
			//So(be.fileno, ShouldEqual, 73)
		})
		Convey("should return err with stack and escalate if internal error code is given", func() {
			err := New(ErrCodeDB, "message")
			So(err.IsException(), ShouldBeTrue)
			So(err.Stack(), ShouldNotBeEmpty)
		})
	})
}

func TestNewWithStack(t *testing.T) {
	Convey("TestNewWithStack", t, func() {
		err := NewWithStack(ErrCodeParams, "message")
		So(err.Stack(), ShouldNotBeEmpty)
	})
}

func TestRestore(t *testing.T) {
	Convey("TestRestore", t, func() {
		err := Restore(ErrCodeParams, "message")
		be := err.(*BizErr)
		So(be, ShouldResemble, &BizErr{
			Code:    ErrCodeParams,
			Message: "message",
		})
	})
}

func Test_bizErr(t *testing.T) {
	Convey("Test_bizErr", t, func() {
		err := &BizErr{
			Code:       1,
			Message:    "message",
			StackTrace: "",

			filename:   "filename",
			fileno:     100,
			methodName: "method",
		}
		Convey("clone", func() {
			cloned := err.clone()
			So(cloned, ShouldResemble, err)

			cloned.filename = "another filename"
			So(cloned, ShouldNotResemble, err)
		})
		Convey("Error", func() {
			So(err.Error(), ShouldEqual, "[filename:100][method]code=1,msg=message")

			chainedErr := err.Chain("1").Chain("2")
			So(chainedErr.Error(), ShouldEqual, "[filename:100][method]code=1,msg=2 -> 1 -> message")
			So(chainedErr.ErrMsg(), ShouldEqual, "message")
		})
		Convey("ErrCode", func() {
			So(err.ErrCode(), ShouldEqual, 1)
		})
		Convey("ErrMsg", func() {
			So(err.ErrMsg(), ShouldEqual, "message")
		})
		Convey("RawErrCode", func() {
			So(err.RawErrCode(), ShouldEqual, 1)
			So(err.Escalate().RawErrCode(), ShouldEqual, 1)
			So(err.Escalate().Degrade().RawErrCode(), ShouldEqual, 1)
		})
		Convey("Escalate", func() {
			So(err.Escalate().ErrCode(), ShouldEqual, exceptionOffset+1)
			So(err.ErrCode(), ShouldEqual, 1)
		})
		Convey("Degrade", func() {
			err := &BizErr{
				Code:    convert2Exception(1),
				Message: "message",
			}
			So(err.Degrade().ErrCode(), ShouldEqual, 1)
			So(err.ErrCode(), ShouldEqual, exceptionOffset+1)
		})
		Convey("Chain", func() {
			So(err.Chain("abc: %d", 1), ShouldResemble, &BizErr{
				Code:       err.Code,
				Message:    "message",
				StackTrace: err.StackTrace,
				filename:   "filename",
				fileno:     100,
				methodName: "method",
				chainedMsg: []string{"abc: 1"},
			})
			So(err.Chain("1").Chain("2"), ShouldResemble, &BizErr{
				Code:       err.Code,
				Message:    "message",
				StackTrace: err.StackTrace,
				filename:   "filename",
				fileno:     100,
				methodName: "method",
				chainedMsg: []string{"1", "2"},
			})
		})
		Convey("IsException", func() {
			So(err.IsException(), ShouldBeFalse)
			So(err.Escalate().IsException(), ShouldBeTrue)
		})
		Convey("IsError", func() {
			So(err.IsError(), ShouldBeTrue)
			So(err.Escalate().IsError(), ShouldBeFalse)
		})
		Convey("Redact", func() {
			err.Code = ErrCodeDB
			err.StackTrace = "stack"
			So(err.Error(), ShouldEqual, "[filename:100][method]code=59900001,msg=message")
			So(err.StackTrace, ShouldEqual, "stack")

			redactedErr := err.Redact()
			So(redactedErr.Error(), ShouldEqual, "[:0][]code=59900001,msg=Internal Error")
			So(redactedErr.(*BizErr).StackTrace, ShouldBeEmpty)
		})
		Convey("HttpStatus", func() {
			oldUseHttpOK4all := useHttpOK4all
			defer func() {
				useHttpOK4all = oldUseHttpOK4all
			}()

			useHttpOK4all = false
			Convey("should return mapped httpStatusCode if hit", func() {
				err.Code = ErrCodeParams
				So(err.HttpStatus(), ShouldEqual, 400)
			})
			Convey("should return the same httpStatusCode with Escalate/Degrade", func() {
				err.Code = ErrCodeParams
				So(err.HttpStatus(), ShouldEqual, 400)
				So(err.Escalate().HttpStatus(), ShouldEqual, 400)
				So(err.Escalate().Degrade().HttpStatus(), ShouldEqual, 400)
			})
			Convey("should return 400 if the err is request error", func() {
				err.Code = requestErrCodeCustomStart + 1
				So(err.HttpStatus(), ShouldEqual, 400)
			})
			Convey("should return 500 if the err is internal error", func() {
				err.Code = internalErrCodeCustomStart + 1
				So(err.HttpStatus(), ShouldEqual, 500)
			})
			Convey("should return 400 if the err is business error", func() {
				err.Code = businessErrCodeCustomStart + 1
				So(err.HttpStatus(), ShouldEqual, 400)
			})
			Convey("should return customized http status code if hit", func() {
				err = &BizErr{
					Code:       UserAlreadyRegistered,
					Message:    "message",
					StackTrace: "",
				}
				MustRegisterHttpStatus(UserAlreadyRegistered, 502)
				defer func() { delete(customErrCodeMap, UserAlreadyRegistered) }() // clear
				So(err.HttpStatus(), ShouldEqual, 502)
			})
			Convey("should return 500 if code is invalid", func() {
				err.Code = -1
				So(err.HttpStatus(), ShouldEqual, 500)
			})
			Convey("should always return 200 for all error code", func() {
				useHttpOK4all = true

				testTables := []struct {
					errCode  ErrCode
					httpCode int
				}{
					{ // request error
						errCode:  ErrCodeParams,
						httpCode: http.StatusOK,
					},
					{ // internal error
						errCode:  ErrCodeDB,
						httpCode: http.StatusOK,
					},
					{ // business error
						errCode:  ErrCodeGeneralBusiness,
						httpCode: http.StatusOK,
					},
					{ // invalid error code
						errCode:  ErrCode(-1),
						httpCode: http.StatusOK,
					},
				}
				for _, each := range testTables {
					err.Code = each.errCode
					So(err.HttpStatus(), ShouldEqual, each.httpCode)
				}
			})
		})
		Convey("WithStack", func() {
			Convey("should ignore if the err already has stack info", func() {
				err := &BizErr{
					Code:       ErrCodeDB,
					Message:    "message",
					StackTrace: "stack",
				}
				So(err.WithStack().Stack(), ShouldEqual, "stack")
			})
			// no stack info
			Convey("should generate stack trace if there is no stack info", func() {
				err := &BizErr{
					Code:    ErrCodeDB,
					Message: "message",
				}
				So(err.WithStack().Stack(), ShouldNotBeEmpty)
			})
		})
		Convey("withCallerInfo", func() {
			be := err.withCallerInfo()
			So(be.filename, ShouldEndWith, "/convey/discovery.go")
		})
	})
}
