package managedclusterset

import (
	"context"
	"reflect"
	"testing"
	"time"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/util/sets"

	clusterfake "open-cluster-management.io/api/client/cluster/clientset/versioned/fake"
	clusterinformers "open-cluster-management.io/api/client/cluster/informers/externalversions"
	clusterv1 "open-cluster-management.io/api/cluster/v1"
	clusterv1beta2 "open-cluster-management.io/api/cluster/v1beta2"
	"open-cluster-management.io/sdk-go/pkg/patcher"

	testingcommon "open-cluster-management.io/ocm/pkg/common/testing"
)

func TestSyncClusterSet(t *testing.T) {
	cases := []struct {
		name               string
		existingClusterSet *clusterv1beta2.ManagedClusterSet
		existingClusters   []*clusterv1.ManagedCluster
		expectCondition    metav1.Condition
		expectErr          bool
	}{
		{
			name: "sync a empty cluster set",
			existingClusterSet: &clusterv1beta2.ManagedClusterSet{
				ObjectMeta: metav1.ObjectMeta{
					Name: "mcs1",
				},
			},
			existingClusters: []*clusterv1.ManagedCluster{
				newManagedCluster("cluster1", map[string]string{
					clusterv1beta2.ClusterSetLabel: "mcs1",
				}),
			},
			expectCondition: metav1.Condition{
				Type:    clusterv1beta2.ManagedClusterSetConditionEmpty,
				Status:  metav1.ConditionFalse,
				Reason:  ReasonClusterSelected,
				Message: "1 ManagedClusters selected",
			},
		},
		{
			name: "sync a legacy clusterset",
			existingClusterSet: &clusterv1beta2.ManagedClusterSet{
				ObjectMeta: metav1.ObjectMeta{
					Name: "mcs1",
				},
				Spec: clusterv1beta2.ManagedClusterSetSpec{
					ClusterSelector: clusterv1beta2.ManagedClusterSelector{
						SelectorType: clusterv1beta2.ExclusiveClusterSetLabel,
					},
				},
			},
			existingClusters: []*clusterv1.ManagedCluster{
				newManagedCluster("cluster1", map[string]string{
					clusterv1beta2.ClusterSetLabel: "mcs1",
				}),
			},
			expectCondition: metav1.Condition{
				Type:    clusterv1beta2.ManagedClusterSetConditionEmpty,
				Status:  metav1.ConditionFalse,
				Reason:  ReasonClusterSelected,
				Message: "1 ManagedClusters selected",
			},
		},
		{
			name: "sync a legacy clusterset, and no cluster matched",
			existingClusterSet: &clusterv1beta2.ManagedClusterSet{
				ObjectMeta: metav1.ObjectMeta{
					Name: "mcs1",
				},
				Spec: clusterv1beta2.ManagedClusterSetSpec{
					ClusterSelector: clusterv1beta2.ManagedClusterSelector{
						SelectorType: clusterv1beta2.ExclusiveClusterSetLabel,
					},
				},
			},
			existingClusters: []*clusterv1.ManagedCluster{
				newManagedCluster("cluster1", map[string]string{
					"vendor": "openShift",
				}),
			},
			expectCondition: metav1.Condition{
				Type:    clusterv1beta2.ManagedClusterSetConditionEmpty,
				Status:  metav1.ConditionTrue,
				Reason:  ReasonNoClusterMatchced,
				Message: "No ManagedCluster selected",
			},
		},
		{
			name: "sync a labelselector clusterset",
			existingClusterSet: &clusterv1beta2.ManagedClusterSet{
				ObjectMeta: metav1.ObjectMeta{
					Name: "mcs1",
				},
				Spec: clusterv1beta2.ManagedClusterSetSpec{
					ClusterSelector: clusterv1beta2.ManagedClusterSelector{
						SelectorType: clusterv1beta2.LabelSelector,
						LabelSelector: &metav1.LabelSelector{
							MatchLabels: map[string]string{
								"vendor": "openShift",
							},
						},
					},
				},
			},
			existingClusters: []*clusterv1.ManagedCluster{
				newManagedCluster("cluster1", map[string]string{
					clusterv1beta2.ClusterSetLabel: "mcs1",
					"vendor":                       "openShift",
				}),
				newManagedCluster("cluster2", map[string]string{
					clusterv1beta2.ClusterSetLabel: "mcs2",
					"vendor":                       "openShift",
				}),
			},
			expectCondition: metav1.Condition{
				Type:    clusterv1beta2.ManagedClusterSetConditionEmpty,
				Status:  metav1.ConditionFalse,
				Reason:  ReasonClusterSelected,
				Message: "2 ManagedClusters selected",
			},
		},
		{
			name: "sync a global clusterset",
			existingClusterSet: &clusterv1beta2.ManagedClusterSet{
				ObjectMeta: metav1.ObjectMeta{
					Name: "mcs1",
				},
				Spec: clusterv1beta2.ManagedClusterSetSpec{
					ClusterSelector: clusterv1beta2.ManagedClusterSelector{
						SelectorType:  clusterv1beta2.LabelSelector,
						LabelSelector: &metav1.LabelSelector{},
					},
				},
			},
			existingClusters: []*clusterv1.ManagedCluster{
				newManagedCluster("cluster1", map[string]string{
					clusterv1beta2.ClusterSetLabel: "mcs1",
					"vendor":                       "openShift",
				}),
				newManagedCluster("cluster2", map[string]string{
					clusterv1beta2.ClusterSetLabel: "mcs2",
					"vendor":                       "openShift",
				}),
			},
			expectCondition: metav1.Condition{
				Type:    clusterv1beta2.ManagedClusterSetConditionEmpty,
				Status:  metav1.ConditionFalse,
				Reason:  ReasonClusterSelected,
				Message: "2 ManagedClusters selected",
			},
		},
		{
			name: "sync a label clusterset with no labelselector specified(no cluster matched)",
			existingClusterSet: &clusterv1beta2.ManagedClusterSet{
				ObjectMeta: metav1.ObjectMeta{
					Name: "mcs1",
				},
				Spec: clusterv1beta2.ManagedClusterSetSpec{
					ClusterSelector: clusterv1beta2.ManagedClusterSelector{
						SelectorType: clusterv1beta2.LabelSelector,
					},
				},
			},
			existingClusters: []*clusterv1.ManagedCluster{
				newManagedCluster("cluster1", map[string]string{
					clusterv1beta2.ClusterSetLabel: "mcs1",
					"vendor":                       "openShift",
				}),
				newManagedCluster("cluster2", map[string]string{
					clusterv1beta2.ClusterSetLabel: "mcs2",
					"vendor":                       "openShift",
				}),
			},
			expectCondition: metav1.Condition{
				Type:    clusterv1beta2.ManagedClusterSetConditionEmpty,
				Status:  metav1.ConditionTrue,
				Reason:  ReasonNoClusterMatchced,
				Message: "No ManagedCluster selected",
			},
		},
		{
			name: "ignore any other clusterset",
			existingClusterSet: &clusterv1beta2.ManagedClusterSet{
				ObjectMeta: metav1.ObjectMeta{
					Name: "mcs1",
				},
				Spec: clusterv1beta2.ManagedClusterSetSpec{
					ClusterSelector: clusterv1beta2.ManagedClusterSelector{
						SelectorType: "SingleClusterLabel",
					},
				},
			},
			expectErr: true,
		},
	}

	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			var objects []runtime.Object
			for _, cluster := range c.existingClusters {
				objects = append(objects, cluster)
			}
			if c.existingClusterSet != nil {
				objects = append(objects, c.existingClusterSet)
			}

			clusterClient := clusterfake.NewSimpleClientset(objects...)

			informerFactory := clusterinformers.NewSharedInformerFactory(clusterClient, 5*time.Minute)
			for _, cluster := range c.existingClusters {
				err := informerFactory.Cluster().V1().ManagedClusters().Informer().GetStore().Add(cluster)
				if err != nil {
					t.Errorf("Failed to add cluster: %v, error: %v", cluster.Name, err)
				}
			}
			if c.existingClusterSet != nil {
				err := informerFactory.Cluster().V1beta2().ManagedClusterSets().Informer().GetStore().Add(c.existingClusterSet)
				if err != nil {
					t.Errorf("Failed to add clusterset: %v, error: %v", c.existingClusterSet.Name, err)
				}
			}

			ctrl := managedClusterSetController{
				patcher: patcher.NewPatcher[
					*clusterv1beta2.ManagedClusterSet, clusterv1beta2.ManagedClusterSetSpec, clusterv1beta2.ManagedClusterSetStatus](
					clusterClient.ClusterV1beta2().ManagedClusterSets()),
				clusterLister:    informerFactory.Cluster().V1().ManagedClusters().Lister(),
				clusterSetLister: informerFactory.Cluster().V1beta2().ManagedClusterSets().Lister(),
			}

			syncErr := ctrl.syncClusterSet(context.Background(), c.existingClusterSet)
			if c.expectErr {
				if syncErr != nil {
					return
				}
				t.Errorf("Should syncClusterSet fail")
				return
			}
			if syncErr != nil {
				t.Errorf("unexpected err: %v", syncErr)
			}
			updatedSet, err := clusterClient.ClusterV1beta2().ManagedClusterSets().Get(context.Background(), c.existingClusterSet.Name, metav1.GetOptions{})
			if err != nil {
				t.Errorf("Failed to get clusterset: %v, error: %v", c.existingClusterSet.Name, err)
			}
			if !hasCondition(updatedSet.Status.Conditions, c.expectCondition) {
				t.Errorf("expected condition:%v. is not found: %v", c.expectCondition, updatedSet.Status.Conditions)
			}
		})
	}
}

