package commonaws

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

	eksv1alpha1 "github.com/crossplane-contrib/provider-aws/apis/eks/manualv1alpha1"
	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"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	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"

	rsv1alpha1 "github.com/tidbcloud/observability/agent-control-server/api/regionalserver/v1alpha1"
	"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 = "test-cluster"

func TestMigrateAMIType(t *testing.T) {
	tests := []struct {
		name    string
		current string
		version string
		want    string
	}{
		{name: "AL2 x86 migrated at 1.33", current: AL2AMIType, version: "1.33", want: BottlerocketAMIType},
		{name: "AL2 x86 migrated at 1.34", current: AL2AMIType, version: "1.34", want: BottlerocketAMIType},
		{name: "AL2 x86 preserved below 1.33", current: AL2AMIType, version: "1.32", want: AL2AMIType},
		{name: "AL2 x86 preserved when version empty", current: AL2AMIType, version: "", want: AL2AMIType},
		{name: "AL2 x86 preserved when version garbage", current: AL2AMIType, version: "garbage", want: AL2AMIType},
		{name: "AL2 arm preserved", current: "AL2_ARM_64", version: "1.33", want: "AL2_ARM_64"},
		{name: "AL2023 preserved", current: "AL2023_x86_64_STANDARD", version: "1.33", want: "AL2023_x86_64_STANDARD"},
		{name: "BOTTLEROCKET unchanged", current: BottlerocketAMIType, version: "1.33", want: BottlerocketAMIType},
		{name: "empty unchanged", current: "", version: "1.33", want: ""},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert.Equal(t, tt.want, MigrateAMIType(tt.current, tt.version))
		})
	}
}

func TestNameSuffix(t *testing.T) {
	tests := []struct {
		name           string
		amiType        string
		suffixRequired string
		version        string
		want           string
	}{
		{name: "AL2 + 1.33 suffixed (migration in progress)", amiType: AL2AMIType, suffixRequired: "", version: "1.33", want: "-bottlerocket"},
		{name: "AL2 + 1.34 suffixed", amiType: AL2AMIType, suffixRequired: "", version: "1.34", want: "-bottlerocket"},
		{name: "AL2 + 1.32 UNSUFFIXED (below gate)", amiType: AL2AMIType, suffixRequired: "", version: "1.32", want: ""},
		{name: "AL2 + empty version UNSUFFIXED", amiType: AL2AMIType, suffixRequired: "", version: "", want: ""},
		{name: "BOTTLEROCKET + flag suffixed (post-migration, any version)", amiType: BottlerocketAMIType, suffixRequired: "true", version: "1.32", want: "-bottlerocket"},
		{name: "BOTTLEROCKET without flag UNSUFFIXED (native, 1.33)", amiType: BottlerocketAMIType, suffixRequired: "", version: "1.33", want: ""},
		{name: "AL2023 never suffixed", amiType: "AL2023_x86_64_STANDARD", suffixRequired: "true", version: "1.33", want: ""},
		{name: "AL2 arm never suffixed", amiType: "AL2_ARM_64", suffixRequired: "true", version: "1.33", want: ""},
		{name: "empty never suffixed", amiType: "", suffixRequired: "", version: "1.33", want: ""},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert.Equal(t, tt.want, NameSuffix(tt.amiType, tt.suffixRequired, tt.version))
		})
	}
}

func makeNodeGroup() *eksv1alpha1.NodeGroup {
	return &eksv1alpha1.NodeGroup{
		ObjectMeta: metav1.ObjectMeta{Name: "tidbcloud-dedicated-admin"},
		Spec: eksv1alpha1.NodeGroupSpec{
			ForProvider: eksv1alpha1.NodeGroupParameters{
				ClusterName: testClusterName,
				NodeRole:    "role-arn",
			},
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: "aws-provider"},
			},
		},
	}
}

func makeVPC(lbs map[string]string) *rsv1alpha1.VPC {
	return &rsv1alpha1.VPC{
		ObjectMeta: metav1.ObjectMeta{Name: "vpc-1", Labels: lbs},
		Status: rsv1alpha1.VPCStatus{
			NodeSubnets: []rsv1alpha1.Subnet{{SubnetID: "subnet-1", AvailabilityZone: "us-east-1a"}},
		},
	}
}

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

