package commongcp

import (
	"context"
	"fmt"
	"strconv"
	"testing"
	"time"

	gkecontainerv1beta1 "github.com/crossplane-contrib/provider-gcp/apis/container/v1beta1"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/crossplane/crossplane-runtime/pkg/meta"
	"github.com/stretchr/testify/assert"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"

	"github.com/tidbcloud/observability/agent-control-server/pkg/config"
	"github.com/tidbcloud/observability/agent-control-server/pkg/consts"
	"github.com/tidbcloud/observability/agent-control-server/pkg/deploy"
	"github.com/tidbcloud/observability/agent-control-server/pkg/reconciler"
	"github.com/tidbcloud/observability/agent-control-server/pkg/testkits"
	"github.com/tidbcloud/observability/agent-control-server/pkg/util"
)

const testClusterName = "cluster-ref"

func assertResultRequeueAfter(t *testing.T, res deploy.Result, d time.Duration) {
	t.Helper()

	requeueResult, errs, retriableErrs := deploy.MinRequeue([]deploy.Result{res})
	assert.NoError(t, errs)
	assert.Error(t, retriableErrs)
	assert.True(t, requeueResult.Requeue)
	assert.Equal(t, d, requeueResult.RequeueAfter)
}

func makeNodePool() *gkecontainerv1beta1.NodePool {
	return &gkecontainerv1beta1.NodePool{
		ObjectMeta: metav1.ObjectMeta{Name: "tidbcloud-dedicated-admin"},
		Spec: gkecontainerv1beta1.NodePoolSpec{
			ForProvider: gkecontainerv1beta1.NodePoolParameters{
				Cluster:    "test-cluster",
				ClusterRef: &xpv1.Reference{Name: "cluster-ref"},
				Config: &gkecontainerv1beta1.NodeConfig{
					ServiceAccount: ptr.To("sa@email.com"),
					ImageType:      ptr.To("COS_CONTAINERD"),
				},
				Version:   ptr.To("1.27.3-gke.100"),
				Locations: []string{"us-central1-a"},
			},
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: "gcp-provider"},
			},
		},
	}
}

func nodeNameFunc(s string) string {
	return "np-" + s
}

