// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package xerrors_test

import (
	"fmt"
	"io"
	"os"
	"path"
	"reflect"
	"regexp"
	"strconv"
	"strings"
	"testing"

	"golang.org/x/xerrors"
)

func TestErrorf(t *testing.T) {
	chained := &wrapped{"chained", nil}
	chain := func(s ...string) (a []string) {
		for _, s := range s {
			a = append(a, cleanPath(s))
		}
		return a
	}
	testCases := []struct {
		got  error
		want []string
	}{{
		xerrors.Errorf("no args"),
		chain("no args/path.TestErrorf/path.go:xxx"),
	}, {
		xerrors.Errorf("no args: %s"),
		chain("no args: %!s(MISSING)/path.TestErrorf/path.go:xxx"),
	}, {
		xerrors.Errorf("nounwrap: %s", "simple"),
		chain(`nounwrap: simple/path.TestErrorf/path.go:xxx`),
	}, {
		xerrors.Errorf("nounwrap: %v", "simple"),
		chain(`nounwrap: simple/path.TestErrorf/path.go:xxx`),
	}, {
		xerrors.Errorf("%s failed: %v", "foo", chained),
		chain("foo failed/path.TestErrorf/path.go:xxx",
			"chained/somefile.go:xxx"),
	}, {
		xerrors.Errorf("no wrap: %s", chained),
		chain("no wrap/path.TestErrorf/path.go:xxx",
			"chained/somefile.go:xxx"),
	}, {
		xerrors.Errorf("%s failed: %w", "foo", chained),
		chain("wraps:foo failed/path.TestErrorf/path.go:xxx",
			"chained/somefile.go:xxx"),
	}, {
		xerrors.Errorf("nowrapv: %v", chained),
		chain("nowrapv/path.TestErrorf/path.go:xxx",
			"chained/somefile.go:xxx"),
	}, {
		xerrors.Errorf("wrapw: %w", chained),
		chain("wraps:wrapw/path.TestErrorf/path.go:xxx",
			"chained/somefile.go:xxx"),
	}, {
		xerrors.Errorf("wrapw %w middle", chained),
		chain("wraps:wrapw chained middle/path.TestErrorf/path.go:xxx",
			"chained/somefile.go:xxx"),
	}, {
		xerrors.Errorf("not wrapped: %+v", chained),
		chain("not wrapped: chained: somefile.go:123/path.TestErrorf/path.go:xxx"),
	}}
	for i, tc := range testCases {
		t.Run(strconv.Itoa(i)+"/"+path.Join(tc.want...), func(t *testing.T) {
			got := errToParts(tc.got)
			if !reflect.DeepEqual(got, tc.want) {
				t.Errorf("Format:\n got: %#v\nwant: %#v", got, tc.want)
			}

			gotStr := tc.got.Error()
			wantStr := fmt.Sprint(tc.got)
			if gotStr != wantStr {
				t.Errorf("Error:\n got: %#v\nwant: %#v", got, tc.want)
			}
		})
	}
}

