package util

import (
	"encoding/json"
	"fmt"
	"github.com/gin-gonic/gin"
	"gogs.mikescher.com/BlackForestBytes/goext/langext"
	"math"
	"reflect"
	"runtime/debug"
	"strings"
	"testing"
)

func AssertJsonMapEqual(t *testing.T, key string, expected map[string]any, actual map[string]any) {
	mkeys := make(map[string]string)
	for k := range expected {
		mkeys[k] = k
	}
	for k := range actual {
		mkeys[k] = k
	}

	for mapkey := range mkeys {

		if _, ok := expected[mapkey]; !ok {
			TestFailFmt(t, "Missing Key expected['%s'] ( assertJsonMapEqual[%s] )", mapkey, key)
		}
		if _, ok := actual[mapkey]; !ok {
			TestFailFmt(t, "Missing Key actual['%s'] ( assertJsonMapEqual[%s] )", mapkey, key)
		}

		AssertEqual(t, key+"."+mapkey, expected[mapkey], actual[mapkey])
	}

}

func AssertEqual(t *testing.T, key string, expected any, actual any) {

	// try to fix types, kinda hacky, but its only unit tests...
	switch vex := expected.(type) {
	case int:
		switch vac := actual.(type) {
		case int:
			// same
		case int32:
			expected = int64(vex)
			actual = int64(vac)
		case int64:
			expected = int64(vex)
		case float32:
			if IsWholeFloat(vac) {
				expected = int64(vex)
				actual = int64(vac)
			}
		case float64:
			if IsWholeFloat(vac) {
				expected = int64(vex)
				actual = int64(vac)
			}
		}
	case int32:
		switch vac := actual.(type) {
		case int:
			expected = int64(vex)
			actual = int64(vac)
		case int32:
			// same
		case int64:
			expected = int64(vex)
		case float32:
			if IsWholeFloat(vac) {
				expected = int64(vex)
				actual = int64(vac)
			}
		case float64:
			if IsWholeFloat(vac) {
				expected = int64(vex)
				actual = int64(vac)
			}
		}
	case int64:
		switch vac := actual.(type) {
		case int:
			actual = int64(vac)
		case int32:
			actual = int64(vac)
		case int64:
			// same
		case float32:
			if IsWholeFloat(vac) {
				actual = int64(vac)
			}
		case float64:
			if IsWholeFloat(vac) {
				actual = int64(vac)
			}
		}
	case float32:
		switch vac := actual.(type) {
		case int:
			if IsWholeFloat(vex) {
				expected = int64(vex)
				actual = int64(vac)
			}
		case int32:
			if IsWholeFloat(vex) {
				expected = int64(vex)
				actual = int64(vac)
			}
		case int64:
			if IsWholeFloat(vex) {
				expected = int64(vex)
			}
		case float32:
			// same
		case float64:
			expected = float64(vex)
		}
	case float64:
		switch vac := actual.(type) {
		case int:
			if IsWholeFloat(vex) {
				expected = int64(vex)
				actual = int64(vac)
			}
		case int32:
			if IsWholeFloat(vex) {
				expected = int64(vex)
				actual = int64(vac)
			}
		case int64:
			if IsWholeFloat(vex) {
				expected = int64(vex)
			}
		case float32:
			actual = float64(vac)
		case float64:
			// same
		}

	}

	if langext.IsNil(expected) && langext.IsNil(actual) {
		return
	}

	if expected != actual {
		t.Errorf("Value [%s] differs (%T <-> %T):\n", key, expected, actual)

		strExp := fmt.Sprintf("%v", expected)
		strAct := fmt.Sprintf("%v", actual)

		if strings.Contains(strAct, "\n") {
			t.Errorf("Actual:\n~~~~~~~~~~~~~~~~\n%v\n~~~~~~~~~~~~~~~~\n\n", actual)
		} else {
			t.Errorf("Actual    := \"%v\"\n", actual)
		}

		if strings.Contains(strExp, "\n") {
			t.Errorf("Expected:\n~~~~~~~~~~~~~~~~\n%v\n~~~~~~~~~~~~~~~~\n\n", expected)
		} else {
			t.Errorf("Expected  := \"%v\"\n", expected)
		}

		t.Error(string(debug.Stack()))

		t.FailNow()
	}
}

