package cluster

import (
	"context"
	"testing"
	"time"

	"github.com/argoproj-labs/argocd-agent/test/fake/kube"
	"github.com/argoproj/argo-cd/v3/common"
	"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
	"github.com/google/uuid"
	"github.com/sirupsen/logrus"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	v1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/util/wait"
)

const secretName = "cluster-123"

func newClusterSecret(t *testing.T, agentName string) (*v1alpha1.Cluster, *v1.Secret) {
	t.Helper()
	labels := map[string]string{
		common.LabelKeySecretType: common.LabelValueSecretTypeCluster,
	}
	if agentName != "" {
		labels[LabelKeyClusterAgentMapping] = agentName
	}
	c := &v1alpha1.Cluster{
		Labels: labels,
		Name:   "my-cluster",
		Server: "127.0.0.1:8080",
	}
	s := &v1.Secret{}
	err := ClusterToSecret(c, s)
	require.NoError(t, err)
	s.Namespace = "argocd"
	s.Name = secretName
	return c, s
}

func Test_StartStop(t *testing.T) {

	// Add the required Redis secret for cluster cache functionality
	redisSecret := &v1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "argocd-redis",
			Namespace: "argocd",
		},
		Data: map[string][]byte{
			"auth": []byte(uuid.NewString()),
		},
	}
	clt := kube.NewFakeClientsetWithResources(redisSecret)
	m, err := NewManager(context.TODO(), "argocd", "", "", "", clt)
	require.NoError(t, err)
	require.NotNil(t, m)
	err = m.Start()
	require.NoError(t, err)
	err = m.Stop()
	require.NoError(t, err)
}

func Test_onClusterAdd(t *testing.T) {
	// Add the required Redis secret for cluster cache functionality
	redisSecret := &v1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "argocd-redis",
			Namespace: "argocd",
		},
		Data: map[string][]byte{
			"auth": []byte(uuid.NewString()),
		},
	}
	clt := kube.NewFakeClientsetWithResources(redisSecret)
	m, err := NewManager(context.TODO(), "argocd", "", "", "", clt)
	require.NoError(t, err)
	require.NotNil(t, m)
	err = m.Start()
	require.NoError(t, err)

	t.Run("Cluster is mapped successfully when secret is added", func(t *testing.T) {
		c, s := newClusterSecret(t, "agent-1")
		_, err := clt.CoreV1().Secrets("argocd").Create(context.TODO(), s, metav1.CreateOptions{})
		require.NoError(t, err)
		var nc *v1alpha1.Cluster
		err = wait.PollUntilContextTimeout(context.Background(), 1*time.Second, 10*time.Second, true, func(ctx context.Context) (done bool, err error) {
			nc = m.Mapping("agent-1")
			return nc != nil, nil
		})
		assert.NoError(t, err)
		assert.NotNil(t, nc)
		assert.Equal(t, c.Name, nc.Name)
		assert.Equal(t, c.Server, nc.Server)
	})

	t.Run("Cluster is mapped when existing secret is labeled", func(t *testing.T) {
		require.False(t, m.HasMapping("agent-2"))
		c, s := newClusterSecret(t, "")
		s.Name = "test-234"
		_, err := clt.CoreV1().Secrets("argocd").Create(context.TODO(), s, metav1.CreateOptions{})
		require.NoError(t, err)
		// Let the informer catch and ignore the event
		time.Sleep(500 * time.Millisecond)
		s.Labels[LabelKeyClusterAgentMapping] = "agent-2"
		_, err = clt.CoreV1().Secrets("argocd").Update(context.TODO(), s, metav1.UpdateOptions{})
		require.NoError(t, err)
		err = wait.PollUntilContextTimeout(context.Background(), 1*time.Second, 10*time.Second, true, func(ctx context.Context) (done bool, err error) {
			if m.HasMapping("agent-2") {
				return m.Mapping("agent-2").Server == c.Server, nil
			}
			return false, nil
		})
		assert.NoError(t, err)
	})

	t.Run("Cluster is remapped when agent label changes", func(t *testing.T) {
		s, err := clt.CoreV1().Secrets("argocd").Get(context.TODO(), "test-234", metav1.GetOptions{})
		require.NoError(t, err)
		require.NotEmpty(t, s.Labels)
		require.Equal(t, "agent-2", s.Labels[LabelKeyClusterAgentMapping])
		require.True(t, m.HasMapping("agent-2"))
		s.Labels[LabelKeyClusterAgentMapping] = "agent-3"
		_, err = clt.CoreV1().Secrets("argocd").Update(context.TODO(), s, metav1.UpdateOptions{})
		require.NoError(t, err)
		err = wait.PollUntilContextTimeout(context.Background(), 1*time.Second, 10*time.Second, true, func(ctx context.Context) (done bool, err error) {
			return !m.HasMapping("agent-2") && m.HasMapping("agent-3"), nil
		})
		assert.NoError(t, err)
	})

	t.Run("Cluster mapping is updated when secret is updated", func(t *testing.T) {
		s, err := clt.CoreV1().Secrets("argocd").Get(context.TODO(), secretName, metav1.GetOptions{})
		require.NoError(t, err)
		s.Data["server"] = []byte("127.0.0.1:8081")
		_, err = clt.CoreV1().Secrets("argocd").Update(context.TODO(), s, metav1.UpdateOptions{})
		assert.NoError(t, err)
		err = wait.PollUntilContextTimeout(context.Background(), 1*time.Second, 10*time.Second, true, func(ctx context.Context) (done bool, err error) {
			c := m.Mapping("agent-1")
			if c == nil {
				return false, nil
			}
			return c.Server == "127.0.0.1:8081", nil
		})
		assert.NoError(t, err)
	})

	t.Run("Cluster mapping is deleted when label is removed", func(t *testing.T) {
		s, err := clt.CoreV1().Secrets("argocd").Get(context.TODO(), secretName, metav1.GetOptions{})
		require.NoError(t, err)
		delete(s.Labels, LabelKeyClusterAgentMapping)
		_, err = clt.CoreV1().Secrets("argocd").Update(context.TODO(), s, metav1.UpdateOptions{})
		assert.NoError(t, err)
		// Cluster should be unmapped
		err = wait.PollUntilContextTimeout(context.Background(), 1*time.Second, 10*time.Second, true, func(ctx context.Context) (done bool, err error) {
			return !m.HasMapping("agent-1"), nil
		})
		assert.NoError(t, err)
	})

	t.Run("Cluster mapping is deleted when secret is deleted", func(t *testing.T) {
		_, err := clt.CoreV1().Secrets("argocd").Get(context.TODO(), "test-234", metav1.GetOptions{})
		require.NoError(t, err)
		require.True(t, m.HasMapping("agent-3"))
		err = clt.CoreV1().Secrets("argocd").Delete(context.TODO(), "test-234", metav1.DeleteOptions{})
		require.NoError(t, err)
		err = wait.PollUntilContextTimeout(context.Background(), 1*time.Second, 10*time.Second, true, func(ctx context.Context) (done bool, err error) {
			return !m.HasMapping("agent-3"), nil
		})
		assert.NoError(t, err)
	})

}

func init() {
	logrus.SetLevel(logrus.TraceLevel)
}
