// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0

package kunstruct

import (
	"reflect"
	"testing"
)

var kunstructured = NewKunstructuredFactoryImpl().FromMap(map[string]interface{}{
	"Kind": "Service",
	"metadata": map[string]interface{}{
		"labels": map[string]interface{}{
			"app": "application-name",
		},
		"name": "service-name",
	},
	"spec": map[string]interface{}{
		"ports": map[string]interface{}{
			"port": int64(80),
		},
	},
	"this": map[string]interface{}{
		"is": map[string]interface{}{
			"aNumber":      int64(1000),
			"aFloat":       float64(1.001),
			"aNilValue":    nil,
			"aBool":        true,
			"anEmptyMap":   map[string]interface{}{},
			"anEmptySlice": []interface{}{},
			"unrecognizable": testing.InternalExample{
				Name: "fooBar",
			},
		},
	},
	"that": []interface{}{
		"idx0",
		"idx1",
		"idx2",
		"idx3",
	},
	"those": []interface{}{
		map[string]interface{}{
			"field1": "idx0foo",
			"field2": "idx0bar",
		},
		map[string]interface{}{
			"field1": "idx1foo",
			"field2": "idx1bar",
		},
		map[string]interface{}{
			"field1": "idx2foo",
			"field2": "idx2bar",
		},
	},
	"these": []interface{}{
		map[string]interface{}{
			"field1": []interface{}{"idx010", "idx011"},
			"field2": []interface{}{"idx020", "idx021"},
		},
		map[string]interface{}{
			"field1": []interface{}{"idx110", "idx111"},
			"field2": []interface{}{"idx120", "idx121"},
		},
		map[string]interface{}{
			"field1": []interface{}{"idx210", "idx211"},
			"field2": []interface{}{"idx220", "idx221"},
		},
	},
	"complextree": []interface{}{
		map[string]interface{}{
			"field1": []interface{}{
				map[string]interface{}{
					"stringsubfield": "idx1010",
					"intsubfield":    int64(1010),
					"floatsubfield":  float64(1.010),
					"boolfield":      true,
				},
				map[string]interface{}{
					"stringsubfield": "idx1011",
					"intsubfield":    int64(1011),
					"floatsubfield":  float64(1.011),
					"boolfield":      false,
				},
			},
			"field2": []interface{}{
				map[string]interface{}{
					"stringsubfield": "idx1020",
					"intsubfield":    int64(1020),
					"floatsubfield":  float64(1.020),
					"boolfield":      true,
				},
				map[string]interface{}{
					"stringsubfield": "idx1021",
					"intsubfield":    int64(1021),
					"floatsubfield":  float64(1.021),
					"boolfield":      false,
				},
			},
		},
		map[string]interface{}{
			"field1": []interface{}{
				map[string]interface{}{
					"stringsubfield": "idx1110",
					"intsubfield":    int64(1110),
					"floatsubfield":  float64(1.110),
					"boolfield":      true,
				},
				map[string]interface{}{
					"stringsubfield": "idx1111",
					"intsubfield":    int64(1111),
					"floatsubfield":  float64(1.111),
					"boolfield":      false,
				},
			},
			"field2": []interface{}{
				map[string]interface{}{
					"stringsubfield": "idx1120",
					"intsubfield":    int64(1120),
					"floatsubfield":  float64(1.1120),
					"boolfield":      true,
				},
				map[string]interface{}{
					"stringsubfield": "idx1121",
					"intsubfield":    int64(1121),
					"floatsubfield":  float64(1.1121),
					"boolfield":      false,
				},
			},
		},
	},
})