func TestNodePoolTask_Create(t *testing.T) {
	nodeMatchLabels := map[string]string{"app": "agent"}
	nodeConfigLabels := map[string]string{"role": "agent"}

	tests := []struct {
		name           string
		setupResources func(client.Client)
		mutateAgent    func(client.Object)
		clientWrapper  func(client.Client) client.Client
		verify         func(t *testing.T, c client.Client, obj client.Object)
		wantErr        string
		wantResult     func() deploy.Result
		wantRequeue    time.Duration
	}{
		{
			name: "desired spec not found",
			mutateAgent: func(obj client.Object) {
				util.SetAnnotation(obj, consts.AnnotationKeyO11yDesiredSpec, "non-existent")
			},
			wantErr: "desired spec not found: non-existent",
		},
		{
			name:    "admin nodepool not found",
			wantErr: "admin nodepool not found",
		},
		{
			name: "success create default",
			setupResources: func(c client.Client) {
				assert.NoError(t, c.Create(context.Background(), makeNodePool()))
			},
			verify: func(t *testing.T, c client.Client, obj client.Object) {
				var np gkecontainerv1beta1.NodePool
				err := c.Get(context.Background(), client.ObjectKey{Name: "np-default"}, &np)
				assert.NoError(t, err)
				assert.Equal(t, "e2-medium", *np.Spec.ForProvider.Config.MachineType)
				assert.Equal(t, "agent", np.Labels["app"])
				assert.Equal(t, "np-default", np.Spec.ForProvider.Config.Labels["Name"])
				assert.Equal(t, "np-default", np.Spec.ForProvider.Config.Labels["tidbcloud.com/nodegroup-name"])
				assert.Equal(t, "agent", np.Spec.ForProvider.Config.Labels["role"])

				assert.Equal(t, "test-cluster", util.GetAnnotation(obj, consts.AnnotationKeyO11yNodeGKECluster))
				assert.Equal(t, "cluster-ref", util.GetAnnotation(obj, consts.AnnotationKeyO11yNodeGKEClusterRef))
				assert.Equal(t, "sa@email.com", util.GetAnnotation(obj, consts.AnnotationKeyO11yNodeSA))
				assert.Equal(t, "gcp-provider", util.GetAnnotation(obj, consts.AnnotationKeyO11yNodeProviderConfigReference))
				assert.Equal(t, "COS_CONTAINERD", util.GetAnnotation(obj, consts.AnnotationKeyO11yNodeImageType))
				assert.Equal(t, "1.27.3-gke.100", util.GetAnnotation(obj, consts.AnnotationKeyO11yNodeVersion))
				assert.Equal(t, "us-central1-a", util.GetAnnotation(obj, consts.AnnotationKeyO11yNodeZone))

				assert.Equal(t, strconv.Itoa(defaultMinSize), util.GetAnnotation(obj, consts.AnnotationKeyO11yNodeMinSize))
				assert.Equal(t, strconv.Itoa(defaultMaxSize), util.GetAnnotation(obj, consts.AnnotationKeyO11yNodeMaxSize))
				assert.Equal(t, strconv.Itoa(defaultDesiredSize), util.GetAnnotation(obj, consts.AnnotationKeyO11yNodeDesiredSize))
				assert.Equal(t, strconv.Itoa(defaultDiskSize), util.GetAnnotation(obj, consts.AnnotationKeyO11yNodeDiskSize))
			},
			wantResult: func() deploy.Result { return deploy.Done() },
		},
		{
			name: "invalid nodeMinSize annotation",
			mutateAgent: func(obj client.Object) {
				util.SetAnnotation(obj, consts.AnnotationKeyO11yNodeMinSize, "invalid")
			},
			setupResources: func(c client.Client) {
				assert.NoError(t, c.Create(context.Background(), makeNodePool()))
			},
			wantErr: "invalid key 'o11y.cloud.pingcap.com/node-min-size'",
		},
		{
			name: "invalid desiredSize annotation",
			mutateAgent: func(obj client.Object) {
				util.SetAnnotation(obj, consts.AnnotationKeyO11yNodeDesiredSize, "invalid")
			},
			setupResources: func(c client.Client) {
				assert.NoError(t, c.Create(context.Background(), makeNodePool()))
			},
			wantErr: "invalid key 'o11y.cloud.pingcap.com/node-desired-size'",
		},
		{
			name: "valid nodeMaxSize annotation",
			mutateAgent: func(obj client.Object) {
				util.SetAnnotation(obj, consts.AnnotationKeyO11yNodeMaxSize, "1024")
			},
			setupResources: func(c client.Client) {
				assert.NoError(t, c.Create(context.Background(), makeNodePool()))
			},
			verify: func(t *testing.T, c client.Client, obj client.Object) {
				assert.Equal(t, "1024", util.GetAnnotation(obj, consts.AnnotationKeyO11yNodeMaxSize))
			},
			wantResult: func() deploy.Result { return deploy.Done() },
		},
		{
			name: "pending scale - previous spec not found",
			mutateAgent: func(obj client.Object) {
				util.SetAnnotation(obj, consts.AnnotationKeyO11ySpecScaleStatus, consts.SpecScaleStatusPendingScale)
				util.SetAnnotation(obj, consts.AnnotationKeyO11yPreviousSpec, "non-existent")
			},
			setupResources: func(c client.Client) {
				assert.NoError(t, c.Create(context.Background(), makeNodePool()))
			},
			wantErr: "previous spec not found: non-existent",
		},
		{
			name: "pending scale - success",
			mutateAgent: func(obj client.Object) {
				util.SetAnnotation(obj, consts.AnnotationKeyO11ySpecScaleStatus, consts.SpecScaleStatusPendingScale)
				util.SetAnnotation(obj, consts.AnnotationKeyO11yDesiredSpec, "large")
				util.SetAnnotation(obj, consts.AnnotationKeyO11yPreviousSpec, "default")
			},
			setupResources: func(c client.Client) {
				assert.NoError(t, c.Create(context.Background(), makeNodePool()))
			},
			verify: func(t *testing.T, c client.Client, obj client.Object) {
				var npLarge gkecontainerv1beta1.NodePool
				err := c.Get(context.Background(), client.ObjectKey{Name: "np-large"}, &npLarge)
				assert.NoError(t, err)
				assert.Equal(t, "e2-highcpu-4", *npLarge.Spec.ForProvider.Config.MachineType)

				var npDefault gkecontainerv1beta1.NodePool
				err = c.Get(context.Background(), client.ObjectKey{Name: "np-default"}, &npDefault)
				assert.NoError(t, err)
				assert.Equal(t, "e2-medium", *npDefault.Spec.ForProvider.Config.MachineType)
			},
			wantResult: func() deploy.Result { return deploy.Done() },
		},
		{
			name: "resource updated - success",
			mutateAgent: func(obj client.Object) {
				util.SetAnnotation(obj, consts.AnnotationKeyO11ySpecScaleStatus, consts.SpecScaleStatusResourceUpdated)
				util.SetAnnotation(obj, consts.AnnotationKeyO11yDesiredSpec, "large")
				util.SetAnnotation(obj, consts.AnnotationKeyO11yPreviousSpec, "default")
			},
			setupResources: func(c client.Client) {
				assert.NoError(t, c.Create(context.Background(), makeNodePool()))

				// Create previous nodepool to be deleted
				prevNP := &gkecontainerv1beta1.NodePool{
					ObjectMeta: metav1.ObjectMeta{Name: "np-default"},
				}
				assert.NoError(t, c.Create(context.Background(), prevNP))
			},
			verify: func(t *testing.T, c client.Client, obj client.Object) {
				var npLarge gkecontainerv1beta1.NodePool
				err := c.Get(context.Background(), client.ObjectKey{Name: "np-large"}, &npLarge)
				assert.NoError(t, err)

				var npDefault gkecontainerv1beta1.NodePool
				err = c.Get(context.Background(), client.ObjectKey{Name: "np-default"}, &npDefault)
				assert.Error(t, err)
				assert.True(t, client.IgnoreNotFound(err) == nil)

				assert.Equal(t, consts.SpecScaleStatusNormal, util.GetAnnotation(obj, consts.AnnotationKeyO11ySpecScaleStatus))
			},
			wantResult: func() deploy.Result { return deploy.Done() },
		},
		{
			name: "resource updated - previous spec empty",
			mutateAgent: func(obj client.Object) {
				util.SetAnnotation(obj, consts.AnnotationKeyO11ySpecScaleStatus, consts.SpecScaleStatusResourceUpdated)
				util.SetAnnotation(obj, consts.AnnotationKeyO11yDesiredSpec, "large")
				util.SetAnnotation(obj, consts.AnnotationKeyO11yPreviousSpec, "")
			},
			setupResources: func(c client.Client) {
				assert.NoError(t, c.Create(context.Background(), makeNodePool()))
			},
			wantErr: "unexpected empty previous spec name",
		},
		{
			name: "resource updated - delete previous failed",
			mutateAgent: func(obj client.Object) {
				util.SetAnnotation(obj, consts.AnnotationKeyO11ySpecScaleStatus, consts.SpecScaleStatusResourceUpdated)
				util.SetAnnotation(obj, consts.AnnotationKeyO11yDesiredSpec, "large")
				util.SetAnnotation(obj, consts.AnnotationKeyO11yPreviousSpec, "default")
			},
			setupResources: func(c client.Client) {
				assert.NoError(t, c.Create(context.Background(), makeNodePool()))

				// Create previous nodepool to be deleted
				prevNP := &gkecontainerv1beta1.NodePool{
					ObjectMeta: metav1.ObjectMeta{Name: "np-default"},
				}
				assert.NoError(t, c.Create(context.Background(), prevNP))
			},
			clientWrapper: func(c client.Client) client.Client {
				return testkits.NewClientGetter(c).WithDeleteFunc(
					func(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error {
						if np, ok := obj.(*gkecontainerv1beta1.NodePool); ok && np.Name == "np-default" {
							return fmt.Errorf("mock delete error")
						}
						return c.Delete(ctx, obj, opts...)
					})
			},
			wantErr: "failed to delete previous nodepool: mock delete error",
		},
		{
			name: "resource updated - previous nodepool still deleting",
			mutateAgent: func(obj client.Object) {
				util.SetAnnotation(obj, consts.AnnotationKeyO11ySpecScaleStatus, consts.SpecScaleStatusResourceUpdated)
				util.SetAnnotation(obj, consts.AnnotationKeyO11yDesiredSpec, "large")
				util.SetAnnotation(obj, consts.AnnotationKeyO11yPreviousSpec, "default")
			},
			setupResources: func(c client.Client) {
				assert.NoError(t, c.Create(context.Background(), makeNodePool()))

				prevNP := &gkecontainerv1beta1.NodePool{
					ObjectMeta: metav1.ObjectMeta{Name: "np-default"},
				}
				assert.NoError(t, c.Create(context.Background(), prevNP))
			},
			clientWrapper: func(c client.Client) client.Client {
				return testkits.NewClientGetter(c).WithDeleteFunc(
					func(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error {
						if np, ok := obj.(*gkecontainerv1beta1.NodePool); ok && np.Name == "np-default" {
							return nil
						}
						return c.Delete(ctx, obj, opts...)
					})
			},
			verify: func(t *testing.T, c client.Client, obj client.Object) {
				assert.Equal(t, consts.SpecScaleStatusResourceUpdated, util.GetAnnotation(obj, consts.AnnotationKeyO11ySpecScaleStatus))
			},
			wantErr:     "nodepool np-default is still present",
			wantRequeue: time.Minute,
		},
		{
			name: "resource updated - previous nodepool stalled pending create",
			mutateAgent: func(obj client.Object) {
				util.SetAnnotation(obj, consts.AnnotationKeyO11ySpecScaleStatus, consts.SpecScaleStatusResourceUpdated)
				util.SetAnnotation(obj, consts.AnnotationKeyO11yDesiredSpec, "large")
				util.SetAnnotation(obj, consts.AnnotationKeyO11yPreviousSpec, "default")
			},
			setupResources: func(c client.Client) {
				assert.NoError(t, c.Create(context.Background(), makeNodePool()))

				prevNP := &gkecontainerv1beta1.NodePool{
					ObjectMeta: metav1.ObjectMeta{Name: "np-default"},
				}
				meta.SetExternalCreatePending(prevNP, time.Now().Add(-11*time.Minute))
				meta.SetExternalCreateFailed(prevNP, time.Now().Add(-12*time.Minute))
				assert.NoError(t, c.Create(context.Background(), prevNP))
			},
			clientWrapper: func(c client.Client) client.Client {
				return testkits.NewClientGetter(c).WithDeleteFunc(
					func(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error {
						if np, ok := obj.(*gkecontainerv1beta1.NodePool); ok && np.Name == "np-default" {
							return nil
						}
						return c.Delete(ctx, obj, opts...)
					})
			},
			verify: func(t *testing.T, c client.Client, obj client.Object) {
				var np gkecontainerv1beta1.NodePool
				assert.NoError(t, c.Get(context.Background(), client.ObjectKey{Name: "np-default"}, &np))
				assert.Zero(t, meta.GetExternalCreatePending(&np))
				assert.False(t, meta.GetExternalCreateFailed(&np).IsZero())
			},
			wantErr:     "nodepool np-default is still present",
			wantRequeue: time.Minute,
		},
	}

	specs := config.CustomerAgentSpecs{
		{Name: "default", InstanceType: "e2-medium"},
		{Name: "large", InstanceType: "e2-highcpu-4"},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			agent := &corev1.ConfigMap{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-agent",
					Namespace: "default",
				},
			}
			if tt.mutateAgent != nil {
				tt.mutateAgent(agent)
			}

			fakeCli := fake.NewClientBuilder().
				WithScheme(testkits.Scheme()).
				WithIndex(&gkecontainerv1beta1.NodePool{}, ".spec.forProvider.clusterRef.name", func(o client.Object) []string {
					np := o.(*gkecontainerv1beta1.NodePool)
					if np.Spec.ForProvider.ClusterRef != nil && np.Spec.ForProvider.ClusterRef.Name != "" {
						return []string{np.Spec.ForProvider.ClusterRef.Name}
					}
					return nil
				}).
				WithObjects(agent).
				Build()
			if tt.setupResources != nil {
				tt.setupResources(fakeCli)
			}

			var cli client.Client = fakeCli
			if tt.clientWrapper != nil {
				cli = tt.clientWrapper(fakeCli)
			}

			ctx := &reconciler.Context[*corev1.ConfigMap]{
				Agent: agent,
				Local: cli,
				Config: &config.RootConfig{
					O11yConfig: config.ObservabilityInfra{
						Region: "us-central1",
					},
				},
			}

			task := NewNodePoolTask(ctx, testClusterName, nodeNameFunc, nodeMatchLabels, nodeConfigLabels, specs, nil)
			res := task.Create(context.Background())
			assert.Equal(t, "NodePool", task.Name())
			if tt.wantErr != "" {
				assert.Error(t, res.Error())
				assert.Contains(t, res.Error().Error(), tt.wantErr)
				if tt.wantRequeue > 0 {
					assertResultRequeueAfter(t, res, tt.wantRequeue)
				}
			} else {
				if tt.wantResult != nil {
					assert.Equal(t, tt.wantResult(), res)
				}
				assert.NoError(t, res.Error())
			}

			if tt.verify != nil {
				tt.verify(t, fakeCli, agent)
			}
		})
	}
}