func TestErrorFormatter(t *testing.T) {
	var (
		simple   = &wrapped{"simple", nil}
		elephant = &wrapped{
			"can't adumbrate elephant",
			detailed{},
		}
		nonascii = &wrapped{"café", nil}
		newline  = &wrapped{"msg with\nnewline",
			&wrapped{"and another\none", nil}}
		fallback  = &wrapped{"fallback", os.ErrNotExist}
		oldAndNew = &wrapped{"new style", formatError("old style")}
		framed    = &withFrameAndMore{
			frame: xerrors.Caller(0),
		}
		opaque = &wrapped{"outer",
			xerrors.Opaque(&wrapped{"mid",
				&wrapped{"inner", nil}})}
	)
	testCases := []struct {
		err    error
		fmt    string
		want   string
		regexp bool
	}{{
		err:  simple,
		fmt:  "%s",
		want: "simple",
	}, {
		err:  elephant,
		fmt:  "%s",
		want: "can't adumbrate elephant: out of peanuts",
	}, {
		err:  &wrapped{"a", &wrapped{"b", &wrapped{"c", nil}}},
		fmt:  "%s",
		want: "a: b: c",
	}, {
		err: simple,
		fmt: "%+v",
		want: "simple:" +
			"\n    somefile.go:123",
	}, {
		err: elephant,
		fmt: "%+v",
		want: "can't adumbrate elephant:" +
			"\n    somefile.go:123" +
			"\n  - out of peanuts:" +
			"\n    the elephant is on strike" +
			"\n    and the 12 monkeys" +
			"\n    are laughing",
	}, {
		err:  &oneNewline{nil},
		fmt:  "%+v",
		want: "123",
	}, {
		err: &oneNewline{&oneNewline{nil}},
		fmt: "%+v",
		want: "123:" +
			"\n  - 123",
	}, {
		err:  &newlineAtEnd{nil},
		fmt:  "%+v",
		want: "newlineAtEnd:\n    detail",
	}, {
		err: &newlineAtEnd{&newlineAtEnd{nil}},
		fmt: "%+v",
		want: "newlineAtEnd:" +
			"\n    detail" +
			"\n  - newlineAtEnd:" +
			"\n    detail",
	}, {
		err: framed,
		fmt: "%+v",
		want: "something:" +
			"\n    golang.org/x/xerrors_test.TestErrorFormatter" +
			"\n        .+/fmt_test.go:101" +
			"\n    something more",
		regexp: true,
	}, {
		err:  fmtTwice("Hello World!"),
		fmt:  "%#v",
		want: "2 times Hello World!",
	}, {
		err:  fallback,
		fmt:  "%s",
		want: "fallback: file does not exist",
	}, {
		err: fallback,
		fmt: "%+v",
		// Note: no colon after the last error, as there are no details.
		want: "fallback:" +
			"\n    somefile.go:123" +
			"\n  - file does not exist",
	}, {
		err:  opaque,
		fmt:  "%s",
		want: "outer: mid: inner",
	}, {
		err: opaque,
		fmt: "%+v",
		want: "outer:" +
			"\n    somefile.go:123" +
			"\n  - mid:" +
			"\n    somefile.go:123" +
			"\n  - inner:" +
			"\n    somefile.go:123",
	}, {
		err:  oldAndNew,
		fmt:  "%v",
		want: "new style: old style",
	}, {
		err:  oldAndNew,
		fmt:  "%q",
		want: `"new style: old style"`,
	}, {
		err: oldAndNew,
		fmt: "%+v",
		// Note the extra indentation.
		// Colon for old style error is rendered by the fmt.Formatter
		// implementation of the old-style error.
		want: "new style:" +
			"\n    somefile.go:123" +
			"\n  - old style:" +
			"\n    otherfile.go:456",
	}, {
		err:  simple,
		fmt:  "%-12s",
		want: "simple      ",
	}, {
		// Don't use formatting flags for detailed view.
		err: simple,
		fmt: "%+12v",
		want: "simple:" +
			"\n    somefile.go:123",
	}, {
		err:  elephant,
		fmt:  "%+50s",
		want: "          can't adumbrate elephant: out of peanuts",
	}, {
		err:  nonascii,
		fmt:  "%q",
		want: `"café"`,
	}, {
		err:  nonascii,
		fmt:  "%+q",
		want: `"caf\u00e9"`,
	}, {
		err:  simple,
		fmt:  "% x",
		want: "73 69 6d 70 6c 65",
	}, {
		err: newline,
		fmt: "%s",
		want: "msg with" +
			"\nnewline: and another" +
			"\none",
	}, {
		err: newline,
		fmt: "%+v",
		want: "msg with" +
			"\n    newline:" +
			"\n    somefile.go:123" +
			"\n  - and another" +
			"\n    one:" +
			"\n    somefile.go:123",
	}, {
		err: &wrapped{"", &wrapped{"inner message", nil}},
		fmt: "%+v",
		want: "somefile.go:123" +
			"\n  - inner message:" +
			"\n    somefile.go:123",
	}, {
		err:  spurious(""),
		fmt:  "%s",
		want: "spurious",
	}, {
		err:  spurious(""),
		fmt:  "%+v",
		want: "spurious",
	}, {
		err:  spurious("extra"),
		fmt:  "%s",
		want: "spurious",
	}, {
		err: spurious("extra"),
		fmt: "%+v",
		want: "spurious:\n" +
			"    extra",
	}, {
		err:  nil,
		fmt:  "%+v",
		want: "<nil>",
	}, {
		err:  (*wrapped)(nil),
		fmt:  "%+v",
		want: "<nil>",
	}, {
		err:  simple,
		fmt:  "%T",
		want: "*xerrors_test.wrapped",
	}, {
		err:  simple,
		fmt:  "%🤪",
		want: "%!🤪(*xerrors_test.wrapped)",
		// For 1.13:
		//  want: "%!🤪(*xerrors_test.wrapped=&{simple <nil>})",
	}, {
		err:  formatError("use fmt.Formatter"),
		fmt:  "%#v",
		want: "use fmt.Formatter",
	}, {
		err: fmtTwice("%s %s", "ok", panicValue{}),
		fmt: "%s",
		// Different Go versions produce different results.
		want:   `ok %!s\(PANIC=(String method: )?panic\)/ok %!s\(PANIC=(String method: )?panic\)`,
		regexp: true,
	}, {
		err:  fmtTwice("%o %s", panicValue{}, "ok"),
		fmt:  "%s",
		want: "{} ok/{} ok",
	}, {
		err: adapted{"adapted", nil},
		fmt: "%+v",
		want: "adapted:" +
			"\n    detail",
	}, {
		err: adapted{"outer", adapted{"mid", adapted{"inner", nil}}},
		fmt: "%+v",
		want: "outer:" +
			"\n    detail" +
			"\n  - mid:" +
			"\n    detail" +
			"\n  - inner:" +
			"\n    detail",
	}}
	for i, tc := range testCases {
		t.Run(fmt.Sprintf("%d/%s", i, tc.fmt), func(t *testing.T) {
			got := fmt.Sprintf(tc.fmt, tc.err)
			var ok bool
			if tc.regexp {
				var err error
				ok, err = regexp.MatchString(tc.want+"$", got)
				if err != nil {
					t.Fatal(err)
				}
			} else {
				ok = got == tc.want
			}
			if !ok {
				t.Errorf("\n got: %q\nwant: %q", got, tc.want)
			}
		})
	}
}

