package components

import (
	"fmt"
	"github.com/google/go-cmp/cmp"
	v1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/client-go/informers"
	"k8s.io/client-go/kubernetes/fake"
	"kubesphere.io/kubesphere/pkg/api/resource/v1alpha2"
	"testing"
	"time"
)

func service(name, namespace string, selector map[string]string) runtime.Object {
	return &v1.Service{
		TypeMeta: metav1.TypeMeta{
			APIVersion: "v1",
			Kind:       "Service",
		},
		ObjectMeta: metav1.ObjectMeta{
			Name:      name,
			Namespace: namespace,
		},
		Spec: v1.ServiceSpec{
			Selector: selector,
		},
	}
}

func pods(name, namespace string, labels map[string]string, healthPods, totalPods int) []runtime.Object {
	var ps []runtime.Object

	for index := 0; index < totalPods; index++ {
		pod := &v1.Pod{
			ObjectMeta: metav1.ObjectMeta{
				Name:      fmt.Sprintf("%s-%d", name, index),
				Namespace: namespace,
				Labels:    labels,
			},
			Status: v1.PodStatus{
				Phase: v1.PodRunning,
				ContainerStatuses: []v1.ContainerStatus{
					{
						Name:  fmt.Sprintf("%s-%d", name, index),
						Ready: true,
					},
				},
			},
		}

		if index >= healthPods {
			pod.Status.Phase = v1.PodPending
			pod.Status.ContainerStatuses[0].Ready = false
		}

		ps = append(ps, pod)
	}

	return ps
}

func nodes(name string, healthNodes, totalNodes int) []runtime.Object {
	var ns []runtime.Object

	for index := 0; index < totalNodes; index++ {
		node := &v1.Node{
			ObjectMeta: metav1.ObjectMeta{
				Name: fmt.Sprintf("%s-%d", name, index),
			},
			Status: v1.NodeStatus{
				Phase: v1.NodeRunning,
				Conditions: []v1.NodeCondition{
					{
						Type:   v1.NodeReady,
						Status: v1.ConditionTrue,
					},
				},
			},
		}

		if index >= healthNodes {
			node.Status.Phase = v1.NodePending
			node.Status.Conditions[0].Status = v1.ConditionFalse
		}

		ns = append(ns, node)
	}

	return ns
}

func TestGetSystemHealthStatus(t *testing.T) {
	var tests = []struct {
		description string
		labels      map[string]string
		namespace   string
		name        string
		healthPods  int
		totalPods   int
		healthNodes int
		totalNodes  int
		expected    v1alpha2.HealthStatus
	}{
		{
			"no backends",
			map[string]string{"app": "foo"},
			"kube-system",
			"",
			0,
			0,
			0,
			0,
			v1alpha2.HealthStatus{
				KubeSphereComponents: []v1alpha2.ComponentStatus{
					{
						Namespace:       "kube-system",
						Label:           map[string]string{"app": "foo"},
						TotalBackends:   0,
						HealthyBackends: 0,
					},
				},
				NodeStatus: v1alpha2.NodeStatus{},
			},
		},
		{
			"all healthy",
			map[string]string{"app": "foo"},
			"kubesphere-system",
			"ks-apiserver",
			2,
			2,
			2,
			2,
			v1alpha2.HealthStatus{
				KubeSphereComponents: []v1alpha2.ComponentStatus{
					{
						Name:            "ks-apiserver",
						Namespace:       "kubesphere-system",
						Label:           map[string]string{"app": "foo"},
						TotalBackends:   2,
						HealthyBackends: 2,
					},
				},
				NodeStatus: v1alpha2.NodeStatus{
					TotalNodes:   2,
					HealthyNodes: 2,
				},
			},
		},
		{
			"all unhealthy",
			map[string]string{"app": "foo"},
			"kubesphere-system",
			"ks-apiserver",
			0,
			2,
			0,
			2,
			v1alpha2.HealthStatus{
				KubeSphereComponents: []v1alpha2.ComponentStatus{
					{
						Name:            "ks-apiserver",
						Namespace:       "kubesphere-system",
						Label:           map[string]string{"app": "foo"},
						TotalBackends:   2,
						HealthyBackends: 0,
					},
				},
				NodeStatus: v1alpha2.NodeStatus{
					TotalNodes:   2,
					HealthyNodes: 0,
				},
			},
		},
		{
			"half healthy",
			map[string]string{"app": "foo"},
			"kubesphere-system",
			"ks-apiserver",
			2,
			4,
			2,
			4,
			v1alpha2.HealthStatus{
				KubeSphereComponents: []v1alpha2.ComponentStatus{
					{
						Name:            "ks-apiserver",
						Namespace:       "kubesphere-system",
						Label:           map[string]string{"app": "foo"},
						TotalBackends:   4,
						HealthyBackends: 2,
					},
				},
				NodeStatus: v1alpha2.NodeStatus{
					TotalNodes:   4,
					HealthyNodes: 2,
				},
			},
		},
	}

	for _, test := range tests {
		t.Run(test.description, func(t *testing.T) {
			ps := pods(test.name, test.namespace, test.labels, test.healthPods, test.totalPods)
			svc := service(test.name, test.namespace, test.labels)
			ns := nodes(test.name, test.healthNodes, test.totalNodes)

			var objs []runtime.Object
			objs = append(objs, ps...)
			objs = append(objs, svc)
			objs = append(objs, ns...)

			client := fake.NewSimpleClientset(objs...)

			informer := informers.NewSharedInformerFactory(client, time.Minute*10)

			informer.Core().V1().Services().Informer().GetIndexer().Add(svc)

			for _, obj := range ps {
				informer.Core().V1().Pods().Informer().GetIndexer().Add(obj)
			}

			for _, obj := range ns {
				informer.Core().V1().Nodes().Informer().GetIndexer().Add(obj)
			}

			c := NewComponentsGetter(informer)
			healthStatus, err := c.GetSystemHealthStatus()
			if err != nil {
				t.Fatal(err)
			}

			if diff := cmp.Diff(healthStatus, test.expected); diff != "" {
				t.Errorf("%T differ (-got, +want): %s", test.expected, diff)
				return
			}
		})
	}

}