func TestNodePoolTask_Delete(t *testing.T) {
	nodeMatchLabels := map[string]string{"app": "agent"}
	nodeConfigLabels := map[string]string{"role": "agent"}

	tests := []struct {
		name           string
		setupResources func(client.Client)
		verify         func(t *testing.T, c client.Client)
	}{
		{
			name: "success delete",
			setupResources: func(c client.Client) {
				np1 := &gkecontainerv1beta1.NodePool{
					ObjectMeta: metav1.ObjectMeta{Name: "np-default"},
				}
				assert.NoError(t, c.Create(context.Background(), np1))
				np2 := &gkecontainerv1beta1.NodePool{
					ObjectMeta: metav1.ObjectMeta{Name: "np-other", Labels: nodeMatchLabels},
				}
				assert.NoError(t, c.Create(context.Background(), np2))
			},
			verify: func(t *testing.T, c client.Client) {
				var np gkecontainerv1beta1.NodePool
				err := c.Get(context.Background(), client.ObjectKey{Name: "np-default"}, &np)
				assert.Error(t, err)
				assert.True(t, client.IgnoreNotFound(err) == nil)

				err = c.Get(context.Background(), client.ObjectKey{Name: "np-other"}, &np)
				assert.Error(t, err)
				assert.True(t, client.IgnoreNotFound(err) == nil)
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			fakeCli := testkits.BuildClient()
			if tt.setupResources != nil {
				tt.setupResources(fakeCli)
			}

			ctx := &reconciler.Context[*corev1.ConfigMap]{
				Local: fakeCli,
			}

			task := NewNodePoolTask(ctx, testClusterName, nodeNameFunc, nodeMatchLabels, nodeConfigLabels, nil, nil)
			res := task.Delete(context.Background())
			assert.NoError(t, res.Error())
			assert.Equal(t, deploy.Done(), res)

			if tt.verify != nil {
				tt.verify(t, fakeCli)
			}
		})
	}
}

