package util

import (
	"fmt"
	"reflect"
	"strconv"
	"strings"
	"testing"
	"time"

	"github.com/pingcap/errors"
)

type CompareResult struct {
	FieldName string
	Value1    interface{}
	Value2    interface{}

	IsArray         bool
	IsStruct        bool
	RecursiveResult []*CompareResult
}

func isStruct(field reflect.Value) bool {
	return field.Kind() == reflect.Struct || (field.Kind() == reflect.Ptr && field.Elem().Kind() == reflect.Struct)
}

func isArray(field reflect.Value) bool {
	return field.Kind() == reflect.Slice || field.Kind() == reflect.Array
}

func CompareList(list0, list1 []interface{},
	compare func(struct1 interface{}, struct2 interface{}) (areEqual bool, differences []*CompareResult, err error)) (isEqual bool, resultList []*CompareResult, err error) {
	var shorterLenghth = len(list0)
	var longerList []interface{}
	isEqual = true
	if len(list0) > len(list1) {
		shorterLenghth = len(list1)
		longerList = list0
		isEqual = false
	} else if len(list0) < len(list1) {
		shorterLenghth = len(list0)
		longerList = list1
		isEqual = false
	}

	for idx := 0; idx < shorterLenghth; idx += 1 {
		elemEqual, elemResult, elemErr := compare(list0[idx], list1[idx])
		isEqual = elemEqual && isEqual
		resultList = append(resultList, elemResult...)
		err = errors.Wrap(elemErr, "")
	}

	for idx := shorterLenghth; idx < len(longerList); idx += 1 {
		result := &CompareResult{
			FieldName:       "[" + strconv.Itoa(idx) + "] " + reflect.Indirect(reflect.ValueOf(longerList[idx])).Type().Name(),
			Value1:          nil,
			Value2:          longerList[idx],
			IsArray:         isArray(reflect.Indirect(reflect.ValueOf(longerList[idx]))),
			IsStruct:        isStruct(reflect.ValueOf(longerList[idx])),
			RecursiveResult: nil,
		}
		resultList = append(resultList, result)
	}

	return isEqual, resultList, err
}

func (cr *CompareResult) String() string {
	wholeString := ""
	if !cr.IsArray && !cr.IsStruct {
		wholeString += fmt.Sprintf("Field:%v, value 1: %v, value 2: %v", cr.FieldName, cr.Value1, cr.Value2)
	} else {
		for _, result := range cr.RecursiveResult {
			wholeString += "-- " + result.String()
		}
	}

	return wholeString
}

func compareField(field1, field2 reflect.Value) (isEqual bool, result *CompareResult, err error) {
	result = nil
	if isStruct(field1) {
		isRecEqual, elemResultList, err := Compare(field1.Interface(), field2.Interface(), nil)
		if !isRecEqual || err != nil {
			result = &CompareResult{}
			result.Value1 = field1.Interface()
			result.Value2 = field2.Interface()
			result.IsArray = false
			result.IsStruct = true
			result.RecursiveResult = elemResultList
		}
	} else if isArray(field1) {
		isRecEqual, elemResultList, err := compareArrayElem(field1, field2)
		if !isRecEqual || err != nil {
			result = &CompareResult{}
			result.Value1 = field1.Interface()
			result.Value2 = field2.Interface()
			result.IsArray = true
			result.IsStruct = false
			result.RecursiveResult = elemResultList
		}
	} else {
		switch val1, val2 := field1.Interface(), field2.Interface(); val1.(type) {
		case int, int64, bool, string, float64, time.Time:
			if val1 != val2 {
				result = &CompareResult{}
				result.Value1 = val1
				result.Value2 = val2
				result.IsArray = false
				result.IsStruct = false
				result.RecursiveResult = nil
			}
		default:
			result = &CompareResult{}
			result.Value1 = val1
			result.Value2 = val2
			result.IsArray = false
			result.IsStruct = false
			result.RecursiveResult = nil
			return false, result, fmt.Errorf("unsupported type: %v", val1)
		}
	}

	return result == nil, result, err
}

