/*
Copyright 2017 The Kubernetes Authors.
Copyright 2020 Authors of Arktos - file modified.

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 integration

import (
	"fmt"
	"strings"
	"testing"
	"time"

	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/util/wait"
	"k8s.io/apimachinery/pkg/util/yaml"

	apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
	clientschema "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme"
	"k8s.io/apiextensions-apiserver/test/integration/fixtures"
)

func TestForProperValidationErrors(t *testing.T) {
	tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
	if err != nil {
		t.Fatal(err)
	}
	defer tearDown()

	noxuDefinition := fixtures.NewNoxuCustomResourceDefinition(apiextensionsv1beta1.NamespaceScoped)
	noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
	if err != nil {
		t.Fatal(err)
	}

	ns := "not-the-default-ns"
	noxuResourceClient := newNamespacedCustomResourceClient(ns, dynamicClient, noxuDefinition)

	tests := []struct {
		name          string
		instanceFn    func() *unstructured.Unstructured
		expectedError string
	}{
		{
			name: "bad version",
			instanceFn: func() *unstructured.Unstructured {
				instance := fixtures.NewVersionedNoxuInstance(ns, "foo", "v2")
				return instance
			},
			expectedError: "the API version in the data (mygroup.example.com/v2) does not match the expected API version (mygroup.example.com/v1beta1)",
		},
		{
			name: "bad kind",
			instanceFn: func() *unstructured.Unstructured {
				instance := fixtures.NewNoxuInstance(ns, "foo")
				instance.Object["kind"] = "SomethingElse"
				return instance
			},
			expectedError: `SomethingElse.mygroup.example.com "foo" is invalid: kind: Invalid value: "SomethingElse": must be WishIHadChosenNoxu`,
		},
	}

	for _, tc := range tests {
		_, err := noxuResourceClient.Create(tc.instanceFn(), metav1.CreateOptions{})
		if err == nil {
			t.Errorf("%v: expected %v", tc.name, tc.expectedError)
			continue
		}
		// this only works when status errors contain the expect kind and version, so this effectively tests serializations too
		if !strings.Contains(err.Error(), tc.expectedError) {
			t.Errorf("%v: expected %v, got %v", tc.name, tc.expectedError, err)
			continue
		}
	}
}

func newNoxuValidationCRDs(scope apiextensionsv1beta1.ResourceScope) []*apiextensionsv1beta1.CustomResourceDefinition {
	validationSchema := &apiextensionsv1beta1.JSONSchemaProps{
		Required: []string{"alpha", "beta"},
		AdditionalProperties: &apiextensionsv1beta1.JSONSchemaPropsOrBool{
			Allows: true,
		},
		Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
			"alpha": {
				Description: "Alpha is an alphanumeric string with underscores",
				Type:        "string",
				Pattern:     "^[a-zA-Z0-9_]*$",
			},
			"beta": {
				Description: "Minimum value of beta is 10",
				Type:        "number",
				Minimum:     float64Ptr(10),
			},
			"gamma": {
				Description: "Gamma is restricted to foo, bar and baz",
				Type:        "string",
				Enum: []apiextensionsv1beta1.JSON{
					{
						Raw: []byte(`"foo"`),
					},
					{
						Raw: []byte(`"bar"`),
					},
					{
						Raw: []byte(`"baz"`),
					},
				},
			},
			"delta": {
				Description: "Delta is a string with a maximum length of 5 or a number with a minimum value of 0",
				AnyOf: []apiextensionsv1beta1.JSONSchemaProps{
					{
						Type:      "string",
						MaxLength: int64Ptr(5),
					},
					{
						Type:    "number",
						Minimum: float64Ptr(0),
					},
				},
			},
		},
	}
	validationSchemaWithDescription := validationSchema.DeepCopy()
	validationSchemaWithDescription.Description = "test"
	return []*apiextensionsv1beta1.CustomResourceDefinition{
		{
			ObjectMeta: metav1.ObjectMeta{Name: "noxus.mygroup.example.com"},
			Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{
				Group:   "mygroup.example.com",
				Version: "v1beta1",
				Names: apiextensionsv1beta1.CustomResourceDefinitionNames{
					Plural:     "noxus",
					Singular:   "nonenglishnoxu",
					Kind:       "WishIHadChosenNoxu",
					ShortNames: []string{"foo", "bar", "abc", "def"},
					ListKind:   "NoxuItemList",
				},
				Scope: apiextensionsv1beta1.NamespaceScoped,
				Validation: &apiextensionsv1beta1.CustomResourceValidation{
					OpenAPIV3Schema: validationSchema,
				},
				Versions: []apiextensionsv1beta1.CustomResourceDefinitionVersion{
					{
						Name:    "v1beta1",
						Served:  true,
						Storage: true,
					},
					{
						Name:    "v1",
						Served:  true,
						Storage: false,
					},
				},
			},
		},
		{
			ObjectMeta: metav1.ObjectMeta{Name: "noxus.mygroup.example.com"},
			Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{
				Group:   "mygroup.example.com",
				Version: "v1beta1",
				Names: apiextensionsv1beta1.CustomResourceDefinitionNames{
					Plural:     "noxus",
					Singular:   "nonenglishnoxu",
					Kind:       "WishIHadChosenNoxu",
					ShortNames: []string{"foo", "bar", "abc", "def"},
					ListKind:   "NoxuItemList",
				},
				Scope: apiextensionsv1beta1.NamespaceScoped,
				Versions: []apiextensionsv1beta1.CustomResourceDefinitionVersion{
					{
						Name:    "v1beta1",
						Served:  true,
						Storage: true,
						Schema: &apiextensionsv1beta1.CustomResourceValidation{
							OpenAPIV3Schema: validationSchema,
						},
					},
					{
						Name:    "v1",
						Served:  true,
						Storage: false,
						Schema: &apiextensionsv1beta1.CustomResourceValidation{
							OpenAPIV3Schema: validationSchemaWithDescription,
						},
					},
				},
			},
		},
	}
}

func newNoxuValidationInstance(namespace, name string) *unstructured.Unstructured {
	return &unstructured.Unstructured{
		Object: map[string]interface{}{
			"apiVersion": "mygroup.example.com/v1beta1",
			"kind":       "WishIHadChosenNoxu",
			"metadata": map[string]interface{}{
				"namespace": namespace,
				"name":      name,
			},
			"alpha": "foo_123",
			"beta":  10,
			"gamma": "bar",
			"delta": "hello",
		},
	}
}

func TestCustomResourceValidation(t *testing.T) {
	tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
	if err != nil {
		t.Fatal(err)
	}
	defer tearDown()

	noxuDefinitions := newNoxuValidationCRDs(apiextensionsv1beta1.NamespaceScoped)
	for _, noxuDefinition := range noxuDefinitions {
		noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
		if err != nil {
			t.Fatal(err)
		}

		ns := "not-the-default-ns"
		for _, v := range noxuDefinition.Spec.Versions {
			noxuResourceClient := newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, v.Name)
			instanceToCreate := newNoxuValidationInstance(ns, "foo")
			instanceToCreate.Object["apiVersion"] = fmt.Sprintf("%s/%s", noxuDefinition.Spec.Group, v.Name)
			_, err = instantiateVersionedCustomResource(t, instanceToCreate, noxuResourceClient, noxuDefinition, v.Name)
			if err != nil {
				t.Fatalf("unable to create noxu instance: %v", err)
			}
			noxuResourceClient.Delete("foo", &metav1.DeleteOptions{})
		}
		if err := fixtures.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil {
			t.Fatal(err)
		}
	}
}

func TestCustomResourceUpdateValidation(t *testing.T) {
	tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
	if err != nil {
		t.Fatal(err)
	}
	defer tearDown()

	noxuDefinitions := newNoxuValidationCRDs(apiextensionsv1beta1.NamespaceScoped)
	for _, noxuDefinition := range noxuDefinitions {
		noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
		if err != nil {
			t.Fatal(err)
		}

		ns := "not-the-default-ns"
		for _, v := range noxuDefinition.Spec.Versions {
			noxuResourceClient := newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, v.Name)
			instanceToCreate := newNoxuValidationInstance(ns, "foo")
			instanceToCreate.Object["apiVersion"] = fmt.Sprintf("%s/%s", noxuDefinition.Spec.Group, v.Name)
			_, err = instantiateVersionedCustomResource(t, instanceToCreate, noxuResourceClient, noxuDefinition, v.Name)
			if err != nil {
				t.Fatalf("unable to create noxu instance: %v", err)
			}

			gottenNoxuInstance, err := noxuResourceClient.Get("foo", metav1.GetOptions{})
			if err != nil {
				t.Fatal(err)
			}

			// invalidate the instance
			gottenNoxuInstance.Object = map[string]interface{}{
				"apiVersion": "mygroup.example.com/v1beta1",
				"kind":       "WishIHadChosenNoxu",
				"metadata": map[string]interface{}{
					"namespace": "not-the-default",
					"name":      "foo",
				},
				"gamma": "bar",
				"delta": "hello",
			}

			_, err = noxuResourceClient.Update(gottenNoxuInstance, metav1.UpdateOptions{})
			if err == nil {
				t.Fatalf("unexpected non-error: alpha and beta should be present while updating %v", gottenNoxuInstance)
			}
			noxuResourceClient.Delete("foo", &metav1.DeleteOptions{})
		}
		if err := fixtures.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil {
			t.Fatal(err)
		}
	}
}

func TestCustomResourceValidationErrors(t *testing.T) {
	tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
	if err != nil {
		t.Fatal(err)
	}
	defer tearDown()

	noxuDefinitions := newNoxuValidationCRDs(apiextensionsv1beta1.NamespaceScoped)
	for _, noxuDefinition := range noxuDefinitions {
		noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
		if err != nil {
			t.Fatal(err)
		}

		ns := "not-the-default-ns"

		tests := []struct {
			name           string
			instanceFn     func() *unstructured.Unstructured
			expectedErrors []string
		}{
			{
				name: "bad alpha",
				instanceFn: func() *unstructured.Unstructured {
					instance := newNoxuValidationInstance(ns, "foo")
					instance.Object["alpha"] = "foo_123!"
					return instance
				},
				expectedErrors: []string{"alpha in body should match '^[a-zA-Z0-9_]*$'"},
			},
			{
				name: "bad beta",
				instanceFn: func() *unstructured.Unstructured {
					instance := newNoxuValidationInstance(ns, "foo")
					instance.Object["beta"] = 5
					return instance
				},
				expectedErrors: []string{"beta in body should be greater than or equal to 10"},
			},
			{
				name: "bad gamma",
				instanceFn: func() *unstructured.Unstructured {
					instance := newNoxuValidationInstance(ns, "foo")
					instance.Object["gamma"] = "qux"
					return instance
				},
				expectedErrors: []string{`gamma: Unsupported value: "qux": supported values: "foo", "bar", "baz"`},
			},
			{
				name: "bad delta",
				instanceFn: func() *unstructured.Unstructured {
					instance := newNoxuValidationInstance(ns, "foo")
					instance.Object["delta"] = "foobarbaz"
					return instance
				},
				expectedErrors: []string{
					"must validate at least one schema (anyOf)",
					"delta in body should be at most 5 chars long",
				},
			},
			{
				name: "absent alpha and beta",
				instanceFn: func() *unstructured.Unstructured {
					instance := newNoxuValidationInstance(ns, "foo")
					instance.Object = map[string]interface{}{
						"apiVersion": "mygroup.example.com/v1beta1",
						"kind":       "WishIHadChosenNoxu",
						"metadata": map[string]interface{}{
							"namespace": "not-the-default-ns",
							"name":      "foo",
						},
						"gamma": "bar",
						"delta": "hello",
					}
					return instance
				},
				expectedErrors: []string{"alpha: Required value", "beta: Required value"},
			},
		}

		for _, tc := range tests {
			for _, v := range noxuDefinition.Spec.Versions {
				noxuResourceClient := newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, v.Name)
				instanceToCreate := tc.instanceFn()
				instanceToCreate.Object["apiVersion"] = fmt.Sprintf("%s/%s", noxuDefinition.Spec.Group, v.Name)
				_, err := noxuResourceClient.Create(instanceToCreate, metav1.CreateOptions{})
				if err == nil {
					t.Errorf("%v: expected %v", tc.name, tc.expectedErrors)
					continue
				}
				// this only works when status errors contain the expect kind and version, so this effectively tests serializations too
				for _, expectedError := range tc.expectedErrors {
					if !strings.Contains(err.Error(), expectedError) {
						t.Errorf("%v: expected %v, got %v", tc.name, expectedError, err)
					}
				}
			}
		}
		if err := fixtures.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil {
			t.Fatal(err)
		}
	}
}

func TestCRValidationOnCRDUpdate(t *testing.T) {
	tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
	if err != nil {
		t.Fatal(err)
	}
	defer tearDown()

	noxuDefinitions := newNoxuValidationCRDs(apiextensionsv1beta1.NamespaceScoped)
	for i, noxuDefinition := range noxuDefinitions {
		for _, v := range noxuDefinition.Spec.Versions {
			// Re-define the CRD to make sure we start with a clean CRD
			noxuDefinition := newNoxuValidationCRDs(apiextensionsv1beta1.NamespaceScoped)[i]
			validationSchema, err := getSchemaForVersion(noxuDefinition, v.Name)
			if err != nil {
				t.Fatal(err)
			}

			// set stricter schema
			validationSchema.OpenAPIV3Schema.Required = []string{"alpha", "beta", "epsilon"}

			noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
			if err != nil {
				t.Fatal(err)
			}
			ns := "not-the-default-ns"
			noxuResourceClient := newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, v.Name)
			instanceToCreate := newNoxuValidationInstance(ns, "foo")
			instanceToCreate.Object["apiVersion"] = fmt.Sprintf("%s/%s", noxuDefinition.Spec.Group, v.Name)

			// CR is rejected
			_, err = instantiateVersionedCustomResource(t, instanceToCreate, noxuResourceClient, noxuDefinition, v.Name)
			if err == nil {
				t.Fatalf("unexpected non-error: CR should be rejected")
			}

			// update the CRD to a less stricter schema
			_, err = UpdateCustomResourceDefinitionWithRetry(apiExtensionClient, "noxus.mygroup.example.com", func(crd *apiextensionsv1beta1.CustomResourceDefinition) {
				validationSchema, err := getSchemaForVersion(crd, v.Name)
				if err != nil {
					t.Fatal(err)
				}
				validationSchema.OpenAPIV3Schema.Required = []string{"alpha", "beta"}
			})
			if err != nil {
				t.Fatal(err)
			}

			// CR is now accepted
			err = wait.Poll(500*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
				_, err := noxuResourceClient.Create(instanceToCreate, metav1.CreateOptions{})
				if _, isStatus := err.(*apierrors.StatusError); isStatus {
					if apierrors.IsInvalid(err) {
						return false, nil
					}
				}
				if err != nil {
					return false, err
				}
				return true, nil
			})
			if err != nil {
				t.Fatal(err)
			}
			noxuResourceClient.Delete("foo", &metav1.DeleteOptions{})
			if err := fixtures.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil {
				t.Fatal(err)
			}
		}
	}
}

func TestForbiddenFieldsInSchema(t *testing.T) {
	tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
	if err != nil {
		t.Fatal(err)
	}
	defer tearDown()

	noxuDefinitions := newNoxuValidationCRDs(apiextensionsv1beta1.NamespaceScoped)
	for i, noxuDefinition := range noxuDefinitions {
		for _, v := range noxuDefinition.Spec.Versions {
			// Re-define the CRD to make sure we start with a clean CRD
			noxuDefinition := newNoxuValidationCRDs(apiextensionsv1beta1.NamespaceScoped)[i]
			validationSchema, err := getSchemaForVersion(noxuDefinition, v.Name)
			if err != nil {
				t.Fatal(err)
			}
			validationSchema.OpenAPIV3Schema.AdditionalProperties.Allows = false

			_, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
			if err == nil {
				t.Fatalf("unexpected non-error: additionalProperties cannot be set to false")
			}

			validationSchema.OpenAPIV3Schema.Properties["zeta"] = apiextensionsv1beta1.JSONSchemaProps{
				Type:        "array",
				UniqueItems: true,
			}
			validationSchema.OpenAPIV3Schema.AdditionalProperties.Allows = true

			_, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
			if err == nil {
				t.Fatalf("unexpected non-error: uniqueItems cannot be set to true")
			}

			validationSchema.OpenAPIV3Schema.Ref = strPtr("#/definition/zeta")
			validationSchema.OpenAPIV3Schema.Properties["zeta"] = apiextensionsv1beta1.JSONSchemaProps{
				Type:        "array",
				UniqueItems: false,
			}

			_, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
			if err == nil {
				t.Fatal("unexpected non-error: $ref cannot be non-empty string")
			}

			validationSchema.OpenAPIV3Schema.Ref = nil

			noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
			if err != nil {
				t.Fatal(err)
			}
			if err := fixtures.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil {
				t.Fatal(err)
			}
		}
	}
}

func TestNonStructuralSchemaConditionUpdate(t *testing.T) {
	tearDown, apiExtensionClient, _, err := fixtures.StartDefaultServerWithClients(t)
	if err != nil {
		t.Fatal(err)
	}
	defer tearDown()

	manifest := `
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: foos.tests.example.com
spec:
  group: tests.example.com
  version: v1beta1
  names:
    plural: foos
    singular: foo
    kind: Foo
    listKind: Foolist
  scope: Namespaced
  validation:
    openAPIV3Schema:
      type: object
      properties:
        a: {}
  versions:
  - name: v1beta1
    served: true
    storage: true
`

	// decode CRD manifest
	obj, _, err := clientschema.Codecs.UniversalDeserializer().Decode([]byte(manifest), nil, nil)
	if err != nil {
		t.Fatalf("failed decoding of: %v\n\n%s", err, manifest)
	}
	crd := obj.(*apiextensionsv1beta1.CustomResourceDefinition)
	name := crd.Name

	// save schema for later
	origSchema := crd.Spec.Validation.OpenAPIV3Schema

	// create CRDs
	t.Logf("Creating CRD %s", crd.Name)
	crd, err = apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Create(crd)
	if err != nil {
		t.Fatalf("unexpected create error: %v", err)
	}

	// wait for condition with violations
	t.Log("Waiting for NonStructuralSchema condition")
	var cond *apiextensionsv1beta1.CustomResourceDefinitionCondition
	err = wait.PollImmediate(100*time.Millisecond, 5*time.Second, func() (bool, error) {
		obj, err := apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Get(name, metav1.GetOptions{})
		if err != nil {
			return false, err
		}
		cond = findCRDCondition(obj, apiextensionsv1beta1.NonStructuralSchema)
		return cond != nil, nil
	})
	if err != nil {
		t.Fatalf("unexpected error waiting for NonStructuralSchema condition: %v", cond)
	}
	if v := "spec.validation.openAPIV3Schema.properties[a].type: Required value: must not be empty for specified object fields"; !strings.Contains(cond.Message, v) {
		t.Fatalf("expected violation %q, but got: %v", v, cond.Message)
	}

	// remove schema
	t.Log("Remove schema")
	for retry := 0; retry < 5; retry++ {
		crd, err = apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Get(name, metav1.GetOptions{})
		if err != nil {
			t.Fatalf("unexpected get error: %v", err)
		}
		crd.Spec.Validation = nil
		crd, err = apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Update(crd)
		if apierrors.IsConflict(err) {
			continue
		}
		if err != nil {
			t.Fatalf("unexpected update error: %v", err)
		}
	}
	if err != nil {
		t.Fatalf("unexpected update error: %v", err)
	}

	// wait for condition to go away
	t.Log("Wait for condition to disappear")
	err = wait.PollImmediate(100*time.Millisecond, 5*time.Second, func() (bool, error) {
		obj, err := apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Get(name, metav1.GetOptions{})
		if err != nil {
			return false, err
		}
		cond = findCRDCondition(obj, apiextensionsv1beta1.NonStructuralSchema)
		return cond == nil, nil
	})
	if err != nil {
		t.Fatalf("unexpected error waiting for NonStructuralSchema condition: %v", cond)
	}

	// readd schema
	t.Log("Readd schema")
	for retry := 0; retry < 5; retry++ {
		crd, err = apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Get(name, metav1.GetOptions{})
		if err != nil {
			t.Fatalf("unexpected get error: %v", err)
		}
		crd.Spec.Validation = &apiextensionsv1beta1.CustomResourceValidation{OpenAPIV3Schema: origSchema}
		crd, err = apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Update(crd)
		if apierrors.IsConflict(err) {
			continue
		}
		if err != nil {
			t.Fatalf("unexpected update error: %v", err)
		}
	}
	if err != nil {
		t.Fatalf("unexpected update error: %v", err)
	}

	// wait for condition with violations
	t.Log("Wait for condition to reappear")
	err = wait.PollImmediate(100*time.Millisecond, 5*time.Second, func() (bool, error) {
		obj, err := apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Get(name, metav1.GetOptions{})
		if err != nil {
			return false, err
		}
		cond = findCRDCondition(obj, apiextensionsv1beta1.NonStructuralSchema)
		return cond != nil, nil
	})
	if err != nil {
		t.Fatalf("unexpected error waiting for NonStructuralSchema condition: %v", cond)
	}
	if v := "spec.validation.openAPIV3Schema.properties[a].type: Required value: must not be empty for specified object fields"; !strings.Contains(cond.Message, v) {
		t.Fatalf("expected violation %q, but got: %v", v, cond.Message)
	}
}

func TestNonStructuralSchemaCondition(t *testing.T) {
	tearDown, apiExtensionClient, _, err := fixtures.StartDefaultServerWithClients(t)
	if err != nil {
		t.Fatal(err)
	}
	defer tearDown()

	tmpl := `
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
spec:
  version: v1beta1
  names:
    plural: foos
    singular: foo
    kind: Foo
    listKind: Foolist
  scope: Namespaced
  validation: GLOBAL_SCHEMA
  versions:
  - name: v1beta1
    served: true
    storage: true
    schema: V1BETA1_SCHEMA
  - name: v1
    served: true
    schema: V1_SCHEMA
`

	type Test struct {
		desc                                  string
		globalSchema, v1Schema, v1beta1Schema string
		expectedCreateErrors                  []string
		unexpectedCreateErrors                []string
		expectedViolations                    []string
		unexpectedViolations                  []string
	}
	tests := []Test{
		{"empty", "", "", "", nil, nil, nil, nil},
		{
			desc: "int-or-string and preserve-unknown-fields true",
			globalSchema: `
x-kubernetes-preserve-unknown-fields: true
x-kubernetes-int-or-string: true
`,
			expectedCreateErrors: []string{
				"spec.validation.openAPIV3Schema.x-kubernetes-preserve-unknown-fields: Invalid value: true: must be false if x-kubernetes-int-or-string is true",
			},
		},
		{
			desc: "int-or-string and embedded-resource true",
			globalSchema: `
type: object
x-kubernetes-embedded-resource: true
x-kubernetes-int-or-string: true
`,
			expectedCreateErrors: []string{
				"spec.validation.openAPIV3Schema.x-kubernetes-embedded-resource: Invalid value: true: must be false if x-kubernetes-int-or-string is true",
			},
		},
		{
			desc: "embedded-resource without preserve-unknown-fields",
			globalSchema: `
type: object
x-kubernetes-embedded-resource: true
`,
			expectedCreateErrors: []string{
				"spec.validation.openAPIV3Schema.properties: Required value: must not be empty if x-kubernetes-embedded-resource is true without x-kubernetes-preserve-unknown-fields",
			},
		},
		{
			desc: "embedded-resource without preserve-unknown-fields, but properties",
			globalSchema: `
type: object
x-kubernetes-embedded-resource: true
properties:
  apiVersion:
    type: string 
  kind:
    type: string
  metadata:
    type: object
`,
			expectedViolations: []string{},
		},
		{
			desc: "embedded-resource with preserve-unknown-fields",
			globalSchema: `
type: object
x-kubernetes-embedded-resource: true
x-kubernetes-preserve-unknown-fields: true
`,
			expectedViolations: []string{},
		},
		{
			desc: "embedded-resource with wrong type",
			globalSchema: `
type: array
x-kubernetes-embedded-resource: true
x-kubernetes-preserve-unknown-fields: true
`,
			expectedCreateErrors: []string{
				"spec.validation.openAPIV3Schema.type: Invalid value: \"array\": must be object if x-kubernetes-embedded-resource is true",
			},
		},
		{
			desc: "embedded-resource with empty type",
			globalSchema: `
type: ""
x-kubernetes-embedded-resource: true
x-kubernetes-preserve-unknown-fields: true
`,
			expectedCreateErrors: []string{
				"spec.validation.openAPIV3Schema.type: Required value: must be object if x-kubernetes-embedded-resource is true",
			},
		},
		{
			desc: "no top-level type",
			globalSchema: `
type: ""
`,
			expectedViolations: []string{
				"spec.validation.openAPIV3Schema.type: Required value: must not be empty at the root",
			},
		},
		{
			desc: "non-object top-level type",
			globalSchema: `
type: "integer"
`,
			expectedViolations: []string{
				"spec.validation.openAPIV3Schema.type: Invalid value: \"integer\": must be object at the root",
			},
		},
		{
			desc: "forbidden in nested value validation",
			globalSchema: `
type: object
properties:
  foo:
    type: string
not:
  type: string
  additionalProperties: true
  title: hello
  description: world
  nullable: true
allOf:
- properties:
    foo: 
      type: string
      additionalProperties: true
      title: hello
      description: world
      nullable: true
anyOf:
- items:
    type: string
    additionalProperties: true
    title: hello
    description: world
    nullable: true
oneOf:
- properties:
    foo: 
      type: string
      additionalProperties: true
      title: hello
      description: world
      nullable: true
`,
			expectedViolations: []string{
				"spec.validation.openAPIV3Schema.anyOf[0].items.type: Forbidden: must be empty to be structural",
				"spec.validation.openAPIV3Schema.anyOf[0].items.additionalProperties: Forbidden: must be undefined to be structural",
				"spec.validation.openAPIV3Schema.anyOf[0].items.title: Forbidden: must be empty to be structural",
				"spec.validation.openAPIV3Schema.anyOf[0].items.description: Forbidden: must be empty to be structural",
				"spec.validation.openAPIV3Schema.anyOf[0].items.nullable: Forbidden: must be false to be structural",
				"spec.validation.openAPIV3Schema.allOf[0].properties[foo].type: Forbidden: must be empty to be structural",
				"spec.validation.openAPIV3Schema.allOf[0].properties[foo].additionalProperties: Forbidden: must be undefined to be structural",
				"spec.validation.openAPIV3Schema.allOf[0].properties[foo].title: Forbidden: must be empty to be structural",
				"spec.validation.openAPIV3Schema.allOf[0].properties[foo].description: Forbidden: must be empty to be structural",
				"spec.validation.openAPIV3Schema.allOf[0].properties[foo].nullable: Forbidden: must be false to be structural",
				"spec.validation.openAPIV3Schema.oneOf[0].properties[foo].type: Forbidden: must be empty to be structural",
				"spec.validation.openAPIV3Schema.oneOf[0].properties[foo].additionalProperties: Forbidden: must be undefined to be structural",
				"spec.validation.openAPIV3Schema.oneOf[0].properties[foo].title: Forbidden: must be empty to be structural",
				"spec.validation.openAPIV3Schema.oneOf[0].properties[foo].description: Forbidden: must be empty to be structural",
				"spec.validation.openAPIV3Schema.oneOf[0].properties[foo].nullable: Forbidden: must be false to be structural",
				"spec.validation.openAPIV3Schema.not.type: Forbidden: must be empty to be structural",
				"spec.validation.openAPIV3Schema.not.additionalProperties: Forbidden: must be undefined to be structural",
				"spec.validation.openAPIV3Schema.not.title: Forbidden: must be empty to be structural",
				"spec.validation.openAPIV3Schema.not.description: Forbidden: must be empty to be structural",
				"spec.validation.openAPIV3Schema.not.nullable: Forbidden: must be false to be structural",
				"spec.validation.openAPIV3Schema.items: Required value: because it is defined in spec.validation.openAPIV3Schema.anyOf[0].items",
			},
			unexpectedViolations: []string{
				"spec.validation.openAPIV3Schema.not.default",
			},
		},
		{
			desc: "invalid regex pattern",
			globalSchema: `
type: object
properties:
  foo:
    type: string
    pattern: "+"
`,
			expectedViolations: []string{
				"spec.validation.openAPIV3Schema.properties[foo].pattern: Invalid value: \"+\": must be a valid regular expression, but isn't: error parsing regexp: missing argument to repetition operator: `+`",
			},
		},
		{
			desc: "forbidden vendor extensions in nested value validation",
			globalSchema: `
type: object
properties:
  int-or-string:
    x-kubernetes-int-or-string: true
  embedded-resource:
    type: object
    x-kubernetes-embedded-resource: true
    x-kubernetes-preserve-unknown-fields: true
not:
  properties:
    int-or-string:
      x-kubernetes-int-or-string: true
    embedded-resource:
      x-kubernetes-embedded-resource: true
      x-kubernetes-preserve-unknown-fields: true
allOf:
- properties:
    int-or-string:
      x-kubernetes-int-or-string: true
    embedded-resource:
      x-kubernetes-embedded-resource: true
      x-kubernetes-preserve-unknown-fields: true
anyOf:
- properties:
    int-or-string:
      x-kubernetes-int-or-string: true
    embedded-resource:
      x-kubernetes-embedded-resource: true
      x-kubernetes-preserve-unknown-fields: true
oneOf:
- properties:
    int-or-string:
      x-kubernetes-int-or-string: true
    embedded-resource:
      x-kubernetes-embedded-resource: true
      x-kubernetes-preserve-unknown-fields: true
`,
			expectedCreateErrors: []string{
				"spec.validation.openAPIV3Schema.allOf[0].properties[embedded-resource].x-kubernetes-preserve-unknown-fields: Forbidden: must be false to be structural",
				"spec.validation.openAPIV3Schema.allOf[0].properties[embedded-resource].x-kubernetes-embedded-resource: Forbidden: must be false to be structural",
				"spec.validation.openAPIV3Schema.allOf[0].properties[int-or-string].x-kubernetes-int-or-string: Forbidden: must be false to be structural",
				"spec.validation.openAPIV3Schema.anyOf[0].properties[embedded-resource].x-kubernetes-preserve-unknown-fields: Forbidden: must be false to be structural",
				"spec.validation.openAPIV3Schema.anyOf[0].properties[embedded-resource].x-kubernetes-embedded-resource: Forbidden: must be false to be structural",
				"spec.validation.openAPIV3Schema.anyOf[0].properties[int-or-string].x-kubernetes-int-or-string: Forbidden: must be false to be structural",
				"spec.validation.openAPIV3Schema.oneOf[0].properties[embedded-resource].x-kubernetes-preserve-unknown-fields: Forbidden: must be false to be structural",
				"spec.validation.openAPIV3Schema.oneOf[0].properties[embedded-resource].x-kubernetes-embedded-resource: Forbidden: must be false to be structural",
				"spec.validation.openAPIV3Schema.oneOf[0].properties[int-or-string].x-kubernetes-int-or-string: Forbidden: must be false to be structural",
				"spec.validation.openAPIV3Schema.not.properties[embedded-resource].x-kubernetes-preserve-unknown-fields: Forbidden: must be false to be structural",
				"spec.validation.openAPIV3Schema.not.properties[embedded-resource].x-kubernetes-embedded-resource: Forbidden: must be false to be structural",
				"spec.validation.openAPIV3Schema.not.properties[int-or-string].x-kubernetes-int-or-string: Forbidden: must be false to be structural",
			},
		},
		{
			desc: "missing types with extensions",
			globalSchema: `
properties:
  foo:
    properties:
      a: {}
  bar:
    items:
      additionalProperties:
        properties:
          a: {}
        items: {}
  abc:
    additionalProperties:
      properties:
        a:
          items:
            additionalProperties:
              items:
  json:
    x-kubernetes-preserve-unknown-fields: true
    properties:
      a: {}
  int-or-string:
    x-kubernetes-int-or-string: true
    properties:
      a: {}
`,
			expectedCreateErrors: []string{
				"spec.validation.openAPIV3Schema.properties[foo].properties[a].type: Required value: must not be empty for specified object fields",
				"spec.validation.openAPIV3Schema.properties[foo].type: Required value: must not be empty for specified object fields",
				"spec.validation.openAPIV3Schema.properties[int-or-string].properties[a].type: Required value: must not be empty for specified object fields",
				"spec.validation.openAPIV3Schema.properties[json].properties[a].type: Required value: must not be empty for specified object fields",
				"spec.validation.openAPIV3Schema.properties[abc].additionalProperties.properties[a].items.additionalProperties.type: Required value: must not be empty for specified object fields",
				"spec.validation.openAPIV3Schema.properties[abc].additionalProperties.properties[a].items.type: Required value: must not be empty for specified array items",
				"spec.validation.openAPIV3Schema.properties[abc].additionalProperties.properties[a].type: Required value: must not be empty for specified object fields",
				"spec.validation.openAPIV3Schema.properties[abc].additionalProperties.type: Required value: must not be empty for specified object fields",
				"spec.validation.openAPIV3Schema.properties[abc].type: Required value: must not be empty for specified object fields",
				"spec.validation.openAPIV3Schema.properties[bar].items.additionalProperties.items.type: Required value: must not be empty for specified array items",
				"spec.validation.openAPIV3Schema.properties[bar].items.additionalProperties.properties[a].type: Required value: must not be empty for specified object fields",
				"spec.validation.openAPIV3Schema.properties[bar].items.additionalProperties.type: Required value: must not be empty for specified object fields",
				"spec.validation.openAPIV3Schema.properties[bar].items.type: Required value: must not be empty for specified array items",
				"spec.validation.openAPIV3Schema.properties[bar].type: Required value: must not be empty for specified object fields",
				"spec.validation.openAPIV3Schema.type: Required value: must not be empty at the root",
			},
		},
		{
			desc: "missing types without extensions",
			globalSchema: `
properties:
  foo:
    properties:
      a: {}
  bar:
    items:
      additionalProperties:
        properties:
          a: {}
        items: {}
  abc:
    additionalProperties:
      properties:
        a:
          items:
            additionalProperties:
              items:
`,
			expectedViolations: []string{
				"spec.validation.openAPIV3Schema.properties[foo].properties[a].type: Required value: must not be empty for specified object fields",
				"spec.validation.openAPIV3Schema.properties[foo].type: Required value: must not be empty for specified object fields",
				"spec.validation.openAPIV3Schema.properties[abc].additionalProperties.properties[a].items.additionalProperties.type: Required value: must not be empty for specified object fields",
				"spec.validation.openAPIV3Schema.properties[abc].additionalProperties.properties[a].items.type: Required value: must not be empty for specified array items",
				"spec.validation.openAPIV3Schema.properties[abc].additionalProperties.properties[a].type: Required value: must not be empty for specified object fields",
				"spec.validation.openAPIV3Schema.properties[abc].additionalProperties.type: Required value: must not be empty for specified object fields",
				"spec.validation.openAPIV3Schema.properties[abc].type: Required value: must not be empty for specified object fields",
				"spec.validation.openAPIV3Schema.properties[bar].items.additionalProperties.items.type: Required value: must not be empty for specified array items",
				"spec.validation.openAPIV3Schema.properties[bar].items.additionalProperties.properties[a].type: Required value: must not be empty for specified object fields",
				"spec.validation.openAPIV3Schema.properties[bar].items.additionalProperties.type: Required value: must not be empty for specified object fields",
				"spec.validation.openAPIV3Schema.properties[bar].items.type: Required value: must not be empty for specified array items",
				"spec.validation.openAPIV3Schema.properties[bar].type: Required value: must not be empty for specified object fields",
				"spec.validation.openAPIV3Schema.type: Required value: must not be empty at the root",
			},
		},
		{
			desc: "int-or-string variants",
			globalSchema: `
type: object
properties:
  a:
    x-kubernetes-int-or-string: true
  b:
    x-kubernetes-int-or-string: true
    anyOf:
    - type: integer
    - type: string
    allOf:
    - pattern: abc
  c:
    x-kubernetes-int-or-string: true
    allOf:
    - anyOf:
      - type: integer
      - type: string
    - pattern: abc
    - pattern: abc
  d:
    x-kubernetes-int-or-string: true
    anyOf:
    - type: integer
    - type: string
      pattern: abc
  e:
    x-kubernetes-int-or-string: true
    allOf:
    - anyOf:
      - type: integer
      - type: string
        pattern: abc
    - pattern: abc
  f:
    x-kubernetes-int-or-string: true
    anyOf:
    - type: integer
    - type: string
    - pattern: abc
  g:
    x-kubernetes-int-or-string: true
    anyOf:
    - type: string
    - type: integer
`,
			expectedCreateErrors: []string{
				"spec.validation.openAPIV3Schema.properties[d].anyOf[0].type: Forbidden: must be empty to be structural",
				"spec.validation.openAPIV3Schema.properties[d].anyOf[1].type: Forbidden: must be empty to be structural",
				"spec.validation.openAPIV3Schema.properties[e].allOf[0].anyOf[0].type: Forbidden: must be empty to be structural",
				"spec.validation.openAPIV3Schema.properties[e].allOf[0].anyOf[1].type: Forbidden: must be empty to be structural",
				"spec.validation.openAPIV3Schema.properties[f].anyOf[0].type: Forbidden: must be empty to be structural",
				"spec.validation.openAPIV3Schema.properties[f].anyOf[1].type: Forbidden: must be empty to be structural",
				"spec.validation.openAPIV3Schema.properties[g].anyOf[0].type: Forbidden: must be empty to be structural",
				"spec.validation.openAPIV3Schema.properties[g].anyOf[1].type: Forbidden: must be empty to be structural",
			},
			unexpectedCreateErrors: []string{
				"spec.validation.openAPIV3Schema.properties[a]",
				"spec.validation.openAPIV3Schema.properties[b]",
				"spec.validation.openAPIV3Schema.properties[c]",
			},
		},
		{
			desc: "forbidden additionalProperties at the root",
			globalSchema: `
type: object
additionalProperties: false
`,
			expectedViolations: []string{
				"spec.validation.openAPIV3Schema.additionalProperties: Forbidden: must not be used at the root",
			},
		},
		{
			desc: "structural incomplete",
			globalSchema: `
type: object
properties:
  b:
    type: object
    properties:
      b:
        type: array
  c:
    type: array
    items:
      type: object
  d:
    type: array
not:
  properties:
    a: {}
    b:
      not:
        properties:
          a: {}
          b:
            items: {}
    c:
      items:
        not:
          items:
            properties:
              a: {}
    d:
      items: {}
allOf:
- properties:
    e: {}
anyOf:
- properties:
    f: {}
oneOf:
- properties:
    g: {}
`,
			expectedViolations: []string{
				"spec.validation.openAPIV3Schema.properties[d].items: Required value: because it is defined in spec.validation.openAPIV3Schema.not.properties[d].items",
				"spec.validation.openAPIV3Schema.properties[a]: Required value: because it is defined in spec.validation.openAPIV3Schema.not.properties[a]",
				"spec.validation.openAPIV3Schema.properties[b].properties[a]: Required value: because it is defined in spec.validation.openAPIV3Schema.not.properties[b].not.properties[a]",
				"spec.validation.openAPIV3Schema.properties[b].properties[b].items: Required value: because it is defined in spec.validation.openAPIV3Schema.not.properties[b].not.properties[b].items",
				"spec.validation.openAPIV3Schema.properties[c].items.items: Required value: because it is defined in spec.validation.openAPIV3Schema.not.properties[c].items.not.items",
				"spec.validation.openAPIV3Schema.properties[e]: Required value: because it is defined in spec.validation.openAPIV3Schema.allOf[0].properties[e]",
				"spec.validation.openAPIV3Schema.properties[f]: Required value: because it is defined in spec.validation.openAPIV3Schema.anyOf[0].properties[f]",
				"spec.validation.openAPIV3Schema.properties[g]: Required value: because it is defined in spec.validation.openAPIV3Schema.oneOf[0].properties[g]",
			},
		},
		{
			desc: "structural complete",
			globalSchema: `
type: object
properties:
  a:
    type: string
  b:
    type: object
    properties:
      a:
        type: string
      b:
        type: array
        items:
          type: string
  c:
    type: array
    items:
      type: array
      items:
        type: object
        properties:
          a:
            type: string
  d:
    type: array
    items:
      type: string
  e:
    type: string
  f:
    type: string
  g:
    type: string
not:
  properties:
    a: {}
    b:
      not:
        properties:
          a: {}
          b:
            items: {}
    c:
      items:
        not:
          items:
            properties:
              a: {}
    d:
      items: {}
allOf:
- properties:
    e: {}
anyOf:
- properties:
    f: {}
oneOf:
- properties:
    g: {}
`,
			expectedViolations: nil,
		},
		{
			desc: "invalid v1beta1 schema",
			v1beta1Schema: `
type: object
properties:
  a: {}
not:
  properties:
    b: {}
`,
			v1Schema: `
type: object
properties:
  a:
    type: string
`,
			expectedViolations: []string{
				"spec.version[v1beta1].schema.openAPIV3Schema.properties[a].type: Required value: must not be empty for specified object fields",
				"spec.version[v1beta1].schema.openAPIV3Schema.properties[b]: Required value: because it is defined in spec.version[v1beta1].schema.openAPIV3Schema.not.properties[b]",
			},
		},
		{
			desc: "invalid v1beta1 and v1 schemas",
			v1beta1Schema: `
type: object
properties:
  a: {}
not:
  properties:
    b: {}
`,
			v1Schema: `
type: object
properties:
  c: {}
not:
  properties:
    d: {}
`,
			expectedViolations: []string{
				"spec.version[v1beta1].schema.openAPIV3Schema.properties[a].type: Required value: must not be empty for specified object fields",
				"spec.version[v1beta1].schema.openAPIV3Schema.properties[b]: Required value: because it is defined in spec.version[v1beta1].schema.openAPIV3Schema.not.properties[b]",
				"spec.version[v1].schema.openAPIV3Schema.properties[c].type: Required value: must not be empty for specified object fields",
				"spec.version[v1].schema.openAPIV3Schema.properties[d]: Required value: because it is defined in spec.version[v1].schema.openAPIV3Schema.not.properties[d]",
			},
		},
		{
			desc: "metadata with non-properties",
			globalSchema: `
type: object
properties:
  metadata:
    minimum: 42.0
`,
			expectedViolations: []string{
				"spec.validation.openAPIV3Schema.properties[metadata]: Forbidden: must not specify anything other than name and generateName, but metadata is implicitly specified",
				"spec.validation.openAPIV3Schema.properties[metadata].type: Required value: must not be empty for specified object fields",
			},
		},
		{
			desc: "metadata with other properties",
			globalSchema: `
type: object
properties:
  metadata:
    properties:
      name:
        pattern: "^[a-z]+$"
      labels:
        type: object
        maxLength: 4
`,
			expectedViolations: []string{
				"spec.validation.openAPIV3Schema.properties[metadata]: Forbidden: must not specify anything other than name and generateName, but metadata is implicitly specified",
				"spec.validation.openAPIV3Schema.properties[metadata].type: Required value: must not be empty for specified object fields",
				"spec.validation.openAPIV3Schema.properties[metadata].properties[name].type: Required value: must not be empty for specified object fields",
			},
		},
		{
			desc: "metadata with name property",
			globalSchema: `
type: object
properties:
  metadata:
    type: object
    properties:
      name:
        type: string
        pattern: "^[a-z]+$"
`,
			expectedViolations: []string{},
		},
		{
			desc: "metadata with generateName property",
			globalSchema: `
type: object
properties:
  metadata:
    type: object
    properties:
      generateName:
        type: string
        pattern: "^[a-z]+$"
`,
			expectedViolations: []string{},
		},
		{
			desc: "metadata with name and generateName property",
			globalSchema: `
type: object
properties:
  metadata:
    type: object
    properties:
      name:
        type: string
        pattern: "^[a-z]+$"
      generateName:
        type: string
        pattern: "^[a-z]+$"
`,
			expectedViolations: []string{},
		},
		{
			desc: "metadata under junctors",
			globalSchema: `
type: object
properties:
  metadata:
    type: object
    properties:
      name:
        type: string
        pattern: "^[a-z]+$"
allOf:
- properties:
    metadata: {}
anyOf:
- properties:
    metadata: {}
oneOf:
- properties:
    metadata: {}
not:
  properties:
    metadata: {}
`,
			expectedViolations: []string{
				"spec.validation.openAPIV3Schema.anyOf[0].properties[metadata]: Forbidden: must not be specified in a nested context",
				"spec.validation.openAPIV3Schema.allOf[0].properties[metadata]: Forbidden: must not be specified in a nested context",
				"spec.validation.openAPIV3Schema.oneOf[0].properties[metadata]: Forbidden: must not be specified in a nested context",
				"spec.validation.openAPIV3Schema.not.properties[metadata]: Forbidden: must not be specified in a nested context",
			},
		},
		{
			desc: "missing items for array",
			globalSchema: `
type: object
properties:
  slice:
    type: array
`,
			expectedViolations: []string{
				"spec.validation.openAPIV3Schema.properties[slice].items: Required value: must be specified",
			},
		},
		{
			desc: "items slice",
			globalSchema: `
type: object
properties:
  slice:
    type: array
    items:
    - type: string
    - type: integer
`,
			expectedCreateErrors: []string{"spec.validation.openAPIV3Schema.properties[slice].items: Forbidden: items must be a schema object and not an array"},
		},
		{
			desc: "items slice in value validation",
			globalSchema: `
type: object
properties:
  slice:
    type: array
    items:
      type: string
    not:
      items:
      - type: string
`,
			expectedCreateErrors: []string{"spec.validation.openAPIV3Schema.properties[slice].not.items: Forbidden: items must be a schema object and not an array"},
		},
	}

	for i := range tests {
		tst := tests[i]
		t.Run(tst.desc, func(t *testing.T) {
			// plug in schemas
			manifest := strings.NewReplacer(
				"GLOBAL_SCHEMA", toValidationJSON(tst.globalSchema),
				"V1BETA1_SCHEMA", toValidationJSON(tst.v1beta1Schema),
				"V1_SCHEMA", toValidationJSON(tst.v1Schema),
			).Replace(tmpl)

			// decode CRD manifest
			obj, _, err := clientschema.Codecs.UniversalDeserializer().Decode([]byte(manifest), nil, nil)
			if err != nil {
				t.Fatalf("failed decoding of: %v\n\n%s", err, manifest)
			}
			crd := obj.(*apiextensionsv1beta1.CustomResourceDefinition)
			crd.Spec.Group = fmt.Sprintf("tests-%d.apiextension.k8s.io", i)
			crd.Name = fmt.Sprintf("foos.%s", crd.Spec.Group)

			// create CRDs
			crd, err = apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Create(crd)
			if len(tst.expectedCreateErrors) > 0 && err == nil {
				t.Fatalf("expected create errors, got none")
			} else if len(tst.expectedCreateErrors) == 0 && err != nil {
				t.Fatalf("unexpected create error: %v", err)
			} else if err != nil {
				for _, expectedErr := range tst.expectedCreateErrors {
					if !strings.Contains(err.Error(), expectedErr) {
						t.Errorf("expected error containing '%s', got '%s'", expectedErr, err.Error())
					}
				}
				for _, unexpectedErr := range tst.unexpectedCreateErrors {
					if strings.Contains(err.Error(), unexpectedErr) {
						t.Errorf("unexpected error containing '%s': '%s'", unexpectedErr, err.Error())
					}
				}
			}
			if err != nil {
				return
			}

			if len(tst.expectedViolations) == 0 {
				// wait for condition to not appear
				var cond *apiextensionsv1beta1.CustomResourceDefinitionCondition
				err := wait.PollImmediate(100*time.Millisecond, 5*time.Second, func() (bool, error) {
					obj, err := apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Get(crd.Name, metav1.GetOptions{})
					if err != nil {
						return false, err
					}
					cond = findCRDCondition(obj, apiextensionsv1beta1.NonStructuralSchema)
					if cond == nil {
						return false, nil
					}
					return true, nil
				})
				if err != wait.ErrWaitTimeout {
					t.Fatalf("expected no NonStructuralSchema condition, but got one: %v", cond)
				}
				return
			}

			// wait for condition to appear with the given violations
			var cond *apiextensionsv1beta1.CustomResourceDefinitionCondition
			err = wait.PollImmediate(100*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
				obj, err := apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Get(crd.Name, metav1.GetOptions{})
				if err != nil {
					return false, err
				}
				cond = findCRDCondition(obj, apiextensionsv1beta1.NonStructuralSchema)
				if cond != nil {
					return true, nil
				}
				return false, nil
			})
			if err != nil {
				t.Fatalf("unexpected error waiting for violations in NonStructuralSchema condition: %v", err)
			}

			// check that the condition looks good
			if cond.Reason != "Violations" {
				t.Errorf("expected reason Violations, got: %v", cond.Reason)
			}
			if cond.Status != apiextensionsv1beta1.ConditionTrue {
				t.Errorf("expected reason True, got: %v", cond.Status)
			}

			// check that we got all violations
			t.Logf("Got violations: %q", cond.Message)
			for _, v := range tst.expectedViolations {
				if strings.Index(cond.Message, v) == -1 {
					t.Errorf("expected violation %q, but didn't get it", v)
				}
			}
			for _, v := range tst.unexpectedViolations {
				if strings.Index(cond.Message, v) != -1 {
					t.Errorf("unexpected violation %q", v)
				}
			}
		})
	}
}

// findCRDCondition returns the condition you're looking for or nil.
func findCRDCondition(crd *apiextensionsv1beta1.CustomResourceDefinition, conditionType apiextensionsv1beta1.CustomResourceDefinitionConditionType) *apiextensionsv1beta1.CustomResourceDefinitionCondition {
	for i := range crd.Status.Conditions {
		if crd.Status.Conditions[i].Type == conditionType {
			return &crd.Status.Conditions[i]
		}
	}

	return nil
}

func toValidationJSON(yml string) string {
	if len(yml) == 0 {
		return "null"
	}
	bs, err := yaml.ToJSON([]byte(yml))
	if err != nil {
		panic(err)
	}
	return fmt.Sprintf("{\"openAPIV3Schema\": %s}", string(bs))
}

func float64Ptr(f float64) *float64 {
	return &f
}

func int64Ptr(f int64) *int64 {
	return &f
}

func strPtr(str string) *string {
	return &str
}
