package controller

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

	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	"github.com/argoproj/argo-cd/v3/common"

	"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
	appsfake "github.com/argoproj/argo-cd/v3/pkg/client/clientset/versioned/fake"
	appinformers "github.com/argoproj/argo-cd/v3/pkg/client/informers/externalversions/application/v1alpha1"
	applisters "github.com/argoproj/argo-cd/v3/pkg/client/listers/application/v1alpha1"
	cacheutil "github.com/argoproj/argo-cd/v3/util/cache"
	"github.com/argoproj/argo-cd/v3/util/cache/appstate"
	"github.com/argoproj/argo-cd/v3/util/db"
	"github.com/argoproj/argo-cd/v3/util/settings"

	clustercache "github.com/argoproj/argo-cd/gitops-engine/pkg/cache"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"k8s.io/client-go/kubernetes/fake"
	"k8s.io/client-go/tools/cache"
)

// Expect cluster cache update is persisted in cluster secret
func TestClusterSecretUpdater(t *testing.T) {
	const fakeNamespace = "fake-ns"
	const updatedK8sVersion = "1.0"
	now := time.Now()

	tests := []struct {
		LastCacheSyncTime *time.Time
		SyncError         error
		ExpectedStatus    v1alpha1.ConnectionStatus
	}{
		{nil, nil, v1alpha1.ConnectionStatusUnknown},
		{&now, nil, v1alpha1.ConnectionStatusSuccessful},
		{&now, errors.New("sync failed"), v1alpha1.ConnectionStatusFailed},
	}

	emptyArgoCDConfigMap := &corev1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Name:      common.ArgoCDConfigMapName,
			Namespace: fakeNamespace,
			Labels: map[string]string{
				"app.kubernetes.io/part-of": "argocd",
			},
		},
		Data: map[string]string{},
	}
	argoCDSecret := &corev1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Name:      common.ArgoCDSecretName,
			Namespace: fakeNamespace,
			Labels: map[string]string{
				"app.kubernetes.io/part-of": "argocd",
			},
		},
		Data: map[string][]byte{
			"admin.password":   nil,
			"server.secretkey": nil,
		},
	}
	kubeclientset := fake.NewClientset(emptyArgoCDConfigMap, argoCDSecret)
	appclientset := appsfake.NewSimpleClientset()
	appInformer := appinformers.NewApplicationInformer(appclientset, "", time.Minute, cache.Indexers{})
	settingsManager := settings.NewSettingsManager(t.Context(), kubeclientset, fakeNamespace)
	argoDB := db.NewDB(fakeNamespace, settingsManager, kubeclientset)
	ctx, cancel := context.WithCancel(t.Context())
	defer cancel()

	appCache := appstate.NewCache(cacheutil.NewCache(cacheutil.NewInMemoryCache(time.Minute)), time.Minute)
	cluster, err := argoDB.CreateCluster(ctx, &v1alpha1.Cluster{Server: "http://minikube"})
	require.NoError(t, err, "Test prepare test data create cluster failed")

	for _, test := range tests {
		info := &clustercache.ClusterInfo{
			Server:            cluster.Server,
			K8SVersion:        updatedK8sVersion,
			LastCacheSyncTime: test.LastCacheSyncTime,
			SyncError:         test.SyncError,
		}

		lister := applisters.NewApplicationLister(appInformer.GetIndexer()).Applications(fakeNamespace)
		updater := NewClusterInfoUpdater(nil, argoDB, lister, appCache, nil, nil, fakeNamespace)

		err = updater.updateClusterInfo(t.Context(), *cluster, info)
		require.NoError(t, err, "Invoking updateClusterInfo failed.")

		var clusterInfo v1alpha1.ClusterInfo
		err = appCache.GetClusterInfo(cluster.Server, &clusterInfo)
		require.NoError(t, err)
		assert.Equal(t, updatedK8sVersion, clusterInfo.ServerVersion)
		assert.Equal(t, test.ExpectedStatus, clusterInfo.ConnectionState.Status)
	}
}