func TestGetDiffClustersets(t *testing.T) {
	cases := []struct {
		name          string
		oldSets       []*clusterv1beta2.ManagedClusterSet
		newSets       []*clusterv1beta2.ManagedClusterSet
		expectDiffSet sets.Set[string]
	}{
		{
			name: "update a set",
			oldSets: []*clusterv1beta2.ManagedClusterSet{
				newManagedClusterSet("s1"), newManagedClusterSet("s2"),
			},
			newSets: []*clusterv1beta2.ManagedClusterSet{
				newManagedClusterSet("s1"), newManagedClusterSet("s3"),
			},
			expectDiffSet: sets.New[string]("s2", "s3"),
		},
		{
			name: "add a set",
			oldSets: []*clusterv1beta2.ManagedClusterSet{
				newManagedClusterSet("s1"),
			},
			newSets: []*clusterv1beta2.ManagedClusterSet{
				newManagedClusterSet("s1"), newManagedClusterSet("s2"),
			},
			expectDiffSet: sets.New[string]("s2"),
		},
		{
			name: "delete a set",
			oldSets: []*clusterv1beta2.ManagedClusterSet{
				newManagedClusterSet("s1"), newManagedClusterSet("s2"),
			},
			newSets: []*clusterv1beta2.ManagedClusterSet{
				newManagedClusterSet("s1"),
			},
			expectDiffSet: sets.New[string]("s2"),
		},
		{
			name:    "old set is nil",
			oldSets: []*clusterv1beta2.ManagedClusterSet{},
			newSets: []*clusterv1beta2.ManagedClusterSet{
				newManagedClusterSet("s1"),
			},
			expectDiffSet: sets.New[string]("s1"),
		},
		{
			name: "new set is nil",
			oldSets: []*clusterv1beta2.ManagedClusterSet{
				newManagedClusterSet("s1"),
			},
			newSets:       []*clusterv1beta2.ManagedClusterSet{},
			expectDiffSet: sets.New[string]("s1"),
		},
	}

	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			retSets := getDiffClusterSetsNames(c.oldSets, c.newSets)
			if !reflect.DeepEqual(retSets, c.expectDiffSet) {
				t.Errorf("Case: %v, Failed to getDiffClustersets. returnSets: %v, expectSets: %v", c.name, retSets, c.expectDiffSet)
			}
		})
	}
}

