package mesh

import (
	"context"
	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/google/go-cmp/cmp"
	"github.com/pkg/errors"
	"github.com/stretchr/testify/assert"
	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"
	"testing"
)

func Test_meshSelectorDesignator_Designate(t *testing.T) {
	meshWithNilNSSelector := &appmesh.Mesh{
		ObjectMeta: metav1.ObjectMeta{
			Name: "mesh-with-nil-ns-selector",
		},
		Spec: appmesh.MeshSpec{
			NamespaceSelector: nil,
		},
	}
	meshWithEmptyNSSelector := &appmesh.Mesh{
		ObjectMeta: metav1.ObjectMeta{
			Name: "mesh-with-empty-ns-selector",
		},
		Spec: appmesh.MeshSpec{
			NamespaceSelector: &metav1.LabelSelector{},
		},
	}
	meshWithNSSelectorMeshX := &appmesh.Mesh{
		ObjectMeta: metav1.ObjectMeta{
			Name: "mesh-with-ns-selector-mesh-x",
		},
		Spec: appmesh.MeshSpec{
			NamespaceSelector: &metav1.LabelSelector{
				MatchLabels: map[string]string{
					"mesh": "x",
				},
			},
		},
	}
	meshWithNSSelectorMeshY := &appmesh.Mesh{
		ObjectMeta: metav1.ObjectMeta{
			Name: "mesh-with-ns-selector-mesh-y",
		},
		Spec: appmesh.MeshSpec{
			NamespaceSelector: &metav1.LabelSelector{
				MatchLabels: map[string]string{
					"mesh": "y",
				},
			},
		},
	}
	now := metav1.Now()
	meshBeenDeleting := &appmesh.Mesh{
		ObjectMeta: metav1.ObjectMeta{
			Name:              "mesh-been-deleting",
			DeletionTimestamp: &now,
		},
		Spec: appmesh.MeshSpec{
			NamespaceSelector: &metav1.LabelSelector{},
		},
	}

	type env struct {
		meshes     []*appmesh.Mesh
		namespaces []*corev1.Namespace
	}
	type args struct {
		obj metav1.Object
	}
	tests := []struct {
		name    string
		env     env
		args    args
		want    *appmesh.Mesh
		wantErr error
	}{
		{
			name: "[a single mesh with empty namespace selector] namespace without labels can be selected",
			env: env{
				meshes: []*appmesh.Mesh{
					meshWithEmptyNSSelector,
				},
				namespaces: []*corev1.Namespace{
					{
						ObjectMeta: metav1.ObjectMeta{
							Name: "awesome-ns",
						},
					},
				},
			},
			args: args{
				obj: &appmesh.VirtualNode{
					ObjectMeta: metav1.ObjectMeta{
						Namespace: "awesome-ns",
						Name:      "my-node",
					},
					Spec: appmesh.VirtualNodeSpec{},
				},
			},
			want:    meshWithEmptyNSSelector,
			wantErr: nil,
		},
		{
			name: "[a single mesh with empty namespace selector] namespace with labels can be selected",
			env: env{
				meshes: []*appmesh.Mesh{
					meshWithEmptyNSSelector,
				},
				namespaces: []*corev1.Namespace{
					{
						ObjectMeta: metav1.ObjectMeta{
							Name: "awesome-ns",
							Labels: map[string]string{
								"any-key": "any-value",
							},
						},
					},
				},
			},
			args: args{
				obj: &appmesh.VirtualNode{
					ObjectMeta: metav1.ObjectMeta{
						Namespace: "awesome-ns",
						Name:      "my-node",
					},
					Spec: appmesh.VirtualNodeSpec{},
				},
			},
			want:    meshWithEmptyNSSelector,
			wantErr: nil,
		},
		{
			name: "[a single mesh with nil namespace selector] namespace without labels cannot be selected",
			env: env{
				meshes: []*appmesh.Mesh{
					meshWithNilNSSelector,
				},
				namespaces: []*corev1.Namespace{
					{
						ObjectMeta: metav1.ObjectMeta{
							Name: "awesome-ns",
						},
					},
				},
			},
			args: args{
				obj: &appmesh.VirtualNode{
					ObjectMeta: metav1.ObjectMeta{
						Namespace: "awesome-ns",
						Name:      "my-node",
					},
					Spec: appmesh.VirtualNodeSpec{},
				},
			},
			want:    nil,
			wantErr: errors.New("failed to find matching mesh for namespace: awesome-ns, expecting 1 but found 0"),
		},
		{
			name: "[a single mesh with nil namespace selector] namespace with labels cannot be selected",
			env: env{
				meshes: []*appmesh.Mesh{
					meshWithNilNSSelector,
				},
				namespaces: []*corev1.Namespace{
					{
						ObjectMeta: metav1.ObjectMeta{
							Name: "awesome-ns",
							Labels: map[string]string{
								"any-key": "any-value",
							},
						},
					},
				},
			},
			args: args{
				obj: &appmesh.VirtualNode{
					ObjectMeta: metav1.ObjectMeta{
						Namespace: "awesome-ns",
						Name:      "my-node",
					},
					Spec: appmesh.VirtualNodeSpec{},
				},
			},
			want:    nil,
			wantErr: errors.New("failed to find matching mesh for namespace: awesome-ns, expecting 1 but found 0"),
		},
		{
			name: "[a single mesh selects namespace with specific labels] namespace with matching labels can be selected",
			env: env{
				meshes: []*appmesh.Mesh{
					meshWithNSSelectorMeshX,
				},
				namespaces: []*corev1.Namespace{
					{
						ObjectMeta: metav1.ObjectMeta{
							Name: "awesome-ns",
							Labels: map[string]string{
								"mesh": "x",
							},
						},
					},
				},
			},
			args: args{
				obj: &appmesh.VirtualNode{
					ObjectMeta: metav1.ObjectMeta{
						Namespace: "awesome-ns",
						Name:      "my-node",
					},
					Spec: appmesh.VirtualNodeSpec{},
				},
			},
			want:    meshWithNSSelectorMeshX,
			wantErr: nil,
		},
		{
			name: "[a single mesh selects namespace with specific labels] namespace without labels cannot be selected",
			env: env{
				meshes: []*appmesh.Mesh{
					meshWithNSSelectorMeshX,
				},
				namespaces: []*corev1.Namespace{
					{
						ObjectMeta: metav1.ObjectMeta{
							Name: "awesome-ns",
						},
					},
				},
			},
			args: args{
				obj: &appmesh.VirtualNode{
					ObjectMeta: metav1.ObjectMeta{
						Namespace: "awesome-ns",
						Name:      "my-node",
					},
					Spec: appmesh.VirtualNodeSpec{},
				},
			},
			want:    nil,
			wantErr: errors.New("failed to find matching mesh for namespace: awesome-ns, expecting 1 but found 0"),
		},
		{
			name: "[a single mesh selects namespace with specific labels] namespace with non-matching labels cannot be selected",
			env: env{
				meshes: []*appmesh.Mesh{
					meshWithNSSelectorMeshX,
				},
				namespaces: []*corev1.Namespace{
					{
						ObjectMeta: metav1.ObjectMeta{
							Name: "awesome-ns",
							Labels: map[string]string{
								"some-key": "some-value",
							},
						},
					},
				},
			},
			args: args{
				obj: &appmesh.VirtualNode{
					ObjectMeta: metav1.ObjectMeta{
						Namespace: "awesome-ns",
						Name:      "my-node",
					},
					Spec: appmesh.VirtualNodeSpec{},
				},
			},
			want:    nil,
			wantErr: errors.New("failed to find matching mesh for namespace: awesome-ns, expecting 1 but found 0"),
		},
		{
			name: "[multiple mesh - one with empty namespace selector, another selects namespace with specific labels] namespaces without labels can be selected",
			env: env{
				meshes: []*appmesh.Mesh{
					meshWithEmptyNSSelector,
					meshWithNSSelectorMeshX,
				},
				namespaces: []*corev1.Namespace{
					{
						ObjectMeta: metav1.ObjectMeta{
							Name: "awesome-ns",
						},
					},
				},
			},
			args: args{
				obj: &appmesh.VirtualNode{
					ObjectMeta: metav1.ObjectMeta{
						Namespace: "awesome-ns",
						Name:      "my-node",
					},
					Spec: appmesh.VirtualNodeSpec{},
				},
			},
			want:    meshWithEmptyNSSelector,
			wantErr: nil,
		},
		{
			name: "[multiple mesh - one with empty namespace selector, another selects namespace with specific labels] namespaces with non-matching labels can be selected",
			env: env{
				meshes: []*appmesh.Mesh{
					meshWithEmptyNSSelector,
					meshWithNSSelectorMeshX,
				},
				namespaces: []*corev1.Namespace{
					{
						ObjectMeta: metav1.ObjectMeta{
							Name: "awesome-ns",
							Labels: map[string]string{
								"some-key": "some-value",
							},
						},
					},
				},
			},
			args: args{
				obj: &appmesh.VirtualNode{
					ObjectMeta: metav1.ObjectMeta{
						Namespace: "awesome-ns",
						Name:      "my-node",
					},
					Spec: appmesh.VirtualNodeSpec{},
				},
			},
			want:    meshWithEmptyNSSelector,
			wantErr: nil,
		},
		{
			name: "[multiple mesh - one with empty namespace selector, another selects namespace with specific labels] namespaces with matching labels cannot be selected",
			env: env{
				meshes: []*appmesh.Mesh{
					meshWithEmptyNSSelector,
					meshWithNSSelectorMeshX,
				},
				namespaces: []*corev1.Namespace{
					{
						ObjectMeta: metav1.ObjectMeta{
							Name: "awesome-ns",
							Labels: map[string]string{
								"mesh": "x",
							},
						},
					},
				},
			},
			args: args{
				obj: &appmesh.VirtualNode{
					ObjectMeta: metav1.ObjectMeta{
						Namespace: "awesome-ns",
						Name:      "my-node",
					},
					Spec: appmesh.VirtualNodeSpec{},
				},
			},
			want:    nil,
			wantErr: errors.New("found multiple matching meshes for namespace: awesome-ns, expecting 1 but found 2: mesh-with-empty-ns-selector,mesh-with-ns-selector-mesh-x"),
		},
		{
			name: "[multiple mesh - both selects namespace with different labels] namespaces with matching labels for one mesh can be selected",
			env: env{
				meshes: []*appmesh.Mesh{
					meshWithNSSelectorMeshX,
					meshWithNSSelectorMeshY,
				},
				namespaces: []*corev1.Namespace{
					{
						ObjectMeta: metav1.ObjectMeta{
							Name: "awesome-ns",
							Labels: map[string]string{
								"mesh": "x",
							},
						},
					},
				},
			},
			args: args{
				obj: &appmesh.VirtualNode{
					ObjectMeta: metav1.ObjectMeta{
						Namespace: "awesome-ns",
						Name:      "my-node",
					},
					Spec: appmesh.VirtualNodeSpec{},
				},
			},
			want:    meshWithNSSelectorMeshX,
			wantErr: nil,
		},
		{
			name: "[multiple mesh - both selects namespace with different labels] namespaces with matching labels for another mesh can be selected",
			env: env{
				meshes: []*appmesh.Mesh{
					meshWithNSSelectorMeshX,
					meshWithNSSelectorMeshY,
				},
				namespaces: []*corev1.Namespace{
					{
						ObjectMeta: metav1.ObjectMeta{
							Name: "awesome-ns",
							Labels: map[string]string{
								"mesh": "y",
							},
						},
					},
				},
			},
			args: args{
				obj: &appmesh.VirtualNode{
					ObjectMeta: metav1.ObjectMeta{
						Namespace: "awesome-ns",
						Name:      "my-node",
					},
					Spec: appmesh.VirtualNodeSpec{},
				},
			},
			want:    meshWithNSSelectorMeshY,
			wantErr: nil,
		},
		{
			name: "[multiple mesh - both selects namespace with different labels] namespaces without labels cannot be selected",
			env: env{
				meshes: []*appmesh.Mesh{
					meshWithNSSelectorMeshX,
					meshWithNSSelectorMeshY,
				},
				namespaces: []*corev1.Namespace{
					{
						ObjectMeta: metav1.ObjectMeta{
							Name: "awesome-ns",
						},
					},
				},
			},
			args: args{
				obj: &appmesh.VirtualNode{
					ObjectMeta: metav1.ObjectMeta{
						Namespace: "awesome-ns",
						Name:      "my-node",
					},
					Spec: appmesh.VirtualNodeSpec{},
				},
			},
			want:    nil,
			wantErr: errors.New("failed to find matching mesh for namespace: awesome-ns, expecting 1 but found 0"),
		},
		{
			name: "[multiple mesh - both selects namespaces with different labels] namespaces with non-matching labels cannot be selected",
			env: env{
				meshes: []*appmesh.Mesh{
					meshWithNSSelectorMeshX,
					meshWithNSSelectorMeshY,
				},
				namespaces: []*corev1.Namespace{
					{
						ObjectMeta: metav1.ObjectMeta{
							Name: "awesome-ns",
							Labels: map[string]string{
								"some-key": "some-value",
							},
						},
					},
				},
			},
			args: args{
				obj: &appmesh.VirtualNode{
					ObjectMeta: metav1.ObjectMeta{
						Namespace: "awesome-ns",
						Name:      "my-node",
					},
					Spec: appmesh.VirtualNodeSpec{},
				},
			},
			want:    nil,
			wantErr: errors.New("failed to find matching mesh for namespace: awesome-ns, expecting 1 but found 0"),
		},
		{
			name: "[a single mesh been deleting] cannot be selected",
			env: env{
				meshes: []*appmesh.Mesh{
					meshBeenDeleting,
				},
				namespaces: []*corev1.Namespace{
					{
						ObjectMeta: metav1.ObjectMeta{
							Name: "awesome-ns",
						},
					},
				},
			},
			args: args{
				obj: &appmesh.VirtualNode{
					ObjectMeta: metav1.ObjectMeta{
						Namespace: "awesome-ns",
						Name:      "my-node",
					},
					Spec: appmesh.VirtualNodeSpec{},
				},
			},
			want:    nil,
			wantErr: errors.New("unable to create new content in mesh mesh-been-deleting because it is being terminated"),
		},
	}
	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 _, mesh := range tt.env.meshes {
				err := k8sClient.Create(ctx, mesh.DeepCopy())
				assert.NoError(t, err)
			}
			for _, ns := range tt.env.namespaces {
				err := k8sClient.Create(ctx, ns.DeepCopy())
				assert.NoError(t, err)
			}

			got, err := designator.Designate(context.Background(), tt.args.obj)
			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))
			}
		})
	}
}
