/*
Copyright 2019 The Kubernetes 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 listtype

import (
	"testing"

	"k8s.io/apimachinery/pkg/util/validation/field"

	"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
	"k8s.io/utils/ptr"
)

func TestValidateListSetsAndMaps(t *testing.T) {
	tests := []struct {
		name   string
		schema *schema.Structural
		obj    map[string]interface{}
		errors []validationMatch
	}{
		{name: "nil"},
		{name: "no schema", obj: make(map[string]interface{})},
		{name: "no object", schema: &schema.Structural{}},
		{name: "list without schema",
			obj: map[string]interface{}{
				"array": []interface{}{"a", "b", "a"},
			},
		},
		{name: "list without items",
			obj: map[string]interface{}{
				"array": []interface{}{"a", "b", "a"},
			},
			schema: &schema.Structural{
				Generic: schema.Generic{
					Type: "object",
				},
				Properties: map[string]schema.Structural{
					"array": {
						Generic: schema.Generic{
							Type: "array",
						},
					},
				},
			},
		},

		{name: "set list with one item",
			obj: map[string]interface{}{
				"array": []interface{}{"a"},
			},
			schema: &schema.Structural{
				Generic: schema.Generic{
					Type: "object",
				},
				Properties: map[string]schema.Structural{
					"array": {
						Extensions: schema.Extensions{
							XListType: ptr.To("set"),
						},
						Generic: schema.Generic{
							Type: "array",
						},
					},
				},
			},
		},
		{name: "set list with two equal items",
			obj: map[string]interface{}{
				"array": []interface{}{"a", "a"},
			},
			schema: &schema.Structural{
				Generic: schema.Generic{
					Type: "object",
				},
				Properties: map[string]schema.Structural{
					"array": {
						Extensions: schema.Extensions{
							XListType: ptr.To("set"),
						},
						Generic: schema.Generic{
							Type: "array",
						},
					},
				},
			},
			errors: []validationMatch{
				duplicate("root", "array[1]"),
			},
		},
		{name: "set list with two different items",
			obj: map[string]interface{}{
				"array": []interface{}{"a", "b"},
			},
			schema: &schema.Structural{
				Generic: schema.Generic{
					Type: "object",
				},
				Properties: map[string]schema.Structural{
					"array": {
						Extensions: schema.Extensions{
							XListType: ptr.To("set"),
						},
						Generic: schema.Generic{
							Type: "array",
						},
					},
				},
			},
		},
		{name: "set list with multiple duplicated items",
			obj: map[string]interface{}{
				"array": []interface{}{"a", "a", "b", "c", "d", "c"},
			},
			schema: &schema.Structural{
				Generic: schema.Generic{
					Type: "object",
				},
				Properties: map[string]schema.Structural{
					"array": {
						Extensions: schema.Extensions{
							XListType: ptr.To("set"),
						},
						Generic: schema.Generic{
							Type: "array",
						},
					},
				},
			},
			errors: []validationMatch{
				duplicate("root", "array[1]"),
				duplicate("root", "array[5]"),
			},
		},

		{name: "normal list with items",
			obj: map[string]interface{}{
				"array": []interface{}{"a", "b", "a"},
			},
			schema: &schema.Structural{
				Generic: schema.Generic{
					Type: "object",
				},
				Properties: map[string]schema.Structural{
					"array": {
						Generic: schema.Generic{
							Type: "array",
						},
						Items: &schema.Structural{
							Generic: schema.Generic{
								Type: "string",
							},
						},
					},
				},
			},
		},
		{name: "set list with items",
			obj: map[string]interface{}{
				"array": []interface{}{"a", "b", "a"},
			},
			schema: &schema.Structural{
				Generic: schema.Generic{
					Type: "object",
				},
				Properties: map[string]schema.Structural{
					"array": {
						Generic: schema.Generic{
							Type: "array",
						},
						Extensions: schema.Extensions{
							XListType: ptr.To("set"),
						},
						Items: &schema.Structural{
							Generic: schema.Generic{
								Type: "string",
							},
						},
					},
				},
			},
			errors: []validationMatch{
				duplicate("root", "array[2]"),
			},
		},
		{name: "set list with items under additionalProperties",
			obj: map[string]interface{}{
				"array": []interface{}{"a", "b", "a"},
			},
			schema: &schema.Structural{
				Generic: schema.Generic{
					Type: "object",
				},
				AdditionalProperties: &schema.StructuralOrBool{
					Structural: &schema.Structural{
						Generic: schema.Generic{
							Type: "array",
						},
						Extensions: schema.Extensions{
							XListType: ptr.To("set"),
						},
						Items: &schema.Structural{
							Generic: schema.Generic{
								Type: "string",
							},
						},
					},
				},
			},
			errors: []validationMatch{
				duplicate("root[array][2]"),
			},
		},
		{name: "set list with items under items",
			obj: map[string]interface{}{
				"array": []interface{}{
					[]interface{}{"a", "b", "a"},
					[]interface{}{"b", "b", "a"},
				},
			},
			schema: &schema.Structural{
				Generic: schema.Generic{
					Type: "object",
				},
				Properties: map[string]schema.Structural{
					"array": {
						Generic: schema.Generic{
							Type: "array",
						},
						Items: &schema.Structural{
							Generic: schema.Generic{
								Type: "array",
							},
							Extensions: schema.Extensions{
								XListType: ptr.To("set"),
							},
							Items: &schema.Structural{
								Generic: schema.Generic{
									Type: "string",
								},
							},
						},
					},
				},
			},
			errors: []validationMatch{
				duplicate("root", "array[0][2]"),
				duplicate("root", "array[1][1]"),
			},
		},

		{name: "nested set lists",
			obj: map[string]interface{}{
				"array": []interface{}{
					"a", "b", "a", []interface{}{"b", "b", "a"},
				},
			},
			schema: &schema.Structural{
				Generic: schema.Generic{
					Type: "object",
				},
				Properties: map[string]schema.Structural{
					"array": {
						Generic: schema.Generic{
							Type: "array",
						},
						Extensions: schema.Extensions{
							XListType: ptr.To("set"),
						},
						Items: &schema.Structural{
							Generic: schema.Generic{
								Type: "array",
							},
							Extensions: schema.Extensions{
								XListType: ptr.To("set"),
							},
						},
					},
				},
			},
			errors: []validationMatch{
				duplicate("root", "array[2]"),
				duplicate("root", "array[3][1]"),
			},
		},

		{name: "set list with compound map items",
			obj: map[string]interface{}{
				"strings":             []interface{}{"a", "b", "a"},
				"integers":            []interface{}{int64(1), int64(2), int64(1)},
				"booleans":            []interface{}{false, true, true},
				"float64":             []interface{}{float64(1.0), float64(2.0), float64(2.0)},
				"nil":                 []interface{}{"a", nil, nil},
				"empty maps":          []interface{}{map[string]interface{}{"a": "b"}, map[string]interface{}{}, map[string]interface{}{}},
				"map values":          []interface{}{map[string]interface{}{"a": "b"}, map[string]interface{}{"a": "c"}, map[string]interface{}{"a": "b"}},
				"nil values":          []interface{}{map[string]interface{}{"a": nil}, map[string]interface{}{"b": "c", "a": nil}},
				"array":               []interface{}{[]interface{}{}, []interface{}{"a"}, []interface{}{"b"}, []interface{}{"a"}},
				"nil array":           []interface{}{[]interface{}{}, []interface{}{nil}, []interface{}{nil, nil}, []interface{}{nil}, []interface{}{"a"}},
				"multiple duplicates": []interface{}{map[string]interface{}{"a": "b"}, map[string]interface{}{"a": "c"}, map[string]interface{}{"a": "b"}, map[string]interface{}{"a": "c"}, map[string]interface{}{"a": "c"}},
			},
			schema: &schema.Structural{
				Generic: schema.Generic{
					Type: "object",
				},
				Properties: map[string]schema.Structural{
					"strings": {
						Generic: schema.Generic{
							Type: "array",
						},
						Items: &schema.Structural{
							Generic: schema.Generic{
								Type: "string",
							},
						},
						Extensions: schema.Extensions{
							XListType: ptr.To("set"),
						},
					},
					"integers": {
						Generic: schema.Generic{
							Type: "array",
						},
						Items: &schema.Structural{
							Generic: schema.Generic{
								Type: "integer",
							},
						},
						Extensions: schema.Extensions{
							XListType: ptr.To("set"),
						},
					},
					"booleans": {
						Generic: schema.Generic{
							Type: "array",
						},
						Items: &schema.Structural{
							Generic: schema.Generic{
								Type: "boolean",
							},
						},
						Extensions: schema.Extensions{
							XListType: ptr.To("set"),
						},
					},
					"float64": {
						Generic: schema.Generic{
							Type: "array",
						},
						Items: &schema.Structural{
							Generic: schema.Generic{
								Type: "number",
							},
						},
						Extensions: schema.Extensions{
							XListType: ptr.To("set"),
						},
					},
					"nil": {
						Generic: schema.Generic{
							Type: "array",
						}, Items: &schema.Structural{
							Generic: schema.Generic{
								Type:     "string",
								Nullable: true,
							},
						},
						Extensions: schema.Extensions{
							XListType: ptr.To("set"),
						},
					},
					"empty maps": {
						Generic: schema.Generic{
							Type: "array",
						},
						Items: &schema.Structural{
							Generic: schema.Generic{
								Type: "object",
							},
							AdditionalProperties: &schema.StructuralOrBool{
								Structural: &schema.Structural{
									Generic: schema.Generic{
										Type: "string",
									},
								},
							},
						},
						Extensions: schema.Extensions{
							XListType: ptr.To("set"),
						},
					},
					"map values": {
						Generic: schema.Generic{
							Type: "array",
						},
						Items: &schema.Structural{
							Generic: schema.Generic{
								Type: "object",
							},
							AdditionalProperties: &schema.StructuralOrBool{
								Structural: &schema.Structural{
									Generic: schema.Generic{
										Type: "string",
									},
								},
							},
						},
						Extensions: schema.Extensions{
							XListType: ptr.To("set"),
						},
					},
					"nil values": {
						Generic: schema.Generic{
							Type: "array",
						},
						Items: &schema.Structural{
							Generic: schema.Generic{
								Type: "object",
							},
							AdditionalProperties: &schema.StructuralOrBool{
								Structural: &schema.Structural{
									Generic: schema.Generic{
										Type:     "string",
										Nullable: true,
									},
								},
							},
						},
						Extensions: schema.Extensions{
							XListType: ptr.To("set"),
						},
					},
					"array": {
						Generic: schema.Generic{
							Type: "array",
						},
						Items: &schema.Structural{
							Generic: schema.Generic{
								Type: "array",
							},
							Items: &schema.Structural{
								Generic: schema.Generic{
									Type: "string",
								},
							},
						},
						Extensions: schema.Extensions{
							XListType: ptr.To("set"),
						},
					},
					"nil array": {
						Generic: schema.Generic{
							Type: "array",
						},
						Items: &schema.Structural{
							Generic: schema.Generic{
								Type: "array",
							},
							Items: &schema.Structural{
								Generic: schema.Generic{
									Type:     "string",
									Nullable: true,
								},
							},
						},
						Extensions: schema.Extensions{
							XListType: ptr.To("set"),
						},
					},
					"multiple duplicates": {
						Generic: schema.Generic{
							Type: "array",
						},
						Items: &schema.Structural{
							Generic: schema.Generic{
								Type: "object",
							},
							AdditionalProperties: &schema.StructuralOrBool{
								Structural: &schema.Structural{
									Generic: schema.Generic{
										Type: "string",
									},
								},
							},
						},
						Extensions: schema.Extensions{
							XListType: ptr.To("set"),
						},
					},
				},
			},
			errors: []validationMatch{
				duplicate("root", "strings[2]"),
				duplicate("root", "integers[2]"),
				duplicate("root", "booleans[2]"),
				duplicate("root", "float64[2]"),
				duplicate("root", "nil[2]"),
				duplicate("root", "empty maps[2]"),
				duplicate("root", "map values[2]"),
				duplicate("root", "array[3]"),
				duplicate("root", "nil array[3]"),
				duplicate("root", "multiple duplicates[2]"),
				duplicate("root", "multiple duplicates[3]"),
			},
		},
		{name: "set list with compound array items",
			obj: map[string]interface{}{
				"array": []interface{}{[]interface{}{}, []interface{}{"a"}, []interface{}{"a"}},
			},
			schema: &schema.Structural{
				Generic: schema.Generic{
					Type: "object",
				},
				Properties: map[string]schema.Structural{
					"array": {
						Generic: schema.Generic{
							Type: "array",
						},
						Extensions: schema.Extensions{
							XListType: ptr.To("set"),
						},
						Items: &schema.Structural{
							Generic: schema.Generic{
								Type: "string",
							},
						},
					},
				},
			},
			errors: []validationMatch{
				duplicate("root", "array[2]"),
			},
		},

		{name: "map list with compound map items",
			obj: map[string]interface{}{
				"strings":                []interface{}{"a"},
				"integers":               []interface{}{int64(1)},
				"booleans":               []interface{}{false},
				"float64":                []interface{}{float64(1.0)},
				"nil":                    []interface{}{nil},
				"array":                  []interface{}{[]interface{}{"a"}},
				"one key":                []interface{}{map[string]interface{}{"a": "0", "c": "2"}, map[string]interface{}{"a": "1", "c": "1"}, map[string]interface{}{"a": "1", "c": "2"}, map[string]interface{}{}},
				"two keys":               []interface{}{map[string]interface{}{"a": "1", "b": "1", "c": "1"}, map[string]interface{}{"a": "1", "b": "2", "c": "2"}, map[string]interface{}{"a": "1", "b": "2", "c": "3"}, map[string]interface{}{}},
				"undefined key":          []interface{}{map[string]interface{}{"a": "1", "b": "1", "c": "1"}, map[string]interface{}{"a": "1", "c": "2"}, map[string]interface{}{"a": "1", "c": "3"}, map[string]interface{}{}},
				"compound key":           []interface{}{map[string]interface{}{"a": []interface{}{}, "c": "1"}, map[string]interface{}{"a": nil, "c": "1"}, map[string]interface{}{"a": []interface{}{"a"}, "c": "1"}, map[string]interface{}{"a": []interface{}{"a", int64(42)}, "c": "2"}, map[string]interface{}{"a": []interface{}{"a", int64(42)}, "c": []interface{}{"3"}}},
				"nil key":                []interface{}{map[string]interface{}{"a": []interface{}{}, "c": "1"}, map[string]interface{}{"a": nil, "c": "1"}, map[string]interface{}{"c": "1"}, map[string]interface{}{"a": nil}},
				"nil item":               []interface{}{nil, map[string]interface{}{"a": "0", "c": "1"}, map[string]interface{}{"a": nil}, map[string]interface{}{"c": "1"}},
				"nil item multiple keys": []interface{}{nil, map[string]interface{}{"b": "0", "c": "1"}, map[string]interface{}{"a": nil}, map[string]interface{}{"c": "1"}},
				"multiple duplicates": []interface{}{
					map[string]interface{}{"a": []interface{}{}, "c": "1"},
					map[string]interface{}{"a": nil, "c": "1"},
					map[string]interface{}{"a": []interface{}{"a"}, "c": "1"},
					map[string]interface{}{"a": []interface{}{"a", int64(42)}, "c": "2"},
					map[string]interface{}{"a": []interface{}{"a", int64(42)}, "c": []interface{}{"3"}},
					map[string]interface{}{"a": []interface{}{"a"}, "c": "1", "d": "1"},
					map[string]interface{}{"a": []interface{}{"a"}, "c": "1", "d": "2"},
				},
			},
			schema: &schema.Structural{
				Generic: schema.Generic{
					Type: "object",
				},
				Properties: map[string]schema.Structural{
					"strings": {
						Generic: schema.Generic{
							Type: "array",
						},
						Items: &schema.Structural{
							Generic: schema.Generic{
								Type: "string",
							},
						},
						Extensions: schema.Extensions{
							XListType:    ptr.To("map"),
							XListMapKeys: []string{"a"},
						},
					},
					"integers": {
						Generic: schema.Generic{
							Type: "array",
						},
						Items: &schema.Structural{
							Generic: schema.Generic{
								Type: "integer",
							},
						},
						Extensions: schema.Extensions{
							XListType:    ptr.To("map"),
							XListMapKeys: []string{"a"},
						},
					},
					"booleans": {
						Generic: schema.Generic{
							Type: "array",
						},
						Items: &schema.Structural{
							Generic: schema.Generic{
								Type: "boolean",
							},
						},
						Extensions: schema.Extensions{
							XListType:    ptr.To("map"),
							XListMapKeys: []string{"a"},
						},
					},
					"float64": {
						Generic: schema.Generic{
							Type: "array",
						},
						Items: &schema.Structural{
							Generic: schema.Generic{
								Type: "number",
							},
						},
						Extensions: schema.Extensions{
							XListType:    ptr.To("map"),
							XListMapKeys: []string{"a"},
						},
					},
					"nil": {
						Generic: schema.Generic{
							Type: "array",
						},
						Items: &schema.Structural{
							Generic: schema.Generic{
								Type:     "string",
								Nullable: true,
							},
						},
						Extensions: schema.Extensions{
							XListType:    ptr.To("map"),
							XListMapKeys: []string{"a"},
						},
					},
					"array": {
						Generic: schema.Generic{
							Type: "array",
						},
						Items: &schema.Structural{
							Generic: schema.Generic{
								Type: "array",
							},
							Items: &schema.Structural{
								Generic: schema.Generic{
									Type: "string",
								},
							},
						},
						Extensions: schema.Extensions{
							XListType:    ptr.To("map"),
							XListMapKeys: []string{"a"},
						},
					},
					"one key": {
						Generic: schema.Generic{
							Type: "array",
						},
						Items: &schema.Structural{
							Generic: schema.Generic{
								Type: "object",
							},
							AdditionalProperties: &schema.StructuralOrBool{
								Structural: &schema.Structural{
									Generic: schema.Generic{
										Type: "string",
									},
								},
							},
						},
						Extensions: schema.Extensions{
							XListType:    ptr.To("map"),
							XListMapKeys: []string{"a"},
						},
					},
					"two keys": {
						Generic: schema.Generic{
							Type: "array",
						},
						Items: &schema.Structural{
							Generic: schema.Generic{
								Type: "object",
							},
							AdditionalProperties: &schema.StructuralOrBool{
								Structural: &schema.Structural{
									Generic: schema.Generic{
										Type: "string",
									},
								},
							},
						},
						Extensions: schema.Extensions{
							XListType:    ptr.To("map"),
							XListMapKeys: []string{"a", "b"},
						},
					},
					"undefined key": {
						Generic: schema.Generic{
							Type: "array",
						},
						Items: &schema.Structural{
							Generic: schema.Generic{
								Type: "object",
							},
							AdditionalProperties: &schema.StructuralOrBool{
								Structural: &schema.Structural{
									Generic: schema.Generic{
										Type: "string",
									},
								},
							},
						},
						Extensions: schema.Extensions{
							XListType:    ptr.To("map"),
							XListMapKeys: []string{"a", "b"},
						},
					},
					"compound key": {
						Generic: schema.Generic{
							Type: "array",
						},
						Items: &schema.Structural{
							Generic: schema.Generic{
								Type: "object",
							},
							AdditionalProperties: &schema.StructuralOrBool{
								Structural: &schema.Structural{
									Generic: schema.Generic{
										Type:     "string",
										Nullable: true,
									},
								},
							},
						},
						Extensions: schema.Extensions{
							XListType:    ptr.To("map"),
							XListMapKeys: []string{"a"},
						},
					},
					"nil key": {
						Generic: schema.Generic{
							Type: "array",
						},
						Items: &schema.Structural{
							Generic: schema.Generic{
								Type: "object",
							},
							Properties: map[string]schema.Structural{
								"a": {
									Generic: schema.Generic{
										Type:     "array",
										Nullable: true,
									},
									Items: &schema.Structural{
										Generic: schema.Generic{
											Type: "string",
										},
									},
								},
								"c": {
									Generic: schema.Generic{
										Type: "string",
									},
								},
							},
						},
						Extensions: schema.Extensions{
							XListType:    ptr.To("map"),
							XListMapKeys: []string{"a"},
						},
					},
					"nil item": {
						Generic: schema.Generic{
							Type: "array",
						},
						Items: &schema.Structural{
							Generic: schema.Generic{
								Type: "object",
							},
							Properties: map[string]schema.Structural{
								"a": {
									Generic: schema.Generic{
										Type:     "array",
										Nullable: true,
									},
									Items: &schema.Structural{
										Generic: schema.Generic{
											Type: "string",
										},
									},
								},
								"c": {
									Generic: schema.Generic{
										Type: "string",
									},
								},
							},
						},
						Extensions: schema.Extensions{
							XListType:    ptr.To("map"),
							XListMapKeys: []string{"a"},
						},
					},
					"nil item multiple keys": {
						Generic: schema.Generic{
							Type: "array",
						},
						Items: &schema.Structural{
							Generic: schema.Generic{
								Type: "object",
							},
							Properties: map[string]schema.Structural{
								"a": {
									Generic: schema.Generic{
										Type:     "array",
										Nullable: true,
									},
									Items: &schema.Structural{
										Generic: schema.Generic{
											Type: "string",
										},
									},
								},
								"b": {
									Generic: schema.Generic{
										Type: "string",
									},
								},
								"c": {
									Generic: schema.Generic{
										Type: "string",
									},
								},
							},
						},
						Extensions: schema.Extensions{
							XListType:    ptr.To("map"),
							XListMapKeys: []string{"a", "b"},
						},
					},
					"multiple duplicates": {
						Generic: schema.Generic{
							Type: "array",
						},
						Items: &schema.Structural{
							Generic: schema.Generic{
								Type: "object",
							},
							AdditionalProperties: &schema.StructuralOrBool{
								Structural: &schema.Structural{
									Generic: schema.Generic{
										Type:     "string",
										Nullable: true,
									},
								},
							},
						},
						Extensions: schema.Extensions{
							XListType:    ptr.To("map"),
							XListMapKeys: []string{"a"},
						},
					},
				},
			},
			errors: []validationMatch{
				invalid("root", "strings[0]"),
				invalid("root", "integers[0]"),
				invalid("root", "booleans[0]"),
				invalid("root", "float64[0]"),
				invalid("root", "array[0]"),
				duplicate("root", "one key[2]"),
				duplicate("root", "two keys[2]"),
				duplicate("root", "undefined key[2]"),
				duplicate("root", "compound key[4]"),
				duplicate("root", "nil key[3]"),
				duplicate("root", "nil item[3]"),
				duplicate("root", "nil item multiple keys[3]"),
				duplicate("root", "multiple duplicates[4]"),
				duplicate("root", "multiple duplicates[5]"),
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			errs := ValidateListSetsAndMaps(field.NewPath("root"), tt.schema, tt.obj)

			seenErrs := make([]bool, len(errs))

			for _, expectedError := range tt.errors {
				found := false
				for i, err := range errs {
					if expectedError.matches(err) && !seenErrs[i] {
						found = true
						seenErrs[i] = true
						break
					}
				}

				if !found {
					t.Errorf("expected %v at %v, got %v", expectedError.errorType, expectedError.path.String(), errs)
				}
			}

			for i, seen := range seenErrs {
				if !seen {
					t.Errorf("unexpected error: %v", errs[i])
				}
			}
		})
	}
}

type validationMatch struct {
	path      *field.Path
	errorType field.ErrorType
}

func (v validationMatch) matches(err *field.Error) bool {
	return err.Type == v.errorType && err.Field == v.path.String()
}

func duplicate(path ...string) validationMatch {
	return validationMatch{path: field.NewPath(path[0], path[1:]...), errorType: field.ErrorTypeDuplicate}
}
func invalid(path ...string) validationMatch {
	return validationMatch{path: field.NewPath(path[0], path[1:]...), errorType: field.ErrorTypeInvalid}
}