func TestNodeGroupTask_Create(t *testing.T) {
	vpcMatchLabels := map[string]string{"vpc": "test"}

	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
	}{
		{
			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 nodegroup not found",
			wantErr: "admin nodegroup not found",
		},
		{
			name: "vpc not found",
			setupResources: func(c client.Client) {
				assert.NoError(t, c.Create(context.Background(), makeNodeGroup()))
			},
			wantErr: "no vpc found",
		},
		{
			name: "vpc labels not match",
			setupResources: func(c client.Client) {
				assert.NoError(t, c.Create(context.Background(), makeNodeGroup()))
				assert.NoError(t, c.Create(context.Background(), makeVPC(map[string]string{"vpc": "test-1"})))
			},
			wantErr: "no vpc found",
		},
		{
			name: "vpc has no subnets",
			setupResources: func(c client.Client) {
				assert.NoError(t, c.Create(context.Background(), makeNodeGroup()))
				vpc := &rsv1alpha1.VPC{
					ObjectMeta: metav1.ObjectMeta{Name: "vpc-1", Labels: vpcMatchLabels},
				}
				assert.NoError(t, c.Create(context.Background(), vpc))
			},
			wantErr: "vpc vpc-1 has no node subnets",
		},
		{
			name: "success create default",
			setupResources: func(c client.Client) {
				assert.NoError(t, c.Create(context.Background(), makeNodeGroup()))
				assert.NoError(t, c.Create(context.Background(), makeVPC(vpcMatchLabels)))
			},
			verify: func(t *testing.T, c client.Client, obj client.Object) {
				var ng eksv1alpha1.NodeGroup
				err := c.Get(context.Background(), client.ObjectKey{Name: "ng-default"}, &ng)
				assert.NoError(t, err)
				assert.Equal(t, "t3.medium", ng.Spec.ForProvider.InstanceTypes[0])
				assert.Equal(t, "role-arn", util.GetAnnotation(obj, consts.AnnotationKeyO11yNodeRole))

				assert.Equal(t, defaultVersion, util.GetAnnotation(obj, consts.AnnotationKeyO11yNodeVersion))
				assert.Equal(t, defaultReleaseVersion, util.GetAnnotation(obj, consts.AnnotationKeyO11yNodeReleaseVersion))
				assert.Equal(t, defaultAMIType, util.GetAnnotation(obj, consts.AnnotationKeyO11yNodeAMIType))

				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: "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(), makeNodeGroup()))
				assert.NoError(t, c.Create(context.Background(), makeVPC(vpcMatchLabels)))
			},
			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(), makeNodeGroup()))
				assert.NoError(t, c.Create(context.Background(), makeVPC(vpcMatchLabels)))
			},
			verify: func(t *testing.T, c client.Client, obj client.Object) {
				var ngLarge eksv1alpha1.NodeGroup
				err := c.Get(context.Background(), client.ObjectKey{Name: "ng-large"}, &ngLarge)
				assert.NoError(t, err)
				assert.Equal(t, "m5.large", ngLarge.Spec.ForProvider.InstanceTypes[0])

				var ngDefault eksv1alpha1.NodeGroup
				err = c.Get(context.Background(), client.ObjectKey{Name: "ng-default"}, &ngDefault)
				assert.NoError(t, err)
				assert.Equal(t, "t3.medium", ngDefault.Spec.ForProvider.InstanceTypes[0])
			},
			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(), makeNodeGroup()))
				assert.NoError(t, c.Create(context.Background(), makeVPC(vpcMatchLabels)))

				// Create previous nodegroup to be deleted
				prevNG := &eksv1alpha1.NodeGroup{
					ObjectMeta: metav1.ObjectMeta{Name: "ng-default"},
				}
				assert.NoError(t, c.Create(context.Background(), prevNG))
			},
			verify: func(t *testing.T, c client.Client, obj client.Object) {
				var ngLarge eksv1alpha1.NodeGroup
				err := c.Get(context.Background(), client.ObjectKey{Name: "ng-large"}, &ngLarge)
				assert.NoError(t, err)

				var ngDefault eksv1alpha1.NodeGroup
				err = c.Get(context.Background(), client.ObjectKey{Name: "ng-default"}, &ngDefault)
				// fake client Get returns NotFound error which is what we want.
				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: "pending scale - previous spec empty",
			mutateAgent: func(obj client.Object) {
				util.SetAnnotation(obj, consts.AnnotationKeyO11ySpecScaleStatus, consts.SpecScaleStatusPendingScale)
				util.SetAnnotation(obj, consts.AnnotationKeyO11yDesiredSpec, "large")
				util.SetAnnotation(obj, consts.AnnotationKeyO11yPreviousSpec, "")
			},
			setupResources: func(c client.Client) {
				assert.NoError(t, c.Create(context.Background(), makeNodeGroup()))
				assert.NoError(t, c.Create(context.Background(), makeVPC(vpcMatchLabels)))
			},
			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(), makeNodeGroup()))
				assert.NoError(t, c.Create(context.Background(), makeVPC(vpcMatchLabels)))

				// Create previous nodegroup to be deleted
				prevNG := &eksv1alpha1.NodeGroup{
					ObjectMeta: metav1.ObjectMeta{Name: "ng-default"},
				}
				assert.NoError(t, c.Create(context.Background(), prevNG))
			},
			clientWrapper: func(c client.Client) client.Client {
				return testkits.NewClientGetter(c).WithDeleteFunc(
					func(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error {
						if ng, ok := obj.(*eksv1alpha1.NodeGroup); ok && ng.Name == "ng-default" {
							return fmt.Errorf("mock delete error")
						}
						return c.Delete(ctx, obj, opts...)
					},
				)
			},
			wantErr: "failed to delete previous nodegroup: mock delete error",
		},
		{
			name: "resource updated - previous nodegroup 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(), makeNodeGroup()))
				assert.NoError(t, c.Create(context.Background(), makeVPC(vpcMatchLabels)))

				// Create previous nodegroup with a stalled pending create so
				// crossplane refuses to process its deletion.
				prevNG := &eksv1alpha1.NodeGroup{
					ObjectMeta: metav1.ObjectMeta{Name: "ng-default"},
				}
				meta.SetExternalCreatePending(prevNG, time.Now().Add(-11*time.Minute))
				meta.SetExternalCreateFailed(prevNG, time.Now().Add(-12*time.Minute))
				assert.NoError(t, c.Create(context.Background(), prevNG))
			},
			clientWrapper: func(c client.Client) client.Client {
				return testkits.NewClientGetter(c).WithDeleteFunc(
					func(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error {
						if ng, ok := obj.(*eksv1alpha1.NodeGroup); ok && ng.Name == "ng-default" {
							return nil
						}
						return c.Delete(ctx, obj, opts...)
					},
				)
			},
			verify: func(t *testing.T, c client.Client, obj client.Object) {
				var ng eksv1alpha1.NodeGroup
				assert.NoError(t, c.Get(context.Background(), client.ObjectKey{Name: "ng-default"}, &ng))
				assert.Zero(t, meta.GetExternalCreatePending(&ng))
				assert.False(t, meta.GetExternalCreateFailed(&ng).IsZero())
			},
			wantErr: "nodegroup ng-default is still present",
		},
		{
			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(), makeNodeGroup()))
				assert.NoError(t, c.Create(context.Background(), makeVPC(vpcMatchLabels)))
			},
			wantErr: "unexpected empty previous spec name",
		},
		{
			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(), makeNodeGroup()))
				assert.NoError(t, c.Create(context.Background(), makeVPC(vpcMatchLabels)))
			},
			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(), makeNodeGroup()))
				assert.NoError(t, c.Create(context.Background(), makeVPC(vpcMatchLabels)))
			},
			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(), makeNodeGroup()))
				assert.NoError(t, c.Create(context.Background(), makeVPC(vpcMatchLabels)))
			},
			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: "admin nodegroup explicit forProvider values",
			setupResources: func(c client.Client) {
				ng := makeNodeGroup()
				ng.Spec.ForProvider.Version = ptr.To("1.33")
				ng.Spec.ForProvider.ReleaseVersion = ptr.To("1.33.0-20250101")
				ng.Spec.ForProvider.AMIType = ptr.To("AL2_x86_64")
				assert.NoError(t, c.Create(context.Background(), ng))
				assert.NoError(t, c.Create(context.Background(), makeVPC(vpcMatchLabels)))
			},
			verify: func(t *testing.T, c client.Client, obj client.Object) {
				assert.Equal(t, "1.33", util.GetAnnotation(obj, consts.AnnotationKeyO11yNodeVersion))
				assert.Equal(t, "1.33.0-20250101", util.GetAnnotation(obj, consts.AnnotationKeyO11yNodeReleaseVersion))
				assert.Equal(t, BottlerocketAMIType, util.GetAnnotation(obj, consts.AnnotationKeyO11yNodeAMIType))
				var ng eksv1alpha1.NodeGroup
				assert.NoError(t, c.Get(context.Background(), client.ObjectKey{Name: "ng-default"}, &ng))
				assert.Equal(t, BottlerocketAMIType, *ng.Spec.ForProvider.AMIType)
			},
			wantResult: func() deploy.Result { return deploy.Done() },
		},
	}

	specs := config.CustomerAgentSpecs{
		{Name: "default", InstanceType: "t3.medium"},
		{Name: "large", InstanceType: "m5.large"},
	}
	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(&eksv1alpha1.NodeGroup{}, ".spec.forProvider.clusterName", func(o client.Object) []string {
					return []string{o.(*eksv1alpha1.NodeGroup).Spec.ForProvider.ClusterName}
				}).
				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-east-1",
					},
				},
			}

			nodegroupLabels := map[string]string{"app": "agent"}
			task := NewNodeGroupTask(ctx, testClusterName, nodeNameFunc, nodegroupLabels, vpcMatchLabels, specs, nil)
			res := task.Create(context.Background())
			assert.Equal(t, "NodeGroup", task.Name())
			if tt.wantErr != "" {
				assert.Error(t, res.Error())
				assert.Contains(t, res.Error().Error(), tt.wantErr)
			} 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 TestNodeGroupTask_Delete(t *testing.T) {
	nodegroupLabels := map[string]string{"app": "agent"}
	vpcMatchLabels := map[string]string{"vpc": "test"}

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

				err = c.Get(context.Background(), client.ObjectKey{Name: "ng-other"}, &ng)
				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 := NewNodeGroupTask(ctx, testClusterName, nodeNameFunc, nodegroupLabels, vpcMatchLabels, 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 TestNodeGroupTask_PostCreate(t *testing.T) {
	nodegroupLabels := map[string]string{"app": "agent"}
	vpcMatchLabels := map[string]string{"vpc": "test"}

	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
	}{
		{
			name: "success pending scale transition",
			mutateAgent: func(obj client.Object) {
				util.SetAnnotation(obj, consts.AnnotationKeyO11ySpecScaleStatus, consts.SpecScaleStatusPendingScale)
			},
			setupResources: func(c client.Client) {
				// Desired nodegroup must exist for CheckNodeGroup to pass
				ng := &eksv1alpha1.NodeGroup{
					ObjectMeta: metav1.ObjectMeta{Name: "ng-default"},
					Status: eksv1alpha1.NodeGroupStatus{
						AtProvider: eksv1alpha1.NodeGroupObservation{
							Status: eksv1alpha1.NodeGroupStatusActive,
						},
					},
				}
				assert.NoError(t, c.Create(context.Background(), ng))
			},
			verify: func(t *testing.T, c client.Client, obj client.Object) {
				assert.Equal(t, consts.SpecScaleStatusNewNodeGroupSynced, util.GetAnnotation(obj, consts.AnnotationKeyO11ySpecScaleStatus))
			},
		},
		{
			name: "check nodegroup fail",
			mutateAgent: func(obj client.Object) {
				// No pending scale status, just standard check
			},
			setupResources: func(c client.Client) {
				// No nodegroup created, so CheckNodeGroup should fail (NotFound)
			},
			wantErr: true,
		},
		{
			name: "check nodegroup fail, clear stalled pending create",
			setupResources: func(c client.Client) {
				ng := &eksv1alpha1.NodeGroup{
					ObjectMeta: metav1.ObjectMeta{Name: "ng-default"},
				}
				meta.SetExternalCreatePending(ng, time.Now().Add(-11*time.Minute))
				meta.SetExternalCreateFailed(ng, time.Now().Add(-12*time.Minute))
				assert.NoError(t, c.Create(context.Background(), ng))
			},
			verify: func(t *testing.T, c client.Client, obj client.Object) {
				var ng eksv1alpha1.NodeGroup
				assert.NoError(t, c.Get(context.Background(), client.ObjectKey{Name: "ng-default"}, &ng))
				assert.Zero(t, meta.GetExternalCreatePending(&ng))
				assert.False(t, meta.GetExternalCreateFailed(&ng).IsZero())
			},
			wantErr: true,
		},
		{
			name: "cleanup deletes stale nodegroups after current is ready",
			setupResources: func(c client.Client) {
				// current NG is Active
				current := &eksv1alpha1.NodeGroup{
					ObjectMeta: metav1.ObjectMeta{Name: "ng-default", Labels: nodegroupLabels},
					Status: eksv1alpha1.NodeGroupStatus{
						AtProvider: eksv1alpha1.NodeGroupObservation{
							Status: eksv1alpha1.NodeGroupStatusActive,
						},
					},
				}
				assert.NoError(t, c.Create(context.Background(), current))
				// legacy NG with same labels but different name (AL2 remnant)
				stale := &eksv1alpha1.NodeGroup{
					ObjectMeta: metav1.ObjectMeta{Name: "ng-legacy", Labels: nodegroupLabels},
				}
				assert.NoError(t, c.Create(context.Background(), stale))
			},
			verify: func(t *testing.T, c client.Client, obj client.Object) {
				// current preserved
				var ng eksv1alpha1.NodeGroup
				assert.NoError(t, c.Get(context.Background(), client.ObjectKey{Name: "ng-default"}, &ng))
				// stale deleted
				err := c.Get(context.Background(), client.ObjectKey{Name: "ng-legacy"}, &ng)
				assert.Error(t, err)
				assert.True(t, apierrors.IsNotFound(err))
			},
		},
		{
			name: "cleanup skipped during SpecScale transition",
			mutateAgent: func(obj client.Object) {
				util.SetAnnotation(obj, consts.AnnotationKeyO11ySpecScaleStatus, consts.SpecScaleStatusPendingScale)
			},
			setupResources: func(c client.Client) {
				current := &eksv1alpha1.NodeGroup{
					ObjectMeta: metav1.ObjectMeta{Name: "ng-default", Labels: nodegroupLabels},
					Status: eksv1alpha1.NodeGroupStatus{
						AtProvider: eksv1alpha1.NodeGroupObservation{
							Status: eksv1alpha1.NodeGroupStatusActive,
						},
					},
				}
				assert.NoError(t, c.Create(context.Background(), current))
				// previousSpec NG must survive during SpecScale
				stale := &eksv1alpha1.NodeGroup{
					ObjectMeta: metav1.ObjectMeta{Name: "ng-legacy", Labels: nodegroupLabels},
				}
				assert.NoError(t, c.Create(context.Background(), stale))
			},
			verify: func(t *testing.T, c client.Client, obj client.Object) {
				// both preserved; cleanup gated by SpecScale status
				var ng eksv1alpha1.NodeGroup
				assert.NoError(t, c.Get(context.Background(), client.ObjectKey{Name: "ng-default"}, &ng))
				assert.NoError(t, c.Get(context.Background(), client.ObjectKey{Name: "ng-legacy"}, &ng))
			},
		},
	}

	specs := config.CustomerAgentSpecs{
		{Name: "default", InstanceType: "t3.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(agent)
			if tt.setupResources != nil {
				tt.setupResources(fakeCli)
			}

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

			task := NewNodeGroupTask(ctx, testClusterName, nodeNameFunc, nodegroupLabels, vpcMatchLabels, specs, nil)
			res := task.PostCreate(context.Background())
			if tt.wantErr {
				assert.Error(t, res.Error())
				assert.False(t, res.IsDoneState())
			} else {
				assert.NoError(t, res.Error())
				assert.Equal(t, deploy.Done(), res)
			}

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

// TestNodeGroupTask_AMITypeMigration_Flow verifies the AL2 → BOTTLEROCKET
// migration across reconciles, using a nameFunc that derives the -bottlerocket
// suffix from the Agent annotations (matching real controller behavior).
//
// Regression coverage:
//  1. First Create provisions the suffixed NG (AL2 annotation already triggers
//     the suffix), rewrites AMIType to BOTTLEROCKET, and sets the sticky
//     SuffixRequired flag.
//  2. Same cycle's PostCreate cannot find the suffixed NG Active yet, so it
//     fails without deleting the legacy NG via cleanup.
//  3. Second Create runs under BOTTLEROCKET + flag, nameFunc still produces
//     the suffixed name (flag is sticky), and the new NG is reconciled.
//  4. Once the suffixed NG is Active, PostCreate cleanup deletes the legacy
//     unsuffixed NG. The flag must persist so subsequent reconciles keep the
//     suffixed name stable.
//
// Any change that reorders the annotation/flag bump in reconcile, drops the
// CheckNodeGroupReady guard in PostCreate, or clears the SuffixRequired flag
// will fail this test.
func TestNodeGroupTask_AMITypeMigration_Flow(t *testing.T) {
	nodegroupLabels := map[string]string{"app": "agent"}
	vpcMatchLabels := map[string]string{"vpc": "test"}

	agent := &corev1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{Name: "test-agent", Namespace: "default"},
	}
	util.SetAnnotation(agent, consts.AnnotationKeyO11yNodeAMIType, AL2AMIType)
	util.SetAnnotation(agent, consts.AnnotationKeyO11yNodeRole, "role-arn")
	util.SetAnnotation(agent, consts.AnnotationKeyO11yNodeProviderConfigReference, "aws-provider")
	util.SetAnnotation(agent, consts.AnnotationKeyO11yNodeVersion, "1.33")
	util.SetAnnotation(agent, consts.AnnotationKeyO11yNodeReleaseVersion, "1.33.0-20250101")
	util.SetAnnotation(agent, consts.AnnotationKeyO11yNodeVPCName, "vpc-1")
	util.SetAnnotation(agent, consts.AnnotationKeyO11yNodeZone, "us-east-1a")
	util.SetAnnotation(agent, consts.AnnotationKeyO11yNodeSubnet, "subnet-1")
	util.SetAnnotation(agent, consts.AnnotationKeyO11yNodeMinSize, "1")
	util.SetAnnotation(agent, consts.AnnotationKeyO11yNodeMaxSize, "3")
	util.SetAnnotation(agent, consts.AnnotationKeyO11yNodeDesiredSize, "1")
	util.SetAnnotation(agent, consts.AnnotationKeyO11yNodeDiskSize, "20")
	util.SetAnnotation(agent, consts.AnnotationKeyO11yNodeCapacityType, "ON_DEMAND")

	legacy := &eksv1alpha1.NodeGroup{
		ObjectMeta: metav1.ObjectMeta{Name: "ng-default", Labels: nodegroupLabels},
		Status: eksv1alpha1.NodeGroupStatus{
			AtProvider: eksv1alpha1.NodeGroupObservation{
				Status: eksv1alpha1.NodeGroupStatusActive,
			},
		},
	}
	fakeCli := testkits.BuildClient(agent, legacy)

	// nameFunc mirrors the real controller: derive the -bottlerocket suffix
	// from the AMIType, SuffixRequired, and Version annotations.
	nameFunc := func(specName string) string {
		base := "ng-" + specName
		return base + NameSuffix(
			util.GetAnnotation(agent, consts.AnnotationKeyO11yNodeAMIType),
			util.GetAnnotation(agent, consts.AnnotationKeyO11yNodeAMISuffixRequired),
			util.GetAnnotation(agent, consts.AnnotationKeyO11yNodeVersion),
		)
	}

	specs := config.CustomerAgentSpecs{{Name: "default", InstanceType: "t3.medium"}}
	ctx := &reconciler.Context[*corev1.ConfigMap]{
		Agent:  agent,
		Local:  fakeCli,
		Config: &config.RootConfig{O11yConfig: config.ObservabilityInfra{Region: "us-east-1"}},
	}
	task := NewNodeGroupTask(ctx, testClusterName, nameFunc, nodegroupLabels, vpcMatchLabels, specs, nil)

	// 1. First Create provisions the suffixed NG, rewrites AMIType, sets the flag.
	res1 := task.Create(context.Background())
	assert.Equal(t, deploy.Done(), res1)
	assert.Equal(t, BottlerocketAMIType,
		util.GetAnnotation(agent, consts.AnnotationKeyO11yNodeAMIType))
	assert.Equal(t, "true",
		util.GetAnnotation(agent, consts.AnnotationKeyO11yNodeAMISuffixRequired))

	// 2. Same cycle's PostCreate must fail (suffixed NG not Active yet) and
	//    leave the legacy NG untouched.
	postRes1 := task.PostCreate(context.Background())
	assert.Error(t, postRes1.Error())
	assert.False(t, postRes1.IsDoneState())
	var legacyStillThere eksv1alpha1.NodeGroup
	assert.NoError(t, fakeCli.Get(context.Background(),
		client.ObjectKey{Name: "ng-default"}, &legacyStillThere))

	// 3. Second Create converges the suffixed NG under the new flag.
	res2 := task.Create(context.Background())
	assert.Equal(t, deploy.Done(), res2)
	var fresh eksv1alpha1.NodeGroup
	assert.NoError(t, fakeCli.Get(context.Background(),
		client.ObjectKey{Name: "ng-default-bottlerocket"}, &fresh))

	// 4. PostCreate cleanup deletes the legacy NG once the suffixed one is Active.
	fresh.Status.AtProvider.Status = eksv1alpha1.NodeGroupStatusActive
	assert.NoError(t, fakeCli.Update(context.Background(), &fresh))

	postRes2 := task.PostCreate(context.Background())
	assert.NoError(t, postRes2.Error())
	assert.True(t, postRes2.IsDoneState())
	assert.Equal(t, "true",
		util.GetAnnotation(agent, consts.AnnotationKeyO11yNodeAMISuffixRequired),
		"SuffixRequired flag must persist after migration completes")

	err := fakeCli.Get(context.Background(),
		client.ObjectKey{Name: "ng-default"}, &eksv1alpha1.NodeGroup{})
	assert.True(t, apierrors.IsNotFound(err))
	// Suffixed NG survives cleanup.
	assert.NoError(t, fakeCli.Get(context.Background(),
		client.ObjectKey{Name: "ng-default-bottlerocket"}, &fresh))
}

// TestNodeGroupTask_NativeBottlerocketNoRename verifies that an Agent
// bootstrapped from a BOTTLEROCKET admin NG (no migration history) keeps the
// unsuffixed NodeGroup name forever. The SuffixRequired flag must never be
// set, and no "-bottlerocket" NG must ever be created.
//
// Regression for the bug where NameSuffix added "-bottlerocket" to every
// BOTTLEROCKET Agent indiscriminately, causing pre-existing native
// BOTTLEROCKET NGs to be renamed (and thus rebuilt) on the next reconcile.
func TestNodeGroupTask_NativeBottlerocketNoRename(t *testing.T) {
	nodegroupLabels := map[string]string{"app": "agent"}
	vpcMatchLabels := map[string]string{"vpc": "test"}

	adminNG := &eksv1alpha1.NodeGroup{
		ObjectMeta: metav1.ObjectMeta{Name: "tidbcloud-dedicated-admin"},
		Spec: eksv1alpha1.NodeGroupSpec{
			ForProvider: eksv1alpha1.NodeGroupParameters{
				AMIType:        ptr.To(BottlerocketAMIType),
				NodeRole:       "role-arn",
				ClusterName:    testClusterName,
				Version:        ptr.To("1.33"),
				ReleaseVersion: ptr.To("1.33.0-20250101"),
			},
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: "aws-provider"},
			},
		},
	}
	existing := &eksv1alpha1.NodeGroup{
		ObjectMeta: metav1.ObjectMeta{Name: "ng-default", Labels: nodegroupLabels},
		Status: eksv1alpha1.NodeGroupStatus{
			AtProvider: eksv1alpha1.NodeGroupObservation{
				Status: eksv1alpha1.NodeGroupStatusActive,
			},
		},
	}

	agent := &corev1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{Name: "test-agent", Namespace: "default"},
	}
	// VPC/zone/subnet annotations are pre-populated because this test focuses
	// on AMI-type bootstrap, not VPC bootstrap. The 5 AMI-related bootstrap
	// annotations are intentionally absent so reconcile bootstraps AMIType
	// from the admin NG, which is BOTTLEROCKET. This is the path that
	// produces a "native" BOTTLEROCKET Agent.
	util.SetAnnotation(agent, consts.AnnotationKeyO11yNodeVPCName, "vpc-1")
	util.SetAnnotation(agent, consts.AnnotationKeyO11yNodeZone, "us-east-1a")
	util.SetAnnotation(agent, consts.AnnotationKeyO11yNodeSubnet, "subnet-1")
	util.SetAnnotation(agent, consts.AnnotationKeyO11yNodeMinSize, "1")
	util.SetAnnotation(agent, consts.AnnotationKeyO11yNodeMaxSize, "3")
	util.SetAnnotation(agent, consts.AnnotationKeyO11yNodeDesiredSize, "1")
	util.SetAnnotation(agent, consts.AnnotationKeyO11yNodeDiskSize, "20")
	util.SetAnnotation(agent, consts.AnnotationKeyO11yNodeCapacityType, "ON_DEMAND")
	fakeCli := fake.NewClientBuilder().
		WithScheme(testkits.Scheme()).
		WithIndex(&eksv1alpha1.NodeGroup{}, ".spec.forProvider.clusterName", func(o client.Object) []string {
			return []string{o.(*eksv1alpha1.NodeGroup).Spec.ForProvider.ClusterName}
		}).
		WithObjects(agent, adminNG, existing).
		Build()

	nameFunc := func(specName string) string {
		base := "ng-" + specName
		return base + NameSuffix(
			util.GetAnnotation(agent, consts.AnnotationKeyO11yNodeAMIType),
			util.GetAnnotation(agent, consts.AnnotationKeyO11yNodeAMISuffixRequired),
			util.GetAnnotation(agent, consts.AnnotationKeyO11yNodeVersion),
		)
	}

	specs := config.CustomerAgentSpecs{{Name: "default", InstanceType: "t3.medium"}}
	ctx := &reconciler.Context[*corev1.ConfigMap]{
		Agent:  agent,
		Local:  fakeCli,
		Config: &config.RootConfig{O11yConfig: config.ObservabilityInfra{Region: "us-east-1"}},
	}
	task := NewNodeGroupTask(ctx, testClusterName, nameFunc, nodegroupLabels, vpcMatchLabels, specs, nil)

	// First Create: bootstrap copies BOTTLEROCKET from the admin NG; no
	// migration, no flag.
	res1 := task.Create(context.Background())
	assert.Equal(t, deploy.Done(), res1)
	assert.Equal(t, BottlerocketAMIType,
		util.GetAnnotation(agent, consts.AnnotationKeyO11yNodeAMIType))
	assert.Equal(t, "",
		util.GetAnnotation(agent, consts.AnnotationKeyO11yNodeAMISuffixRequired),
		"SuffixRequired flag must NOT be set for native BOTTLEROCKET")

	assert.NoError(t, fakeCli.Get(context.Background(),
		client.ObjectKey{Name: "ng-default"}, &eksv1alpha1.NodeGroup{}))

	err := fakeCli.Get(context.Background(),
		client.ObjectKey{Name: "ng-default-bottlerocket"}, &eksv1alpha1.NodeGroup{})
	assert.True(t, apierrors.IsNotFound(err),
		"suffixed NG must not exist for native BOTTLEROCKET Agent")

	// Idempotent across reconciles.
	res2 := task.Create(context.Background())
	assert.Equal(t, deploy.Done(), res2)
	err = fakeCli.Get(context.Background(),
		client.ObjectKey{Name: "ng-default-bottlerocket"}, &eksv1alpha1.NodeGroup{})
	assert.True(t, apierrors.IsNotFound(err),
		"suffixed NG must still not exist after second reconcile")
}

