/*
 * 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 kerrors

import (
	"errors"
	"fmt"
	"io"
	"os"
	"runtime/debug"
	"strings"
	"testing"

	"github.com/cloudwego/kitex/internal/test"
)

func TestIsKitexError(t *testing.T) {
	errs := []error{
		ErrInternalException,
		ErrServiceDiscovery,
		ErrGetConnection,
		ErrLoadbalance,
		ErrRPCTimeout,
		ErrACL,
		ErrCircuitBreak,
		ErrRemoteOrNetwork,
		ErrOverlimit,
		ErrNotSupported,
		ErrNoResolver,
		ErrNoDestService,
		ErrNoDestAddress,
		ErrNoConnection,
		ErrNoMoreInstance,
		ErrConnOverLimit,
		ErrQPSOverLimit,
		// streaming errors
		ErrStreamingProtocol,
		ErrStreamingCanceled,
	}
	for _, e := range errs {
		test.Assert(t, IsKitexError(e))
	}

	e := errors.New("any error")
	test.Assert(t, !IsKitexError(e))

	e = ErrInternalException.WithCause(e)
	test.Assert(t, IsKitexError(e))
}

func TestIs(t *testing.T) {
	any := errors.New("any error")

	test.Assert(t, !errors.Is(any, ErrACL))

	var e error
	e = ErrACL
	test.Assert(t, errors.Is(e, ErrACL))

	e = ErrACL.WithCause(any)
	test.Assert(t, errors.Is(e, ErrACL))
	test.Assert(t, errors.Is(e, any))
}

func TestError(t *testing.T) {
	basic := "basic"
	extra := "extra"
	be := &basicError{basic}
	test.Assert(t, be.Error() == basic)
	detailedMsg := appendErrMsg(basic, extra)
	test.Assert(t, (&DetailedError{basic: be, extraMsg: extra}).Error() == detailedMsg)
}

func TestWithCause(t *testing.T) {
	ae := errors.New("any error")
	be := &basicError{"basic"}
	de := be.WithCause(ae)

	test.Assert(t, be.Error() == "basic")
	test.Assert(t, strings.HasPrefix(de.Error(), be.Error()))
	test.Assert(t, strings.HasSuffix(de.Error(), ae.Error()))

	xe, ok := de.(interface{ ErrorType() error })
	test.Assert(t, ok)
	test.Assert(t, xe.ErrorType() == be)

	ye, ok := de.(interface{ Unwrap() error })
	test.Assert(t, ok)
	test.Assert(t, ye.Unwrap() == ae)
}

func TestWithCauseAndStack(t *testing.T) {
	ae := errors.New("any error")
	be := &basicError{"basic"}
	stack := string(debug.Stack())
	de := be.WithCauseAndStack(ae, stack)

	test.Assert(t, be.Error() == "basic")
	test.Assert(t, strings.HasPrefix(de.Error(), be.Error()))
	test.Assert(t, strings.HasSuffix(de.Error(), ae.Error()))

	xe, ok := de.(interface{ ErrorType() error })
	test.Assert(t, ok)
	test.Assert(t, xe.ErrorType() == be)

	ye, ok := de.(interface{ Unwrap() error })
	test.Assert(t, ok)
	test.Assert(t, ye.Unwrap() == ae)

	se, ok := de.(interface{ Stack() string })
	test.Assert(t, ok)
	test.Assert(t, se.Stack() == stack)
}

type timeoutError struct{}

func (te *timeoutError) Error() string { return "timeout" }
func (te *timeoutError) Timeout() bool { return true }

func TestTimeout(t *testing.T) {
	var ae, ke error
	ae = errors.New("any error")
	osCheck := func(err error) bool {
		return os.IsTimeout(err)
	}

	ke = &basicError{"non-timeout"}
	TimeoutCheckFunc = osCheck
	test.Assert(t, !IsTimeoutError(ke))
	TimeoutCheckFunc = nil
	test.Assert(t, !IsTimeoutError(ke))

	ke = ErrRPCTimeout
	TimeoutCheckFunc = osCheck
	test.Assert(t, IsTimeoutError(ke))
	TimeoutCheckFunc = nil
	test.Assert(t, IsTimeoutError(ke))

	ke = ErrRPCTimeout.WithCause(ae)
	TimeoutCheckFunc = osCheck
	test.Assert(t, IsTimeoutError(ke))
	TimeoutCheckFunc = nil
	test.Assert(t, IsTimeoutError(ke))

	ke = ErrOverlimit.WithCause(ae)
	TimeoutCheckFunc = osCheck
	test.Assert(t, !IsTimeoutError(ke))
	TimeoutCheckFunc = nil
	test.Assert(t, !IsTimeoutError(ke))

	ae = &timeoutError{}
	TimeoutCheckFunc = osCheck
	test.Assert(t, IsTimeoutError(ae))
	TimeoutCheckFunc = nil
	test.Assert(t, !IsTimeoutError(ae))

	ke = ErrOverlimit.WithCause(ae)
	TimeoutCheckFunc = osCheck
	test.Assert(t, IsTimeoutError(ke))
	TimeoutCheckFunc = nil
	test.Assert(t, !IsTimeoutError(ke))
}

func TestWithCause1(t *testing.T) {
	ae := &basicError{"basic"}
	be := ErrRPCTimeout.WithCause(ae)
	if e2, ok := be.(*DetailedError); ok {
		e2.WithExtraMsg("retry circuite break")
	}
	test.Assert(t, be.Error() == "rpc timeout[retry circuite break]: basic", be)
}

type _err struct {
	error
}

// Format the error.
func (de *_err) Format(s fmt.State, verb rune) {
	switch verb {
	case 'v':
		if s.Flag('+') {
			_, _ = io.WriteString(s, "some message only when v+")
			return
		}
		fallthrough
	case 's', 'q':
		_, _ = io.WriteString(s, de.Error())
	}
}

func TestFormat(t *testing.T) {
	businessError := &_err{
		error: errors.New("some_business_error"),
	}
	basicErr := &basicError{
		message: "fake_msg",
	}
	err := basicErr.WithCause(businessError)
	got := fmt.Sprintf("%+v", err)
	test.Assert(t, got == "fake_msg: some message only when v+")

	got = fmt.Sprintf("%v", err)
	test.Assert(t, got == "fake_msg: some_business_error")
}

func BenchmarkWithCause3(b *testing.B) {
	b.ReportAllocs()
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		ae := &basicError{"basic"}
		be := ErrRPCTimeout.WithCause(ae)
		if e2, ok := be.(*DetailedError); ok {
			e2.WithExtraMsg("测试")
		}
	}
}
