/*
Copyright 2024 The Karmada Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package apiclient

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"testing"
	"time"

	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/apimachinery/pkg/runtime"
	clientset "k8s.io/client-go/kubernetes"
	fakeclientset "k8s.io/client-go/kubernetes/fake"
	"k8s.io/client-go/rest"
	fakerest "k8s.io/client-go/rest/fake"
	apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
	aggregator "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset"
	fakeAggregator "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/fake"

	"github.com/karmada-io/karmada/operator/pkg/constants"
)

func TestWaitForAPI(t *testing.T) {
	tests := []struct {
		name          string
		karmadaWriter *KarmadaWaiter
		wantErr       bool
	}{
		{
			name: "WaitForAPI_WaitingForAPIServerHealthyStatus_Timeout",
			karmadaWriter: &KarmadaWaiter{
				karmadaConfig: &rest.Config{},
				client: &MockK8SRESTClient{
					RESTClientConnector: &fakerest.RESTClient{
						NegotiatedSerializer: runtime.NewSimpleNegotiatedSerializer(runtime.SerializerInfo{}),
						Client: fakerest.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
							return nil, fmt.Errorf("unexpected error, endpoint %s does not exist", req.URL.Path)
						}),
					},
				},
				timeout: time.Second,
			},
			wantErr: true,
		},
		{
			name: "WaitForAPI_WaitingForAPIServerHealthyStatus_APIServerIsHealthy",
			karmadaWriter: &KarmadaWaiter{
				karmadaConfig: &rest.Config{},
				client: &MockK8SRESTClient{
					RESTClientConnector: &fakerest.RESTClient{
						NegotiatedSerializer: runtime.NewSimpleNegotiatedSerializer(runtime.SerializerInfo{}),
						Client: fakerest.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
							if req.URL.Path == "/healthz" {
								// Return a fake 200 OK response.
								return &http.Response{
									StatusCode: http.StatusOK,
									Body:       http.NoBody,
								}, nil
							}
							return nil, fmt.Errorf("unexpected error, endpoint %s does not exist", req.URL.Path)
						}),
					},
				},
				timeout: time.Millisecond,
			},
			wantErr: false,
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			err := test.karmadaWriter.WaitForAPI()
			if err == nil && test.wantErr {
				t.Errorf("expected an error, but got none")
			}
			if err != nil && !test.wantErr {
				t.Errorf("unexpected error, got: %v", err)
			}
		})
	}
}

func TestWaitForAPIService(t *testing.T) {
	name := "karmada-demo-apiservice"
	tests := []struct {
		name          string
		karmadaWriter *KarmadaWaiter
		apiService    *apiregistrationv1.APIService
		client        aggregator.Interface
		prep          func(aggregator.Interface, *apiregistrationv1.APIService) error
		wantErr       bool
	}{
		{
			name: "WaitForAPIService_WaitingForKarmadaAPIServiceAvailableStatus_Timeout",
			karmadaWriter: &KarmadaWaiter{
				karmadaConfig: &rest.Config{},
				timeout:       time.Second,
			},
			apiService: &apiregistrationv1.APIService{
				ObjectMeta: metav1.ObjectMeta{
					Name: name,
				},
				Spec: apiregistrationv1.APIServiceSpec{
					Service: &apiregistrationv1.ServiceReference{
						Name:      "karmada-demo-service",
						Namespace: "test",
					},
					Version: "v1beta1",
				},
			},
			client: fakeAggregator.NewSimpleClientset(),
			prep: func(client aggregator.Interface, _ *apiregistrationv1.APIService) error {
				aggregateClientFromConfigBuilder = func(*rest.Config) (aggregator.Interface, error) {
					return client, nil
				}
				return nil
			},
			wantErr: true,
		},
		{
			name: "WaitForAPIService_WaitingForKarmadaAPIServiceAvailableStatus_KarmadaAPIServiceIsAvailable",
			karmadaWriter: &KarmadaWaiter{
				karmadaConfig: &rest.Config{},
				timeout:       time.Millisecond,
			},
			apiService: &apiregistrationv1.APIService{
				ObjectMeta: metav1.ObjectMeta{
					Name: name,
				},
				Spec: apiregistrationv1.APIServiceSpec{
					Service: &apiregistrationv1.ServiceReference{
						Name:      "karmada-demo-service",
						Namespace: "test",
					},
					Version: "v1beta1",
				},
			},
			client: fakeAggregator.NewSimpleClientset(),
			prep: func(client aggregator.Interface, apiService *apiregistrationv1.APIService) error {
				apiServiceCreated, err := client.ApiregistrationV1().APIServices().Create(context.TODO(), apiService, metav1.CreateOptions{})
				if err != nil {
					return fmt.Errorf("failed to create api service %s, got err: %v", apiService.Name, err)
				}
				apiServiceCreated.Status = apiregistrationv1.APIServiceStatus{
					Conditions: []apiregistrationv1.APIServiceCondition{
						{
							Type:   apiregistrationv1.Available,
							Status: apiregistrationv1.ConditionTrue,
						},
					},
				}
				if _, err = client.ApiregistrationV1().APIServices().Update(context.TODO(), apiServiceCreated, metav1.UpdateOptions{}); err != nil {
					return fmt.Errorf("failed to update api service with available status, got err: %v", err)
				}
				aggregateClientFromConfigBuilder = func(*rest.Config) (aggregator.Interface, error) {
					return client, nil
				}
				return nil
			},
			wantErr: false,
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			if err := test.prep(test.client, test.apiService); err != nil {
				t.Errorf("failed to prep waiting for Karmada API Service, got err: %v", err)
			}
			err := test.karmadaWriter.WaitForAPIService(name)
			if err == nil && test.wantErr {
				t.Errorf("expected an error, but got none")
			}
			if err != nil && !test.wantErr {
				t.Errorf("unexpected error, got: %v", err)
			}
		})
	}
}

func TestWaitForPods(t *testing.T) {
	name, namespace := "karmada-demo-apiserver", "test"
	karmadaAPIServerLabels := labels.Set{"karmada-app": constants.KarmadaAPIServer}
	var replicas int32 = 2
	tests := []struct {
		name          string
		karmadaWriter *KarmadaWaiter
		prep          func(client clientset.Interface) error
		wantErr       bool
	}{
		{
			name: "WaitForPods_WaitingForAllKarmadaAPIServerPods_Timeout",
			karmadaWriter: &KarmadaWaiter{
				karmadaConfig: &rest.Config{},
				client:        fakeclientset.NewSimpleClientset(),
				timeout:       time.Second,
			},
			prep: func(client clientset.Interface) error {
				_, err := CreatePods(client, namespace, name, replicas, karmadaAPIServerLabels, false)
				if err != nil {
					return fmt.Errorf("failed to create pods, got err: %v", err)
				}
				return nil
			},
			wantErr: true,
		},
		{
			name: "WaitForPods_WaitingForAllKarmadaAPIServerPods_AllAreUpAndRunning",
			karmadaWriter: &KarmadaWaiter{
				karmadaConfig: &rest.Config{},
				client:        fakeclientset.NewSimpleClientset(),
				timeout:       time.Second * 2,
			},
			prep: func(client clientset.Interface) error {
				pods, err := CreatePods(client, namespace, name, replicas, karmadaAPIServerLabels, false)
				if err != nil {
					return fmt.Errorf("failed to create pods, got err: %v", err)
				}
				time.AfterFunc(time.Second, func() {
					for _, pod := range pods {
						if err := UpdatePodStatus(client, pod); err != nil {
							fmt.Printf("failed to update pod status, got err: %v", err)
							return
						}
					}
				})
				return nil
			},
			wantErr: false,
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			if err := test.prep(test.karmadaWriter.client); err != nil {
				t.Errorf("failed to prep before waiting for all Karmada APIServer pods , got err: %v", err)
			}
			err := test.karmadaWriter.WaitForPods(karmadaAPIServerLabels.String(), namespace)
			if err == nil && test.wantErr {
				t.Errorf("expected an error, but got none")
			}
			if err != nil && !test.wantErr {
				t.Errorf("unexpected error, got: %v", err)
			}
		})
	}
}

func TestWaitForSomePods(t *testing.T) {
	name, namespace := "karmada-demo-apiserver", "test"
	karmadaAPIServerLabels := labels.Set{"karmada-app": constants.KarmadaAPIServer}
	var replicas int32 = 2
	tests := []struct {
		name          string
		karmadaWriter *KarmadaWaiter
		prep          func(client clientset.Interface) error
		wantErr       bool
	}{
		{
			name: "WaitForSomePods_WaitingForSomeKarmadaAPIServerPods_Timeout",
			karmadaWriter: &KarmadaWaiter{
				karmadaConfig: &rest.Config{},
				client:        fakeclientset.NewSimpleClientset(),
				timeout:       time.Second,
			},
			prep: func(client clientset.Interface) error {
				_, err := CreatePods(client, namespace, name, replicas, karmadaAPIServerLabels, false)
				if err != nil {
					return fmt.Errorf("failed to create pods, got err: %v", err)
				}
				return nil
			},
			wantErr: true,
		},
		{
			name: "WaitForSomePods_WaitingForSomeKarmadaAPIServerPods_SomeAreUpAndRunning",
			karmadaWriter: &KarmadaWaiter{
				karmadaConfig: &rest.Config{},
				client:        fakeclientset.NewSimpleClientset(),
				timeout:       time.Millisecond,
			},
			prep: func(client clientset.Interface) error {
				pods, err := CreatePods(client, namespace, name, replicas, karmadaAPIServerLabels, false)
				if err != nil {
					return fmt.Errorf("failed to create pods, got err: %v", err)
				}
				for _, pod := range pods[:1] {
					if err := UpdatePodStatus(client, pod); err != nil {
						return fmt.Errorf("failed to update pod status, got err: %v", err)
					}
				}
				return nil
			},
			wantErr: false,
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			if err := test.prep(test.karmadaWriter.client); err != nil {
				t.Errorf("failed to prep before waiting for some Karmada APIServer pods , got err: %v", err)
			}
			err := test.karmadaWriter.WaitForSomePods(karmadaAPIServerLabels.String(), namespace, 1)
			if err == nil && test.wantErr {
				t.Errorf("expected an error, but got none")
			}
			if err != nil && !test.wantErr {
				t.Errorf("unexpected error, got: %v", err)
			}
		})
	}
}

func TestTryRunCommand(t *testing.T) {
	tests := []struct {
		name             string
		failureThreshold int
		targetFunc       func() error
		prep             func() error
		wantErr          bool
	}{
		{
			name:             "TryRunCommand_HitTheFailureThreshold_CommandTimedOut",
			failureThreshold: 2,
			targetFunc: func() error {
				return errors.New("unexpected error")
			},
			prep: func() error {
				initialBackoffDuration = time.Millisecond
				return nil
			},
			wantErr: true,
		},
		{
			name:             "TryRunCommand_BelowFailureThreshold_CommandRunSuccessfully",
			failureThreshold: 2,
			targetFunc:       func() error { return nil },
			prep: func() error {
				initialBackoffDuration = time.Millisecond
				return nil
			},
			wantErr: false,
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			if err := test.prep(); err != nil {
				t.Errorf("failed to prep before trying to running command, got err: %v", err)
			}
			err := TryRunCommand(test.targetFunc, test.failureThreshold)
			if err == nil && test.wantErr {
				t.Errorf("expected an error, but got none")
			}
			if err != nil && !test.wantErr {
				t.Errorf("unexpected error, got: %v", err)
			}
		})
	}
}

func TestIsPodRunning(t *testing.T) {
	tests := []struct {
		name string
		pod  *corev1.Pod
		want bool
	}{
		{
			name: "IsPodRunning_PodInPendingState_PodIsNotRunningYet",
			pod: &corev1.Pod{
				Status: corev1.PodStatus{
					Phase: corev1.PodPending,
				},
			},
			want: false,
		},
		{
			name: "IsPodRunning_WithDeletionTimestamp_PodIsNotRunningYet",
			pod: &corev1.Pod{
				ObjectMeta: metav1.ObjectMeta{
					DeletionTimestamp: &metav1.Time{Time: time.Now()},
				},
				Status: corev1.PodStatus{
					Phase: corev1.PodRunning,
				},
			},
			want: false,
		},
		{
			name: "IsPodRunning_PodReadyConditionReadinessIsFalse_PodIsNotRunningYet",
			pod: &corev1.Pod{
				ObjectMeta: metav1.ObjectMeta{
					DeletionTimestamp: nil,
				},
				Status: corev1.PodStatus{
					Phase: corev1.PodRunning,
					Conditions: []corev1.PodCondition{
						{
							Type:   corev1.PodReady,
							Status: corev1.ConditionFalse,
						},
					},
				},
			},
			want: false,
		},
		{
			name: "IsPodRunning_PodSatisfyAllRunningConditions_PodIsAlreadyRunning",
			pod: &corev1.Pod{
				ObjectMeta: metav1.ObjectMeta{
					DeletionTimestamp: nil,
				},
				Status: corev1.PodStatus{
					Phase: corev1.PodRunning,
					Conditions: []corev1.PodCondition{
						{
							Type:   corev1.PodReady,
							Status: corev1.ConditionTrue,
						},
					},
				},
			},
			want: true,
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			if got := isPodRunning(*test.pod); got != test.want {
				t.Errorf("expected pod running status %t, but got %t", test.want, got)
			}
		})
	}
}
