package agherr

import (
	"bytes"
	"errors"
	"fmt"
	"testing"

	"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestError_Error(t *testing.T) {
	testCases := []struct {
		err  error
		name string
		want string
	}{{
		err:  Many("a"),
		name: "simple",
		want: "a",
	}, {
		err:  Many("a", errors.New("b")),
		name: "wrapping",
		want: "a: b",
	}, {
		err:  Many("a", errors.New("b"), errors.New("c"), errors.New("d")),
		name: "wrapping several",
		want: "a: b (hidden: c, d)",
	}, {
		err:  Many("a", Many("b", errors.New("c"), errors.New("d"))),
		name: "wrapping wrapper",
		want: "a: b: c (hidden: d)",
	}}

	for _, tc := range testCases {
		assert.Equal(t, tc.want, tc.err.Error(), tc.name)
	}
}

func TestError_Unwrap(t *testing.T) {
	var _ wrapper = &manyError{}

	const (
		errSimple = iota
		errWrapped
		errNil
	)

	errs := []error{
		errSimple:  errors.New("a"),
		errWrapped: fmt.Errorf("err: %w", errors.New("nested")),
		errNil:     nil,
	}

	testCases := []struct {
		want    error
		wrapped error
		name    string
	}{{
		want:    errs[errSimple],
		wrapped: Many("a", errs[errSimple]),
		name:    "simple",
	}, {
		want:    errs[errWrapped],
		wrapped: Many("b", errs[errWrapped]),
		name:    "nested",
	}, {
		want:    errs[errNil],
		wrapped: Many("c", errs[errNil]),
		name:    "nil passed",
	}, {
		want:    nil,
		wrapped: Many("d"),
		name:    "nil not passed",
	}}

	for _, tc := range testCases {
		assert.Equal(t, tc.want, errors.Unwrap(tc.wrapped), tc.name)
	}
}

func TestAnnotate(t *testing.T) {
	const s = "1234"
	const wantMsg = `bad string "1234": test`

	// Don't use const, because we can't take a pointer of a constant.
	var errTest error = Error("test")

	t.Run("nil", func(t *testing.T) {
		var errPtr *error
		assert.NotPanics(t, func() {
			Annotate("bad string %q: %w", errPtr, s)
		})
	})

	t.Run("non_nil", func(t *testing.T) {
		errPtr := &errTest
		assert.NotPanics(t, func() {
			Annotate("bad string %q: %w", errPtr, s)
		})

		require.NotNil(t, errPtr)

		err := *errPtr
		require.Error(t, err)

		assert.Equal(t, wantMsg, err.Error())
	})

	t.Run("defer", func(t *testing.T) {
		f := func() (err error) {
			defer Annotate("bad string %q: %w", &errTest, s)

			return errTest
		}

		err := f()
		require.Error(t, err)

		assert.Equal(t, wantMsg, err.Error())
	})
}

func TestLogPanic(t *testing.T) {
	buf := &bytes.Buffer{}
	aghtest.ReplaceLogWriter(t, buf)

	t.Run("prefix", func(t *testing.T) {
		const (
			panicMsg        = "spooky!"
			prefix          = "packagename"
			errWithNoPrefix = "[error] recovered from panic: spooky!"
			errWithPrefix   = "[error] packagename: recovered from panic: spooky!"
		)

		panicFunc := func(prefix string) {
			defer LogPanic(prefix)

			panic(panicMsg)
		}

		panicFunc("")
		assert.Contains(t, buf.String(), errWithNoPrefix)
		buf.Reset()

		panicFunc(prefix)
		assert.Contains(t, buf.String(), errWithPrefix)
		buf.Reset()
	})

	t.Run("don't_panic", func(t *testing.T) {
		require.NotPanics(t, func() {
			defer LogPanic("")
		})

		assert.Empty(t, buf.String())
	})
}