package bugsnag

import (
	"fmt"
	"io/ioutil"
	"log"
	"net"
	"net/http"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/bitly/go-simplejson"
)

func TestConfigure(t *testing.T) {
	Configure(Configuration{
		APIKey: testAPIKey,
	})

	if Config.APIKey != testAPIKey {
		t.Errorf("Setting APIKey didn't work")
	}

	if New().Config.APIKey != testAPIKey {
		t.Errorf("Setting APIKey didn't work for new notifiers")
	}
}

var postedJSON = make(chan []byte, 10)
var testOnce sync.Once
var testEndpoint string
var testAPIKey = "166f5ad3590596f9aa8d601ea89af845"

func startTestServer() {
	testOnce.Do(func() {
		mux := http.NewServeMux()
		mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
			body, err := ioutil.ReadAll(r.Body)
			if err != nil {
				panic(err)
			}
			postedJSON <- body
		})

		l, err := net.Listen("tcp", "127.0.0.1:0")
		if err != nil {
			panic(err)
		}
		testEndpoint = "http://" + l.Addr().String() + "/"

		go http.Serve(l, mux)
	})
}

type _recurse struct {
	*_recurse
}

func TestNotify(t *testing.T) {
	startTestServer()

	recurse := _recurse{}
	recurse._recurse = &recurse

	OnBeforeNotify(func(event *Event, config *Configuration) error {
		if event.Context == "testing" {
			event.GroupingHash = "lol"
		}
		return nil
	})

	Notify(fmt.Errorf("hello world"),
		Configuration{
			APIKey:          testAPIKey,
			Endpoint:        testEndpoint,
			ReleaseStage:    "test",
			AppVersion:      "1.2.3",
			Hostname:        "web1",
			ProjectPackages: []string{"github.com/bugsnag/bugsnag-go"},
		},
		User{Id: "123", Name: "Conrad", Email: "me@cirw.in"},
		Context{"testing"},
		MetaData{"test": {
			"password": "sneaky",
			"value":    "able",
			"broken":   complex(1, 2),
			"recurse":  recurse,
		}},
	)

	json, err := simplejson.NewJson(<-postedJSON)

	if err != nil {
		t.Fatal(err)
	}

	if json.Get("apiKey").MustString() != testAPIKey {
		t.Errorf("Wrong api key in payload")
	}

	if json.GetPath("notifier", "name").MustString() != "Bugsnag Go" {
		t.Errorf("Wrong notifier name in payload")
	}

	event := json.Get("events").GetIndex(0)

	for k, value := range map[string]string{
		"payloadVersion":                 "2",
		"severity":                       "warning",
		"context":                        "testing",
		"groupingHash":                   "lol",
		"app.releaseStage":               "test",
		"app.version":                    "1.2.3",
		"device.hostname":                "web1",
		"user.id":                        "123",
		"user.name":                      "Conrad",
		"user.email":                     "me@cirw.in",
		"metaData.test.password":         "[REDACTED]",
		"metaData.test.value":            "able",
		"metaData.test.broken":           "[complex128]",
		"metaData.test.recurse._recurse": "[RECURSION]",
	} {
		key := strings.Split(k, ".")
		if event.GetPath(key...).MustString() != value {
			t.Errorf("Wrong %v: %v != %v", key, event.GetPath(key...).MustString(), value)
		}
	}

	exception := event.Get("exceptions").GetIndex(0)

	if exception.Get("message").MustString() != "hello world" {
		t.Errorf("Wrong message in payload")
	}

	if exception.Get("errorClass").MustString() != "*errors.errorString" {
		t.Errorf("Wrong errorClass in payload: %v", exception.Get("errorClass").MustString())
	}

	frame0 := exception.Get("stacktrace").GetIndex(0)
	if frame0.Get("file").MustString() != "bugsnag_test.go" ||
		frame0.Get("method").MustString() != "TestNotify" ||
		frame0.Get("inProject").MustBool() != true ||
		frame0.Get("lineNumber").MustInt() == 0 {
		t.Errorf("Wrong frame0")
	}

	frame1 := exception.Get("stacktrace").GetIndex(1)

	if frame1.Get("file").MustString() != "testing/testing.go" ||
		frame1.Get("method").MustString() != "tRunner" ||
		frame1.Get("inProject").MustBool() != false ||
		frame1.Get("lineNumber").MustInt() == 0 {
		t.Errorf("Wrong frame1")
	}
}