func TestNodePoolTask_PostCreate(t *testing.T) {
	nodeMatchLabels := map[string]string{"app": "agent"}
	nodeConfigLabels := map[string]string{"role": "agent"}

	tests := []struct {
		name           string
		setupResources func(client.Client)
		mutateAgent    func(client.Object)
		verify         func(t *testing.T, c client.Client, obj client.Object)
		wantErr        bool
		wantRequeue    time.Duration
	}{
		{
			name: "success pending scale transition",
			mutateAgent: func(obj client.Object) {
				util.SetAnnotation(obj, consts.AnnotationKeyO11ySpecScaleStatus, consts.SpecScaleStatusPendingScale)
			},
			setupResources: func(c client.Client) {
				np := &gkecontainerv1beta1.NodePool{
					ObjectMeta: metav1.ObjectMeta{Name: "np-default"},
					Status: gkecontainerv1beta1.NodePoolStatus{
						AtProvider: gkecontainerv1beta1.NodePoolObservation{
							Status: gkecontainerv1beta1.NodePoolStateRunning,
						},
					},
				}
				assert.NoError(t, c.Create(context.Background(), np))
			},
			verify: func(t *testing.T, c client.Client, obj client.Object) {
				assert.Equal(t, consts.SpecScaleStatusNewNodeGroupSynced, util.GetAnnotation(obj, consts.AnnotationKeyO11ySpecScaleStatus))
			},
		},
		{
			name: "fail check nodepool",
			setupResources: func(c client.Client) {
				np := &gkecontainerv1beta1.NodePool{
					ObjectMeta: metav1.ObjectMeta{Name: "np-default"},
					Status: gkecontainerv1beta1.NodePoolStatus{
						AtProvider: gkecontainerv1beta1.NodePoolObservation{
							Status: "PROVISIONING",
						},
					},
				}
				assert.NoError(t, c.Create(context.Background(), np))
			},
			wantErr:     true,
			wantRequeue: time.Minute,
		},
		{
			name: "fail check nodepool, clear stalled pending create",
			setupResources: func(c client.Client) {
				np := &gkecontainerv1beta1.NodePool{
					ObjectMeta: metav1.ObjectMeta{Name: "np-default"},
					Status: gkecontainerv1beta1.NodePoolStatus{
						AtProvider: gkecontainerv1beta1.NodePoolObservation{
							Status: "PROVISIONING",
						},
					},
				}
				meta.SetExternalCreatePending(np, time.Now().Add(-11*time.Minute))
				meta.SetExternalCreateFailed(np, time.Now().Add(-12*time.Minute))
				assert.NoError(t, c.Create(context.Background(), np))
			},
			verify: func(t *testing.T, c client.Client, obj client.Object) {
				var np gkecontainerv1beta1.NodePool
				assert.NoError(t, c.Get(context.Background(), client.ObjectKey{Name: "np-default"}, &np))
				assert.Zero(t, meta.GetExternalCreatePending(&np))
				assert.False(t, meta.GetExternalCreateFailed(&np).IsZero())
			},
			wantErr:     true,
			wantRequeue: time.Minute,
		},
	}

	specs := config.CustomerAgentSpecs{
		{Name: "default", InstanceType: "e2-medium"},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			agent := &corev1.ConfigMap{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-agent",
					Namespace: "default",
				},
			}
			if tt.mutateAgent != nil {
				tt.mutateAgent(agent)
			}

			fakeCli := testkits.BuildClient()
			if tt.setupResources != nil {
				tt.setupResources(fakeCli)
			}

			ctx := &reconciler.Context[*corev1.ConfigMap]{
				Agent: agent,
				Local: fakeCli,
			}

			task := NewNodePoolTask(ctx, testClusterName, nodeNameFunc, nodeMatchLabels, nodeConfigLabels, specs, nil)
			res := task.PostCreate(context.Background())
			if tt.wantErr {
				assert.Error(t, res.Error())
				if tt.wantRequeue > 0 {
					assertResultRequeueAfter(t, res, tt.wantRequeue)
				}
			} else {
				assert.NoError(t, res.Error())
				assert.Equal(t, deploy.Done(), res)
			}

			if tt.verify != nil {
				tt.verify(t, fakeCli, agent)
			}
		})
	}
}

