package coredns_test

import (
	"context"
	"fmt"
	"testing"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	appsv1 "k8s.io/api/apps/v1"
	v1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	kubeclient "k8s.io/client-go/kubernetes"
	"k8s.io/client-go/kubernetes/fake"

	api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5"
	"github.com/weaveworks/eksctl/pkg/fargate/coredns"
	"github.com/weaveworks/eksctl/pkg/testutils"
	"github.com/weaveworks/eksctl/pkg/utils/names"
	"github.com/weaveworks/eksctl/pkg/utils/retry"
)

func TestFargateCoreDNS(t *testing.T) {
	testutils.RegisterAndRun(t)
}

var (
	minimumProfileSelectingCoreDNS = []*api.FargateProfile{
		{
			Name: "min-selecting-coredns",
			Selectors: []api.FargateProfileSelector{
				{Namespace: "kube-system"},
			},
		},
	}

	multipleProfilesWithOneSelectingCoreDNS = []*api.FargateProfile{
		{
			Name: "foo",
			Selectors: []api.FargateProfileSelector{
				{Namespace: "foo"},
			},
		},
		{
			Name: "selecting-coredns",
			Selectors: []api.FargateProfileSelector{
				{Namespace: "fooo"},
				{Namespace: "kube-system"},
				{Namespace: "baar"},
			},
		},
		{
			Name: "bar",
			Selectors: []api.FargateProfileSelector{
				{Namespace: "bar"},
			},
		},
	}

	profileNotSelectingCoreDNSBecauseOfLabels = []*api.FargateProfile{
		{
			Name: "not-selecting-coredns-because-of-labels",
			Selectors: []api.FargateProfileSelector{
				{
					Namespace: "kube-system",
					Labels: map[string]string{
						"foo": "bar",
					},
				},
			},
		},
	}

	profileSelectingCoreDNSWithComponentLabel = []*api.FargateProfile{
		{
			Name: "selecting-coredns-with-label",
			Selectors: []api.FargateProfileSelector{
				{
					Namespace: "kube-system",
					Labels: map[string]string{
						"eks.amazonaws.com/component": "coredns",
					},
				},
			},
		},
	}

	profileNotSelectingCoreDNSBecauseOfNamespace = []*api.FargateProfile{
		{
			Name: "not-selecting-coredns-because-of-namespace",
			Selectors: []api.FargateProfileSelector{
				{
					Namespace: "default",
				},
			},
		},
	}

	retryPolicy = &retry.ConstantBackoff{
		// Retry without waiting at all, in order to speed tests up.
		Time: 0, TimeUnit: time.Second, MaxRetries: 1,
	}
)

