package fserrors

import (
	"context"
	"errors"
	"fmt"
	"io"
	"net"
	"net/url"
	"os"
	"syscall"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
)

// withMessage wraps an error with a message
//
// This is for backwards compatibility with the now removed github.com/pkg/errors
type withMessage struct {
	cause error
	msg   string
}

func (w *withMessage) Error() string { return w.msg + ": " + w.cause.Error() }
func (w *withMessage) Cause() error  { return w.cause }

// wrap returns an error annotating err with a stack trace
// at the point Wrap is called, and the supplied message.
// If err is nil, Wrap returns nil.
func wrap(err error, message string) error {
	if err == nil {
		return nil
	}
	return &withMessage{
		cause: err,
		msg:   message,
	}
}

var errUseOfClosedNetworkConnection = errors.New("use of closed network connection")

// make a plausible network error with the underlying errno
func makeNetErr(errno syscall.Errno) error {
	return &net.OpError{
		Op:     "write",
		Net:    "tcp",
		Source: &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 123},
		Addr:   &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 8080},
		Err: &os.SyscallError{
			Syscall: "write",
			Err:     errno,
		},
	}
}

type myError1 struct {
	Err error
}

func (e myError1) Error() string { return e.Err.Error() }

type myError2 struct {
	Err error
}

func (e *myError2) Error() string {
	if e == nil {
		return "myError2(nil)"
	}
	if e.Err == nil {
		return "myError2{Err: nil}"
	}
	return e.Err.Error()
}

type myError3 struct {
	Err int
}

func (e *myError3) Error() string { return "hello" }

type myError4 struct {
	e error
}

func (e *myError4) Error() string { return e.e.Error() }

type myError5 struct{}

func (e *myError5) Error() string { return "" }

func (e *myError5) Temporary() bool { return true }

type errorCause struct {
	e error
}

func (e *errorCause) Error() string { return fmt.Sprintf("%#v", e) }

func (e *errorCause) Cause() error { return e.e }

func TestCause(t *testing.T) {
	e3 := &myError3{3}
	e4 := &myError4{io.EOF}
	e5 := &myError5{}
	eNil1 := &myError2{nil}
	eNil2 := &myError2{Err: (*myError2)(nil)}
	errPotato := errors.New("potato")
	nilCause1 := &errorCause{nil}
	nilCause2 := &errorCause{(*myError2)(nil)}

	for i, test := range []struct {
		err           error
		wantRetriable bool
		wantErr       error
	}{
		{nil, false, nil},
		{errPotato, false, errPotato},
		{fmt.Errorf("potato: %w", errPotato), false, errPotato},
		{fmt.Errorf("potato2: %w", wrap(errPotato, "potato")), false, errPotato},
		{errUseOfClosedNetworkConnection, false, errUseOfClosedNetworkConnection},
		{makeNetErr(syscall.EAGAIN), true, syscall.EAGAIN},
		{makeNetErr(syscall.Errno(123123123)), false, syscall.Errno(123123123)},
		{eNil1, false, eNil1},
		{eNil2, false, eNil2.Err},
		{myError1{io.EOF}, false, io.EOF},
		{&myError2{io.EOF}, false, io.EOF},
		{e3, false, e3},
		{e4, false, e4},
		{e5, true, e5},
		{&errorCause{errPotato}, false, errPotato},
		{nilCause1, false, nilCause1},
		{nilCause2, false, nilCause2.e},
	} {
		gotRetriable, gotErr := Cause(test.err)
		what := fmt.Sprintf("test #%d: %v", i, test.err)
		assert.Equal(t, test.wantErr, gotErr, what)
		assert.Equal(t, test.wantRetriable, gotRetriable, what)
	}
}

func TestShouldRetry(t *testing.T) {
	for i, test := range []struct {
		err  error
		want bool
	}{
		{nil, false},
		{errors.New("potato"), false},
		{fmt.Errorf("connection: %w", errUseOfClosedNetworkConnection), true},
		{io.EOF, true},
		{io.ErrUnexpectedEOF, true},
		{makeNetErr(syscall.EAGAIN), true},
		{makeNetErr(syscall.Errno(123123123)), false},
		{&url.Error{Op: "post", URL: "/", Err: io.EOF}, true},
		{&url.Error{Op: "post", URL: "/", Err: errUseOfClosedNetworkConnection}, true},
		{&url.Error{Op: "post", URL: "/", Err: fmt.Errorf("net/http: HTTP/1.x transport connection broken: %v", fmt.Errorf("http: ContentLength=%d with Body length %d", 100663336, 99590598))}, true},
		{
			wrap(&url.Error{
				Op:  "post",
				URL: "http://localhost/",
				Err: makeNetErr(syscall.EPIPE),
			}, "potato error"),
			true,
		},
		{
			wrap(&url.Error{
				Op:  "post",
				URL: "http://localhost/",
				Err: makeNetErr(syscall.Errno(123123123)),
			}, "listing error"),
			false,
		},
	} {
		got := ShouldRetry(test.err)
		assert.Equal(t, test.want, got, fmt.Sprintf("test #%d: %v", i, test.err))
	}
}

func TestRetryAfter(t *testing.T) {
	e := NewErrorRetryAfter(time.Second)
	after := e.RetryAfter()
	dt := time.Until(after)
	assert.True(t, dt >= 900*time.Millisecond && dt <= 1100*time.Millisecond)
	assert.True(t, IsRetryAfterError(e))
	assert.False(t, IsRetryAfterError(io.EOF))
	assert.Equal(t, time.Time{}, RetryAfterErrorTime(io.EOF))
	assert.False(t, IsRetryAfterError(nil))
	assert.Contains(t, e.Error(), "try again after")

	t0 := time.Now()
	err := fmt.Errorf("potato: %w", ErrorRetryAfter(t0))
	assert.Equal(t, t0, RetryAfterErrorTime(err))
	assert.True(t, IsRetryAfterError(err))
	assert.Contains(t, e.Error(), "try again after")
}

func TestContextError(t *testing.T) {
	var err = io.EOF
	ctx, cancel := context.WithCancel(context.Background())

	assert.False(t, ContextError(ctx, &err))
	assert.Equal(t, io.EOF, err)

	cancel()

	assert.True(t, ContextError(ctx, &err))
	assert.Equal(t, io.EOF, err)

	err = nil

	assert.True(t, ContextError(ctx, &err))
	assert.Equal(t, context.Canceled, err)
}