package bugsnag_test

import (
	"fmt"
	"strings"
	"testing"

	simplejson "github.com/bitly/go-simplejson"
	"github.com/bugsnag/bugsnag-go"
	"github.com/bugsnag/bugsnag-go/errors"
	. "github.com/bugsnag/bugsnag-go/testutil"
)

var bugsnaggedReports chan []byte

func notifierSetup(url string) *bugsnag.Notifier {
	return bugsnag.New(bugsnag.Configuration{
		APIKey:    TestAPIKey,
		Endpoints: bugsnag.Endpoints{Notify: url, Sessions: url + "/sessions"},
	})
}

func crash(s interface{}) int {
	return s.(int)
}

func TestStackframesAreSkippedCorrectly(t *testing.T) {
	ts, reports := Setup()
	bugsnaggedReports = reports
	defer ts.Close()
	notifier := notifierSetup(ts.URL)

	bugsnag.Configure(bugsnag.Configuration{
		APIKey:    TestAPIKey,
		Endpoints: bugsnag.Endpoints{Notify: ts.URL, Sessions: ts.URL + "/sessions"},
	})

	t.Run("notifier.Notify", func(st *testing.T) {
		notifier.Notify(fmt.Errorf("oopsie"))
		assertStackframesMatch(t, []errors.StackFrame{
			errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func1", File: "notifier_test.go"},
		})
	})
	t.Run("bugsnag.Notify", func(st *testing.T) {
		bugsnag.Notify(fmt.Errorf("oopsie"))
		assertStackframesMatch(t, []errors.StackFrame{
			errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func2", File: "notifier_test.go"},
		})
	})

	t.Run("notifier.NotifySync", func(st *testing.T) {
		notifier.NotifySync(fmt.Errorf("oopsie"), true)
		assertStackframesMatch(t, []errors.StackFrame{
			errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func3", File: "notifier_test.go"},
		})
	})

	t.Run("notifier.AutoNotify", func(st *testing.T) {
		func() {
			defer func() { recover() }()
			defer notifier.AutoNotify()
			crash("NaN")
		}()
		assertStackframesMatch(t, []errors.StackFrame{
			errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func4.1", File: "notifier_test.go"},
			errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func4", File: "notifier_test.go"},
		})
	})
	t.Run("bugsnag.AutoNotify", func(st *testing.T) {
		func() {
			defer func() { recover() }()
			defer bugsnag.AutoNotify()
			crash("NaN")
		}()
		assertStackframesMatch(t, []errors.StackFrame{
			errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func5.1", File: "notifier_test.go"},
			errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func5", File: "notifier_test.go"},
		})
	})

	// Expect the following frames to be present for *.Recover
	/*
		{ "file": "runtime/panic.go", "method": "gopanic" },
		{ "file": "runtime/iface.go", "method": "panicdottypeE" },
		{ "file": "$GOPATH/src/github.com/bugsnag/bugsnag-go/notifier_test.go", "method": "TestStackframesAreSkippedCorrectly.func4.1" },
		{ "file": "$GOPATH/src/github.com/bugsnag/bugsnag-go/notifier_test.go", "method": "TestStackframesAreSkippedCorrectly.func4" },
		{ "file": "testing/testing.go", "method": "tRunner" },
		{ "file": "runtime/asm_amd64.s", "method": "goexit" }
	*/
	t.Run("notifier.Recover", func(st *testing.T) {
		func() {
			defer notifier.Recover()
			crash("NaN")
		}()
		assertStackframesMatch(t, []errors.StackFrame{
			errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func6.1", File: "notifier_test.go"},
			errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func6", File: "notifier_test.go"},
		})
	})
	t.Run("bugsnag.Recover", func(st *testing.T) {
		func() {
			defer bugsnag.Recover()
			crash("NaN")
		}()
		assertStackframesMatch(t, []errors.StackFrame{
			errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func7.1", File: "notifier_test.go"},
			errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func7", File: "notifier_test.go"},
		})
	})
}

func TestModifyingEventsWithCallbacks(t *testing.T) {
	server, eventQueue := Setup()
	defer server.Close()
	notifier := notifierSetup(server.URL)

	bugsnag.Configure(bugsnag.Configuration{
		APIKey:    TestAPIKey,
		Endpoints: bugsnag.Endpoints{Notify: server.URL, Sessions: server.URL + "/sessions"},
	})

	t.Run("bugsnag.Notify change unhandled in block", func(st *testing.T) {
		notifier.Notify(fmt.Errorf("ahoy"), func(event *bugsnag.Event) {
			event.Unhandled = true
		})
		json, _ := simplejson.NewJson(<-eventQueue)
		event := GetIndex(json, "events", 0)
		exception := GetIndex(event, "exceptions", 0)
		message := exception.Get("message").MustString()
		unhandled := event.Get("unhandled").MustBool()
		overridden := event.Get("severityReason").Get("unhandledOverridden").MustBool()
		if message != "ahoy" {
			st.Errorf("incorrect error message '%s'", message)
		}
		if !unhandled {
			st.Errorf("failed to change handled-ness in block")
		}
		if !overridden {
			st.Errorf("failed to set handledness change in block")
		}
	})

	t.Run("bugsnag.Notify with block", func(st *testing.T) {
		notifier.Notify(fmt.Errorf("bnuuy"), bugsnag.Context{String: "should be overridden"}, func(event *bugsnag.Event) {
			event.Context = "known unknowns"
		})
		json, _ := simplejson.NewJson(<-eventQueue)
		event := GetIndex(json, "events", 0)
		context := event.Get("context").MustString()
		exception := GetIndex(event, "exceptions", 0)
		class := exception.Get("errorClass").MustString()
		message := exception.Get("message").MustString()
		if class != "*errors.errorString" {
			st.Errorf("incorrect error class '%s'", class)
		}
		if message != "bnuuy" {
			st.Errorf("incorrect error message '%s'", message)
		}
		if context != "known unknowns" {
			st.Errorf("failed to change context in block. '%s'", context)
		}
		if event.Get("unhandled").MustBool() {
			st.Errorf("error is unexpectedly unhandled")
		}
		if overridden, err := event.Get("severityReason").Get("unhandledOverridden").Bool(); err == nil {
			// if err == nil, then the value existed in the payload. the expectation
			// is that unhandledOverridden is not sent when handled-ness is not changed.
			st.Errorf("error unexpectedly has unhandledOverridden: %v", overridden)
		}
	})
}

func assertStackframesMatch(t *testing.T, expected []errors.StackFrame) {
	var lastmatch int = 0
	var matched int = 0
	event, _ := simplejson.NewJson(<-bugsnaggedReports)
	json := GetIndex(event, "events", 0)
	stacktrace := GetIndex(json, "exceptions", 0).Get("stacktrace")
	for i := 0; i < len(stacktrace.MustArray()); i++ {
		actualFrame := stacktrace.GetIndex(i)
		file := actualFrame.Get("file").MustString()
		method := actualFrame.Get("method").MustString()
		for index, expectedFrame := range expected {
			if index < lastmatch {
				continue
			}
			if strings.HasSuffix(file, expectedFrame.File) && expectedFrame.Name == method {
				lastmatch = index
				matched++
			}
		}
	}

	if matched != len(expected) {
		s, _ := stacktrace.EncodePretty()
		t.Errorf("failed to find matches for %d frames: '%v'\ngot: '%v'", len(expected)-matched, expected[matched:], string(s))
	}
}
