package merry

import (
	"errors"
	"fmt"
	"reflect"
	"runtime"
	"strconv"
	"strings"
	"testing"

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

func TestNew(t *testing.T) {
	_, _, rl, _ := runtime.Caller(0)
	err := New("bang")
	if HTTPCode(err) != 500 {
		t.Errorf("http code should have been 500, was %v", HTTPCode(err))
	}
	if err.Error() != "bang" {
		t.Errorf("error message should have been bang, was %v", err.Error())
	}
	f, l := Location(err)
	if !strings.Contains(f, "errors_test.go") {
		t.Errorf("error message should have contained errors_test.go, was %s", f)
	}
	if l != rl+1 {
		t.Errorf("error line should have been %d, was %d", rl+1, 8)
	}
}

func TestErrorf(t *testing.T) {
	_, _, rl, _ := runtime.Caller(0)
	err := Errorf("chitty chitty %v %v", "bang", "bang")
	if HTTPCode(err) != 500 {
		t.Errorf("http code should have been 500, was %v", HTTPCode(err))
	}
	if err.Error() != "chitty chitty bang bang" {
		t.Errorf("error message should have been chitty chitty bang bang, was %v", err.Error())
	}
	f, l := Location(err)
	if !strings.Contains(f, "errors_test.go") {
		t.Errorf("error message should have contained errors_test.go, was %s", f)
	}
	if l != rl+1 {
		t.Errorf("error line should have been %d, was %d", rl+1, 8)
	}
}

func TestUserError(t *testing.T) {
	_, _, rl, _ := runtime.Caller(0)
	err := UserError("bang")
	assert.Equal(t, "bang", UserMessage(err))
	assert.Empty(t, Message(err))
	_, l := Location(err)
	assert.Equal(t, rl+1, l)
}

func TestUserErrorf(t *testing.T) {
	_, _, rl, _ := runtime.Caller(0)
	err := UserErrorf("bang %v", "bang")
	assert.Equal(t, "bang bang", UserMessage(err))
	assert.Empty(t, Message(err))
	_, l := Location(err)
	assert.Equal(t, rl+1, l)
}

func TestDetails(t *testing.T) {
	var err error = New("bang")
	deets := Details(err)
	t.Log(deets)
	lines := strings.Split(deets, "\n")
	if lines[0] != "bang" {
		t.Errorf("first line should have been bang: %v", lines[0])
	}
	if !strings.Contains(deets, Stacktrace(err)) {
		t.Error("should have contained the error stacktrace")
	}

	err = WithUserMessage(err, "stay calm")
	deets = Details(err)
	t.Log(deets)
	assert.Contains(t, deets, "User Message: stay calm")

	// Allow nil error
	assert.Empty(t, Details(nil))
}

func TestStacktrace(t *testing.T) {
	_, _, rl, _ := runtime.Caller(0)
	var err error = New("bang")

	assert.NotEmpty(t, Stack(err))
	s := Stacktrace(err)
	t.Log(s)
	lines := strings.Split(s, "\n")
	assert.NotEmpty(t, lines)
	assert.Equal(t, "github.com/ansel1/merry.TestStacktrace", lines[0])
	assert.Contains(t, lines[1], fmt.Sprintf("errors_test.go:%d", rl+1))
	// Allow nil error
	assert.Empty(t, Stacktrace(nil))
}

func TestWrap(t *testing.T) {
	err := errors.New("simple")
	_, _, rl, _ := runtime.Caller(0)
	wrapped := WrapSkipping(err, 0)
	f, l := Location(wrapped)
	if !strings.Contains(f, "errors_test.go") {
		t.Errorf("error message should have contained errors_test.go, was %s", f)
	}
	if l != rl+1 {
		t.Errorf("error line should have been %d, was %d", rl+1, l)
	}

	rich2 := WrapSkipping(wrapped, 0)
	if wrapped != rich2 {
		t.Error("rich and rich2 are not the same.  Wrap should have been no-op if rich was already a RichError")
	}
	if !reflect.DeepEqual(Stack(wrapped), Stack(rich2)) {
		t.Log(Details(rich2))
		t.Error("wrap should have left the stacktrace alone if the original error already had a stack")
	}
	// wrapping nil -> nil
	assert.Nil(t, Wrap(nil))
	assert.Nil(t, WrapSkipping(nil, 1))
}

func TestHere(t *testing.T) {
	ParseError := New("Parse error")
	InvalidCharSet := WithMessage(ParseError, "Invalid charset").WithHTTPCode(400)
	InvalidSyntax := ParseError.WithMessage("Syntax error")

	if !Is(InvalidCharSet, ParseError) {
		t.Error("InvalidCharSet should be a ParseError")
	}

	_, _, rl, _ := runtime.Caller(0)
	pe := Here(ParseError)
	_, l := Location(pe)
	if l != rl+1 {
		t.Errorf("Extend should capture a new stack.  Expected %d, got %d", rl+1, l)
	}

	if !Is(pe, ParseError) {
		t.Error("pe should be a ParseError")
	}
	if Is(pe, InvalidCharSet) {
		t.Error("pe should not be an InvalidCharSet")
	}
	if pe.Error() != "Parse error" {
		t.Errorf("child error's message is wrong, expected: Parse error, got %v", pe.Error())
	}
	icse := Here(InvalidCharSet)
	if !Is(icse, ParseError) {
		t.Error("icse should be a ParseError")
	}
	if !Is(icse, InvalidCharSet) {
		t.Error("icse should be an InvalidCharSet")
	}
	if Is(icse, InvalidSyntax) {
		t.Error("icse should not be an InvalidSyntax")
	}
	if icse.Error() != "Invalid charset" {
		t.Errorf("child's message is wrong.  Expected: Invalid charset, got: %v", icse.Error())
	}
	if HTTPCode(icse) != 400 {
		t.Errorf("child's http code is wrong.  Expected 400, got %v", HTTPCode(icse))
	}

	// nil -> nil
	assert.Nil(t, Here(nil))
}

func TestUnwrap(t *testing.T) {
	inner := errors.New("bing")
	wrapper := WrapSkipping(inner, 0)
	if Unwrap(wrapper) != inner {
		t.Errorf("unwrapped error should have been the inner err, was %#v", inner)
	}

	doubleWrap := wrapper.WithMessage("blag")
	if Unwrap(doubleWrap) != inner {
		t.Errorf("unwrapped should recurse to inner, but got %#v", inner)
	}

	// nil -> nil
	assert.Nil(t, Unwrap(nil))
}

func TestNilValues(t *testing.T) {
	// Quirk of go
	// http://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/index.html#nil_in_nil_in_vals
	// an interface value isn't nil unless both the type *and* the value are nil
	// make sure we aren't accidentally returning nil values but non-nil types
	type e struct{}
	var anE *e
	type f interface{}
	var anF f
	if anF != nil {
		t.Error("anF should have been nil here, because it doesn't have a concete type yet")
	}
	anF = anE
	if anF == nil {
		t.Error("anF should have been not nil here, because it now has a concrete type")
	}
	if WithMessage(WithHTTPCode(Wrap(nil), 400), "hey") != nil {
		t.Error("by using interfaces in all the returns, this should have remained a true nil value")
	}
}

func TestIs(t *testing.T) {
	ParseError := errors.New("blag")
	cp := Here(ParseError)
	if !Is(cp, ParseError) {
		t.Error("Is(child, parent) should be true")
	}
	if Is(ParseError, cp) {
		t.Error("Is(parent, child) should not be true")
	}
	if !Is(ParseError, ParseError) {
		t.Error("errors are always themselves")
	}
	if !Is(cp, cp) {
		t.Error("should work when comparing rich error to itself")
	}
	if Is(Here(ParseError), cp) {
		t.Error("Is(sibling, sibling) should not be true")
	}
	err2 := errors.New("blag")
	if Is(ParseError, err2) {
		t.Error("These should not have been equal")
	}
	if Is(Here(err2), cp) {
		t.Error("these were not copies of the same error")
	}
	if Is(Here(err2), ParseError) {
		t.Error("underlying errors were not equal")
	}

	nilTests := []struct {
		arg1, arg2 error
		expect     bool
		msg        string
	}{
		{nil, New("t"), false, "nil is not any concrete error"},
		{New("t"), nil, false, "no concrete error is nil"},
		{nil, nil, true, "nil is nil"},
	}
	for _, tst := range nilTests {
		assert.Equal(t, tst.expect, Is(tst.arg1, tst.arg2), tst.msg)
	}
}

func TestHTTPCode(t *testing.T) {
	basicErr := errors.New("blag")
	if c := HTTPCode(basicErr); c != 500 {
		t.Errorf("default code should be 500, was %d", c)
	}
	err := New("blug")
	if c := HTTPCode(err); c != 500 {
		t.Errorf("default code for rich errors should be 500, was %d", c)
	}
	errWCode := err.WithHTTPCode(404)
	if c := HTTPCode(errWCode); c != 404 {
		t.Errorf("the code should be set to 404, was %d", c)
	}
	if HTTPCode(err) != 500 {
		t.Error("original error should not have been modified")
	}

	// nil -> nil
	assert.Nil(t, WithHTTPCode(nil, 404))
	assert.Equal(t, 200, HTTPCode(nil), "The code for nil is 200 (ok)")
}

func TestImplicitWrapping(t *testing.T) {
	// WithXXX functions will implicitly wrap non-merry errors
	// but if they do so, they should skip a frame, so the merry error's stack
	// appears to start wherever the WithXXX function was called

	_, _, rl, _ := runtime.Caller(0)
	tests := []struct {
		f     func() error
		fname string
	}{
		{fname: "WithHTTPCode", f: func() error { return WithHTTPCode(errors.New("bug"), 404) }},
		{fname: "WithUserMessage", f: func() error { return WithUserMessage(errors.New("bug"), "asdf") }},
		{fname: "WithUserMessages", f: func() error { return WithUserMessagef(errors.New("bug"), "asdf") }},
		{fname: "WithMessage", f: func() error { return WithMessage(errors.New("bug"), "asdf") }},
		{fname: "WithMessagef", f: func() error { return WithMessagef(errors.New("bug"), "asdf") }},
		{fname: "WithValue", f: func() error { return WithValue(errors.New("bug"), "asdf", "asdf") }},
		{fname: "Append", f: func() error { return Append(errors.New("bug"), "asdf") }},
		{fname: "Appendf", f: func() error { return Appendf(errors.New("bug"), "asdf") }},
		{fname: "Prepend", f: func() error { return Prepend(errors.New("bug"), "asdf") }},
		{fname: "Prependf", f: func() error { return Prependf(errors.New("bug"), "asdf") }},
	}
	for i, test := range tests {
		t.Log("Testing ", test.fname)
		err := test.f()
		f, l := Location(err)
		assert.Contains(t, f, "errors_test.go", "error message should have contained errors_test.go")
		assert.Equal(t, rl+5+i, l, "error line number was incorrect")
	}
}

func TestWithMessage(t *testing.T) {
	err1 := New("blug")
	err2 := err1.WithMessage("blee")
	err3 := err2.WithMessage("red")
	assert.EqualError(t, err1, "blug")
	assert.EqualError(t, err2, "blee", "should have overridden the underlying message")
	assert.EqualError(t, err3, "red")
	assert.Equal(t, Stack(err1), Stack(err2), "stack should not have been altered")

	// nil -> nil
	assert.Nil(t, WithMessage(nil, ""))
}

func TestWithMessagef(t *testing.T) {
	err1 := New("blug")
	err2 := err1.WithMessagef("super %v", "stew")
	err3 := err1.WithMessagef("blue %v", "red")
	assert.EqualError(t, err1, "blug")
	assert.EqualError(t, err2, "super stew")
	assert.EqualError(t, err3, "blue red")
	assert.Equal(t, Stack(err1), Stack(err2), "stack should not have been altered")
	// nil -> nil
	assert.Nil(t, WithMessagef(nil, "", ""))
}

func TestMessage(t *testing.T) {
	tests := []error{
		errors.New("one"),
		WithMessage(errors.New("blue"), "one"),
		New("one"),
	}
	for _, test := range tests {
		assert.Equal(t, "one", test.Error())
		assert.Equal(t, "one", Message(test))
	}

	// when verbose is on, Error() changes, but Message() doesn't
	defer SetVerboseDefault(false)
	SetVerboseDefault(true)
	e := New("two")
	assert.Equal(t, "two", Message(e))
	assert.NotEqual(t, "two", e.Error())

	// when error is nil, return ""
	assert.Empty(t, Message(nil))

}

func TestWithUserMessage(t *testing.T) {
	fault := New("seg fault")
	e := WithUserMessage(fault, "a glitch")
	assert.Equal(t, "seg fault", e.Error())
	assert.Equal(t, "a glitch", UserMessage(e))
	e = WithUserMessagef(e, "not a %s deal", "huge")
	assert.Equal(t, "not a huge deal", UserMessage(e))
	// If user message is set and regular message isn't, set regular message to user message
	e = New("").WithUserMessage("a blag")
	assert.Equal(t, "a blag", UserMessage(e))
	assert.Equal(t, "a blag", e.Error())
}

func TestAppend(t *testing.T) {
	blug := New("blug")
	err := blug.Append("blog")
	assert.Equal(t, err.Error(), "blug: blog")
	err = Append(err, "blig")
	assert.Equal(t, err.Error(), "blug: blog: blig")
	err = blug.Appendf("%s", "blog")
	assert.Equal(t, err.Error(), "blug: blog")
	err = Appendf(err, "%s", "blig")
	assert.Equal(t, err.Error(), "blug: blog: blig")

	// nil -> nil
	assert.Nil(t, Append(nil, ""))
	assert.Nil(t, Appendf(nil, "", ""))
}

func TestPrepend(t *testing.T) {
	blug := New("blug")
	err := blug.Prepend("blog")
	assert.Equal(t, err.Error(), "blog: blug")
	err = Prepend(err, "blig")
	assert.Equal(t, err.Error(), "blig: blog: blug")
	err = blug.Prependf("%s", "blog")
	assert.Equal(t, err.Error(), "blog: blug")
	err = Prependf(err, "%s", "blig")
	assert.Equal(t, err.Error(), "blig: blog: blug")

	// nil -> nil
	assert.Nil(t, Prepend(nil, ""))
	assert.Nil(t, Prependf(nil, "", ""))
}

func TestLocation(t *testing.T) {
	// nil -> nil
	f, l := Location(nil)
	assert.Equal(t, "", f)
	assert.Equal(t, 0, l)
}

func TestSourceLine(t *testing.T) {
	source := SourceLine(nil)
	assert.Equal(t, source, "")

	err := New("foo")
	source = SourceLine(err)
	t.Log(source)
	assert.NotEqual(t, source, "")

	parts := strings.Split(source, ":")
	assert.Equal(t, len(parts), 2)

	if !strings.HasSuffix(parts[0], "errors_test.go") {
		t.Error("source line should contain file name")
	}
	if i, e := strconv.Atoi(parts[1]); e != nil {
		t.Errorf("not a number: %s", parts[1])
	} else if i <= 0 {
		t.Errorf("source line must be > 1: %s", parts[1])
	}
}

func TestValue(t *testing.T) {
	// nil -> nil
	assert.Nil(t, WithValue(nil, "", ""))
	assert.Nil(t, Value(nil, ""))
}

func TestValues(t *testing.T) {
	// nil -> nil
	values := Values(nil)
	assert.Nil(t, values)

	var e error
	e = New("bad stuff")
	e = WithValue(e, "key1", "val1")
	e = WithValue(e, "key2", "val2")

	values = Values(e)
	assert.NotNil(t, values)
	assert.Equal(t, values["key1"], "val1")
	assert.Equal(t, values["key2"], "val2")
	assert.NotNil(t, values[stack])

	// make sure the last value attached is returned
	e = WithValue(e, "key3", "val3")
	e = WithValue(e, "key3", "val4")
	values = Values(e)
	assert.Equal(t, values["key3"], "val4")

}

func TestStackCaptureEnabled(t *testing.T) {
	// on by default
	assert.True(t, StackCaptureEnabled())

	SetStackCaptureEnabled(false)
	assert.False(t, StackCaptureEnabled())
	e := New("yikes")
	assert.Empty(t, Stack(e))
	// let's just make sure none of the print functions bomb when there's no stack
	assert.Empty(t, SourceLine(e))
	f, l := Location(e)
	assert.Empty(t, f)
	assert.Equal(t, 0, l)
	assert.Empty(t, Stacktrace(e))
	assert.NotPanics(t, func() { Details(e) })

	// turn it back on
	SetStackCaptureEnabled(true)
	assert.True(t, StackCaptureEnabled())

	e = New("mommy")
	assert.NotEmpty(t, Stack(e))
}

func TestVerboseDefault(t *testing.T) {
	defer SetVerboseDefault(false)
	// off by default
	assert.False(t, VerboseDefault())

	SetVerboseDefault(true)
	assert.True(t, VerboseDefault())
	e := New("yikes")
	// test verbose on
	assert.Equal(t, Details(e), e.Error())
	// test verbose off
	SetVerboseDefault(false)
	s := e.Error()
	assert.Equal(t, Message(e), s)
	assert.Equal(t, "yikes", s)
}

func TestMerryErr_Error(t *testing.T) {
	origVerbose := verbose
	defer func() {
		verbose = origVerbose
	}()

	// test with verbose on
	verbose = false

	tests := []struct {
		desc                 string
		verbose              bool
		message, userMessage string
		expected             string
	}{
		{
			desc:     "with message",
			message:  "blue",
			expected: "blue",
		},
		{
			desc:        "with user message",
			userMessage: "red",
			expected:    "red",
		},
	}
	for _, test := range tests {
		t.Log("error message tests: " + test.desc)
		verbose = test.verbose
		err := New(test.message).WithUserMessage(test.userMessage)
		t.Log(err.Error())
		assert.Equal(t, test.expected, err.Error())
	}

}

func TestMerryErr_Format(t *testing.T) {
	e := New("Hi")
	assert.Equal(t, fmt.Sprintf("%v", e), e.Error())
	assert.Equal(t, fmt.Sprintf("%s", e), e.Error())
	assert.Equal(t, fmt.Sprintf("%q", e), fmt.Sprintf("%q", e.Error()))
	assert.Equal(t, fmt.Sprintf("%+v", e), Details(e))
}

func BenchmarkNew_withStackCapture(b *testing.B) {
	for i := 0; i < b.N; i++ {
		New("boom")
	}
}

func BenchmarkNew_withoutStackCapture(b *testing.B) {
	SetStackCaptureEnabled(false)
	for i := 0; i < b.N; i++ {
		New("boom")
	}
}
