// Copyright 2019 Istio Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package util

import (
	"reflect"
	"testing"
)

// TODO: add missing unit tests (istio/istio#17246).

// errToString returns the string representation of err and the empty string if
// err is nil.
func errToString(err error) string {
	if err == nil {
		return ""
	}
	return err.Error()
}

// to ptr conversion utility functions
func toInt8Ptr(i int8) *int8 { return &i }

func TestIsValueNil(t *testing.T) {
	if !IsValueNil(nil) {
		t.Error("got IsValueNil(nil) false, want true")
	}
	if !IsValueNil((*int)(nil)) {
		t.Error("got IsValueNil(ptr) false, want true")
	}
	if !IsValueNil(map[int]int(nil)) {
		t.Error("got IsValueNil(map) false, want true")
	}
	if !IsValueNil([]int(nil)) {
		t.Error("got IsValueNil(slice) false, want true")
	}
	if !IsValueNil(interface{}(nil)) {
		t.Error("got IsValueNil(interface) false, want true")
	}

	if IsValueNil(toInt8Ptr(42)) {
		t.Error("got IsValueNil(ptr) true, want false")
	}
	if IsValueNil(map[int]int{42: 42}) {
		t.Error("got IsValueNil(map) true, want false")
	}
	if IsValueNil([]int{1, 2, 3}) {
		t.Error("got IsValueNil(slice) true, want false")
	}
	if IsValueNil(interface{}(42)) {
		t.Error("got IsValueNil(interface) true, want false")
	}
}

func TestIsValueNilOrDefault(t *testing.T) {
	if !IsValueNilOrDefault(nil) {
		t.Error("got IsValueNilOrDefault(nil) false, want true")
	}
	if !IsValueNilOrDefault((*int)(nil)) {
		t.Error("got IsValueNilOrDefault(ptr) false, want true")
	}
	if !IsValueNilOrDefault(map[int]int(nil)) {
		t.Error("got IsValueNilOrDefault(map) false, want true")
	}
	if !IsValueNilOrDefault([]int(nil)) {
		t.Error("got IsValueNilOrDefault(slice) false, want true")
	}
	if !IsValueNilOrDefault(interface{}(nil)) {
		t.Error("got IsValueNilOrDefault(interface) false, want true")
	}
	if !IsValueNilOrDefault(int(0)) {
		t.Error("got IsValueNilOrDefault(int(0)) false, want true")
	}
	if !IsValueNilOrDefault("") {
		t.Error("got IsValueNilOrDefault(\"\") false, want true")
	}
	if !IsValueNilOrDefault(false) {
		t.Error("got IsValueNilOrDefault(false) false, want true")
	}
	i := 32
	ip := &i
	if IsValueNilOrDefault(&ip) {
		t.Error("got IsValueNilOrDefault(ptr to ptr) false, want true")
	}
}

func TestIsValueFuncs(t *testing.T) {
	testInt := int(42)
	testStruct := struct{}{}
	testSlice := []bool{}
	testMap := map[bool]bool{}
	var testNilSlice []bool
	var testNilMap map[bool]bool

	allValues := []interface{}{nil, testInt, &testInt, testStruct, &testStruct, testNilSlice, testSlice, &testSlice, testNilMap, testMap, &testMap}

	tests := []struct {
		desc     string
		function func(v reflect.Value) bool
		okValues []interface{}
	}{
		{
			desc:     "IsValuePtr",
			function: IsValuePtr,
			okValues: []interface{}{&testInt, &testStruct, &testSlice, &testMap},
		},
		{
			desc:     "IsValueStruct",
			function: IsValueStruct,
			okValues: []interface{}{testStruct},
		},
		{
			desc:     "IsValueInterface",
			function: IsValueInterface,
			okValues: []interface{}{},
		},
		{
			desc:     "IsValueStructPtr",
			function: IsValueStructPtr,
			okValues: []interface{}{&testStruct},
		},
		{
			desc:     "IsValueMap",
			function: IsValueMap,
			okValues: []interface{}{testNilMap, testMap},
		},
		{
			desc:     "IsValueSlice",
			function: IsValueSlice,
			okValues: []interface{}{testNilSlice, testSlice},
		},
		{
			desc:     "IsValueScalar",
			function: IsValueScalar,
			okValues: []interface{}{testInt, &testInt},
		},
	}

	for _, tt := range tests {
		for vidx, v := range allValues {
			if got, want := tt.function(reflect.ValueOf(v)), isInListOfInterface(tt.okValues, v); got != want {
				t.Errorf("%s with %s (#%d): got: %t, want: %t", tt.desc, reflect.TypeOf(v), vidx, got, want)
			}
		}
	}
}

