package common

import (
	"context"
	"testing"

	ec2v1alpha1 "github.com/crossplane-contrib/provider-aws/apis/ec2/v1alpha1"
	manualv1alpha1 "github.com/crossplane-contrib/provider-aws/apis/eks/manualv1alpha1"
	gkev1beta1 "github.com/crossplane-contrib/provider-gcp/apis/container/v1beta1"
	"github.com/stretchr/testify/require"
	azurev1beta1 "github.com/upbound/provider-azure/apis/containerservice/v1beta1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/namer"
	regionalv1alpha1 "github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/config"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/appbox/task"
)

func TestNodePoolGCName(t *testing.T) {
	require.Equal(t, "NodePoolGC", NewNodePoolGC(&task.Context{}).Name())
}

func TestNodePoolGCDelete(t *testing.T) {
	t.Run("EKS deletes matching nodegroup and launch template", func(t *testing.T) {
		matching := &manualv1alpha1.NodeGroup{
			ObjectMeta: metav1.ObjectMeta{Name: "matching", Labels: nodePoolGCLabels(cloudv1.ComponentBackup)},
			Spec:       manualv1alpha1.NodeGroupSpec{ForProvider: manualv1alpha1.NodeGroupParameters{ClusterName: "appbox-cluster"}},
		}
		launchTemplate := &ec2v1alpha1.LaunchTemplate{ObjectMeta: metav1.ObjectMeta{Name: matching.Name}}
		other := &manualv1alpha1.NodeGroup{
			ObjectMeta: metav1.ObjectMeta{Name: "other", Labels: nodePoolGCLabels(cloudv1.ComponentRestore)},
			Spec:       manualv1alpha1.NodeGroupSpec{ForProvider: manualv1alpha1.NodeGroupParameters{ClusterName: "other-cluster"}},
		}
		gc, local := newNodePoolGCTest(t, consts.ProviderTypeEKS, matching, launchTemplate, other)

		require.True(t, gc.Delete(context.Background()).IsDoneState())
		require.True(t, apierrors.IsNotFound(local.Get(context.Background(), client.ObjectKeyFromObject(matching), &manualv1alpha1.NodeGroup{})))
		require.True(t, apierrors.IsNotFound(local.Get(context.Background(), client.ObjectKeyFromObject(launchTemplate), &ec2v1alpha1.LaunchTemplate{})))
		require.NoError(t, local.Get(context.Background(), client.ObjectKeyFromObject(other), &manualv1alpha1.NodeGroup{}))
		require.True(t, gc.PostDelete(context.Background()).IsDoneState())
	})

	t.Run("GKE deletes only BR nodepools for the APPBox cluster", func(t *testing.T) {
		matching := &gkev1beta1.NodePool{
			ObjectMeta: metav1.ObjectMeta{Name: "matching", Labels: nodePoolGCLabels(cloudv1.ComponentCompactBackup)},
			Spec:       gkev1beta1.NodePoolSpec{ForProvider: gkev1beta1.NodePoolParameters{Cluster: "projects/p/locations/r/clusters/appbox-cluster"}},
		}
		otherComponent := &gkev1beta1.NodePool{
			ObjectMeta: metav1.ObjectMeta{Name: "other-component", Labels: nodePoolGCLabels(cloudv1.ComponentTiKV)},
			Spec:       gkev1beta1.NodePoolSpec{ForProvider: gkev1beta1.NodePoolParameters{Cluster: "projects/p/locations/r/clusters/appbox-cluster"}},
		}
		gc, local := newNodePoolGCTest(t, consts.ProviderTypeGKE, matching, otherComponent)

		require.True(t, gc.Delete(context.Background()).IsDoneState())
		require.True(t, apierrors.IsNotFound(local.Get(context.Background(), client.ObjectKeyFromObject(matching), &gkev1beta1.NodePool{})))
		require.NoError(t, local.Get(context.Background(), client.ObjectKeyFromObject(otherComponent), &gkev1beta1.NodePool{}))
	})

	t.Run("AKS deletes matching nodepool", func(t *testing.T) {
		matching := &azurev1beta1.KubernetesClusterNodePool{
			ObjectMeta: metav1.ObjectMeta{Name: "matching", Labels: nodePoolGCLabels(cloudv1.ComponentRestore)},
			Spec: azurev1beta1.KubernetesClusterNodePoolSpec{ForProvider: azurev1beta1.KubernetesClusterNodePoolParameters{
				KubernetesClusterID: ptr.To("/subscriptions/s/resourceGroups/r/providers/Microsoft.ContainerService/managedClusters/appbox-cluster"),
			}},
		}
		gc, local := newNodePoolGCTest(t, consts.ProviderTypeAKS, matching)

		require.True(t, gc.Delete(context.Background()).IsDoneState())
		require.True(t, apierrors.IsNotFound(local.Get(context.Background(), client.ObjectKeyFromObject(matching), &azurev1beta1.KubernetesClusterNodePool{})))
	})

	t.Run("waits until nodepool deletion completes", func(t *testing.T) {
		matching := &manualv1alpha1.NodeGroup{
			ObjectMeta: metav1.ObjectMeta{
				Name:       "terminating",
				Labels:     nodePoolGCLabels(cloudv1.ComponentBackup),
				Finalizers: []string{"test/finalizer"},
			},
			Spec: manualv1alpha1.NodeGroupSpec{ForProvider: manualv1alpha1.NodeGroupParameters{ClusterName: "appbox-cluster"}},
		}
		gc, _ := newNodePoolGCTest(t, consts.ProviderTypeEKS, matching)

		require.True(t, gc.Delete(context.Background()).IsDoneState())
		result := gc.PostDelete(context.Background())
		require.False(t, result.IsDoneState())
		require.ErrorContains(t, result.Error(), "waiting for 1 APPBox nodepool")
	})
}

func newNodePoolGCTest(t *testing.T, provider string, objects ...client.Object) (*NodePoolGC, client.Client) {
	t.Helper()
	scheme := runtime.NewScheme()
	require.NoError(t, manualv1alpha1.SchemeBuilder.AddToScheme(scheme))
	require.NoError(t, ec2v1alpha1.SchemeBuilder.AddToScheme(scheme))
	require.NoError(t, gkev1beta1.SchemeBuilder.AddToScheme(scheme))
	require.NoError(t, azurev1beta1.SchemeBuilder.AddToScheme(scheme))
	local := fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build()
	ctx := &task.Context{
		Local: local,
		APPBox: &regionalv1alpha1.APPBox{Status: regionalv1alpha1.APPBoxStatus{
			KubernetesClusterName: "appbox-cluster",
		}},
		Config: config.RootConfig{ControllerConfig: config.ControllerConfig{Provider: provider}},
	}
	return NewNodePoolGC(ctx), local
}

func nodePoolGCLabels(component cloudv1.ComponentType) map[string]string {
	return map[string]string{
		consts.TagKeyServiceType:   consts.ServiceTypeDedicated,
		consts.TagKeyUsedBy:        consts.UsedByCustomerTiDB,
		namer.ComponentSelectorKey: string(component),
	}
}
