package inject

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/webhook"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/pkg/errors"
	"github.com/stretchr/testify/assert"
	admissionv1 "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 getConfig(fp func(Config) Config) Config {
	conf := Config{
		IgnoredIPs:                  "169.254.169.254",
		LogLevel:                    "debug",
		Preview:                     false,
		SidecarImageRepository:      "public.ecr.aws/appmesh/aws-appmesh-envoy",
		SidecarImageTag:             "v1.29.12.3-prod",
		InitImage:                   "840364872350.dkr.ecr.us-west-2.amazonaws.com/aws-appmesh-proxy-route-manager:v7-prod",
		SidecarMemoryRequests:       "32Mi",
		SidecarCpuRequests:          "10m",
		EnableIAMForServiceAccounts: true,
	}
	if fp != nil {
		conf = fp(conf)
	}
	return conf
}

func getPod(annotations map[string]string) *corev1.Pod {
	pod := &corev1.Pod{
		TypeMeta: metav1.TypeMeta{
			APIVersion: "v1",
			Kind:       "Pod",
		},
		ObjectMeta: metav1.ObjectMeta{
			Namespace: "default",
			Name:      "foo",
			Annotations: map[string]string{
				"some-key": "some-value",
			},
		},
		Spec: corev1.PodSpec{
			Containers: []corev1.Container{{
				Name:  "bar",
				Image: "bar:v1",
				Ports: []corev1.ContainerPort{{ContainerPort: 80}, {ContainerPort: 443}},
			},
			},
		},
		Status: corev1.PodStatus{},
	}
	if annotations != nil {
		for k, v := range annotations {
			pod.Annotations[k] = v
		}
	}
	return pod
}

func getVn(ports []int) *appmesh.VirtualNode {
	vn := &appmesh.VirtualNode{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: "awesome-ns",
			Name:      "my-vn",
		},
		Spec: appmesh.VirtualNodeSpec{
			AWSName: aws.String(""),
			MeshRef: &appmesh.MeshReference{
				Name: "my-mesh",
				UID:  "408d3036-7dec-11ea-b156-0e30aabe1ca8",
			},
		},
	}
	for _, p := range ports {
		listener := appmesh.Listener{PortMapping: appmesh.PortMapping{Port: appmesh.PortNumber(p)}}
		vn.Spec.Listeners = append(vn.Spec.Listeners, listener)
	}
	return vn
}

func getVg(ports []int) *appmesh.VirtualGateway {
	vg := &appmesh.VirtualGateway{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: "awesome-ns",
			Name:      "my-vg",
		},
		Spec: appmesh.VirtualGatewaySpec{
			AWSName: aws.String(""),
			MeshRef: &appmesh.MeshReference{
				Name: "my-mesh",
				UID:  "408d3036-7dec-11ea-b156-0e30aabe1ca8",
			},
		},
	}
	for _, p := range ports {
		listener := appmesh.VirtualGatewayListener{PortMapping: appmesh.VirtualGatewayPortMapping{Port: appmesh.PortNumber(p)}}
		vg.Spec.Listeners = append(vg.Spec.Listeners, listener)
	}
	return vg
}

func getMesh() *appmesh.Mesh {
	return &appmesh.Mesh{
		ObjectMeta: metav1.ObjectMeta{
			Name: "my-mesh",
		},
		Spec: appmesh.MeshSpec{
			AWSName: aws.String("my-mesh"),
		},
	}
}