func TestGetUpdatedClusterInfo_AppCount(t *testing.T) {
	const fakeNamespace = "fake-ns"
	const clusterServer = "https://prod.example.com"
	const clusterName = "prod"

	emptyArgoCDConfigMap := &corev1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Name:      common.ArgoCDConfigMapName,
			Namespace: fakeNamespace,
			Labels:    map[string]string{"app.kubernetes.io/part-of": "argocd"},
		},
		Data: map[string]string{},
	}
	argoCDSecret := &corev1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Name:      common.ArgoCDSecretName,
			Namespace: fakeNamespace,
			Labels:    map[string]string{"app.kubernetes.io/part-of": "argocd"},
		},
		Data: map[string][]byte{"admin.password": nil, "server.secretkey": nil},
	}
	clusterSecret := &corev1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "prod-cluster",
			Namespace: fakeNamespace,
			Labels:    map[string]string{common.LabelKeySecretType: common.LabelValueSecretTypeCluster},
			Annotations: map[string]string{
				common.AnnotationKeyManagedBy: common.AnnotationValueManagedByArgoCD,
			},
		},
		Data: map[string][]byte{
			"name":   []byte(clusterName),
			"server": []byte(clusterServer),
			"config": []byte("{}"),
		},
	}

	kubeclientset := fake.NewClientset(emptyArgoCDConfigMap, argoCDSecret, clusterSecret)
	settingsManager := settings.NewSettingsManager(t.Context(), kubeclientset, fakeNamespace)
	argoDB := db.NewDB(fakeNamespace, settingsManager, kubeclientset)

	apps := []*v1alpha1.Application{
		{Spec: v1alpha1.ApplicationSpec{Destination: v1alpha1.ApplicationDestination{Name: clusterName}}},
		{Spec: v1alpha1.ApplicationSpec{Destination: v1alpha1.ApplicationDestination{Server: clusterServer}}},
		{Spec: v1alpha1.ApplicationSpec{Destination: v1alpha1.ApplicationDestination{Server: "https://other.example.com"}}},
	}

	updater := &clusterInfoUpdater{db: argoDB, namespace: fakeNamespace}
	cluster := v1alpha1.Cluster{Server: clusterServer}

	info := updater.getUpdatedClusterInfo(t.Context(), apps, cluster, nil, metav1.Now())

	assert.Equal(t, int64(2), info.ApplicationsCount)
}

func TestGetUpdatedClusterInfo_AmbiguousName(t *testing.T) {
	const fakeNamespace = "fake-ns"
	const clusterServer = "https://prod.example.com"
	const clusterName = "prod"

	emptyArgoCDConfigMap := &corev1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Name:      common.ArgoCDConfigMapName,
			Namespace: fakeNamespace,
			Labels:    map[string]string{"app.kubernetes.io/part-of": "argocd"},
		},
		Data: map[string]string{},
	}
	argoCDSecret := &corev1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Name:      common.ArgoCDSecretName,
			Namespace: fakeNamespace,
			Labels:    map[string]string{"app.kubernetes.io/part-of": "argocd"},
		},
		Data: map[string][]byte{"admin.password": nil, "server.secretkey": nil},
	}
	makeClusterSecret := func(secretName, server string) *corev1.Secret {
		return &corev1.Secret{
			ObjectMeta: metav1.ObjectMeta{
				Name:      secretName,
				Namespace: fakeNamespace,
				Labels:    map[string]string{common.LabelKeySecretType: common.LabelValueSecretTypeCluster},
				Annotations: map[string]string{
					common.AnnotationKeyManagedBy: common.AnnotationValueManagedByArgoCD,
				},
			},
			Data: map[string][]byte{
				"name":   []byte(clusterName),
				"server": []byte(server),
				"config": []byte("{}"),
			},
		}
	}

	// Two secrets share the same cluster name
	kubeclientset := fake.NewClientset(
		emptyArgoCDConfigMap, argoCDSecret,
		makeClusterSecret("prod-cluster-1", clusterServer),
		makeClusterSecret("prod-cluster-2", "https://prod2.example.com"),
	)
	settingsManager := settings.NewSettingsManager(t.Context(), kubeclientset, fakeNamespace)
	argoDB := db.NewDB(fakeNamespace, settingsManager, kubeclientset)

	apps := []*v1alpha1.Application{
		{Spec: v1alpha1.ApplicationSpec{Destination: v1alpha1.ApplicationDestination{Name: clusterName}}},
	}

	updater := &clusterInfoUpdater{db: argoDB, namespace: fakeNamespace}
	cluster := v1alpha1.Cluster{Server: clusterServer}

	info := updater.getUpdatedClusterInfo(t.Context(), apps, cluster, nil, metav1.Now())

	assert.Equal(t, int64(0), info.ApplicationsCount, "ambiguous name should not count app")
}