func TestEnqueueUpdateClusterClusterSet(t *testing.T) {
	cases := []struct {
		name                string
		existingClusterSets []*clusterv1beta2.ManagedClusterSet
		oldCluster          *clusterv1.ManagedCluster
		newCluster          *clusterv1.ManagedCluster
		expectQueueLen      int
	}{
		{
			name: "update a cluster's clusterset",
			existingClusterSets: []*clusterv1beta2.ManagedClusterSet{
				newManagedClusterSet("mcs1"),
				newManagedClusterSet("mcs2"),
			},
			oldCluster:     newClusterWithLabel("c1", map[string]string{clusterv1beta2.ClusterSetLabel: "mcs1"}),
			newCluster:     newClusterWithLabel("c1", map[string]string{clusterv1beta2.ClusterSetLabel: "mcs2"}),
			expectQueueLen: 2,
		},
		{
			name: "add a cluster's clusterset",
			existingClusterSets: []*clusterv1beta2.ManagedClusterSet{
				newManagedClusterSet("mcs1"),
				newManagedClusterSet("mcs2"),
			},
			oldCluster:     newClusterWithLabel("c1", nil),
			newCluster:     newClusterWithLabel("c1", map[string]string{clusterv1beta2.ClusterSetLabel: "mcs2"}),
			expectQueueLen: 1,
		},
		{
			name: "remove a cluster's clusterset",
			existingClusterSets: []*clusterv1beta2.ManagedClusterSet{
				newManagedClusterSet("mcs1"),
				newManagedClusterSet("mcs2"),
			},
			newCluster:     newClusterWithLabel("c1", nil),
			oldCluster:     newClusterWithLabel("c1", map[string]string{clusterv1beta2.ClusterSetLabel: "mcs2"}),
			expectQueueLen: 1,
		},
	}

	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			var objects []runtime.Object

			for _, clusterset := range c.existingClusterSets {
				objects = append(objects, clusterset)
			}
			clusterClient := clusterfake.NewSimpleClientset(objects...)

			informerFactory := clusterinformers.NewSharedInformerFactory(clusterClient, 5*time.Minute)
			for _, clusterset := range c.existingClusterSets {
				err := informerFactory.Cluster().V1beta2().ManagedClusterSets().Informer().GetStore().Add(clusterset)
				if err != nil {
					t.Errorf("Failed to add clusterset: %v, error: %v", clusterset, err)
				}
			}
			syncCtx := testingcommon.NewFakeSyncContext(t, "fake")

			ctrl := managedClusterSetController{
				clusterSetLister: informerFactory.Cluster().V1beta2().ManagedClusterSets().Lister(),
				queue:            syncCtx.Queue(),
			}

			ctrl.enqueueUpdateClusterClusterSet(c.oldCluster, c.newCluster)
			if syncCtx.Queue().Len() != c.expectQueueLen {
				t.Errorf("Expect len:%v, return len:%v", c.expectQueueLen, syncCtx.Queue().Len())
			}
		})
	}
}

func newManagedCluster(name string, labels map[string]string) *clusterv1.ManagedCluster {
	cluster := &clusterv1.ManagedCluster{
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
		},
	}
	cluster.Labels = labels
	return cluster
}

func newManagedClusterSet(name string) *clusterv1beta2.ManagedClusterSet {
	clusterSet := &clusterv1beta2.ManagedClusterSet{
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
		},
	}
	return clusterSet
}

func hasCondition(conditions []metav1.Condition, expectCondition metav1.Condition) bool {
	for _, condition := range conditions {
		if condition.Type != expectCondition.Type {
			continue
		}
		if condition.Status != expectCondition.Status {
			continue
		}
		if condition.Reason != expectCondition.Reason {
			continue
		}
		if condition.Message != expectCondition.Message {
			continue
		}
		return true
	}
	return false
}

func newClusterWithLabel(name string, labels map[string]string) *clusterv1.ManagedCluster {
	cluster := &clusterv1.ManagedCluster{
		ObjectMeta: metav1.ObjectMeta{
			Name:   name,
			Labels: labels,
		},
	}
	return cluster
}
