/*
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 federatedresourcequota

import (
	"context"
	"testing"

	"github.com/stretchr/testify/assert"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/client-go/tools/record"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"

	clusterv1alpha1 "github.com/karmada-io/karmada/pkg/apis/cluster/v1alpha1"
	policyv1alpha1 "github.com/karmada-io/karmada/pkg/apis/policy/v1alpha1"
	workv1alpha1 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha1"
	"github.com/karmada-io/karmada/pkg/util"
)

// setupTest initializes a test environment with the given runtime objects
// It returns a fake client and a SyncController for use in tests
func setupTest(t *testing.T, objs ...runtime.Object) (client.Client, *SyncController) {
	scheme := runtime.NewScheme()
	assert.NoError(t, policyv1alpha1.Install(scheme))
	assert.NoError(t, workv1alpha1.Install(scheme))
	assert.NoError(t, clusterv1alpha1.Install(scheme))
	assert.NoError(t, corev1.AddToScheme(scheme))

	fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objs...).Build()
	controller := &SyncController{
		Client:        fakeClient,
		EventRecorder: record.NewFakeRecorder(100),
	}
	return fakeClient, controller
}

// TestCleanUpWorks tests the cleanUpWorks function of the SyncController
func TestCleanUpWorks(t *testing.T) {
	tests := []struct {
		name          string
		existingWorks []runtime.Object
		namespace     string
		quotaName     string
		expectedError bool
	}{
		{
			name: "Successfully delete works",
			existingWorks: []runtime.Object{
				&workv1alpha1.Work{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "work-1",
						Namespace: "default",
						Labels: map[string]string{
							util.FederatedResourceQuotaNamespaceLabel: "default",
							util.FederatedResourceQuotaNameLabel:      "test-quota",
						},
					},
				},
				&workv1alpha1.Work{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "work-2",
						Namespace: "default",
						Labels: map[string]string{
							util.FederatedResourceQuotaNamespaceLabel: "default",
							util.FederatedResourceQuotaNameLabel:      "test-quota",
						},
					},
				},
			},
			namespace:     "default",
			quotaName:     "test-quota",
			expectedError: false,
		},
		{
			name:          "No works to delete",
			existingWorks: []runtime.Object{},
			namespace:     "default",
			quotaName:     "test-quota",
			expectedError: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			fakeClient, controller := setupTest(t, tt.existingWorks...)

			err := controller.cleanUpWorks(context.Background(), tt.namespace, tt.quotaName)

			if tt.expectedError {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
			}

			// Verify that works are deleted
			workList := &workv1alpha1.WorkList{}
			err = fakeClient.List(context.Background(), workList, client.MatchingLabels{
				util.FederatedResourceQuotaNamespaceLabel: tt.namespace,
				util.FederatedResourceQuotaNameLabel:      tt.quotaName,
			})
			assert.NoError(t, err)
			assert.Empty(t, workList.Items)
		})
	}
}

// It verifies that works are correctly created for the given FederatedResourceQuota and clusters
func TestBuildWorks(t *testing.T) {
	tests := []struct {
		name          string
		quota         *policyv1alpha1.FederatedResourceQuota
		clusters      []clusterv1alpha1.Cluster
		expectedError bool
		expectedWorks int
	}{
		{
			name: "Successfully build works for all clusters",
			quota: &policyv1alpha1.FederatedResourceQuota{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-quota",
					Namespace: "default",
				},
				Spec: policyv1alpha1.FederatedResourceQuotaSpec{
					StaticAssignments: []policyv1alpha1.StaticClusterAssignment{
						{
							ClusterName: "cluster1",
							Hard: corev1.ResourceList{
								corev1.ResourceCPU: resource.MustParse("1"),
							},
						},
						{
							ClusterName: "cluster2",
							Hard: corev1.ResourceList{
								corev1.ResourceCPU: resource.MustParse("2"),
							},
						},
					},
				},
			},
			clusters: []clusterv1alpha1.Cluster{
				{ObjectMeta: metav1.ObjectMeta{Name: "cluster1"}},
				{ObjectMeta: metav1.ObjectMeta{Name: "cluster2"}},
			},
			expectedError: false,
			expectedWorks: 2,
		},
		{
			name: "No clusters available",
			quota: &policyv1alpha1.FederatedResourceQuota{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-quota",
					Namespace: "default",
				},
				Spec: policyv1alpha1.FederatedResourceQuotaSpec{
					StaticAssignments: []policyv1alpha1.StaticClusterAssignment{
						{
							ClusterName: "cluster1",
							Hard: corev1.ResourceList{
								corev1.ResourceCPU: resource.MustParse("1"),
							},
						},
					},
				},
			},
			clusters:      []clusterv1alpha1.Cluster{},
			expectedError: false,
			expectedWorks: 0,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			fakeClient, controller := setupTest(t)

			err := controller.buildWorks(context.Background(), tt.quota, tt.clusters)

			if tt.expectedError {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
			}

			// Verify the number of created works
			workList := &workv1alpha1.WorkList{}
			err = fakeClient.List(context.Background(), workList, client.MatchingLabels{
				util.FederatedResourceQuotaNamespaceLabel: tt.quota.Namespace,
				util.FederatedResourceQuotaNameLabel:      tt.quota.Name,
			})
			assert.NoError(t, err)
			assert.Len(t, workList.Items, tt.expectedWorks)
		})
	}
}

// TestExtractClusterHardResourceList tests the extractClusterHardResourceList function
func TestExtractClusterHardResourceList(t *testing.T) {
	tests := []struct {
		name           string
		spec           policyv1alpha1.FederatedResourceQuotaSpec
		clusterName    string
		expectedResult corev1.ResourceList
	}{
		{
			name: "Cluster found in static assignments",
			spec: policyv1alpha1.FederatedResourceQuotaSpec{
				StaticAssignments: []policyv1alpha1.StaticClusterAssignment{
					{
						ClusterName: "cluster1",
						Hard: corev1.ResourceList{
							corev1.ResourceCPU:    resource.MustParse("1"),
							corev1.ResourceMemory: resource.MustParse("1Gi"),
						},
					},
				},
			},
			clusterName: "cluster1",
			expectedResult: corev1.ResourceList{
				corev1.ResourceCPU:    resource.MustParse("1"),
				corev1.ResourceMemory: resource.MustParse("1Gi"),
			},
		},
		{
			name: "Cluster not found in static assignments",
			spec: policyv1alpha1.FederatedResourceQuotaSpec{
				StaticAssignments: []policyv1alpha1.StaticClusterAssignment{
					{
						ClusterName: "cluster1",
						Hard: corev1.ResourceList{
							corev1.ResourceCPU: resource.MustParse("1"),
						},
					},
				},
			},
			clusterName:    "cluster2",
			expectedResult: nil,
		},
		{
			name: "Empty static assignments",
			spec: policyv1alpha1.FederatedResourceQuotaSpec{
				StaticAssignments: []policyv1alpha1.StaticClusterAssignment{},
			},
			clusterName:    "cluster1",
			expectedResult: nil,
		},
		{
			name: "Multiple static assignments",
			spec: policyv1alpha1.FederatedResourceQuotaSpec{
				StaticAssignments: []policyv1alpha1.StaticClusterAssignment{
					{
						ClusterName: "cluster1",
						Hard: corev1.ResourceList{
							corev1.ResourceCPU: resource.MustParse("1"),
						},
					},
					{
						ClusterName: "cluster2",
						Hard: corev1.ResourceList{
							corev1.ResourceCPU: resource.MustParse("2"),
						},
					},
				},
			},
			clusterName: "cluster2",
			expectedResult: corev1.ResourceList{
				corev1.ResourceCPU: resource.MustParse("2"),
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := extractClusterHardResourceList(tt.spec, tt.clusterName)
			assert.Equal(t, tt.expectedResult, result)
		})
	}
}