func crashyHandler(w http.ResponseWriter, r *http.Request) {
	c := make(chan int)
	close(c)
	c <- 1
}

func runCrashyServer(rawData ...interface{}) (net.Listener, error) {
	l, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		return nil, err
	}

	mux := http.NewServeMux()
	mux.HandleFunc("/", crashyHandler)
	srv := http.Server{
		Addr:     l.Addr().String(),
		Handler:  Handler(mux, rawData...),
		ErrorLog: log.New(ioutil.Discard, log.Prefix(), 0),
	}

	go srv.Serve(l)
	return l, err
}

func TestHandler(t *testing.T) {
	startTestServer()

	l, err := runCrashyServer(Configuration{
		APIKey:          testAPIKey,
		Endpoint:        testEndpoint,
		ProjectPackages: []string{"github.com/bugsnag/bugsnag-go"},
		Logger:          log.New(ioutil.Discard, log.Prefix(), log.Flags()),
	}, SeverityInfo)
	if err != nil {
		t.Fatal(err)
	}
	http.Get("http://" + l.Addr().String() + "/ok?foo=bar")
	l.Close()

	json, err := simplejson.NewJson(<-postedJSON)
	if err != nil {
		t.Fatal(err)
	}

	if json.Get("apiKey").MustString() != testAPIKey {
		t.Errorf("Wrong api key in payload")
	}

	if json.GetPath("notifier", "name").MustString() != "Bugsnag Go" {
		t.Errorf("Wrong notifier name in payload")
	}

	event := json.Get("events").GetIndex(0)

	for k, value := range map[string]string{
		"payloadVersion":          "2",
		"severity":                "info",
		"user.id":                 "127.0.0.1",
		"metaData.Request.Url":    "http://" + l.Addr().String() + "/ok?foo=bar",
		"metaData.Request.Method": "GET",
	} {
		key := strings.Split(k, ".")
		if event.GetPath(key...).MustString() != value {
			t.Errorf("Wrong %v: %v != %v", key, event.GetPath(key...).MustString(), value)
		}
	}

	if event.GetPath("metaData", "Request", "Params", "foo").GetIndex(0).MustString() != "bar" {
		t.Errorf("missing GET params in request metadata")
	}

	if event.GetPath("metaData", "Headers", "Accept-Encoding").GetIndex(0).MustString() != "gzip" {
		t.Errorf("missing GET params in request metadata: %v", event.GetPath("metaData", "Headers"))
	}

	exception := event.Get("exceptions").GetIndex(0)

	if !strings.Contains(exception.Get("message").MustString(), "send on closed channel") {
		t.Errorf("Wrong message in payload: %v '%v'", exception.Get("message").MustString(), "runtime error: send on closed channel")
	}

	errorClass := exception.Get("errorClass").MustString()
	if errorClass != "runtime.errorCString" && errorClass != "*errors.errorString" {
		t.Errorf("Wrong errorClass in payload: %v, '%v'", exception.Get("errorClass").MustString(), "runtime.errorCString")
	}

	frame0 := exception.Get("stacktrace").GetIndex(0)

	file0 := frame0.Get("file").MustString()
	if !strings.HasPrefix(file0, "runtime/panic") ||
		frame0.Get("inProject").MustBool() != false {
		t.Errorf("Wrong frame0: %v", frame0)
	}

	frame3 := exception.Get("stacktrace").GetIndex(3)

	if frame3.Get("file").MustString() != "bugsnag_test.go" ||
		frame3.Get("method").MustString() != "crashyHandler" ||
		frame3.Get("inProject").MustBool() != true ||
		frame3.Get("lineNumber").MustInt() == 0 {
		t.Errorf("Wrong frame3: %v", frame3)
	}
}

func TestAutoNotify(t *testing.T) {

	var panicked interface{}

	func() {
		defer func() {
			panicked = recover()
		}()
		defer AutoNotify(Configuration{Endpoint: testEndpoint, APIKey: testAPIKey})

		panic("eggs")
	}()

	if panicked.(string) != "eggs" {
		t.Errorf("didn't re-panic")
	}

	json, err := simplejson.NewJson(<-postedJSON)
	if err != nil {
		t.Fatal(err)
	}

	event := json.Get("events").GetIndex(0)

	if event.Get("severity").MustString() != "error" {
		t.Errorf("severity should be error")
	}
	exception := event.Get("exceptions").GetIndex(0)

	if exception.Get("message").MustString() != "eggs" {
		t.Errorf("caught wrong panic")
	}
}