func Test_InjectEnvoyContainerVN(t *testing.T) {
	type args struct {
		ms  *appmesh.Mesh
		vn  *appmesh.VirtualNode
		pod *corev1.Pod
	}
	type expected struct {
		init       int
		containers int
		xray       bool
		proxyinit  bool
	}
	tests := []struct {
		name    string
		conf    Config
		args    args
		want    expected
		wantErr error
	}{
		{
			name: "Inject Envoy container with proxyinit",
			conf: getConfig(nil),
			args: args{
				ms:  getMesh(),
				vn:  getVn(nil),
				pod: getPod(nil),
			},
			want: expected{
				init:       1,
				containers: 2,
				xray:       false,
				proxyinit:  true,
			},
		},
		{
			name: "Inject Envoy container with xray",
			conf: getConfig(func(cnf Config) Config {
				cnf.EnableXrayTracing = true
				cnf.XrayDaemonPort = 2000
				cnf.XraySamplingRate = "0.05"
				cnf.XRayImage = "public.ecr.aws/xray/aws-xray-daemon"
				return cnf
			}),
			args: args{
				ms:  getMesh(),
				vn:  getVn(nil),
				pod: getPod(nil),
			},
			want: expected{
				init:       1,
				containers: 3,
				xray:       true,
				proxyinit:  true,
			},
		},
		{
			name: "AppMesh CNI Enabled",
			conf: getConfig(nil),
			args: args{
				ms: getMesh(),
				vn: getVn(nil),
				pod: getPod(map[string]string{
					AppMeshCNIAnnotation: "enabled",
				}),
			},
			want: expected{
				init:       0,
				containers: 2,
				xray:       false,
				proxyinit:  false,
			},
		},
		{
			name: "Enable Jaeger Tracing",
			conf: getConfig(func(cnf Config) Config {
				cnf.EnableJaegerTracing = true
				cnf.JaegerAddress = "addr"
				cnf.JaegerPort = "1234"
				return cnf
			}),
			args: args{
				ms:  getMesh(),
				vn:  getVn(nil),
				pod: getPod(nil),
			},
			want: expected{
				init:       1,
				containers: 2,
				xray:       false,
				proxyinit:  true,
			},
		},
		{
			name: "Enable Datadog Tracing",
			conf: getConfig(func(cnf Config) Config {
				cnf.EnableDatadogTracing = true
				cnf.DatadogAddress = "addr"
				cnf.DatadogPort = 1234
				return cnf
			}),
			args: args{
				ms:  getMesh(),
				vn:  getVn(nil),
				pod: getPod(nil),
			},
			want: expected{
				init:       1,
				containers: 2,
				xray:       false,
				proxyinit:  true,
			},
		},
		{
			name: "With Pod Annotations",
			conf: getConfig(nil),
			args: args{
				ms: getMesh(),
				vn: getVn(nil),
				pod: getPod(map[string]string{
					AppMeshCPURequestAnnotation:         "20m",
					AppMeshMemoryRequestAnnotation:      "64Mi",
					AppMeshPreviewAnnotation:            "0",
					AppMeshEgressIgnoredPortsAnnotation: "33",
				}),
			},
			want: expected{
				init:       1,
				containers: 2,
				xray:       false,
				proxyinit:  true,
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			inj := NewSidecarInjector(tt.conf, "000000000000", "us-west-2", "v1.4.1", "v1.4.1", nil, nil, nil, nil)
			pod := tt.args.pod
			inj.injectAppMeshPatches(tt.args.ms, tt.args.vn, nil, pod)
			assert.Equal(t, tt.want.init, len(pod.Spec.InitContainers), "Numbers of init containers mismatch")
			assert.Equal(t, tt.want.containers, len(pod.Spec.Containers), "Numbers of containers mismatch")
			if tt.want.xray {
				found := false
				for _, v := range pod.Spec.Containers {
					if v.Name == "xray-daemon" {
						found = true
					}
				}
				assert.True(t, found, "X-ray container not found")
			}
			if tt.want.proxyinit {
				found := false
				for _, v := range pod.Spec.InitContainers {
					if v.Name == "proxyinit" {
						found = true
					}
				}
				assert.True(t, found, "Proxyinit container not found")
			}
		})
	}
}