func AssertTrue(t *testing.T, key string, v bool) {
	if !v {
		t.Errorf("AssertTrue(%s) failed", key)
		t.Error(string(debug.Stack()))
		t.FailNow()
	}
}

func AssertNotDefault[T comparable](t *testing.T, key string, v T) {
	if v == *new(T) {
		t.Errorf("AssertNotDefault(%s) failed", key)
		t.Error(ljson(v))
		t.Error(string(debug.Stack()))
		t.FailNow()
	}
}

func AssertNotDefaultAny[T any](t *testing.T, key string, v T) {
	if ljson(v) == ljson(*new(T)) {
		t.Errorf("AssertNotDefault(%s) failed", key)
		t.Error(ljson(v))
		t.Error(string(debug.Stack()))
		t.FailNow()
	}
}

func AssertNotEqual(t *testing.T, key string, expected any, actual any) {
	if expected == actual || (langext.IsNil(expected) && langext.IsNil(actual)) {
		t.Errorf("Value [%s] does not differ (%T <-> %T):\n", key, expected, actual)

		str1 := fmt.Sprintf("%v", expected)
		str2 := fmt.Sprintf("%v", actual)

		if strings.Contains(str1, "\n") {
			t.Errorf("Actual:\n~~~~~~~~~~~~~~~~\n%v\n~~~~~~~~~~~~~~~~\n\n", expected)
		} else {
			t.Errorf("Actual       := \"%v\"\n", expected)
		}

		if strings.Contains(str2, "\n") {
			t.Errorf("Not Expected:\n~~~~~~~~~~~~~~~~\n%v\n~~~~~~~~~~~~~~~~\n\n", actual)
		} else {
			t.Errorf("Not Expected := \"%v\"\n", actual)
		}

		t.Error(string(debug.Stack()))

		t.FailNow()
	}
}

func AssertStrRepEqual(t *testing.T, key string, expected any, actual any) {
	strExp := fmt.Sprintf("%v", unpointer(expected))
	strAct := fmt.Sprintf("%v", unpointer(actual))

	if strAct != strExp {
		t.Errorf("Value [%s] differs (%T <-> %T):\n", key, expected, actual)

		if strings.Contains(strAct, "\n") {
			t.Errorf("Actual:\n~~~~~~~~~~~~~~~~\n%v\n~~~~~~~~~~~~~~~~\n\n", strAct)
		} else {
			t.Errorf("Actual    := \"%v\"\n", strAct)
		}

		if strings.Contains(strExp, "\n") {
			t.Errorf("Expected:\n~~~~~~~~~~~~~~~~\n%v\n~~~~~~~~~~~~~~~~\n\n", strExp)
		} else {
			t.Errorf("Expected  := \"%v\"\n", strExp)
		}

		t.Error(string(debug.Stack()))

		t.FailNow()
	}
}

func AssertNotStrRepEqual(t *testing.T, key string, expected any, actual any) {
	strExp := fmt.Sprintf("%v", unpointer(expected))
	strAct := fmt.Sprintf("%v", unpointer(actual))

	if strAct == strExp {
		t.Errorf("Value [%s] does not differ (%T <-> %T):\n", key, expected, actual)

		if strings.Contains(strAct, "\n") {
			t.Errorf("Actual:\n~~~~~~~~~~~~~~~~\n%v\n~~~~~~~~~~~~~~~~\n\n", strAct)
		} else {
			t.Errorf("Actual    := \"%v\"\n", strAct)
		}

		if strings.Contains(strExp, "\n") {
			t.Errorf("Expected:\n~~~~~~~~~~~~~~~~\n%v\n~~~~~~~~~~~~~~~~\n\n", strExp)
		} else {
			t.Errorf("Expected  := \"%v\"\n", strExp)
		}

		t.Error(string(debug.Stack()))

		t.FailNow()
	}
}