func TestRecover(t *testing.T) {
	var panicked interface{}

	func() {
		defer func() {
			panicked = recover()
		}()
		defer Recover(Configuration{Endpoint: testEndpoint, APIKey: testAPIKey})

		panic("ham")
	}()

	if panicked != nil {
		t.Errorf("re-panick'd")
	}

	json, err := simplejson.NewJson(<-postedJSON)
	if err != nil {
		t.Fatal(err)
	}

	event := json.Get("events").GetIndex(0)

	if event.Get("severity").MustString() != "warning" {
		t.Errorf("severity should be warning")
	}
	exception := event.Get("exceptions").GetIndex(0)

	if exception.Get("message").MustString() != "ham" {
		t.Errorf("caught wrong panic")
	}
}

func handleGet(w http.ResponseWriter, r *http.Request) {

}

var createAccount = handleGet

type _job struct {
	Name    string
	Process func()
}

func ExampleAutoNotify() interface{} {
	return func(w http.ResponseWriter, request *http.Request) {
		defer AutoNotify(request, Context{"createAccount"})

		createAccount(w, request)
	}
}

func ExampleRecover(job _job) {
	go func() {
		defer Recover(Context{job.Name}, SeverityWarning)

		job.Process()
	}()
}

func ExampleConfigure() {
	Configure(Configuration{
		APIKey: "YOUR_API_KEY_HERE",

		ReleaseStage: "production",

		// See Configuration{} for other fields
	})
}

func ExampleHandler() {
	// Set up your http handlers as usual
	http.HandleFunc("/", handleGet)

	// use bugsnag.Handler(nil) to wrap the default http handlers
	// so that Bugsnag is automatically notified about panics.
	http.ListenAndServe(":1234", Handler(nil))
}

func ExampleHandler_customServer() {
	// If you're using a custom server, set the handlers explicitly.
	http.HandleFunc("/", handleGet)

	srv := http.Server{
		Addr:        ":1234",
		ReadTimeout: 10 * time.Second,
		// use bugsnag.Handler(nil) to wrap the default http handlers
		// so that Bugsnag is automatically notified about panics.
		Handler: Handler(nil),
	}
	srv.ListenAndServe()
}

func ExampleHandler_customHandlers() {
	// If you're using custom handlers, wrap the handlers explicitly.
	handler := http.NewServeMux()
	http.HandleFunc("/", handleGet)
	// use bugsnag.Handler(handler) to wrap the handlers so that Bugsnag is
	// automatically notified about panics
	http.ListenAndServe(":1234", Handler(handler))
}

func ExampleNotify() {
	_, err := net.Listen("tcp", ":80")

	if err != nil {
		Notify(err)
	}
}

func ExampleNotify_details(userID string) {
	_, err := net.Listen("tcp", ":80")

	if err != nil {
		Notify(err,
			// show as low-severity
			SeverityInfo,
			// set the context
			Context{"createlistener"},
			// pass the user id in to count users affected.
			User{Id: userID},
			// custom meta-data tab
			MetaData{
				"Listen": {
					"Protocol": "tcp",
					"Port":     "80",
				},
			},
		)
	}

}

type Job struct {
	Retry     bool
	UserId    string
	UserEmail string
	Name      string
	Params    map[string]string
}

func ExampleOnBeforeNotify() {
	OnBeforeNotify(func(event *Event, config *Configuration) error {

		// Search all the RawData for any *Job pointers that we're passed in
		// to bugsnag.Notify() and friends.
		for _, datum := range event.RawData {
			if job, ok := datum.(*Job); ok {
				// don't notify bugsnag about errors in retries
				if job.Retry {
					return fmt.Errorf("bugsnag middleware: not notifying about job retry")
				}

				// add the job as a tab on Bugsnag.com
				event.MetaData.AddStruct("Job", job)

				// set the user correctly
				event.User = &User{Id: job.UserId, Email: job.UserEmail}
			}
		}

		// continue notifying as normal
		return nil
	})
}