func compareArrayElem(field1, field2 reflect.Value) (areEqual bool, differences []*CompareResult, err error) {
	var shortLenght = 0
	var longerField = field1
	var isFirstLonger = false
	if field1.Len() > field2.Len() {
		shortLenght = field2.Len()
		longerField = field1
		isFirstLonger = true
	} else if field1.Len() < field2.Len() {
		shortLenght = field1.Len()
		longerField = field2
	}

	var arrayElemResultList = make([]*CompareResult, 0)
	for idx := 0; idx < shortLenght; idx += 1 {
		item1 := field1.Index(idx)
		item2 := field2.Index(idx)
		item1, item2 = reflect.Indirect(item1), reflect.Indirect(item2)

		// the element of the array can be struct or scalar types
		isEqual, elemResult, elemErr := compareField(item1, item2)
		if !isEqual || elemErr != nil {
			elemResult.FieldName = "[" + strconv.Itoa(idx) + "] " + field1.Type().Name()
			arrayElemResultList = append(arrayElemResultList, elemResult)
		}
	}

	for idx := shortLenght; idx < longerField.Len(); idx += 1 {
		item := longerField.Index(idx)
		var elemFieldResult = &CompareResult{}
		elemFieldResult.FieldName = "[" + strconv.Itoa(idx) + "] " + field1.Type().Name()
		if isFirstLonger {
			elemFieldResult.Value1 = item
			elemFieldResult.Value2 = nil
		} else {
			elemFieldResult.Value1 = nil
			elemFieldResult.Value2 = item
		}

		err = errors.Wrap(err, "["+strconv.Itoa(idx)+"] "+field1.Type().Name()+" not identical")
	}

	return len(differences) == 0, differences, err
}

// https://groups.google.com/g/golang-nuts/c/neGz_Rxtxxw
// https://www.cnblogs.com/liweijiee/p/13879163.html
func Compare(struct1 interface{}, struct2 interface{}, filterFieldList []string) (areEqual bool, differences []*CompareResult, err error) {
	if struct1 == nil || struct2 == nil {
		return false, nil, fmt.Errorf("One of the inputs cannot be nil. struct1: %v, struct2 : %v ", struct1, struct2)
	}

	//Get values of the structs
	v1, v2 := reflect.ValueOf(struct1), reflect.ValueOf(struct2)

	//Handle pointers, if a non-pointer struct is passed in, Indirect will just return the element
	v1, v2 = reflect.Indirect(v1), reflect.Indirect(v2)
	if !v1.IsValid() || !v2.IsValid() {
		return false, nil, fmt.Errorf("Types cannot be nil. v1 %v - v2 %v", v1.IsValid(), v2.IsValid())
	}

	//Cache v1 struct type
	structType := v1.Type()

	//Verify both v1 and v2 are the same type
	if structType != v2.Type() {
		return false, nil, fmt.Errorf("Structs must be the same type. Struct1 %v - Stuct2 -%v", structType, v2.Type())
	}

	//Verify v1 is a struct, if v1 is a struct then v2 is also a struct because we have already verified the types are equal
	if v1.Kind() != reflect.Struct {
		return false, nil, fmt.Errorf("Types must both be structs.  Kind1: %v, Kind2 :%v", v1.Kind(), v2.Kind())
	}

	//Initialize differences to ensure length of 0 on return
	differences = make([]*CompareResult, 0)

	for idx, numFields := 0, v1.NumField(); idx < numFields; idx += 1 {
		//Get values of the structure's fields
		field1, field2 := v1.Field(idx), v2.Field(idx)

		//Get a reference to the field type
		fieldType := structType.Field(idx)

		isFilterField := false
		for _, filteredField := range filterFieldList {
			if 0 == strings.Compare(fieldType.Name, filteredField) {
				isFilterField = true
				break
			}
		}
		if isFilterField {
			continue
		}

		//If the field name is unexported, skip
		if fieldType.PkgPath != "" {
			continue
		}

		// Handle nil pointers, if a non-pointer field is passed in, Indirect will just return the element
		field1, field2 = reflect.Indirect(field1), reflect.Indirect(field2)

		switch valid1, valid2 := field1.IsValid(), field2.IsValid(); {
		case valid1 && valid2:
			//If both are valid, do nothing
		case valid1:
			//If only field1 is valid, set field2 to reflect.Zero
			field2 = reflect.Zero(field1.Type())
		case valid2:
			//If only field1 is valid, set field2 to reflect.Zero
			field1 = reflect.Zero(field2.Type())
		default:
			//Both are invalid so skip loop body
			continue
		}

		if field1.Kind() == reflect.Interface {
			return false, nil, fmt.Errorf("Type of field cannot be interface. field1: %v, field2: %v", field1, field2)
		}

		isEqual, result, err := compareField(field1, field2)
		if !isEqual || err != nil {
			result.FieldName = fieldType.Name
			differences = append(differences, result)
		}
	}

	return len(differences) == 0, differences, err
}