var _ = Describe("coredns", func() {
	Describe("IsSchedulableOnFargate", func() {
		It("should return true when a Fargate profile matches kube-system and doesn't have any label", func() {
			Expect(coredns.IsSchedulableOnFargate(minimumProfileSelectingCoreDNS)).To(BeTrue())
			Expect(coredns.IsSchedulableOnFargate(multipleProfilesWithOneSelectingCoreDNS)).To(BeTrue())
		})

		It("should return true when a Fargate profile matches kube-system and has the CoreDNS component label", func() {
			Expect(coredns.IsSchedulableOnFargate(profileSelectingCoreDNSWithComponentLabel)).To(BeTrue())
		})

		It("should return true when provided the default Fargate profile", func() {
			cfg := api.NewClusterConfig()
			cfg.SetDefaultFargateProfile()
			Expect(coredns.IsSchedulableOnFargate(cfg.FargateProfiles)).To(BeTrue())
		})

		It("should return false when a Fargate profile matches kube-system but has labels", func() {
			Expect(coredns.IsSchedulableOnFargate(profileNotSelectingCoreDNSBecauseOfLabels)).To(BeFalse())
		})

		It("should return false when a Fargate profile doesn't match kube-system", func() {
			Expect(coredns.IsSchedulableOnFargate(profileNotSelectingCoreDNSBecauseOfNamespace)).To(BeFalse())
		})

		It("should return false when not provided any Fargate profile", func() {
			Expect(coredns.IsSchedulableOnFargate([]*api.FargateProfile{})).To(BeFalse())
		})
	})

	Describe("ScheduleOnFargate", func() {
		It("should set the compute-type annotation to 'fargate'", func() {
			// Given:
			mockClientset := mockClientsetWith(deployment("ec2", 0, 2))
			deployment, err := mockClientset.AppsV1().Deployments(coredns.Namespace).Get(context.Background(), coredns.Name, metav1.GetOptions{})
			Expect(err).To(Not(HaveOccurred()))
			Expect(deployment.Spec.Template.Annotations).NotTo(BeNil())
			Expect(deployment.Spec.Template.Annotations).To(HaveKeyWithValue(coredns.ComputeTypeAnnotationKey, "ec2"))
			// When:
			err = coredns.ScheduleOnFargate(mockClientset)
			Expect(err).To(Not(HaveOccurred()))
			// Then:
			deployment, err = mockClientset.AppsV1().Deployments(coredns.Namespace).Get(context.Background(), coredns.Name, metav1.GetOptions{})
			Expect(err).To(Not(HaveOccurred()))
			Expect(deployment.Spec.Template.Annotations).NotTo(BeNil())
			Expect(deployment.Spec.Template.Annotations).To(HaveKeyWithValue(coredns.ComputeTypeAnnotationKey, "fargate"))
		})
	})

	Describe("WaitForScheduleOnFargate", func() {
		It("should error if the annotations are not set", func() {
			// Given:
			mockClientset := mockClientsetWith(
				deployment("fargate", 2, 2), podWithAnnotations("fargate", v1.PodRunning, nil), pod("fargate", v1.PodRunning),
			)
			// When:
			err := coredns.WaitForScheduleOnFargate(mockClientset, retryPolicy)
			// Then:
			Expect(err).To(HaveOccurred())
		})

		It("should wait for coredns to be scheduled on Fargate and return w/o any error", func() {
			// Given:
			mockClientset := mockClientsetWith(
				deployment("fargate", 2, 2), pod("fargate", v1.PodRunning), pod("fargate", v1.PodRunning),
			)
			// When:
			err := coredns.WaitForScheduleOnFargate(mockClientset, retryPolicy)
			// Then:
			Expect(err).To(Not(HaveOccurred()))
		})

		It("should time out if coredns cannot be scheduled within the allotted time", func() {
			failureCases := [][]runtime.Object{
				{deployment("ec2", 2, 2), pod("ec2", v1.PodRunning), pod("ec2", v1.PodRunning)},
				{deployment("ec2", 0, 2), pod("ec2", v1.PodPending), pod("ec2", v1.PodPending)},
				{deploymentWithAnnotations("ec2", 0, 2, nil), podWithAnnotations("ec2", v1.PodFailed, nil), podWithAnnotations("ec2", v1.PodFailed, nil)},
				{deploymentWithAnnotations("ec2", 0, 2, nil), podWithAnnotations("ec2", v1.PodFailed, map[string]string{}), podWithAnnotations("ec2", v1.PodFailed, map[string]string{})},
				{deployment("fargate", 0, 2), pod("fargate", v1.PodPending), pod("fargate", v1.PodPending)},
				{deployment("fargate", 0, 2), pod("fargate", v1.PodFailed), pod("fargate", v1.PodFailed)},
				{deployment("fargate", 0, 2), pod("fargate", v1.PodPending), pod("fargate", v1.PodFailed)},
				{deployment("fargate", 1, 2), pod("fargate", v1.PodRunning), pod("fargate", v1.PodPending)},
				{deployment("fargate", 1, 2), pod("fargate", v1.PodRunning), pod("fargate", v1.PodFailed)},
			}
			for _, failureCase := range failureCases {
				// Given:
				mockClientset := mockClientsetWith(failureCase...)
				// When:
				err := coredns.WaitForScheduleOnFargate(mockClientset, retryPolicy)
				// Then:
				Expect(err).To(HaveOccurred())
				Expect(err.Error()).To(Equal("timed out while waiting for \"coredns\" to be scheduled on Fargate"))
			}
		})

		It("Should timeout if coredns pods do not have the correct annotations", func() {
			failureCases := [][]runtime.Object{
				{deploymentWithAnnotations("fargate", 0, 2, nil), podWithAnnotations("fargate", v1.PodPending, nil), podWithAnnotations("fargate", v1.PodRunning, nil)},
				{deploymentWithAnnotations("ec2", 0, 2, nil), podWithAnnotations("ec2", v1.PodFailed, nil), podWithAnnotations("ec2", v1.PodRunning, nil)},
				{deploymentWithAnnotations("ec2", 0, 2, map[string]string{}), podWithAnnotations("ec2", v1.PodRunning, map[string]string{}), podWithAnnotations("ec2", v1.PodRunning, map[string]string{})},
			}
			for _, failureCase := range failureCases {
				// Given:
				mockClientset := mockClientsetWith(failureCase...)
				// When:
				err := coredns.WaitForScheduleOnFargate(mockClientset, retryPolicy)
				// Then:
				Expect(err).To(MatchError("timed out while waiting for \"coredns\" to be scheduled on Fargate"))
			}
		})
	})

})