func TestAdaptor(t *testing.T) {
	testCases := []struct {
		err    error
		fmt    string
		want   string
		regexp bool
	}{{
		err: adapted{"adapted", nil},
		fmt: "%+v",
		want: "adapted:" +
			"\n    detail",
	}, {
		err: adapted{"outer", adapted{"mid", adapted{"inner", nil}}},
		fmt: "%+v",
		want: "outer:" +
			"\n    detail" +
			"\n  - mid:" +
			"\n    detail" +
			"\n  - inner:" +
			"\n    detail",
	}}
	for i, tc := range testCases {
		t.Run(fmt.Sprintf("%d/%s", i, tc.fmt), func(t *testing.T) {
			got := fmt.Sprintf(tc.fmt, tc.err)
			if got != tc.want {
				t.Errorf("\n got: %q\nwant: %q", got, tc.want)
			}
		})
	}
}

var _ xerrors.Formatter = wrapped{}

type wrapped struct {
	msg string
	err error
}

func (e wrapped) Error() string { return "should call Format" }

func (e wrapped) Format(s fmt.State, verb rune) {
	xerrors.FormatError(&e, s, verb)
}

func (e wrapped) FormatError(p xerrors.Printer) (next error) {
	p.Print(e.msg)
	p.Detail()
	p.Print("somefile.go:123")
	return e.err
}

var _ xerrors.Formatter = detailed{}

type detailed struct{}

func (e detailed) Error() string { panic("should have called FormatError") }

func (detailed) FormatError(p xerrors.Printer) (next error) {
	p.Printf("out of %s", "peanuts")
	p.Detail()
	p.Print("the elephant is on strike\n")
	p.Printf("and the %d monkeys\nare laughing", 12)
	return nil
}

type withFrameAndMore struct {
	frame xerrors.Frame
}

func (e *withFrameAndMore) Error() string { return fmt.Sprint(e) }

func (e *withFrameAndMore) Format(s fmt.State, v rune) {
	xerrors.FormatError(e, s, v)
}

func (e *withFrameAndMore) FormatError(p xerrors.Printer) (next error) {
	p.Print("something")
	if p.Detail() {
		e.frame.Format(p)
		p.Print("something more")
	}
	return nil
}

type spurious string

func (e spurious) Error() string { return fmt.Sprint(e) }

// move to 1_12 test file
func (e spurious) Format(s fmt.State, verb rune) {
	xerrors.FormatError(e, s, verb)
}

