package test

import (
	"encoding/json"
	"fmt"
	"reflect"
	"slices"
	"time"

	"github.com/tidbcloud/devops/pkg/infra/utils/pointer"

	"github.com/google/go-cmp/cmp"
)

func MarshalIndentOrDie(v any) string {
	data, err := json.MarshalIndent(v, "", "  ")
	if err != nil {
		panic(err)
	}
	return string(data)
}

func DeepCopyOrDie[T any](v *T) *T {
	n, err := pointer.DeepCopy(v)
	if err != nil {
		panic(err)
	}
	return n
}

func IsEqual(expect, actual any, opts ...cmp.Option) error {
	if diff := cmp.Diff(expect, actual, opts...); diff != "" {
		return fmt.Errorf("unexpected (-want, +got):\n%s", diff)
	}
	return nil
}

// ParseTimeOrDie parses time string with layout or panic.
func ParseTimeOrDie(layout, value string) *time.Time {
	t, err := time.Parse(layout, value)
	if err != nil {
		panic(fmt.Errorf("failed to parse time: %s", err.Error()))
	}

	return &t
}

// CheckNonZeroValue checks if obj and sub fields are zero value.
func CheckNonZeroValue(obj any, ignoredFields []string) error {
	return checkNonZeroValue(obj, "", ignoredFields)
}

func checkNonZeroValue(obj any, path string, ignoredFields []string) error {
	if slices.Index(ignoredFields, path) != -1 {
		return nil
	}

	objValue := reflect.ValueOf(obj)
	objType := objValue.Type()

	switch objValue.Kind() {
	case reflect.Struct:
		for i := 0; i < objValue.NumField(); i++ {
			field := objType.Field(i)
			fieldValue := objValue.Field(i)

			if fieldValue.CanInterface() { // unexported field
				err := checkNonZeroValue(fieldValue.Interface(), path+"."+field.Name, ignoredFields)
				if err != nil {
					return err
				}
			}
		}
		return nil

	case reflect.Pointer:
		if objValue.IsNil() {
			return fmt.Errorf("field '%s' is zero value 'nil'", path)
		}
		return checkNonZeroValue(objValue.Elem().Interface(), path, ignoredFields)

	case reflect.Slice, reflect.Array:
		if objValue.IsNil() || objValue.Len() == 0 {
			return fmt.Errorf("field '%s' is zero value '%v'", path, objValue.Interface())
		}
		for i := 0; i < objValue.Len(); i++ {
			if objValue.Index(i).Kind() == reflect.Uint8 { // skip []byte
				continue
			}

			err := checkNonZeroValue(objValue.Index(i).Interface(), fmt.Sprintf("%s[%d]", path, i), ignoredFields)
			if err != nil {
				return err
			}
		}
		return nil

	case reflect.Map:
		if objValue.IsNil() || objValue.Len() == 0 {
			return fmt.Errorf("field '%s' is zero value '%v'", path, objValue.Interface())
		}

		for _, key := range objValue.MapKeys() {
			if objValue.MapIndex(key).Kind() == reflect.Uint8 { // skip []byte
				continue
			}

			err := checkNonZeroValue(objValue.MapIndex(key).Interface(), fmt.Sprintf("%s[%v]", path, key.Interface()), ignoredFields)
			if err != nil {
				return err
			}
		}
		return nil

	default:
		zeroValue := reflect.Zero(objValue.Type())
		if reflect.DeepEqual(objValue.Interface(), zeroValue.Interface()) {
			return fmt.Errorf("field '%s' is zero value '%v'", path, objValue.Interface())
		}
		return nil
	}
}