func TestNodePoolTask_PostDelete(t *testing.T) {
	nodeMatchLabels := map[string]string{"app": "agent"}
	nodeConfigLabels := map[string]string{"role": "agent"}

	tests := []struct {
		name           string
		setupResources func(client.Client)
		verify         func(t *testing.T, c client.Client)
		wantErr        bool
		wantRequeue    time.Duration
	}{
		{
			name: "success check delete",
			setupResources: func(c client.Client) {
				// No resources
			},
			wantErr: false,
		},
		{
			name: "fail check delete default",
			setupResources: func(c client.Client) {
				np := &gkecontainerv1beta1.NodePool{
					ObjectMeta: metav1.ObjectMeta{Name: "np-default"},
				}
				assert.NoError(t, c.Create(context.Background(), np))
			},
			wantErr:     true,
			wantRequeue: time.Minute,
		},
		{
			name: "fail check delete, clear stalled pending create",
			setupResources: func(c client.Client) {
				np := &gkecontainerv1beta1.NodePool{
					ObjectMeta: metav1.ObjectMeta{Name: "np-default"},
				}
				meta.SetExternalCreatePending(np, time.Now().Add(-11*time.Minute))
				meta.SetExternalCreateFailed(np, time.Now().Add(-12*time.Minute))
				assert.NoError(t, c.Create(context.Background(), np))
			},
			verify: func(t *testing.T, c client.Client) {
				var np gkecontainerv1beta1.NodePool
				assert.NoError(t, c.Get(context.Background(), client.ObjectKey{Name: "np-default"}, &np))
				assert.Zero(t, meta.GetExternalCreatePending(&np))
				assert.False(t, meta.GetExternalCreateFailed(&np).IsZero())
			},
			wantErr:     true,
			wantRequeue: time.Minute,
		},
		{
			name: "fail check delete other",
			setupResources: func(c client.Client) {
				np := &gkecontainerv1beta1.NodePool{
					ObjectMeta: metav1.ObjectMeta{Name: "np-other", Labels: nodeMatchLabels},
				}
				assert.NoError(t, c.Create(context.Background(), np))
			},
			wantErr:     true,
			wantRequeue: time.Minute,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			fakeCli := testkits.BuildClient()
			if tt.setupResources != nil {
				tt.setupResources(fakeCli)
			}

			ctx := &reconciler.Context[*corev1.ConfigMap]{
				Local: fakeCli,
			}

			task := NewNodePoolTask(ctx, testClusterName, nodeNameFunc, nodeMatchLabels, nodeConfigLabels, nil, nil)
			res := task.PostDelete(context.Background())
			if tt.wantErr {
				assert.Error(t, res.Error())
				if tt.wantRequeue > 0 {
					assertResultRequeueAfter(t, res, tt.wantRequeue)
				}
			} else {
				assert.NoError(t, res.Error())
				assert.Equal(t, deploy.Done(), res)
			}

			if tt.verify != nil {
				tt.verify(t, fakeCli)
			}
		})
	}
}

