/*
Copyright 2023 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 validatingadmissionpolicystatus

import (
	"context"
	"strings"
	"testing"
	"time"

	admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
	"k8s.io/apimachinery/pkg/api/meta/testrestmapper"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/util/wait"
	validatingadmissionpolicy "k8s.io/apiserver/pkg/admission/plugin/policy/validating"
	"k8s.io/apiserver/pkg/cel/openapi/resolver"
	"k8s.io/client-go/informers"
	"k8s.io/client-go/kubernetes/fake"
	"k8s.io/client-go/kubernetes/scheme"
	"k8s.io/kubernetes/pkg/generated/openapi"
)

func TestTypeChecking(t *testing.T) {
	for _, tc := range []struct {
		name           string
		policy         *admissionregistrationv1.ValidatingAdmissionPolicy
		assertFieldRef func(warnings []admissionregistrationv1.ExpressionWarning, t *testing.T) // warning.fieldRef
		assertWarnings func(warnings []admissionregistrationv1.ExpressionWarning, t *testing.T) // warning.warning
	}{
		{
			name: "deployment with correct expression",
			policy: withGVRMatch([]string{"apps"}, []string{"v1"}, []string{"deployments"}, withValidations([]admissionregistrationv1.Validation{
				{
					Expression: "object.spec.replicas > 1",
				},
			}, makePolicy("replicated-deployment"))),
			assertFieldRef: toHaveLengthOf(0),
			assertWarnings: toHaveLengthOf(0),
		},
		{
			name: "deployment with type confusion",
			policy: withGVRMatch([]string{"apps"}, []string{"v1"}, []string{"deployments"}, withValidations([]admissionregistrationv1.Validation{
				{
					Expression: "object.spec.replicas < 100", // this one passes
				},
				{
					Expression: "object.spec.replicas > '1'", // '1' should be int
				},
			}, makePolicy("confused-deployment"))),
			assertFieldRef: toBe("spec.validations[1].expression"),
			assertWarnings: toHaveSubstring(`found no matching overload for '_>_' applied to '(int, string)'`),
		},
		{
			name: "two expressions different type checking errors",
			policy: withGVRMatch([]string{"apps"}, []string{"v1"}, []string{"deployments"}, withValidations([]admissionregistrationv1.Validation{
				{
					Expression: "object.spec.nonExistingFirst > 1",
				},
				{
					Expression: "object.spec.replicas > '1'", // '1' should be int
				},
			}, makePolicy("confused-deployment"))),
			assertFieldRef: toBe("spec.validations[0].expression", "spec.validations[1].expression"),
			assertWarnings: toHaveSubstring(
				"undefined field 'nonExistingFirst'",
				`found no matching overload for '_>_' applied to '(int, string)'`,
			),
		},
		{
			name: "one expression, two warnings",
			policy: withGVRMatch([]string{"apps"}, []string{"v1"}, []string{"deployments"}, withValidations([]admissionregistrationv1.Validation{
				{
					Expression: "object.spec.replicas < 100", // this one passes
				},
				{
					Expression: "object.spec.replicas > '1' && object.spec.nonExisting == 1",
				},
			}, makePolicy("confused-deployment"))),
			assertFieldRef: toBe("spec.validations[1].expression"),
			assertWarnings: toHaveMultipleSubstrings([]string{"undefined field 'nonExisting'", `found no matching overload for '_>_' applied to '(int, string)'`}),
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
			defer cancel()
			policy := tc.policy.DeepCopy()
			policy.ObjectMeta.Generation = 1 // fake storage does not do this automatically
			client := fake.NewSimpleClientset(policy)
			informerFactory := informers.NewSharedInformerFactory(client, 0)
			typeChecker := &validatingadmissionpolicy.TypeChecker{
				SchemaResolver: resolver.NewDefinitionsSchemaResolver(openapi.GetOpenAPIDefinitions, scheme.Scheme),
				RestMapper:     testrestmapper.TestOnlyStaticRESTMapper(scheme.Scheme),
			}
			controller, err := NewController(
				informerFactory.Admissionregistration().V1().ValidatingAdmissionPolicies(),
				client.AdmissionregistrationV1().ValidatingAdmissionPolicies(),
				typeChecker,
			)
			if err != nil {
				t.Fatalf("cannot create controller: %v", err)
			}
			informerFactory.Start(ctx.Done())
			informerFactory.WaitForCacheSync(ctx.Done())
			go controller.Run(ctx, 1)
			err = wait.PollUntilContextCancel(ctx, time.Second, false, func(ctx context.Context) (done bool, err error) {
				name := policy.Name
				// wait until the typeChecking is set, which means the type checking
				// is complete.
				updated, err := client.AdmissionregistrationV1().ValidatingAdmissionPolicies().Get(ctx, name, metav1.GetOptions{})
				if err != nil {
					return false, err
				}
				if updated.Status.TypeChecking != nil {
					policy = updated
					return true, nil
				}
				return false, nil
			})
			if err != nil {
				t.Fatal(err)
			}
			tc.assertFieldRef(policy.Status.TypeChecking.ExpressionWarnings, t)
			tc.assertWarnings(policy.Status.TypeChecking.ExpressionWarnings, t)
			if err != nil {
				t.Fatalf("failed to initialize controller: %v", err)
			}
		})
	}

}

func toBe(expected ...string) func(warnings []admissionregistrationv1.ExpressionWarning, t *testing.T) {
	return func(warnings []admissionregistrationv1.ExpressionWarning, t *testing.T) {
		if len(expected) != len(warnings) {
			t.Fatalf("mismatched length, expect %d, got %d", len(expected), len(warnings))
		}
		for i := range expected {
			if expected[i] != warnings[i].FieldRef {
				t.Errorf("expected %q but got %q", expected[i], warnings[i].FieldRef)
			}
		}
	}
}

func toHaveSubstring(substrings ...string) func(warnings []admissionregistrationv1.ExpressionWarning, t *testing.T) {
	return func(warnings []admissionregistrationv1.ExpressionWarning, t *testing.T) {
		if len(substrings) != len(warnings) {
			t.Fatalf("mismatched length, expect %d, got %d", len(substrings), len(warnings))
		}
		for i := range substrings {
			if !strings.Contains(warnings[i].Warning, substrings[i]) {
				t.Errorf("missing expected substring %q in %v", substrings[i], warnings[i])
			}
		}
	}
}

func toHaveMultipleSubstrings(substrings ...[]string) func(warnings []admissionregistrationv1.ExpressionWarning, t *testing.T) {
	return func(warnings []admissionregistrationv1.ExpressionWarning, t *testing.T) {
		if len(substrings) != len(warnings) {
			t.Fatalf("mismatched length, expect %d, got %d", len(substrings), len(warnings))
		}
		for i, expectedSubstrings := range substrings {
			for _, s := range expectedSubstrings {
				if !strings.Contains(warnings[i].Warning, s) {
					t.Errorf("missing expected substring %q in %v", substrings[i], warnings[i])
				}
			}
		}
	}
}

func toHaveLengthOf(n int) func(warnings []admissionregistrationv1.ExpressionWarning, t *testing.T) {
	return func(warnings []admissionregistrationv1.ExpressionWarning, t *testing.T) {
		if n != len(warnings) {
			t.Fatalf("mismatched length, expect %d, got %d", n, len(warnings))
		}
	}
}

func withGVRMatch(groups []string, versions []string, resources []string, policy *admissionregistrationv1.ValidatingAdmissionPolicy) *admissionregistrationv1.ValidatingAdmissionPolicy {
	policy.Spec.MatchConstraints = &admissionregistrationv1.MatchResources{
		ResourceRules: []admissionregistrationv1.NamedRuleWithOperations{
			{
				RuleWithOperations: admissionregistrationv1.RuleWithOperations{
					Operations: []admissionregistrationv1.OperationType{
						"*",
					},
					Rule: admissionregistrationv1.Rule{
						APIGroups:   groups,
						APIVersions: versions,
						Resources:   resources,
					},
				},
			},
		},
	}
	return policy
}

func withValidations(validations []admissionregistrationv1.Validation, policy *admissionregistrationv1.ValidatingAdmissionPolicy) *admissionregistrationv1.ValidatingAdmissionPolicy {
	policy.Spec.Validations = validations
	return policy
}

func makePolicy(name string) *admissionregistrationv1.ValidatingAdmissionPolicy {
	return &admissionregistrationv1.ValidatingAdmissionPolicy{
		ObjectMeta: metav1.ObjectMeta{Name: name},
	}
}