// TestNodeGroupTask_AL2OnOldVersionNoMigrate verifies that an AL2 Agent on
// k8s 1.32 (at MaxAL2Version) is NOT migrated. The AMIType
// annotation stays AL2, no SuffixRequired flag is set, no suffixed NG is
// created, and the existing unsuffixed NG survives untouched.
//
// Regression for the version-gate: PR #3223 migrated unconditionally, which
// would disrupt every AL2 Agent regardless of whether the EKS AL2 deprecation
// actually applied.
func TestNodeGroupTask_AL2OnOldVersionNoMigrate(t *testing.T) {
	nodegroupLabels := map[string]string{"app": "agent"}
	vpcMatchLabels := map[string]string{"vpc": "test"}

	agent := &corev1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{Name: "test-agent", Namespace: "default"},
	}
	util.SetAnnotation(agent, consts.AnnotationKeyO11yNodeAMIType, AL2AMIType)
	util.SetAnnotation(agent, consts.AnnotationKeyO11yNodeRole, "role-arn")
	util.SetAnnotation(agent, consts.AnnotationKeyO11yNodeProviderConfigReference, "aws-provider")
	// Version at MaxAL2Version — must block migration.
	util.SetAnnotation(agent, consts.AnnotationKeyO11yNodeVersion, "1.32")
	util.SetAnnotation(agent, consts.AnnotationKeyO11yNodeReleaseVersion, "1.32.0-20250101")
	util.SetAnnotation(agent, consts.AnnotationKeyO11yNodeVPCName, "vpc-1")
	util.SetAnnotation(agent, consts.AnnotationKeyO11yNodeZone, "us-east-1a")
	util.SetAnnotation(agent, consts.AnnotationKeyO11yNodeSubnet, "subnet-1")
	util.SetAnnotation(agent, consts.AnnotationKeyO11yNodeMinSize, "1")
	util.SetAnnotation(agent, consts.AnnotationKeyO11yNodeMaxSize, "3")
	util.SetAnnotation(agent, consts.AnnotationKeyO11yNodeDesiredSize, "1")
	util.SetAnnotation(agent, consts.AnnotationKeyO11yNodeDiskSize, "20")
	util.SetAnnotation(agent, consts.AnnotationKeyO11yNodeCapacityType, "ON_DEMAND")

	existing := &eksv1alpha1.NodeGroup{
		ObjectMeta: metav1.ObjectMeta{Name: "ng-default", Labels: nodegroupLabels},
		Status: eksv1alpha1.NodeGroupStatus{
			AtProvider: eksv1alpha1.NodeGroupObservation{
				Status: eksv1alpha1.NodeGroupStatusActive,
			},
		},
	}
	fakeCli := testkits.BuildClient(agent, existing)

	nameFunc := func(specName string) string {
		base := "ng-" + specName
		return base + NameSuffix(
			util.GetAnnotation(agent, consts.AnnotationKeyO11yNodeAMIType),
			util.GetAnnotation(agent, consts.AnnotationKeyO11yNodeAMISuffixRequired),
			util.GetAnnotation(agent, consts.AnnotationKeyO11yNodeVersion),
		)
	}

	specs := config.CustomerAgentSpecs{{Name: "default", InstanceType: "t3.medium"}}
	ctx := &reconciler.Context[*corev1.ConfigMap]{
		Agent:  agent,
		Local:  fakeCli,
		Config: &config.RootConfig{O11yConfig: config.ObservabilityInfra{Region: "us-east-1"}},
	}
	task := NewNodeGroupTask(ctx, testClusterName, nameFunc, nodegroupLabels, vpcMatchLabels, specs, nil)

	// Reconcile must not migrate.
	res1 := task.Create(context.Background())
	assert.Equal(t, deploy.Done(), res1)
	assert.Equal(t, AL2AMIType,
		util.GetAnnotation(agent, consts.AnnotationKeyO11yNodeAMIType),
		"AMIType annotation must remain AL2 below 1.33")
	assert.Equal(t, "",
		util.GetAnnotation(agent, consts.AnnotationKeyO11yNodeAMISuffixRequired),
		"SuffixRequired flag must NOT be set below 1.33")

	// Existing unsuffixed NG survives.
	var ng eksv1alpha1.NodeGroup
	assert.NoError(t, fakeCli.Get(context.Background(),
		client.ObjectKey{Name: "ng-default"}, &ng))
	// NG spec keeps AL2.
	if ng.Spec.ForProvider.AMIType != nil {
		assert.Equal(t, AL2AMIType, *ng.Spec.ForProvider.AMIType,
			"NG AMIType must remain AL2 below 1.33")
	}

	// No suffixed NG must ever be created.
	err := fakeCli.Get(context.Background(),
		client.ObjectKey{Name: "ng-default-bottlerocket"}, &eksv1alpha1.NodeGroup{})
	assert.True(t, apierrors.IsNotFound(err),
		"suffixed NG must not exist when migration is gated off")

	// Idempotent across reconciles.
	res2 := task.Create(context.Background())
	assert.Equal(t, deploy.Done(), res2)
	err = fakeCli.Get(context.Background(),
		client.ObjectKey{Name: "ng-default-bottlerocket"}, &eksv1alpha1.NodeGroup{})
	assert.True(t, apierrors.IsNotFound(err))
}

func TestNodeGroupTask_PostDelete(t *testing.T) {
	nodegroupLabels := map[string]string{"app": "agent"}
	vpcMatchLabels := map[string]string{"vpc": "test"}

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

	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,
				Agent: &corev1.ConfigMap{}, // Agent needed for ApplicationID access if labels depend on it, but here hardcoded
			}
			// Note: In nodegroup.go Delete/PostDelete uses t.nodegroupLabels which are passed in constructor
			// But check code in agentbuild uses t.nodegroupLabels.

			task := NewNodeGroupTask(ctx, testClusterName, nodeNameFunc, nodegroupLabels, vpcMatchLabels, nil, nil)
			res := task.PostDelete(context.Background())
			if tt.wantErr {
				assert.Error(t, res.Error())
				assert.False(t, res.IsDoneState())
			} else {
				assert.NoError(t, res.Error())
				assert.Equal(t, deploy.Done(), res)
			}
		})
	}
}