func TestVerifyNodePoolSpec(t *testing.T) {
	name := "test-nodepool"

	tests := []struct {
		name    string
		spec    gkecontainerv1beta1.NodePoolSpec
		wantErr string
	}{
		{
			name: "clusterRef is nil",
			spec: gkecontainerv1beta1.NodePoolSpec{
				ForProvider: gkecontainerv1beta1.NodePoolParameters{
					ClusterRef: nil,
				},
			},
			wantErr: "clusterRef is nil in nodepool test-nodepool",
		},
		{
			name: "config is nil",
			spec: gkecontainerv1beta1.NodePoolSpec{
				ForProvider: gkecontainerv1beta1.NodePoolParameters{
					ClusterRef: &xpv1.Reference{Name: "cluster-ref"},
					Config:     nil,
				},
			},
			wantErr: "config is nil in nodepool test-nodepool",
		},
		{
			name: "serviceAccount is nil",
			spec: gkecontainerv1beta1.NodePoolSpec{
				ForProvider: gkecontainerv1beta1.NodePoolParameters{
					ClusterRef: &xpv1.Reference{Name: "cluster-ref"},
					Config: &gkecontainerv1beta1.NodeConfig{
						ServiceAccount: nil,
					},
				},
			},
			wantErr: "serviceAccount is nil in nodepool test-nodepool",
		},
		{
			name: "imageType is nil",
			spec: gkecontainerv1beta1.NodePoolSpec{
				ForProvider: gkecontainerv1beta1.NodePoolParameters{
					ClusterRef: &xpv1.Reference{Name: "cluster-ref"},
					Config: &gkecontainerv1beta1.NodeConfig{
						ServiceAccount: ptr.To("sa"),
						ImageType:      nil,
					},
				},
			},
			wantErr: "imageType is nil in nodepool test-nodepool",
		},
		{
			name: "providerConfigReference is nil",
			spec: gkecontainerv1beta1.NodePoolSpec{
				ForProvider: gkecontainerv1beta1.NodePoolParameters{
					ClusterRef: &xpv1.Reference{Name: "cluster-ref"},
					Config: &gkecontainerv1beta1.NodeConfig{
						ServiceAccount: ptr.To("sa"),
						ImageType:      ptr.To("COS_CONTAINERD"),
					},
				},
				ResourceSpec: xpv1.ResourceSpec{
					ProviderConfigReference: nil,
				},
			},
			wantErr: "providerConfigReference is nil in nodepool test-nodepool",
		},
		{
			name: "version is nil",
			spec: gkecontainerv1beta1.NodePoolSpec{
				ForProvider: gkecontainerv1beta1.NodePoolParameters{
					ClusterRef: &xpv1.Reference{Name: "cluster-ref"},
					Config: &gkecontainerv1beta1.NodeConfig{
						ServiceAccount: ptr.To("sa"),
						ImageType:      ptr.To("COS_CONTAINERD"),
					},
					Version: nil,
				},
				ResourceSpec: xpv1.ResourceSpec{
					ProviderConfigReference: &xpv1.Reference{Name: "gcp-provider"},
				},
			},
			wantErr: "version is nil in nodepool test-nodepool",
		},
		{
			name: "locations is empty",
			spec: gkecontainerv1beta1.NodePoolSpec{
				ForProvider: gkecontainerv1beta1.NodePoolParameters{
					ClusterRef: &xpv1.Reference{Name: "cluster-ref"},
					Config: &gkecontainerv1beta1.NodeConfig{
						ServiceAccount: ptr.To("sa"),
						ImageType:      ptr.To("COS_CONTAINERD"),
					},
					Version:   ptr.To("1.27"),
					Locations: nil,
				},
				ResourceSpec: xpv1.ResourceSpec{
					ProviderConfigReference: &xpv1.Reference{Name: "gcp-provider"},
				},
			},
			wantErr: "locations is empty in nodepool test-nodepool",
		},
		{
			name: "success",
			spec: gkecontainerv1beta1.NodePoolSpec{
				ForProvider: gkecontainerv1beta1.NodePoolParameters{
					ClusterRef: &xpv1.Reference{Name: "cluster-ref"},
					Config: &gkecontainerv1beta1.NodeConfig{
						ServiceAccount: ptr.To("sa"),
						ImageType:      ptr.To("COS_CONTAINERD"),
					},
					Version:   ptr.To("1.27"),
					Locations: []string{"us-central1-a"},
				},
				ResourceSpec: xpv1.ResourceSpec{
					ProviderConfigReference: &xpv1.Reference{Name: "gcp-provider"},
				},
			},
			wantErr: "",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := verifyNodePoolSpec(name, tt.spec)
			if tt.wantErr != "" {
				assert.Error(t, err)
				assert.Equal(t, tt.wantErr, err.Error())
			} else {
				assert.NoError(t, err)
			}
		})
	}
}