func Test_InjectEnvoyContainerVG(t *testing.T) {
	type args struct {
		ms  *appmesh.Mesh
		vg  *appmesh.VirtualGateway
		pod *corev1.Pod
	}
	type expected struct {
		containers int
		xray       bool
	}
	tests := []struct {
		name    string
		conf    Config
		args    args
		want    expected
		wantErr error
	}{
		{
			name: "Inject Envoy container with xray",
			conf: getConfig(func(cnf Config) Config {
				cnf.EnableXrayTracing = true
				cnf.XrayDaemonPort = 2000
				cnf.XraySamplingRate = "0.05"
				cnf.XRayImage = "public.ecr.aws/xray/aws-xray-daemon"
				return cnf
			}),
			args: args{
				ms:  getMesh(),
				vg:  getVg(nil),
				pod: getPod(nil),
			},
			want: expected{
				containers: 2,
				xray:       true,
			},
			wantErr: nil,
		},
		{
			name: "Inject Envoy container with xray - missing xray daemon port",
			conf: getConfig(func(cnf Config) Config {
				cnf.EnableXrayTracing = true
				cnf.XRayImage = "public.ecr.aws/xray/aws-xray-daemon"
				return cnf
			}),
			args: args{
				ms:  getMesh(),
				vg:  getVg(nil),
				pod: getPod(nil),
			},
			want: expected{
				containers: 1,
				xray:       true,
			},
			wantErr: errors.New("Missing configuration parameters: xRayDaemonPort"),
		},
		{
			name: "Inject Envoy container with xray - missing xray image",
			conf: getConfig(func(cnf Config) Config {
				cnf.EnableXrayTracing = true
				cnf.XrayDaemonPort = 2000
				cnf.XraySamplingRate = "0.05"
				return cnf
			}),
			args: args{
				ms:  getMesh(),
				vg:  getVg(nil),
				pod: getPod(nil),
			},
			want: expected{
				containers: 1,
				xray:       true,
			},
			wantErr: errors.New("Missing configuration parameters: xRayImage"),
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			inj := NewSidecarInjector(tt.conf, "000000000000", "us-west-2", "v1.4.1", "v1.4.1", nil, nil, nil, nil)
			pod := tt.args.pod
			err := inj.injectAppMeshPatches(tt.args.ms, nil, tt.args.vg, pod)
			if tt.wantErr != nil {
				assert.EqualError(t, err, tt.wantErr.Error())
			} else {
				assert.Equal(t, tt.want.containers, len(pod.Spec.Containers), "Numbers of containers mismatch")
				if tt.want.xray {
					found := false
					for _, v := range pod.Spec.Containers {
						if v.Name == "xray-daemon" {
							found = true
						}
					}
					assert.True(t, found, "X-ray container not found")
				}
			}
		})
	}
}