func TestGetFieldValue(t *testing.T) {
	tests := []struct {
		name          string
		pathToField   string
		expectedValue interface{}
		errorExpected bool
		errorMsg      string
	}{
		{
			name:          "oneField",
			pathToField:   "Kind",
			expectedValue: "Service",
			errorExpected: false,
		},
		{
			name:          "twoFields",
			pathToField:   "metadata.name",
			expectedValue: "service-name",
			errorExpected: false,
		},
		{
			name:          "threeFields",
			pathToField:   "spec.ports.port",
			expectedValue: int64(80),
			errorExpected: false,
		},
		{
			name:          "empty",
			pathToField:   "",
			errorExpected: true,
			errorMsg:      "no field named ''",
		},
		{
			name:          "emptyDotEmpty",
			pathToField:   ".",
			errorExpected: true,
			errorMsg:      "no field named '.'",
		},
		{
			name:          "twoFieldsOneMissing",
			pathToField:   "metadata.banana",
			errorExpected: true,
			errorMsg:      "no field named 'metadata.banana'",
		},
		{
			name:          "deeperMissingField",
			pathToField:   "this.is.aDeep.field.that.does.not.exist",
			errorExpected: true,
			errorMsg:      "no field named 'this.is.aDeep.field.that.does.not.exist'",
		},
		{
			name:          "emptyMap",
			pathToField:   "this.is.anEmptyMap",
			errorExpected: false,
			expectedValue: map[string]interface{}{},
		},
		{
			name:          "emptySlice",
			pathToField:   "this.is.anEmptySlice",
			errorExpected: false,
			expectedValue: []interface{}{},
		},
		{
			name:          "numberAsValue",
			pathToField:   "this.is.aNumber",
			errorExpected: false,
			expectedValue: int64(1000),
		},
		{
			name:          "floatAsValue",
			pathToField:   "this.is.aFloat",
			errorExpected: false,
			expectedValue: float64(1.001),
		},
		{
			name:          "boolAsValue",
			pathToField:   "this.is.aBool",
			errorExpected: false,
			expectedValue: true,
		},
		{
			name:          "nilAsValue",
			pathToField:   "this.is.aNilValue",
			errorExpected: false,
			expectedValue: nil,
		},
		{
			name:          "unrecognizable",
			pathToField:   "this.is.unrecognizable.Name",
			errorExpected: true,
			errorMsg:      ".this.is.unrecognizable.Name accessor error: {fooBar <nil>  false} is of the type testing.InternalExample, expected map[string]interface{}",
		},
		{
			name:          "validStringIndex",
			pathToField:   "that[2]",
			expectedValue: "idx2",
			errorExpected: false,
		},
		{
			name:          "outOfBoundIndex",
			pathToField:   "that[99]",
			errorMsg:      "no field named 'that[99]'",
			errorExpected: true,
		},
		{
			name:          "accessorError",
			pathToField:   "that[downwardapi]",
			errorMsg:      ".that.downwardapi accessor error: [idx0 idx1 idx2 idx3] is of the type []interface {}, expected map[string]interface{}",
			errorExpected: true,
		},
		{
			name:          "unknownSlice",
			pathToField:   "unknown[0]",
			errorMsg:      "no field named 'unknown[0]'",
			errorExpected: true,
		},
		{
			name:          "sliceInSlice",
			pathToField:   "that[2][0]",
			errorExpected: true,
			errorMsg:      "no field named 'that[2][0]'",
		},
		{
			name:          "validStructIndex",
			pathToField:   "those[1]",
			errorExpected: false,
			expectedValue: map[string]interface{}{"field1": "idx1foo", "field2": "idx1bar"},
		},
		{
			name:          "validStructSubField",
			pathToField:   "those[1].field2",
			errorExpected: false,
			expectedValue: "idx1bar",
		},
		{
			name:          "validStructSubFieldIndex",
			pathToField:   "these[1].field2[1]",
			errorExpected: false,
			expectedValue: "idx121",
		},
		{
			name:          "validStructSubFieldOutOfBoundIndex",
			pathToField:   "these[1].field2[99]",
			errorExpected: true,
			errorMsg:      "no field named 'these[1].field2[99]'",
		},
		{
			name:          "validStructSubFieldIndexSubfield",
			pathToField:   "complextree[1].field2[1].stringsubfield",
			errorExpected: false,
			expectedValue: "idx1121",
		},
		{
			name:          "validStructSubFieldIndexInvalidName",
			pathToField:   "complextree[1].field2[1].invalidsubfield",
			errorExpected: true,
			errorMsg:      "no field named 'complextree[1].field2[1].invalidsubfield'",
		},
		{
			name:          "validDownwardAPILabels",
			pathToField:   `metadata.labels["app"]`,
			errorExpected: false,
			expectedValue: "application-name",
		},
		{
			name:          "validDownwardAPISpecs",
			pathToField:   `spec.ports['port']`,
			errorExpected: false,
			expectedValue: int64(80),
		},
		{
			name:          "validDownwardAPIThis",
			pathToField:   `this.is[aFloat]`,
			errorExpected: false,
			expectedValue: float64(1.001),
		},
		{
			name:          "downwardAPIInvalidLabel",
			pathToField:   `metadata.labels["theisnotanint"]`,
			errorExpected: true,
			errorMsg:      `no field named 'metadata.labels["theisnotanint"]'`,
		},
		{
			name:          "downwardAPIInvalidLabel2",
			pathToField:   `invalidfield.labels["app"]`,
			errorExpected: true,
			errorMsg:      `no field named 'invalidfield.labels["app"]'`,
		},
		{
			name:          "invalidIndexInIndex",
			pathToField:   "complextree[1[0]]",
			errorExpected: true,
			errorMsg:      "no field named 'complextree[1[0]]'",
		},
		{
			name:          "invalidClosingBrackets",
			pathToField:   "complextree[1]]",
			errorExpected: true,
			errorMsg:      "no field named 'complextree[1]]'",
		},
		{
			name:          "validFieldsWithQuotes",
			pathToField:   "'complextree'[1].field2[1].'stringsubfield'",
			errorExpected: false,
			expectedValue: "idx1121",
		},
	}

	for _, test := range tests {
		s, err := kunstructured.GetFieldValue(test.pathToField)
		if test.errorExpected {
			compareExpectedError(t, test.name, test.pathToField, err, test.errorMsg)
			continue
		}
		if err != nil {
			unExpectedError(t, test.name, test.pathToField, err)
		}
		compareValues(t, test.name, test.pathToField, test.expectedValue, s)
	}
}

