package errors_test

import (
	"encoding/json"
	"fmt"
	"testing"

	go_errors "errors"

	pkg_errors "github.com/pkg/errors"

	uf_errors "gitlab.com/uafrica/go-utils/errors"
)

//use this struct to define various types of errors to experiment with
type Test struct {
	title string
	err   error
}

var tests []Test

//define the errors for testing
//in each case, the main error message is "<this is broken>"
//in some cases that is passed up and wrapped with more messages
//the tests use different combinations of go errors, fmt.Errorf(), github.com/pkg/errors and our own go-utils/errors library
//NOTE: I put all the new calls on new lines to report different line numbers in the stack...
func init() {
	tests = []Test{
		//a very simple go "errors.New()" error - does not have a stack
		{title: "go.New(...)", err: go_errors.New("<this is broken>")},

		//a fmt.Errorf() error - does not have a stack
		{title: "fmt.Errorf(...)", err: fmt.Errorf("<this is broken>")},

		//a better github.com/pkg/errors.New() error that includes the call stack
		{title: "pkg.New(...)", err: pkg_errors.New("<this is broken>")},

		//our own error type that always records the caller:
		{title: "uafrica.New(...)", err: uf_errors.New("<this is broken>")},

		//one can use pkg WithStack() to add a stack to an error that does not have a stack
		{title: "pkg.WithStack(go.New(...))", err: pkg_errors.WithStack(
			go_errors.New("<this is broken>"),
		)},

		//if you are ok with pkg's way to add a stack manually (I am not),
		//then you could easily make this mistake to add a stack to an error that already has a stack
		//by default pkg then prints two stacks and the rest of the error messages are after the last stack
		//making it very hard to figure out what went wrong and where...
		{title: "pkg.WithStack(pkg.New())", err: pkg_errors.WithStack(
			pkg_errors.New("<this is broken>"),
		)},

		//one can also add a stack with our own errors,
		//note that we always want an explanation which is just to enforce good practice:
		{title: "uafrica.Wrapf(go.New(...))", err: uf_errors.Wrapf(
			go_errors.New("<this is broken>"),
			"<it is not working>",
		)},

		//and when we wrap an error that already has a stack, it will not duplicate,
		//note that we always want an explanation which is just to enforce good practice:
		{title: "uafrica.Wrapf(pkg.New(...))", err: uf_errors.Wrapf(
			pkg_errors.New("<this is broken>"),
			"<it is not working>",
		)},

		//pkg can also wrap an error with a text explanation,
		//but the synctax is annoying as one has to add the stack and the message separately
		//(compared to a simple Wrapf() call to imply a stack as well...)

		//we firstly wrap an error that has NO stack:
		{title: "pkg.WithMessage(pkg.WithStack(go.New()))", err: pkg_errors.WithMessage(
			pkg_errors.WithStack(
				go_errors.New("<this is broken>"),
			),
			"<it is not working>",
		)},

		//we will typically pass an error up several layers before it is reported
		//and the original error may come without a stack,
		//and another package might have added a stack using pkg
		//then we still add our own explanations on top of that
		//in both the following two examples, we have the following stack:
		//
		//		db not open						(without a stack)
		//		query failed					(without a stack, using fmt.Errorf("query failed: %v", err))
		//		failed to read list				(stack added with pkg.WithStack(), then message added with pkg.WithMessage())
		//		failed to read users from db	(demonstrated with pkg.WithMessage() and our own Wrapf())
		//		cannot display list of users	(demonstrated with pkg.WithMessage() and our own Wrapf())
		//
		//first using only pkg:
		{title: "full stack with pkg", err: pkg_errors.WithMessage(
			pkg_errors.WithMessage(
				pkg_errors.WithStack( //assuming this comes from 3rd party lib e.g. sql or pg - we cannot change this or deeper errors
					pkg_errors.WithMessage(
						fmt.Errorf(
							"query failed: %v",
							go_errors.New("db not open"),
						),
						"failed to read list",
					),
				),
				"failed to read users from db",
			),
			"cannot display list of users",
		)},

		//same thing but wrapping at the top with out own messages and stack
		{title: "full stack with uafrica", err: uf_errors.Wrapf(
			uf_errors.Wrapf(
				pkg_errors.WithStack( //assuming this comes from 3rd party lib e.g. sql or pg - we cannot change this or deeper errors
					pkg_errors.WithMessage(
						fmt.Errorf(
							"query failed: %v",
							go_errors.New("db not open"),
						),
						"failed to read list",
					),
				),
				"failed to read users from db",
			),
			"cannot display list of users",
		)},

		//============================================================================================================================================
		//in the final full stack case, here is the comparison:
		//============================================================================================================================================
		//  OUTPUT OF err.Errorf()
		//  ----------------------
		//
		//	pkg:		cannot display list of users: failed to read users from db: failed to read list: query failed: db not open
		//
		//	uafrica:	cannot display list of users, because failed to read users from db, because failed to read list: query failed: db not open
		//
		//	(at the end, uafrica could not insert ", because ..." because the deepest level was just text from fmt.Errorf(...))
		//============================================================================================================================================
		//  OUTPUT OF Printf("%s") for simple error
		//  ---------------------------------------
		//
		//	pkg:		cannot display list of users: failed to read users from db: failed to read list: query failed: db not open
		//
		//	uafrica:	cannot display list of users
		//
		//	Note that pkg has no simple way to just display the top level error, while we can do it with formatting directive "%s"
		//============================================================================================================================================
		//  OUTPUT OF Printf("%+v") for full error stack
		//  --------------------------------------------
		//
		//	pkg:
		//		query failed: db not open
		// 		failed to read list
		// 		command-line-arguments_test.init.0
		// 			/Users/jansemmelink/uafrica/go-utils/errors/other_test.go:96
		// 		runtime.doInit
		// 			/usr/local/go/src/runtime/proc.go:6498
		// 		runtime.doInit
		// 			/usr/local/go/src/runtime/proc.go:6475
		// 		runtime.main
		// 			/usr/local/go/src/runtime/proc.go:238
		// 		runtime.goexit
		// 			/usr/local/go/src/runtime/asm_arm64.s:1133
		// 		failed to read users from db
		// 		cannot display list of users
		//
		//	uafrica:
		//		command-line-arguments_test.init/other_test.go(111): 0() cannot display list of users, because
		//		command-line-arguments_test.init/other_test.go(112): 0() failed to read users from db, because
		//		command-line-arguments_test.init.0/other_test.go(113): init.0(): failed to read list: query failed: db not open
		//
		//	Difference:
		//		- pkg       starts with the lowest level then what effects it had (opposite order from how it prints err.Error())
		//		  uafrica   maintains the order of effect then the reason why, which kind of makes more sense
		//		- pkg       write multiple lines for each stack entry, and does not keep the messages to the stack lines
		//		  uafrica   put the error message next to the stack line that wrote it
		//		- pkg       wrapping with text, does not include the stack again, so line 94 and 95 that passed the error on, is not mentioned
		//        uafrica   mention each line that handled the error: 111, 112, 113 (called libs in line 114..117 did not include the stack)
		//		- pkg       reports full package and FULL filename (host path where the code was compiled) which is not useful in production
		//		  uafrica   reports the package and BASE filename and function name instead.
		//============================================================================================================================================
		//  Compact Stack:
		//	--------------
		//	In streaming log files we do not want a multi-line display of error messages, so uafrica also supports "%c" and "%+c" formatting to print
		//	on one line with ';' instead of new lines. The '+' adds the source package, file, line and function name.
		//
		//	"%c" is same as err.Error():
		//
		//		cannot display list of users, because failed to read users from db, because failed to read list: query failed: db not open
		//
		//	"%+c" is that plus the source references:
		//
		//		command-line-arguments_test.init/error_formats_test.go(111): 0() cannot display list of users, because command-line-arguments_test.init/error_formats_test.go(112): 0() failed to read users from db, because command-line-arguments_test.init.0/error_formats_test.go(113): init.0(): failed to read list: query failed: db not open
		//
		//	"%v"  is same as "%c"  over multiple lines:
		//
		//		cannot display list of users, because
		// 		failed to read users from db, because
		// 		failed to read list: query failed: db not open
		//
		//	"%+v" is same as "%+c" over multiple lines: (as in previous section where compared with pkg):
		//
		//		command-line-arguments_test.init/error_formats_test.go(111): 0() cannot display list of users, because
		//		command-line-arguments_test.init/error_formats_test.go(112): 0() failed to read users from db, because
		//		command-line-arguments_test.init.0/error_formats_test.go(113): init.0(): failed to read list: query failed: db not open
		//
		//============================================================================================================================================
		//	JSON:
		//	-----
		//  Having our own errors package allows us also flexibility to do things like creating a JSON error description.
		//	This is illustrated in the JSON output:
		//		{"error":"cannot display list of users","source":{"package":"command-line-arguments_test.init","file":"other_test.go","line":111,"function":"0"},
		//		"cause":{"error":"failed to read users from db","source":{"package":"command-line-arguments_test.init","file":"other_test.go","line":112,
		//		"function":"0"},"cause":{"error":"failed to read list: query failed: db not open","source":{"package":"command-line-arguments_test.init.0",
		//		"file":"other_test.go","line":113,"function":"init.0"},"cause":{"error":"failed to read list: query failed: db not open","cause":{
		//		"error":"query failed: db not open"}}}}}
		//	Which we can pretty print with jq into a very readable stack:
		//
		//  {
		// 	  "error": "cannot display list of users",
		// 	  "source": {
		// 	    "package": "command-line-arguments_test.init",
		// 	    "file": "other_test.go",
		// 	    "line": 111,
		// 	    "function": "0"
		// 	  },
		// 	  "cause": {
		// 	    "error": "failed to read users from db",
		// 	    "source": {
		// 	      "package": "command-line-arguments_test.init",
		// 	      "file": "other_test.go",
		// 	      "line": 112,
		// 	      "function": "0"
		// 	    },
		// 	    "cause": {
		// 	      "error": "failed to read list: query failed: db not open",
		// 	      "source": {
		// 	        "package": "command-line-arguments_test.init.0",
		// 	        "file": "other_test.go",
		// 	        "line": 113,
		// 	        "function": "init.0"
		// 	      },
		// 	      "cause": {
		// 	        "error": "failed to read list: query failed: db not open",
		// 	        "cause": {
		// 	          "error": "query failed: db not open"
		// 	        }
		// 	      }
		// 	    }
		// 	  }
		// 	}
		//============================================================================================================================================
	}
}

func TestErrorFormats(t *testing.T) {
	for _, test := range tests {
		show(t, test.title, test.err)
	}
}

func show(t *testing.T, title string, err error) {
	t.Logf("------------------------------------------------------------------------------------")
	t.Logf("-----[ %-70.70s ]-----", title)
	t.Logf("------------------------------------------------------------------------------------")
	t.Logf("err.Error():  %s", err.Error())
	t.Logf("Printf(%%s):  %s", err)
	if _, ok := err.(*uf_errors.CustomError); ok { //compact only supported on our own lib:
		t.Logf("Printf(%%c):  %c", err)
		t.Logf("Printf(%%+c): %+c", err)
	}
	t.Logf("Printf(%%v):  %v", err)
	t.Logf("Printf(%%+v): %+v", err)
	if e, ok := err.(*uf_errors.CustomError); ok { //description only supported on our own lib:
		desc := e.Description()
		jsonDesc, _ := json.Marshal(desc)
		t.Logf("JSON: %s", string(jsonDesc))
	}
}