func TestFail(t *testing.T, msg string) {
	t.Error(msg)
	t.Error(string(debug.Stack()))
	t.FailNow()
}

func TestFailFmt(t *testing.T, format string, args ...any) {
	t.Errorf(format, args...)
	t.Error(string(debug.Stack()))
	t.FailNow()
}

func TestFailErr(t *testing.T, e error) {
	t.Error(fmt.Sprintf("Failed with error:\n%s\n\nError:\n%+v\n\nTrace:\n%s", e.Error(), e, string(debug.Stack())))
	t.Error(string(debug.Stack()))
	t.FailNow()
}

func TestFailIfErr(t *testing.T, e error) {
	if e != nil {
		TestFailErr(t, e)
	}
}

func AssertArrAny[T any](t *testing.T, key string, arr []T, fn func(T) bool) {
	if !langext.ArrAny(arr, fn) {
		t.Errorf("AssertArrAny(%s) failed", key)
		t.Error(string(debug.Stack()))
		t.FailNow()
	}
}

func AssertAny(v any) {
	// used to prevent golang "unused variable error"
}

func unpointer(v any) any {
	if v == nil {
		return v
	}

	val := reflect.ValueOf(v)
	if val.Kind() == reflect.Ptr {
		if val.IsNil() {
			return v
		}
		val = val.Elem()
		return unpointer(val.Interface())
	}
	return v
}

func AssertMultiNonEmpty(t *testing.T, key string, args ...any) {
	for i := 0; i < len(args); i++ {

		reflval := reflect.ValueOf(args[i])

		if args[i] == nil || reflval.IsZero() {
			t.Errorf("Value %s[%d] is empty (AssertMultiNonEmpty)", key, i)
			t.FailNow()
		}
	}
}

func AssertMappedSet[T langext.OrderedConstraint](t *testing.T, key string, expected []T, values []gin.H, objkey string) {

	actual := make([]T, 0)
	for idx, vv := range values {
		if tv, ok := vv[objkey].(T); ok {
			actual = append(actual, tv)
		} else {
			TestFailFmt(t, "[%s]->[%d] is wrong type (expected: %T, actual: %T)", key, idx, *new(T), vv)
		}
	}

	langext.Sort(actual)
	langext.Sort(expected)

	if !langext.ArrEqualsExact(actual, expected) {
		t.Errorf("Value [%s] differs (%T <-> %T):\n", key, expected, actual)

		t.Errorf("Actual    := [%v]\n", ljson(actual))
		t.Errorf("Expected  := [%v]\n", ljson(expected))

		t.Error(string(debug.Stack()))

		t.FailNow()
	}
}

func AssertMappedArr[T langext.OrderedConstraint](t *testing.T, key string, expected []T, values []gin.H, objkey string) {

	actual := make([]T, 0)
	for idx, vv := range values {
		if tv, ok := vv[objkey].(T); ok {
			actual = append(actual, tv)
		} else {
			TestFailFmt(t, "[%s]->[%d] is wrong type (expected: %T, actual: %T)", key, idx, *new(T), vv)
		}
	}

	if !langext.ArrEqualsExact(actual, expected) {
		t.Errorf("Value [%s] differs (%T <-> %T):\n", key, expected, actual)

		t.Errorf("Actual    := [%v]\n", actual)
		t.Errorf("Expected  := [%v]\n", expected)

		t.Error(string(debug.Stack()))

		t.FailNow()
	}
}

func IsWholeFloat[T langext.FloatConstraint](v T) bool {
	_, frac := math.Modf(math.Abs(float64(v)))
	return frac == 0.0
}

func ljson(v any) string {
	b, _ := json.Marshal(v)
	return string(b)
}