func TestCompareDiffString(t *testing.T) {
	type testStruct struct {
		Str string
	}

	struct1 := testStruct{"A"}
	struct2 := testStruct{"BB"}
	_, results, err := Compare(struct1, struct2, nil)
	if err != nil {
		t.Errorf("Compare error: %v", err)
	}
	if results != nil && len(results) == 1 {
		result := results[0]
		if result.FieldName != "Str" || result.Value1 != "A" || result.Value2 != "BB" {
			t.Errorf("Name: %v, Value1: %v, Value2: %v \n", result.FieldName, result.Value1, result.Value2)
		}
	} else {
		t.Errorf("Incorrect number of results returned")
	}
}

func TestCompareDiffInt(t *testing.T) {
	type testStruct struct {
		Integer int
	}

	struct1 := testStruct{1}
	struct2 := testStruct{2}
	_, results, err := Compare(struct1, struct2, nil)
	if err != nil {
		t.Errorf("Compare error: %v", err)
	}
	if results != nil && len(results) == 1 {
		result := results[0]
		if result.FieldName != "Integer" || result.Value1 != 1 || result.Value2 != 2 {
			t.Errorf("Name: %v, Value1: %v, Value2: %v \n", result.FieldName, result.Value1, result.Value2)
		}
	} else {
		t.Errorf("Incorrect number of results returned")
	}
}

func TestCompareDiffFloat(t *testing.T) {
	type testStruct struct {
		F float64
	}

	struct1 := testStruct{1.01}
	struct2 := testStruct{2.02}
	_, results, err := Compare(struct1, struct2, nil)
	if err != nil {
		t.Errorf("Compare error: %v", err)
	}
	if results != nil && len(results) == 1 {
		result := results[0]
		if result.FieldName != "F" || result.Value1 != 1.01 || result.Value2 != 2.02 {
			t.Errorf("Name: %v, Value1: %v, Value2: %v \n", result.FieldName, result.Value1, result.Value2)
		}
	} else {
		t.Errorf("Incorrect number of results returned")
	}
}

func TestCompareDiffBool(t *testing.T) {
	type testStruct struct {
		B bool
	}

	struct1 := testStruct{true}
	struct2 := testStruct{false}
	_, results, err := Compare(struct1, struct2, nil)
	if err != nil {
		t.Errorf("Compare error: %v", err)
	}
	if results != nil && len(results) == 1 {
		result := results[0]
		if result.FieldName != "B" || result.Value1 != true || result.Value2 != false {
			t.Errorf("Name: %v, Value1: %v, Value2: %v \n", result.FieldName, result.Value1, result.Value2)
		}
	} else {
		t.Errorf("Incorrect number of results returned")
	}
}

func TestCompareDiffTime(t *testing.T) {
	type testStruct struct {
		Ti time.Time
	}

	struct1 := testStruct{time.Now()}
	struct2 := testStruct{time.Now().AddDate(0, 0, 1)}
	_, results, err := Compare(struct1, struct2, nil)
	if err != nil {
		t.Errorf("Compare error: %v", err)
	}
	if results != nil && len(results) == 1 {
		result := results[0]
		if result.FieldName != "Ti" {
			t.Errorf("Name: %v, Value1: %v, Value2: %v \n", result.FieldName, result.Value1, result.Value2)
		}
	} else {
		t.Errorf("Incorrect number of results returned")
	}
}

func TestCompareSameString(t *testing.T) {
	type testStruct struct {
		Str string
	}

	struct1 := testStruct{"Luke"}
	struct2 := testStruct{"Luke"}
	_, results, err := Compare(struct1, struct2, nil)
	if err != nil {
		t.Errorf("Compare error: %v", err)
	}
	if results == nil || len(results) != 0 {
		t.Errorf("Incorrect number of results returned")
	}
}