func TestValuesAreSameType(t *testing.T) {
	type EnumType int64

	tests := []struct {
		inDesc string
		inV1   interface{}
		inV2   interface{}
		want   bool
	}{
		{
			inDesc: "success both are int32 types",
			inV1:   int32(42),
			inV2:   int32(43),
			want:   true,
		},
		{
			inDesc: "fail unmatching int types",
			inV1:   int16(42),
			inV2:   int32(43),
			want:   false,
		},
		{
			inDesc: "fail unmatching int and string type",
			inV1:   int32(42),
			inV2:   "42",
			want:   false,
		},
		{
			inDesc: "fail EnumType and int64 types",
			inV1:   EnumType(42),
			inV2:   int64(43),
			want:   false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.inDesc, func(t *testing.T) {
			got := ValuesAreSameType(reflect.ValueOf(tt.inV1), reflect.ValueOf(tt.inV2))
			if got != tt.want {
				t.Errorf("got %v, want %v for comparing %T against %T", got, tt.want, tt.inV1, tt.inV2)
			}
		})
	}
}

func TestIsTypeFuncs(t *testing.T) {
	testInt := int(42)
	testStruct := struct{}{}
	testSlice := []bool{}
	testSliceOfInterface := []interface{}{}
	testMap := map[bool]bool{}
	var testNilSlice []bool
	var testNilMap map[bool]bool

	allTypes := []interface{}{nil, testInt, &testInt, testStruct, &testStruct, testNilSlice,
		testSlice, &testSlice, testSliceOfInterface, testNilMap, testMap, &testMap}

	tests := []struct {
		desc     string
		function func(v reflect.Type) bool
		okTypes  []interface{}
	}{
		{
			desc:     "IsTypeStructPtr",
			function: IsTypeStructPtr,
			okTypes:  []interface{}{&testStruct},
		},
		{
			desc:     "IsTypeSlicePtr",
			function: IsTypeSlicePtr,
			okTypes:  []interface{}{&testSlice},
		},
		{
			desc:     "IsTypeMap",
			function: IsTypeMap,
			okTypes:  []interface{}{testNilMap, testMap},
		},
		{
			desc:     "IsTypeInterface",
			function: IsTypeInterface,
			okTypes:  []interface{}{},
		},
		{
			desc:     "IsTypeSliceOfInterface",
			function: IsTypeSliceOfInterface,
			okTypes:  []interface{}{testSliceOfInterface},
		},
	}

	for _, tt := range tests {
		for vidx, v := range allTypes {
			if got, want := tt.function(reflect.TypeOf(v)), isInListOfInterface(tt.okTypes, v); got != want {
				t.Errorf("%s with %s (#%d): got: %t, want: %t", tt.desc, reflect.TypeOf(v), vidx, got, want)
			}
		}
	}
}

type interfaceContainer struct {
	I anInterface
}

type anInterface interface {
	IsU()
}

type implementsInterface struct {
	A string
}

func (*implementsInterface) IsU() {}

func TestIsValueInterface(t *testing.T) {
	intf := &interfaceContainer{
		I: &implementsInterface{
			A: "a",
		},
	}
	iField := reflect.ValueOf(intf).Elem().FieldByName("I")
	if !IsValueInterface(iField) {
		t.Errorf("IsValueInterface(): got false, want true")
	}
}

func TestIsTypeInterface(t *testing.T) {
	intf := &interfaceContainer{
		I: &implementsInterface{
			A: "a",
		},
	}
	testIfField := reflect.ValueOf(intf).Elem().Field(0)

	if !IsTypeInterface(testIfField.Type()) {
		t.Errorf("IsTypeInterface(): got false, want true")
	}
}

func isInListOfInterface(lv []interface{}, v interface{}) bool {
	for _, vv := range lv {
		if reflect.DeepEqual(vv, v) {
			return true
		}
	}
	return false
}

