package virtualnode

import (
	"context"
	"testing"

	appmesh "github.com/aws/aws-app-mesh-controller-for-k8s/apis/appmesh/v1beta2"
	"github.com/aws/aws-app-mesh-controller-for-k8s/pkg/equality"
	"github.com/aws/aws-app-mesh-controller-for-k8s/pkg/webhook"
	"github.com/google/go-cmp/cmp"
	"github.com/pkg/errors"
	"github.com/stretchr/testify/assert"
	v1 "k8s.io/api/admission/v1"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
	testclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)

func Test_membershipDesignator_Designate(t *testing.T) {
	testNS := &corev1.Namespace{
		ObjectMeta: metav1.ObjectMeta{
			Name: "awesome-ns",
		},
		Spec: corev1.NamespaceSpec{},
	}
	secondTestNS := &corev1.Namespace{
		ObjectMeta: metav1.ObjectMeta{
			Name: "awesome-ns2",
		},
		Spec: corev1.NamespaceSpec{},
	}
	vnWithNilPodSelector := &appmesh.VirtualNode{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: testNS.Name,
			Name:      "vn-with-nil-pod-selector",
		},
		Spec: appmesh.VirtualNodeSpec{
			PodSelector: nil,
		},
	}
	vnWithEmptyPodSelector := &appmesh.VirtualNode{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: testNS.Name,
			Name:      "vn-with-empty-pod-selector",
		},
		Spec: appmesh.VirtualNodeSpec{
			PodSelector: &metav1.LabelSelector{},
		},
		Status: appmesh.VirtualNodeStatus{},
	}
	vnWithPodSelectorPodX := &appmesh.VirtualNode{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: testNS.Name,
			Name:      "vn-with-pod-selector-pod-x",
		},
		Spec: appmesh.VirtualNodeSpec{
			PodSelector: &metav1.LabelSelector{
				MatchLabels: map[string]string{
					"pod-x": "true",
				},
			},
		},
	}
	vnWithPodSelectorPodY := &appmesh.VirtualNode{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: testNS.Name,
			Name:      "vn-with-pod-selector-pod-y",
		},
		Spec: appmesh.VirtualNodeSpec{
			PodSelector: &metav1.LabelSelector{
				MatchLabels: map[string]string{
					"pod-y": "true",
				},
			},
		},
	}

	vnWithPodSelectorPodXSecondNs := &appmesh.VirtualNode{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: secondTestNS.Name,
			Name:      "vn-with-pod-selector-pod-x",
		},
		Spec: appmesh.VirtualNodeSpec{
			PodSelector: &metav1.LabelSelector{
				MatchLabels: map[string]string{
					"pod-x": "true",
				},
			},
		},
	}

	type env struct {
		namespaces   []*corev1.Namespace
		virtualNodes []*appmesh.VirtualNode
	}
	type args struct {
		pod *corev1.Pod
	}
	tests := []struct {
		name    string
		env     env
		args    args
		want    *appmesh.VirtualNode
		wantErr error
	}{
		{
			name: "[a single virtualNode with empty pod selector] pod without labels can be selected",
			env: env{
				namespaces: []*corev1.Namespace{testNS},
				virtualNodes: []*appmesh.VirtualNode{
					vnWithEmptyPodSelector,
				},
			},
			args: args{
				pod: &corev1.Pod{
					ObjectMeta: metav1.ObjectMeta{
						Namespace: testNS.Name,
						Name:      "my-pod",
					},
					Spec: corev1.PodSpec{},
				},
			},
			want:    vnWithEmptyPodSelector,
			wantErr: nil,
		},
		{
			name: "[a single virtualNode with empty pod selector] pod with labels can be selected",
			env: env{
				namespaces: []*corev1.Namespace{testNS},
				virtualNodes: []*appmesh.VirtualNode{
					vnWithEmptyPodSelector,
				},
			},
			args: args{
				pod: &corev1.Pod{
					ObjectMeta: metav1.ObjectMeta{
						Namespace: testNS.Name,
						Name:      "my-pod",
						Labels: map[string]string{
							"any-key": "any-value",
						},
					},
					Spec: corev1.PodSpec{},
				},
			},
			want:    vnWithEmptyPodSelector,
			wantErr: nil,
		},
		{
			name: "[a single virtualNode with nil pod selector] pod without labels cannot be selected",
			env: env{
				namespaces: []*corev1.Namespace{testNS},
				virtualNodes: []*appmesh.VirtualNode{
					vnWithNilPodSelector,
				},
			},
			args: args{
				pod: &corev1.Pod{
					ObjectMeta: metav1.ObjectMeta{
						Namespace: testNS.Name,
						Name:      "my-pod",
					},
					Spec: corev1.PodSpec{},
				},
			},
			want:    nil,
			wantErr: nil,
		},
		{
			name: "[a single virtualNode with nil pod selector] pod with labels cannot be selected",
			env: env{
				namespaces: []*corev1.Namespace{testNS},
				virtualNodes: []*appmesh.VirtualNode{
					vnWithNilPodSelector,
				},
			},
			args: args{
				pod: &corev1.Pod{
					ObjectMeta: metav1.ObjectMeta{
						Namespace: testNS.Name,
						Name:      "my-pod",
						Labels: map[string]string{
							"any-key": "any-value",
						},
					},
					Spec: corev1.PodSpec{},
				},
			},
			want:    nil,
			wantErr: nil,
		},
		{
			name: "[a single virtualNode selects pod with specific labels] pod with matching labels can be selected",
			env: env{
				namespaces: []*corev1.Namespace{testNS},
				virtualNodes: []*appmesh.VirtualNode{
					vnWithPodSelectorPodX,
				},
			},
			args: args{
				pod: &corev1.Pod{
					ObjectMeta: metav1.ObjectMeta{
						Namespace: testNS.Name,
						Name:      "my-pod",
						Labels: map[string]string{
							"pod-x": "true",
						},
					},
					Spec: corev1.PodSpec{},
				},
			},
			want:    vnWithPodSelectorPodX,
			wantErr: nil,
		},
		{
			name: "[a single virtualNode selects pod with specific labels] pod without labels cannot be selected",
			env: env{
				namespaces: []*corev1.Namespace{testNS},
				virtualNodes: []*appmesh.VirtualNode{
					vnWithPodSelectorPodX,
				},
			},
			args: args{
				pod: &corev1.Pod{
					ObjectMeta: metav1.ObjectMeta{
						Namespace: testNS.Name,
						Name:      "my-pod",
					},
					Spec: corev1.PodSpec{},
				},
			},
			want:    nil,
			wantErr: nil,
		},
		{
			name: "[a single virtualNode selects pod with specific labels] pod with non-matching labels cannot be selected",
			env: env{
				namespaces: []*corev1.Namespace{testNS},
				virtualNodes: []*appmesh.VirtualNode{
					vnWithPodSelectorPodX,
				},
			},
			args: args{
				pod: &corev1.Pod{
					ObjectMeta: metav1.ObjectMeta{
						Namespace: testNS.Name,
						Name:      "my-pod",
						Labels: map[string]string{
							"some-key": "some-value",
						},
					},
					Spec: corev1.PodSpec{},
				},
			},
			want:    nil,
			wantErr: nil,
		},
		{
			name: "[multiple virtualNode - both selects namespace with different labels] pod with matching labels for one virtualNode can be selected",
			env: env{
				namespaces: []*corev1.Namespace{testNS},
				virtualNodes: []*appmesh.VirtualNode{
					vnWithPodSelectorPodX,
					vnWithPodSelectorPodY,
				},
			},
			args: args{
				pod: &corev1.Pod{
					ObjectMeta: metav1.ObjectMeta{
						Namespace: testNS.Name,
						Name:      "my-pod",
						Labels: map[string]string{
							"pod-x": "true",
						},
					},
					Spec: corev1.PodSpec{},
				},
			},
			want:    vnWithPodSelectorPodX,
			wantErr: nil,
		},
		{
			name: "[multiple virtualNode - both selects namespace with different labels] pod with matching labels for another virtualNode can be selected",
			env: env{
				namespaces: []*corev1.Namespace{testNS},
				virtualNodes: []*appmesh.VirtualNode{
					vnWithPodSelectorPodX,
					vnWithPodSelectorPodY,
				},
			},
			args: args{
				pod: &corev1.Pod{
					ObjectMeta: metav1.ObjectMeta{
						Namespace: testNS.Name,
						Name:      "my-pod",
						Labels: map[string]string{
							"pod-y": "true",
						},
					},
					Spec: corev1.PodSpec{},
				},
			},
			want:    vnWithPodSelectorPodY,
			wantErr: nil,
		},
		{
			name: "[multiple virtualNode - both selects namespace with different labels] pod with matching labels for both virtualNode cannot be selected",
			env: env{
				namespaces: []*corev1.Namespace{testNS},
				virtualNodes: []*appmesh.VirtualNode{
					vnWithPodSelectorPodX,
					vnWithPodSelectorPodY,
				},
			},
			args: args{
				pod: &corev1.Pod{
					ObjectMeta: metav1.ObjectMeta{
						Namespace: testNS.Name,
						Name:      "my-pod",
						Labels: map[string]string{
							"pod-x": "true",
							"pod-y": "true",
						},
					},
					Spec: corev1.PodSpec{},
				},
			},
			want:    nil,
			wantErr: errors.New("found multiple matching VirtualNodes for pod awesome-ns/my-pod: awesome-ns/vn-with-pod-selector-pod-x,awesome-ns/vn-with-pod-selector-pod-y"),
		},
		{
			name: "[multiple virtualNode different namespaces with same name ] only the virtualNode for pod namespaces will be listed and used",
			env: env{
				namespaces: []*corev1.Namespace{testNS, secondTestNS},
				virtualNodes: []*appmesh.VirtualNode{
					vnWithPodSelectorPodX,
					vnWithPodSelectorPodXSecondNs,
				},
			},
			args: args{
				pod: &corev1.Pod{
					ObjectMeta: metav1.ObjectMeta{
						Namespace: testNS.Name,
						Name:      "my-pod",
						Labels: map[string]string{
							"pod-x": "true",
						},
					},
					Spec: corev1.PodSpec{},
				},
			},
			want:    vnWithPodSelectorPodX,
			wantErr: nil,
		},
		{
			name: "[multiple virtualNode - both selects namespace with different labels] pod without labels cannot be selected",
			env: env{
				namespaces: []*corev1.Namespace{testNS},
				virtualNodes: []*appmesh.VirtualNode{
					vnWithPodSelectorPodX,
					vnWithPodSelectorPodY,
				},
			},
			args: args{
				pod: &corev1.Pod{
					ObjectMeta: metav1.ObjectMeta{
						Namespace: testNS.Name,
						Name:      "my-pod",
					},
					Spec: corev1.PodSpec{},
				},
			},
			want:    nil,
			wantErr: nil,
		},
		{
			name: "[multiple virtualNode - both selects namespace with different labels] pod with non-matching labels cannot be selected",
			env: env{
				namespaces: []*corev1.Namespace{testNS},
				virtualNodes: []*appmesh.VirtualNode{
					vnWithPodSelectorPodX,
					vnWithPodSelectorPodY,
				},
			},
			args: args{
				pod: &corev1.Pod{
					ObjectMeta: metav1.ObjectMeta{
						Namespace: testNS.Name,
						Name:      "my-pod",
						Labels: map[string]string{
							"some-key": "some-value",
						},
					},
					Spec: corev1.PodSpec{},
				},
			},
			want:    nil,
			wantErr: nil,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctx := context.Background()
			k8sSchema := runtime.NewScheme()
			clientgoscheme.AddToScheme(k8sSchema)
			appmesh.AddToScheme(k8sSchema)
			k8sClient := testclient.NewFakeClientWithScheme(k8sSchema)
			designator := NewMembershipDesignator(k8sClient)

			for _, ns := range tt.env.namespaces {
				err := k8sClient.Create(ctx, ns.DeepCopy())
				assert.NoError(t, err)
			}
			for _, mesh := range tt.env.virtualNodes {
				err := k8sClient.Create(ctx, mesh.DeepCopy())
				assert.NoError(t, err)
			}
			ctx = webhook.ContextWithAdmissionRequest(ctx, admission.Request{
				AdmissionRequest: v1.AdmissionRequest{Namespace: "awesome-ns"},
			})

			got, err := designator.Designate(ctx, tt.args.pod)
			if tt.wantErr != nil {
				assert.EqualError(t, err, tt.wantErr.Error())
			} else {
				assert.NoError(t, err)
				opts := equality.IgnoreFakeClientPopulatedFields()
				assert.True(t, cmp.Equal(tt.want, got, opts), "diff", cmp.Diff(tt.want, got, opts))
			}
		})
	}
}