func mockClientsetWith(objects ...runtime.Object) kubeclient.Interface {
	return fake.NewSimpleClientset(objects...)
}

func deploymentWithAnnotations(computeType string, numReady, numReplicas int32, annotations map[string]string) *appsv1.Deployment {
	return &appsv1.Deployment{
		TypeMeta: metav1.TypeMeta{
			Kind:       "Deployment",
			APIVersion: appsv1.SchemeGroupVersion.String(),
		},
		ObjectMeta: metav1.ObjectMeta{
			Namespace: coredns.Namespace,
			Name:      coredns.Name,
		},
		Spec: appsv1.DeploymentSpec{
			Replicas: &numReplicas,
			Template: v1.PodTemplateSpec{
				ObjectMeta: metav1.ObjectMeta{
					Annotations: annotations,
				},
			},
		},
		Status: appsv1.DeploymentStatus{
			ReadyReplicas: numReady,
		},
	}
}

func deployment(computeType string, numReady, numReplicas int32) *appsv1.Deployment {
	return deploymentWithAnnotations(computeType, numReady, numReplicas, map[string]string{
		coredns.ComputeTypeAnnotationKey: computeType,
	})
}

const chars = "abcdef0123456789"

func podWithAnnotations(computeType string, phase v1.PodPhase, annotatations map[string]string) *v1.Pod {
	pod := &v1.Pod{
		TypeMeta: metav1.TypeMeta{
			Kind:       "Pod",
			APIVersion: "v1",
		},
		ObjectMeta: metav1.ObjectMeta{
			Namespace: coredns.Namespace,
			Name:      fmt.Sprintf("%s-%s-%s", coredns.Name, names.RandomName(10, chars), names.RandomName(5, chars)),
			Labels: map[string]string{
				"eks.amazonaws.com/component": coredns.Name,
			},
			Annotations: annotatations,
		},
		Status: v1.PodStatus{
			Phase: phase,
		},
	}
	if pod.Status.Phase == v1.PodRunning {
		if computeType == "fargate" {
			pod.Spec.NodeName = "fargate-ip-192-168-xxx-yyy.ap-northeast-1.compute.internal"
		} else {
			pod.Spec.NodeName = "ip-192-168-23-122.ap-northeast-1.compute.internal"
		}
	}
	return pod
}

func pod(computeType string, phase v1.PodPhase) *v1.Pod {
	return podWithAnnotations(computeType, phase, map[string]string{
		coredns.ComputeTypeAnnotationKey: computeType,
	})
}
