package multierror

import (
	"errors"
	"fmt"
	"reflect"
	"testing"
)

func TestError_Impl(t *testing.T) {
	var _ error = new(Error)
}

func TestErrorError_custom(t *testing.T) {
	errors := []error{
		errors.New("foo"),
		errors.New("bar"),
	}

	fn := func(es []error) string {
		return "foo"
	}

	multi := &Error{Errors: errors, ErrorFormat: fn}
	if multi.Error() != "foo" {
		t.Fatalf("bad: %s", multi.Error())
	}
}

func TestErrorError_default(t *testing.T) {
	expected := `2 errors occurred:
	* foo
	* bar

`

	errors := []error{
		errors.New("foo"),
		errors.New("bar"),
	}

	multi := &Error{Errors: errors}
	if multi.Error() != expected {
		t.Fatalf("bad: %s", multi.Error())
	}
}

func TestErrorErrorOrNil(t *testing.T) {
	err := new(Error)
	if err.ErrorOrNil() != nil {
		t.Fatalf("bad: %#v", err.ErrorOrNil())
	}

	err.Errors = []error{errors.New("foo")}
	if v := err.ErrorOrNil(); v == nil {
		t.Fatal("should not be nil")
	} else if !reflect.DeepEqual(v, err) {
		t.Fatalf("bad: %#v", v)
	}
}

func TestErrorWrappedErrors(t *testing.T) {
	errors := []error{
		errors.New("foo"),
		errors.New("bar"),
	}

	multi := &Error{Errors: errors}
	if !reflect.DeepEqual(multi.Errors, multi.WrappedErrors()) {
		t.Fatalf("bad: %s", multi.WrappedErrors())
	}
}

func TestErrorUnwrap(t *testing.T) {
	t.Run("with errors", func(t *testing.T) {
		err := &Error{Errors: []error{
			errors.New("foo"),
			errors.New("bar"),
			errors.New("baz"),
		}}

		var current error = err
		for i := 0; i < len(err.Errors); i++ {
			current = errors.Unwrap(current)
			if !errors.Is(current, err.Errors[i]) {
				t.Fatal("should be next value")
			}
		}

		if errors.Unwrap(current) != nil {
			t.Fatal("should be nil at the end")
		}
	})

	t.Run("with no errors", func(t *testing.T) {
		err := &Error{Errors: nil}
		if errors.Unwrap(err) != nil {
			t.Fatal("should be nil")
		}
	})

	t.Run("with nil multierror", func(t *testing.T) {
		var err *Error
		if errors.Unwrap(err) != nil {
			t.Fatal("should be nil")
		}
	})
}

func TestErrorIs(t *testing.T) {
	errBar := errors.New("bar")

	t.Run("with errBar", func(t *testing.T) {
		err := &Error{Errors: []error{
			errors.New("foo"),
			errBar,
			errors.New("baz"),
		}}

		if !errors.Is(err, errBar) {
			t.Fatal("should be true")
		}
	})

	t.Run("with errBar wrapped by fmt.Errorf", func(t *testing.T) {
		err := &Error{Errors: []error{
			errors.New("foo"),
			fmt.Errorf("errorf: %w", errBar),
			errors.New("baz"),
		}}

		if !errors.Is(err, errBar) {
			t.Fatal("should be true")
		}
	})

	t.Run("without errBar", func(t *testing.T) {
		err := &Error{Errors: []error{
			errors.New("foo"),
			errors.New("baz"),
		}}

		if errors.Is(err, errBar) {
			t.Fatal("should be false")
		}
	})
}

func TestErrorAs(t *testing.T) {
	match := &nestedError{}

	t.Run("with the value", func(t *testing.T) {
		err := &Error{Errors: []error{
			errors.New("foo"),
			match,
			errors.New("baz"),
		}}

		var target *nestedError
		if !errors.As(err, &target) {
			t.Fatal("should be true")
		}
		if target == nil {
			t.Fatal("target should not be nil")
		}
	})

	t.Run("with the value wrapped by fmt.Errorf", func(t *testing.T) {
		err := &Error{Errors: []error{
			errors.New("foo"),
			fmt.Errorf("errorf: %w", match),
			errors.New("baz"),
		}}

		var target *nestedError
		if !errors.As(err, &target) {
			t.Fatal("should be true")
		}
		if target == nil {
			t.Fatal("target should not be nil")
		}
	})

	t.Run("without the value", func(t *testing.T) {
		err := &Error{Errors: []error{
			errors.New("foo"),
			errors.New("baz"),
		}}

		var target *nestedError
		if errors.As(err, &target) {
			t.Fatal("should be false")
		}
		if target != nil {
			t.Fatal("target should be nil")
		}
	})
}

// nestedError implements error and is used for tests.
type nestedError struct{}

func (*nestedError) Error() string { return "" }