func TestCompareSameInt(t *testing.T) {
	type testStruct struct {
		Integer int
	}

	struct1 := testStruct{1}
	struct2 := testStruct{1}
	_, results, err := Compare(struct1, struct2, nil)
	if err != nil {
		t.Errorf("Compare error: %v", err)
	}
	if results == nil || len(results) != 0 {
		t.Errorf("Incorrect number of results returned")
	}
}

func TestCompareSameBool(t *testing.T) {
	type testStruct struct {
		b bool
	}

	struct1 := testStruct{true}
	struct2 := testStruct{true}
	_, results, err := Compare(struct1, struct2, nil)
	if err != nil {
		t.Errorf("Compare error: %v", err)
	}
	if results == nil || len(results) != 0 {
		t.Errorf("Incorrect number of results returned")
	}
}

func TestCompareMultiple(t *testing.T) {
	type testStruct struct {
		Str     string
		Integer int
		F       float64
		B       bool
	}

	struct1 := testStruct{"a", 1, 1.01, true}
	struct2 := testStruct{"b", 2, 2.02, false}
	_, results, err := Compare(struct1, struct2, nil)
	if err != nil {
		t.Errorf("Compare error: %v", err)
	}
	if results == nil || len(results) != 4 {
		t.Errorf("Incorrect number of results returned")
	}
}

func TestCompareDiffStringPtr(t *testing.T) {
	type testStruct struct {
		Str *string
	}

	strA := "A"
	strBB := "BB"
	struct1 := testStruct{&strA}
	struct2 := testStruct{&strBB}
	_, results, err := Compare(struct1, struct2, nil)
	if err != nil {
		t.Errorf("Compare error: %v", err)
	}
	if results != nil && len(results) == 1 {
		result := results[0]
		if result.FieldName != "Str" || result.Value1 != "A" || result.Value2 != "BB" {
			t.Errorf("Name: %v, Value1: %v, Value2: %v \n", result.FieldName, result.Value1, result.Value2)
		}
	} else {
		t.Errorf("Incorrect number of results returned")
	}
}

func TestCompareDiffStringNilPtr(t *testing.T) {
	type testStruct struct {
		Str *string
	}

	str := "BB"
	struct1 := testStruct{nil}
	struct2 := testStruct{&str}
	_, results, err := Compare(struct1, struct2, nil)
	if err != nil {
		t.Errorf("Compare error: %v", err)
	}
	if results != nil && len(results) == 1 {
		result := results[0]
		if result.FieldName != "Str" || result.Value1 != "" || result.Value2 != "BB" {
			t.Errorf("Name: %v, Value1: %v, Value2: %v \n", result.FieldName, result.Value1, result.Value2)
		}
	} else {
		t.Errorf("Incorrect number of results returned")
	}
}

func TestCompareSameStringPtr(t *testing.T) {
	type testStruct struct {
		Str *string
	}

	strAA := "AA"
	strAA2 := "AA"
	struct1 := testStruct{&strAA}
	struct2 := testStruct{&strAA2}
	_, results, err := Compare(struct1, struct2, nil)
	if err != nil {
		t.Errorf("Compare error: %v", err)
	}
	if results == nil || len(results) != 0 {
		t.Errorf("Incorrect number of results returned")
	}
}

func TestCompareDiffIntPtr(t *testing.T) {
	type testStruct struct {
		In *int
	}

	i1 := 1
	i2 := 2
	struct1 := testStruct{&i1}
	struct2 := testStruct{&i2}
	_, results, err := Compare(struct1, struct2, nil)
	if err != nil {
		t.Errorf("Compare error: %v", err)
	}
	if results != nil && len(results) == 1 {
		result := results[0]
		if result.FieldName != "In" || result.Value1 != 1 || result.Value2 != 2 {
			t.Errorf("Name: %v, Value1: %v, Value2: %v \n", result.FieldName, result.Value1, result.Value2)
		}
	} else {
		t.Errorf("Incorrect number of results returned")
	}
}