func TestGetString(t *testing.T) {
	tests := []struct {
		name          string
		pathToField   string
		expectedValue string
		errorExpected bool
		errorMsg      string
	}{
		{
			name:          "oneField",
			pathToField:   "Kind",
			expectedValue: "Service",
			errorExpected: false,
		},
		{
			name:          "twoFields",
			pathToField:   "metadata.name",
			expectedValue: "service-name",
			errorExpected: false,
		},
		{
			name:          "emptyMap",
			pathToField:   "this.is.anEmptyMap",
			errorExpected: true,
			errorMsg:      ".this.is.anEmptyMap accessor error: map[] is of the type map[string]interface {}, expected string",
		},
		{
			name:          "twoFieldsOneMissing",
			pathToField:   "metadata.banana",
			errorExpected: true,
			errorMsg:      "no field named 'metadata.banana'",
		},
		{
			name:          "emptySlice",
			pathToField:   "this.is.anEmptySlice",
			errorExpected: true,
			errorMsg:      ".this.is.anEmptySlice accessor error: [] is of the type []interface {}, expected string",
		},
		{
			name:          "numberAsValue",
			pathToField:   "this.is.aNumber",
			errorExpected: true,
			errorMsg:      ".this.is.aNumber accessor error: 1000 is of the type int64, expected string",
		},
		{
			name:          "nilAsValue",
			pathToField:   "this.is.aNilValue",
			errorExpected: true,
			errorMsg:      ".this.is.aNilValue accessor error: <nil> is of the type <nil>, expected string",
		},
		{
			name:          "validStringIndex",
			pathToField:   "that[2]",
			expectedValue: "idx2",
			errorExpected: false,
		},
		{
			name:          "validStructIndex",
			pathToField:   "those[1]",
			errorExpected: true,
			errorMsg:      ".[1] accessor error: map[field1:idx1foo field2:idx1bar] is of the type map[string]interface {}, expected string",
		},
		{
			name:          "validStructSubField",
			pathToField:   "those[1].field2",
			errorExpected: false,
			expectedValue: "idx1bar",
		},
		{
			name:          "validStructSubFieldIndex",
			pathToField:   "these[1].field2[1]",
			errorExpected: false,
			expectedValue: "idx121",
		},
		{
			name:          "validStructSubFieldIndexSubfield",
			pathToField:   "complextree[1].field2[1].stringsubfield",
			errorExpected: false,
			expectedValue: "idx1121",
		},
		{
			name:          "invalidIndexInMap",
			pathToField:   "this.is[1]",
			errorExpected: true,
			errorMsg:      "no field named 'this.is[1]'",
		},
		{
			name:          "anotherInvalidIndexInMap",
			pathToField:   "this.is[1].aString",
			errorExpected: true,
			errorMsg:      "no field named 'this.is[1].aString'",
		},
		{
			name:          "validDownwardAPIField",
			pathToField:   `metadata.labels["app"]`,
			errorExpected: false,
			expectedValue: "application-name",
		},
	}

	for _, test := range tests {
		s, err := kunstructured.GetString(test.pathToField)
		if test.errorExpected {
			compareExpectedError(t, test.name, test.pathToField, err, test.errorMsg)
			continue
		}
		if err != nil {
			unExpectedError(t, test.name, test.pathToField, err)
		}
		compareValues(t, test.name, test.pathToField, test.expectedValue, s)
	}
}