func TestGetComponentStatus(t *testing.T) {
	var tests = []struct {
		description   string
		name          string
		namespace     string
		labels        map[string]string
		healthPods    int
		totalPods     int
		expected      v1alpha2.ComponentStatus
		expectedError bool
	}{
		{
			"no component",
			"random",
			"foo",
			map[string]string{"app": "foo"},
			2,
			4,
			v1alpha2.ComponentStatus{
				Name:            "",
				Namespace:       "",
				SelfLink:        "",
				Label:           nil,
				StartedAt:       time.Time{},
				TotalBackends:   0,
				HealthyBackends: 0,
			},
			true,
		},
		{
			"all healthy",
			"ks-apiserver",
			"kubesphere-system",
			map[string]string{"app": "foo"},
			2,
			4,
			v1alpha2.ComponentStatus{
				Name:            "ks-apiserver",
				Namespace:       "kubesphere-system",
				Label:           map[string]string{"app": "foo"},
				TotalBackends:   4,
				HealthyBackends: 2,
			},
			false,
		},
		{
			"all unhealthy",
			"ks-apiserver",
			"kubesphere-system",
			map[string]string{"app": "foo"},
			0,
			4,
			v1alpha2.ComponentStatus{
				Name:            "ks-apiserver",
				Namespace:       "kubesphere-system",
				Label:           map[string]string{"app": "foo"},
				TotalBackends:   4,
				HealthyBackends: 0,
			},
			false,
		},
		{
			"half healthy",
			"ks-apiserver",
			"kubesphere-system",
			map[string]string{"app": "foo"},
			2,
			4,
			v1alpha2.ComponentStatus{
				Name:            "ks-apiserver",
				Namespace:       "kubesphere-system",
				Label:           map[string]string{"app": "foo"},
				TotalBackends:   4,
				HealthyBackends: 2,
			},
			false,
		},
	}

	for _, test := range tests {
		t.Run(test.description, func(t *testing.T) {
			ps := pods(test.name, test.namespace, test.labels, test.healthPods, test.totalPods)
			svc := service(test.name, test.namespace, test.labels)

			var objs []runtime.Object
			objs = append(objs, ps...)
			objs = append(objs, svc)

			client := fake.NewSimpleClientset(objs...)

			informer := informers.NewSharedInformerFactory(client, time.Minute*10)

			informer.Core().V1().Services().Informer().GetIndexer().Add(svc)

			for _, obj := range ps {
				informer.Core().V1().Pods().Informer().GetIndexer().Add(obj)
			}

			c := NewComponentsGetter(informer)
			healthStatus, err := c.GetComponentStatus(test.name)
			if err == nil && test.expectedError {
				t.Fatalf("expected error while got nothing")
			} else if err != nil && !test.expectedError {
				t.Fatal(err)
			}

			if diff := cmp.Diff(healthStatus, test.expected); diff != "" {
				t.Errorf("%T differ (-got, +want): %s", test.expected, diff)
				return
			}
		})
	}
}