func TestCompareDiffIntNilPtr(t *testing.T) {
	type testStruct struct {
		In *int
	}

	var i1 *int
	i1 = nil
	i2 := 2
	struct1 := testStruct{i1}
	struct2 := testStruct{&i2}
	_, results, err := Compare(struct1, struct2, nil)
	if err != nil {
		t.Errorf("Compare error: %v", err)
	}
	if results != nil && len(results) == 1 {
		result := results[0]
		if result.FieldName != "In" || result.Value1 != 0 || result.Value2 != 2 {
			t.Errorf("Name: %v, Value1: %v, Value2: %v \n", result.FieldName, result.Value1, result.Value2)
		}
	} else {
		t.Errorf("Incorrect number of results returned")
	}
}

func TestCompareSameIntPtr(t *testing.T) {
	type testStruct struct {
		In *int
	}

	i1 := 1
	i2 := 1
	struct1 := testStruct{&i1}
	struct2 := testStruct{&i2}
	_, results, err := Compare(struct1, struct2, nil)
	if err != nil {
		t.Errorf("Compare error: %v", err)
	}
	if results == nil || len(results) != 0 {
		t.Errorf("Incorrect number of results returned: %v", len(results))
	}
}

func TestCompareDiffBoolPtr(t *testing.T) {
	type testStruct struct {
		B *bool
	}

	b1 := true
	b2 := false
	struct1 := testStruct{&b1}
	struct2 := testStruct{&b2}
	_, results, err := Compare(struct1, struct2, nil)
	if err != nil {
		t.Errorf("Compare error: %v", err)
	}
	if results != nil && len(results) == 1 {
		result := results[0]
		if result.FieldName != "B" || result.Value1 != true || result.Value2 != false {
			t.Errorf("Name: %v, Value1: %v, Value2: %v \n", result.FieldName, result.Value1, result.Value2)
		}
	} else {
		t.Errorf("Incorrect number of results returned")
	}
}

func TestCompareSameBoolPtr(t *testing.T) {
	type testStruct struct {
		B *bool
	}

	b1 := true
	b2 := true
	struct1 := testStruct{&b1}
	struct2 := testStruct{&b2}
	_, results, err := Compare(struct1, struct2, nil)
	if err != nil {
		t.Errorf("Compare error: %v", err)
	}
	if results == nil || len(results) != 0 {
		t.Errorf("Incorrect number of results returned")
	}
}

func TestCompareDiffTimePtr(t *testing.T) {
	type testStruct struct {
		Ti *time.Time
	}

	time1 := time.Now()
	time2 := time.Now().AddDate(0, 0, 1)

	struct1 := testStruct{&time1}
	struct2 := testStruct{&time2}
	_, results, err := Compare(struct1, struct2, nil)
	if err != nil {
		t.Errorf("Compare error: %v", err)
	}
	if results != nil && len(results) == 1 {
		result := results[0]
		if result.FieldName != "Ti" {
			t.Errorf("Name: %v, Value1: %v, Value2: %v \n", result.FieldName, result.Value1, result.Value2)
		}
	} else {
		t.Errorf("Incorrect number of results returned")
	}
}

func TestComparePtrStruct(t *testing.T) {
	type testStruct struct {
		Str string
	}

	struct1 := &testStruct{"A"}
	struct2 := &testStruct{"BB"}
	_, results, err := Compare(struct1, struct2, nil)
	if err != nil {
		t.Errorf("Compare error: %v", err)
	}
	if results != nil && len(results) == 1 {
		result := results[0]
		if result.FieldName != "Str" || result.Value1 != "A" || result.Value2 != "BB" {
			t.Errorf("Name: %v, Value1: %v, Value2: %v \n", result.FieldName, result.Value1, result.Value2)
		}
	} else {
		t.Errorf("Incorrect number of results returned: %v", len(results))
	}
}

func BenchmarkCompareMultiple(b *testing.B) {
	b.StopTimer()

	type testStruct struct {
		Str     string
		Integer int
		F       float64
		B       bool
	}

	struct1 := &testStruct{"a", 1, 1.01, true}
	struct2 := &testStruct{"b", 2, 2.02, false}

	b.StartTimer()
	for i := 0; i < b.N; i++ {
		_, _, _ = Compare(struct1, struct2, nil)
	}
}