func TestGetInt64(t *testing.T) {
	tests := []struct {
		name          string
		pathToField   string
		expectedValue int64
		errorExpected bool
		errorMsg      string
	}{
		{
			name:          "numberAsValue",
			pathToField:   "this.is.aNumber",
			errorExpected: false,
			expectedValue: int64(1000),
		},
		{
			name:          "validStructSubFieldIndexSubfield",
			pathToField:   "complextree[1].field2[1].intsubfield",
			errorExpected: false,
			expectedValue: int64(1121),
		},
		{
			name:          "twoFieldsOneMissing",
			pathToField:   "metadata.banana",
			errorExpected: true,
			errorMsg:      "no field named 'metadata.banana'",
		},
		{
			name:          "validStructSubFieldOutOfBoundIndex",
			pathToField:   "these[1].field2[99]",
			errorExpected: true,
			errorMsg:      "no field named 'these[1].field2[99]'",
		},
		{
			name:          "validDownwardAPISpecs",
			pathToField:   `spec.ports['port']`,
			errorExpected: false,
			expectedValue: int64(80),
		},
	}

	for _, test := range tests {
		s, err := kunstructured.GetInt64(test.pathToField)
		if test.errorExpected {
			compareExpectedError(t, test.name, test.pathToField, err, test.errorMsg)
			continue
		}
		if err != nil {
			unExpectedError(t, test.name, test.pathToField, err)
		}
		compareValues(t, test.name, test.pathToField, test.expectedValue, s)
	}
}

func TestGetFloat64(t *testing.T) {
	tests := []struct {
		name          string
		pathToField   string
		expectedValue float64
		errorExpected bool
		errorMsg      string
	}{
		{
			name:          "floatAsValue",
			pathToField:   "this.is.aFloat",
			errorExpected: false,
			expectedValue: float64(1.001),
		},
		{
			name:          "validStructSubFieldIndexSubfield",
			pathToField:   "complextree[1].field2[1].floatsubfield",
			errorExpected: false,
			expectedValue: float64(1.1121),
		},
		{
			name:          "validDownwardAPIThis",
			pathToField:   `this.is[aFloat]`,
			errorExpected: false,
			expectedValue: float64(1.001),
		},
		{
			name:          "twoFieldsOneMissing",
			pathToField:   "metadata.banana",
			errorExpected: true,
			errorMsg:      "no field named 'metadata.banana'",
		},
		{
			name:          "validStructSubFieldOutOfBoundIndex",
			pathToField:   "these[1].field2[99]",
			errorExpected: true,
			errorMsg:      "index 99 is out of bounds",
		},
	}

	for _, test := range tests {
		s, err := kunstructured.GetFloat64(test.pathToField)
		if test.errorExpected {
			compareExpectedError(t, test.name, test.pathToField, err, test.errorMsg)
			continue
		}
		if err != nil {
			unExpectedError(t, test.name, test.pathToField, err)
		}
		compareValues(t, test.name, test.pathToField, test.expectedValue, s)
	}
}