func TestUpdateClusterLabels(t *testing.T) {
	shouldNotBeInvoked := func(_ context.Context, _ *v1alpha1.Cluster) (*v1alpha1.Cluster, error) {
		shouldNotHappen := errors.New("if an error happens here, something's wrong")
		require.NoError(t, shouldNotHappen)
		return nil, shouldNotHappen
	}
	tests := []struct {
		name          string
		clusterInfo   *clustercache.ClusterInfo
		cluster       v1alpha1.Cluster
		updateCluster func(context.Context, *v1alpha1.Cluster) (*v1alpha1.Cluster, error)
		wantErr       assert.ErrorAssertionFunc
	}{
		{
			"enableClusterInfoLabels = false",
			&clustercache.ClusterInfo{
				Server:     "kubernetes.svc.local",
				K8SVersion: "1.28",
			},
			v1alpha1.Cluster{
				Server: "kubernetes.svc.local",
				Labels: nil,
			},
			shouldNotBeInvoked,
			assert.NoError,
		},
		{
			"clusterInfo = nil",
			nil,
			v1alpha1.Cluster{
				Server: "kubernetes.svc.local",
				Labels: map[string]string{"argocd.argoproj.io/auto-label-cluster-info": "true"},
			},
			shouldNotBeInvoked,
			assert.NoError,
		},
		{
			"clusterInfo.k8sversion == cluster k8s label",
			&clustercache.ClusterInfo{
				Server:     "kubernetes.svc.local",
				K8SVersion: "1.28",
			},
			v1alpha1.Cluster{
				Server: "kubernetes.svc.local",
				Labels: map[string]string{"argocd.argoproj.io/kubernetes-version": "1.28", "argocd.argoproj.io/auto-label-cluster-info": "true"},
			},
			shouldNotBeInvoked,
			assert.NoError,
		},
		{
			"clusterInfo.k8sversion != cluster k8s label, no error",
			&clustercache.ClusterInfo{
				Server:     "kubernetes.svc.local",
				K8SVersion: "1.28",
			},
			v1alpha1.Cluster{
				Server: "kubernetes.svc.local",
				Labels: map[string]string{"argocd.argoproj.io/kubernetes-version": "1.27", "argocd.argoproj.io/auto-label-cluster-info": "true"},
			},
			func(_ context.Context, cluster *v1alpha1.Cluster) (*v1alpha1.Cluster, error) {
				assert.Equal(t, "1.28", cluster.Labels["argocd.argoproj.io/kubernetes-version"])
				return nil, nil
			},
			assert.NoError,
		},
		{
			"clusterInfo.k8sversion != cluster k8s label, some error",
			&clustercache.ClusterInfo{
				Server:     "kubernetes.svc.local",
				K8SVersion: "1.28",
			},
			v1alpha1.Cluster{
				Server: "kubernetes.svc.local",
				Labels: map[string]string{"argocd.argoproj.io/kubernetes-version": "1.27", "argocd.argoproj.io/auto-label-cluster-info": "true"},
			},
			func(_ context.Context, cluster *v1alpha1.Cluster) (*v1alpha1.Cluster, error) {
				assert.Equal(t, "1.28", cluster.Labels["argocd.argoproj.io/kubernetes-version"])
				return nil, errors.New("some error happened while saving")
			},
			assert.Error,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tt.wantErr(t, updateClusterLabels(t.Context(), tt.clusterInfo, tt.cluster, tt.updateCluster), fmt.Sprintf("updateClusterLabels(%v, %v, %v)", t.Context(), tt.clusterInfo, tt.cluster))
		})
	}
}