func TestSidecarInjector_determineSidecarInjectMode(t *testing.T) {
	nsEnabledSidecarInject := &corev1.Namespace{
		ObjectMeta: metav1.ObjectMeta{
			Name: "awesome-ns",
			Labels: map[string]string{
				"appmesh.k8s.aws/sidecarInjectorWebhook": "enabled",
			},
		},
	}

	nsDisabledSidecarInject := &corev1.Namespace{
		ObjectMeta: metav1.ObjectMeta{
			Name: "awesome-ns",
			Labels: map[string]string{
				"appmesh.k8s.aws/sidecarInjectorWebhook": "disabled",
			},
		},
	}

	nsUnspecifiedSidecarInject := &corev1.Namespace{
		ObjectMeta: metav1.ObjectMeta{
			Name:   "awesome-ns",
			Labels: map[string]string{},
		},
	}

	podEnabledSidecarInject := &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: "awesome-ns",
			Name:      "my-pod",
			Annotations: map[string]string{
				"appmesh.k8s.aws/sidecarInjectorWebhook": "enabled",
			},
		},
	}
	podDisabledSidecarInject := &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: "awesome-ns",
			Name:      "my-pod",
			Annotations: map[string]string{
				"appmesh.k8s.aws/sidecarInjectorWebhook": "disabled",
			},
		},
	}
	podInvalidModeSidecarInject := &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: "awesome-ns",
			Name:      "my-pod",
			Annotations: map[string]string{
				"appmesh.k8s.aws/sidecarInjectorWebhook": "typostring",
			},
		},
	}
	podUnspecifiedSidecarInject := &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{
			Namespace:   "awesome-ns",
			Name:        "my-pod",
			Annotations: map[string]string{},
		},
	}
	// see https://github.com/kubernetes/kubernetes/issues/76680
	podWithUnspecifiedNameAndNamespace := &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{
			Annotations: map[string]string{},
		},
	}

	type env struct {
		namespaces []*corev1.Namespace
	}
	type args struct {
		pod *corev1.Pod
	}
	tests := []struct {
		name    string
		env     env
		args    args
		want    sidecarInjectMode
		wantErr error
	}{
		{
			name: "when pod enable sidecar inject - ns inject enabled",
			env: env{
				namespaces: []*corev1.Namespace{nsEnabledSidecarInject},
			},
			args: args{
				pod: podEnabledSidecarInject,
			},
			want: sidecarInjectModeEnabled,
		},
		{
			name: "when pod disable sidecar inject - ns inject enabled",
			env: env{
				namespaces: []*corev1.Namespace{nsEnabledSidecarInject},
			},
			args: args{
				pod: podDisabledSidecarInject,
			},
			want: sidecarInjectModeDisabled,
		},
		{
			name: "when pod invalid string sidecar inject - ns inject enabled",
			env: env{
				namespaces: []*corev1.Namespace{nsEnabledSidecarInject},
			},
			args: args{
				pod: podInvalidModeSidecarInject,
			},
			want: sidecarInjectModeUnspecified,
		},
		{
			name: "when pod unspecified sidecar inject - ns inject enabled",
			env: env{
				namespaces: []*corev1.Namespace{nsEnabledSidecarInject},
			},
			args: args{
				pod: podUnspecifiedSidecarInject,
			},
			want: sidecarInjectModeEnabled,
		},
		{
			name: "when pod unspecified name and namespace - ns inject enabled",
			env: env{
				namespaces: []*corev1.Namespace{nsEnabledSidecarInject},
			},
			args: args{
				pod: podWithUnspecifiedNameAndNamespace,
			},
			want: sidecarInjectModeEnabled,
		},
		{
			name: "when pod enable sidecar inject - ns inject disabled",
			env: env{
				namespaces: []*corev1.Namespace{nsDisabledSidecarInject},
			},
			args: args{
				pod: podEnabledSidecarInject,
			},
			want: sidecarInjectModeEnabled,
		},
		{
			name: "when pod disable sidecar inject - ns inject disabled",
			env: env{
				namespaces: []*corev1.Namespace{nsDisabledSidecarInject},
			},
			args: args{
				pod: podDisabledSidecarInject,
			},
			want: sidecarInjectModeDisabled,
		},
		{
			name: "when pod invalid string sidecar inject - ns inject disabled",
			env: env{
				namespaces: []*corev1.Namespace{nsDisabledSidecarInject},
			},
			args: args{
				pod: podInvalidModeSidecarInject,
			},
			want: sidecarInjectModeUnspecified,
		},
		{
			name: "when pod unspecified sidecar inject - ns inject disabled",
			env: env{
				namespaces: []*corev1.Namespace{nsDisabledSidecarInject},
			},
			args: args{
				pod: podUnspecifiedSidecarInject,
			},
			want: sidecarInjectModeDisabled,
		},
		{
			name: "when pod unspecified name and namespace - ns inject disabled",
			env: env{
				namespaces: []*corev1.Namespace{nsDisabledSidecarInject},
			},
			args: args{
				pod: podWithUnspecifiedNameAndNamespace,
			},
			want: sidecarInjectModeDisabled,
		},
		{
			name: "when pod unspecified sidecar inject - ns inject unspecified",
			env: env{
				namespaces: []*corev1.Namespace{nsUnspecifiedSidecarInject},
			},
			args: args{
				pod: podUnspecifiedSidecarInject,
			},
			want: sidecarInjectModeUnspecified,
		},
	}
	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)
			m := &SidecarInjector{
				k8sClient: k8sClient,
			}

			for _, ns := range tt.env.namespaces {
				err := k8sClient.Create(ctx, ns.DeepCopy())
				assert.NoError(t, err)
			}
			ctx = webhook.ContextWithAdmissionRequest(ctx, admission.Request{
				AdmissionRequest: admissionv1.AdmissionRequest{Namespace: "awesome-ns"},
			})
			got, err := m.determineSidecarInjectMode(ctx, tt.args.pod)
			if tt.wantErr != nil {
				assert.EqualError(t, err, tt.wantErr.Error())
			} else {
				assert.NoError(t, err)
				assert.Equal(t, tt.want, got)
			}
		})
	}
}