func TestGetBool(t *testing.T) {
	tests := []struct {
		name          string
		pathToField   string
		expectedValue bool
		errorExpected bool
		errorMsg      string
	}{
		{
			name:          "boolAsValue",
			pathToField:   "this.is.aBool",
			errorExpected: false,
			expectedValue: true,
		},
		{
			name:          "validStructSubFieldIndexSubfield",
			pathToField:   "complextree[1].field2[1].boolfield",
			errorExpected: false,
			expectedValue: false,
		},
		{
			name:          "twoFieldsOneMissing",
			pathToField:   "metadata.banana",
			errorExpected: true,
			errorMsg:      "no field named 'metadata.banana'",
		},
		{
			name:          "validStructSubFieldOutOfBoundIndex",
			pathToField:   "these[1].field2[99]",
			errorExpected: true,
			errorMsg:      "no field named 'these[1].field2[99]'",
		},
	}

	for _, test := range tests {
		s, err := kunstructured.GetBool(test.pathToField)
		if test.errorExpected {
			compareExpectedError(t, test.name, test.pathToField, err, test.errorMsg)
			continue
		}
		if err != nil {
			unExpectedError(t, test.name, test.pathToField, err)
		}
		compareValues(t, test.name, test.pathToField, test.expectedValue, s)
	}
}

func TestGetStringMap(t *testing.T) {
	tests := []struct {
		name          string
		pathToField   string
		errorExpected bool
		errorMsg      string
	}{
		{
			name:          "validStringMap",
			pathToField:   "those[2]",
			errorExpected: false,
		},
		{
			name:          "twoFieldsOneMissing",
			pathToField:   "metadata.banana",
			errorExpected: true,
			errorMsg:      "no field named 'metadata.banana'",
		},
		{
			name:          "validStructSubFieldOutOfBoundIndex",
			pathToField:   "these[1].field2[99]",
			errorExpected: true,
			errorMsg:      "no field named 'these[1].field2[99]'",
		},
	}

	for _, test := range tests {
		_, err := kunstructured.GetStringMap(test.pathToField)
		if test.errorExpected {
			compareExpectedError(t, test.name, test.pathToField, err, test.errorMsg)
			continue
		}
		if err != nil {
			unExpectedError(t, test.name, test.pathToField, err)
		}
	}
}

func TestGetMap(t *testing.T) {
	tests := []struct {
		name          string
		pathToField   string
		errorExpected bool
		errorMsg      string
	}{
		{
			name:          "validMap",
			pathToField:   "those[2]",
			errorExpected: false,
		},
		{
			name:          "validStructSubFieldIndexSubfield",
			pathToField:   "complextree[1].field2[1]",
			errorExpected: false,
		},
		{
			name:          "twoFieldsOneMissing",
			pathToField:   "metadata.banana",
			errorExpected: true,
			errorMsg:      "no field named 'metadata.banana'",
		},
		{
			name:          "validStructSubFieldOutOfBoundIndex",
			pathToField:   "these[1].field2[99]",
			errorExpected: true,
			errorMsg:      "no field named 'these[1].field2[99]'",
		},
	}

	for _, test := range tests {
		_, err := kunstructured.GetMap(test.pathToField)
		if test.errorExpected {
			compareExpectedError(t, test.name, test.pathToField, err, test.errorMsg)
			continue
		}
		if err != nil {
			unExpectedError(t, test.name, test.pathToField, err)
		}
	}
}