func TestDeleteFromSlicePtr(t *testing.T) {
	parentSlice := []int{42, 43, 44, 45}
	var parentSliceI interface{} = parentSlice
	if err := DeleteFromSlicePtr(&parentSliceI, 1); err != nil {
		t.Fatalf("got error: %s, want error: nil", err)
	}
	wantSlice := []int{42, 44, 45}
	if got, want := parentSliceI, wantSlice; !reflect.DeepEqual(got, want) {
		t.Errorf("got:\n%v\nwant:\n%v\n", got, want)
	}

	badParent := struct{}{}
	wantErr := `deleteFromSlicePtr parent type is *struct {}, must be *[]interface{}`
	if got, want := errToString(DeleteFromSlicePtr(&badParent, 1)), wantErr; got != want {
		t.Fatalf("got error: %s, want error: %s", got, want)
	}
}

func TestUpdateSlicePtr(t *testing.T) {
	parentSlice := []int{42, 43, 44, 45}
	var parentSliceI interface{} = parentSlice
	if err := UpdateSlicePtr(&parentSliceI, 1, 42); err != nil {
		t.Fatalf("got error: %s, want error: nil", err)
	}
	wantSlice := []int{42, 42, 44, 45}
	if got, want := parentSliceI, wantSlice; !reflect.DeepEqual(got, want) {
		t.Errorf("got:\n%v\nwant:\n%v\n", got, want)
	}

	badParent := struct{}{}
	wantErr := `updateSlicePtr parent type is *struct {}, must be *[]interface{}`
	if got, want := errToString(UpdateSlicePtr(&badParent, 1, 42)), wantErr; got != want {
		t.Fatalf("got error: %s, want error: %s", got, want)
	}
}

func TestInsertIntoMap(t *testing.T) {
	parentMap := map[int]string{42: "forty two", 43: "forty three"}
	key := 44
	value := "forty four"
	if err := InsertIntoMap(parentMap, key, value); err != nil {
		t.Fatalf("got error: %s, want error: nil", err)
	}
	wantMap := map[int]string{42: "forty two", 43: "forty three", 44: "forty four"}
	if got, want := parentMap, wantMap; !reflect.DeepEqual(got, want) {
		t.Errorf("got:\n%v\nwant:\n%v\n", got, want)
	}

	badParent := struct{}{}
	wantErr := `insertIntoMap parent type is *struct {}, must be map`
	if got, want := errToString(InsertIntoMap(&badParent, key, value)), wantErr; got != want {
		t.Fatalf("got error: %s, want error: %s", got, want)
	}
}

var (
	allIntTypes     = []interface{}{int(-42), int8(-43), int16(-44), int32(-45), int64(-46)}
	allUintTypes    = []interface{}{uint(42), uint8(43), uint16(44), uint32(45), uint64(46)}
	allIntegerTypes = append(allIntTypes, allUintTypes...)
	nonIntTypes     = []interface{}{nil, "", []int{}, map[string]bool{}}
	allTypes        = append(allIntegerTypes, nonIntTypes...)
)

func TestIsInteger(t *testing.T) {

	tests := []struct {
		desc     string
		function func(v reflect.Kind) bool
		want     []interface{}
	}{
		{
			desc:     "ints",
			function: IsIntKind,
			want:     allIntTypes,
		},
		{
			desc:     "uints",
			function: IsUintKind,
			want:     allUintTypes,
		},
	}

	for _, tt := range tests {
		var got []interface{}
		for _, v := range allTypes {
			if tt.function(reflect.ValueOf(v).Kind()) {
				got = append(got, v)
			}
		}
		if !reflect.DeepEqual(got, tt.want) {
			t.Errorf("%s: got %v, want %v", tt.desc, got, tt.want)
		}
	}
}

func TestToIntValue(t *testing.T) {
	var got []int
	for _, v := range allTypes {
		if i, ok := ToIntValue(v); ok {
			got = append(got, i)
		}
	}
	want := []int{-42, -43, -44, -45, -46, 42, 43, 44, 45, 46}
	if !reflect.DeepEqual(got, want) {
		t.Errorf("got %v, want %v", got, want)
	}
}