func (e spurious) FormatError(p xerrors.Printer) (next error) {
	p.Print("spurious")
	p.Detail() // Call detail even if we don't print anything
	if e == "" {
		p.Print()
	} else {
		p.Print("\n", string(e)) // print extraneous leading newline
	}
	return nil
}

type oneNewline struct {
	next error
}

func (e *oneNewline) Error() string { return fmt.Sprint(e) }

func (e *oneNewline) Format(s fmt.State, verb rune) {
	xerrors.FormatError(e, s, verb)
}

func (e *oneNewline) FormatError(p xerrors.Printer) (next error) {
	p.Print("1")
	p.Print("2")
	p.Print("3")
	p.Detail()
	p.Print("\n")
	return e.next
}

type newlineAtEnd struct {
	next error
}

func (e *newlineAtEnd) Error() string { return fmt.Sprint(e) }

func (e *newlineAtEnd) Format(s fmt.State, verb rune) {
	xerrors.FormatError(e, s, verb)
}

func (e *newlineAtEnd) FormatError(p xerrors.Printer) (next error) {
	p.Print("newlineAtEnd")
	p.Detail()
	p.Print("detail\n")
	return e.next
}

type adapted struct {
	msg string
	err error
}

func (e adapted) Error() string { return e.msg }

func (e adapted) Format(s fmt.State, verb rune) {
	xerrors.FormatError(e, s, verb)
}

func (e adapted) FormatError(p xerrors.Printer) error {
	p.Print(e.msg)
	p.Detail()
	p.Print("detail")
	return e.err
}

// formatError is an error implementing Format instead of xerrors.Formatter.
// The implementation mimics the implementation of github.com/pkg/errors.
type formatError string

func (e formatError) Error() string { return string(e) }

func (e formatError) Format(s fmt.State, verb rune) {
	// Body based on pkg/errors/errors.go
	switch verb {
	case 'v':
		if s.Flag('+') {
			io.WriteString(s, string(e))
			fmt.Fprintf(s, ":\n%s", "otherfile.go:456")
			return
		}
		fallthrough
	case 's':
		io.WriteString(s, string(e))
	case 'q':
		fmt.Fprintf(s, "%q", string(e))
	}
}

func (e formatError) GoString() string {
	panic("should never be called")
}

type fmtTwiceErr struct {
	format string
	args   []any
}

func fmtTwice(format string, a ...any) error {
	return fmtTwiceErr{format, a}
}

func (e fmtTwiceErr) Error() string { return fmt.Sprint(e) }

func (e fmtTwiceErr) Format(s fmt.State, verb rune) {
	xerrors.FormatError(e, s, verb)
}

func (e fmtTwiceErr) FormatError(p xerrors.Printer) (next error) {
	p.Printf(e.format, e.args...)
	p.Print("/")
	p.Printf(e.format, e.args...)
	return nil
}

func (e fmtTwiceErr) GoString() string {
	return "2 times " + fmt.Sprintf(e.format, e.args...)
}

type panicValue struct{}

func (panicValue) String() string { panic("panic") }

var rePath = regexp.MustCompile(`( [^ ]+)\/(xerrors_test|fmt_test)\.`)
var reLine = regexp.MustCompile(":[0-9]+\n?$")

func cleanPath(s string) string {
	s = rePath.ReplaceAllString(s, "/path.")
	s = reLine.ReplaceAllString(s, ":xxx")
	s = strings.ReplaceAll(s, "\n   ", "")
	s = strings.ReplaceAll(s, " /", "/")
	return s
}

func errToParts(err error) (a []string) {
	for err != nil {
		var p testPrinter
		if xerrors.Unwrap(err) != nil {
			p.str += "wraps:"
		}
		f, ok := err.(xerrors.Formatter)
		if !ok {
			a = append(a, err.Error())
			break
		}
		err = f.FormatError(&p)
		a = append(a, cleanPath(p.str))
	}
	return a

}

type testPrinter struct {
	str string
}

func (p *testPrinter) Print(a ...any) {
	p.str += fmt.Sprint(a...)
}

func (p *testPrinter) Printf(format string, a ...any) {
	p.str += fmt.Sprintf(format, a...)
}

func (p *testPrinter) Detail() bool {
	p.str += " /"
	return true
}