func TestGetStringSlice(t *testing.T) {
	tests := []struct {
		name          string
		pathToField   string
		errorExpected bool
		errorMsg      string
	}{
		{
			name:          "validStringSlice",
			pathToField:   "that",
			errorExpected: false,
		},
		{
			name:          "twoFieldsOneMissing",
			pathToField:   "metadata.banana",
			errorExpected: true,
			errorMsg:      "no field named 'metadata.banana'",
		},
		{
			name:          "validStructSubFieldOutOfBoundIndex",
			pathToField:   "these[1].field2[99]",
			errorExpected: true,
			errorMsg:      "no field named 'these[1].field2[99]'",
		},
	}

	for _, test := range tests {
		_, err := kunstructured.GetStringSlice(test.pathToField)
		if test.errorExpected {
			compareExpectedError(t, test.name, test.pathToField, err, test.errorMsg)
			continue
		}
		if err != nil {
			unExpectedError(t, test.name, test.pathToField, err)
		}
	}
}

func TestGetSlice(t *testing.T) {
	tests := []struct {
		name          string
		pathToField   string
		errorExpected bool
		errorMsg      string
	}{
		{
			name:          "validSlice1",
			pathToField:   "that",
			errorExpected: false,
		},
		{
			name:          "validSlice2",
			pathToField:   "those",
			errorExpected: false,
		},
		{
			name:          "validSlice3",
			pathToField:   "these",
			errorExpected: false,
		},
		{
			name:          "validSlice4",
			pathToField:   "complextree",
			errorExpected: false,
		},
		{
			name:          "twoFieldsOneMissing",
			pathToField:   "metadata.banana",
			errorExpected: true,
			errorMsg:      "no field named 'metadata.banana'",
		},
		{
			name:          "validStructSubFieldOutOfBoundIndex",
			pathToField:   "these[1].field2[99]",
			errorExpected: true,
			errorMsg:      "no field named 'these[1].field2[99]'",
		},
	}

	for _, test := range tests {
		_, err := kunstructured.GetSlice(test.pathToField)
		if test.errorExpected {
			compareExpectedError(t, test.name, test.pathToField, err, test.errorMsg)
			continue
		}
		if err != nil {
			unExpectedError(t, test.name, test.pathToField, err)
		}
	}
}

// unExpectedError function handles unexpected error
func unExpectedError(t *testing.T, name string, pathToField string, err error) {
	t.Fatalf("%q; path %q - unexpected error %v", name, pathToField, err)
}

// compareExpectedError compares the expectedError and the actualError return by GetFieldValue
func compareExpectedError(t *testing.T, name string, pathToField string, err error, errorMsg string) {
	if err == nil {
		t.Fatalf("%q; path %q - should return error, but no error returned",
			name, pathToField)
	}

	if errorMsg != err.Error() {
		t.Fatalf("%q; path %q - expected error: \"%s\", got error: \"%v\"",
			name, pathToField, errorMsg, err.Error())
	}
}

// compareValues compares the expectedValue and actualValue returned by GetFieldValue
func compareValues(t *testing.T, name string, pathToField string, expectedValue interface{}, actualValue interface{}) {
	t.Helper()
	switch typedV := expectedValue.(type) {
	case nil, string, bool, float64, int, int64:
		if expectedValue != actualValue {
			t.Fatalf("%q; Got: %v Expected: %v", name, actualValue, expectedValue)
		}
	case map[string]interface{}:
		if !reflect.DeepEqual(expectedValue, actualValue) {
			t.Fatalf("%q; Got: %v Expected: %v", name, actualValue, expectedValue)
		}
	case []interface{}:
		if !reflect.DeepEqual(expectedValue, actualValue) {
			t.Fatalf("%q; Got: %v Expected: %v", name, actualValue, expectedValue)
		}
	default:
		t.Logf("%T value at `%s`", typedV, pathToField)
	}
}
