package server

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/http/httptest"
	"testing"

	eksv1alpha1 "github.com/crossplane-contrib/provider-aws/apis/eks/manualv1alpha1"
	gkecontainerv1beta1 "github.com/crossplane-contrib/provider-gcp/apis/container/v1beta1"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	aksv1beta1 "github.com/upbound/provider-azure/apis/containerservice/v1beta1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/client"

	agentv2 "github.com/tidbcloud/observability/agent-control-server/api/v2"
	commonazure "github.com/tidbcloud/observability/agent-control-server/pkg/commontasks/azure"
	"github.com/tidbcloud/observability/agent-control-server/pkg/consts"
)

type listErrorClient struct {
	client.Client
	err error
}

func (c listErrorClient) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error {
	return c.err
}

func TestHandleListNodeGroups(t *testing.T) {
	// Production NodeGroups carry a -<specName> suffix (e.g. -medium) chosen by
	// autoresourcing, and are labeled by the controllers with stable keys. The
	// server looks them up by label, so the response fields populate correctly
	// even though the NodeGroupName field in the response is the unsuffixed
	// logical name.
	ng := &eksv1alpha1.NodeGroup{
		ObjectMeta: metav1.ObjectMeta{
			Name: "tidbcloud-o11y-k8s-project-1-medium",
			Labels: map[string]string{
				"o11y.tidbcloud.com/component": "o11y",
				"o11y.tidbcloud.com/project":   "project-1",
			},
		},
		Spec: eksv1alpha1.NodeGroupSpec{
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: "default"},
			},
			ForProvider: eksv1alpha1.NodeGroupParameters{
				ScalingConfig: &eksv1alpha1.NodeGroupScalingConfig{
					DesiredSize: ptr.To[int32](2),
				},
				Version: ptr.To("1.32"),
			},
		},
		Status: eksv1alpha1.NodeGroupStatus{
			AtProvider: eksv1alpha1.NodeGroupObservation{
				Status: eksv1alpha1.NodeGroupStatusActive,
			},
		},
	}
	tidbNg := &eksv1alpha1.NodeGroup{
		ObjectMeta: metav1.ObjectMeta{
			Name: "tidbcloud-o11y-tidb-cluster-abc-medium",
			Labels: map[string]string{
				"o11y.tidbcloud.com/component": "o11y",
				"o11y.tidbcloud.com/cluster":   "cluster-abc",
			},
		},
		Spec: eksv1alpha1.NodeGroupSpec{
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: "default"},
			},
			ForProvider: eksv1alpha1.NodeGroupParameters{
				ScalingConfig: &eksv1alpha1.NodeGroupScalingConfig{
					DesiredSize: ptr.To[int32](3),
				},
				Version: ptr.To("1.31"),
			},
		},
		Status: eksv1alpha1.NodeGroupStatus{
			AtProvider: eksv1alpha1.NodeGroupObservation{
				Status: eksv1alpha1.NodeGroupStatusActive,
			},
		},
	}

	tests := []struct {
		name             string
		query            string
		setupResources   []client.Object
		expectedCode     int
		expectedResponse []NodeGroup
	}{
		{
			name:  "list all o11y nodegroups",
			query: "",
			setupResources: []client.Object{
				&agentv2.O11yK8sAgent{
					ObjectMeta: metav1.ObjectMeta{
						Name: "k8s-agent-1",
						Annotations: map[string]string{
							consts.AnnotationKeyO11yNodeVersion: "1.33", // annotation says 1.33 (upgrade pending)
						},
					},
					Spec: agentv2.O11yK8sAgentSpec{
						KubernetesClusterName: "cluster-1",
						O11y: agentv2.O11yConfig{
							TenantID:      "tenant-1",
							ProjectID:     "project-1",
							ApplicationID: "app-1",
						},
					},
					Status: agentv2.O11yK8sAgentStatus{Phase: "Normal"},
				},
				&agentv2.O11yTiDBAgent{
					ObjectMeta: metav1.ObjectMeta{
						Name: "tidb-agent-1",
						Annotations: map[string]string{
							consts.AnnotationKeyO11yNodeVersion: "1.32",
						},
					},
					Spec: agentv2.O11yTiDBAgentSpec{
						KubernetesClusterName: "cluster-1",
						O11y: agentv2.O11yConfig{
							TenantID:      "tenant-1",
							ProjectID:     "project-1",
							ApplicationID: "cluster-abc",
						},
					},
					Status: agentv2.O11yTiDBAgentStatus{Phase: "Normal"},
				},
				ng,
				tidbNg,
			},
			expectedCode: http.StatusOK,
			expectedResponse: []NodeGroup{
				{
					// Version and count come from the NodeGroup CR, not the annotation.
					// Annotation says 1.33 but NodeGroup CR says 1.32.
					Kind:                  "o11y-k8s-nodes",
					Count:                 2,
					Version:               "1.32",
					Ready:                 true,
					AgentType:             consts.AgentO11yK8s,
					AgentName:             "k8s-agent-1",
					KubernetesClusterName: "cluster-1",
					TenantID:              "tenant-1",
					ProjectID:             "project-1",
					ApplicationID:         "app-1",
					NodeGroupName:         "tidbcloud-o11y-k8s-project-1",
					Phase:                 "Normal",
				},
				{
					// NodeGroup CR with -medium suffix is found via the cluster
					// label even though the response NodeGroupName is the
					// unsuffixed logical name.
					Kind:                  "o11y-tidb-nodes",
					Count:                 3,
					Version:               "1.31",
					Ready:                 true,
					AgentType:             consts.AgentO11yTidb,
					AgentName:             "tidb-agent-1",
					KubernetesClusterName: "cluster-1",
					TenantID:              "tenant-1",
					ProjectID:             "project-1",
					ApplicationID:         "cluster-abc",
					NodeGroupName:         "tidbcloud-o11y-tidb-cluster-abc",
					Phase:                 "Normal",
				},
			},
		},
		{
			name:  "filter by tenantId",
			query: "?tenantId=tenant-1",
			setupResources: []client.Object{
				&agentv2.O11yK8sAgent{
					ObjectMeta: metav1.ObjectMeta{
						Name: "k8s-agent-1",
						Annotations: map[string]string{
							consts.AnnotationKeyO11yNodeVersion: "1.32",
						},
					},
					Spec: agentv2.O11yK8sAgentSpec{
						KubernetesClusterName: "cluster-1",
						O11y: agentv2.O11yConfig{
							TenantID:      "tenant-1",
							ProjectID:     "project-1",
							ApplicationID: "app-1",
						},
					},
					Status: agentv2.O11yK8sAgentStatus{Phase: "Normal"},
				},
				&agentv2.O11yK8sAgent{
					ObjectMeta: metav1.ObjectMeta{
						Name: "k8s-agent-2",
						Annotations: map[string]string{
							consts.AnnotationKeyO11yNodeVersion: "1.31",
						},
					},
					Spec: agentv2.O11yK8sAgentSpec{
						KubernetesClusterName: "cluster-2",
						O11y: agentv2.O11yConfig{
							TenantID:      "tenant-2",
							ProjectID:     "project-2",
							ApplicationID: "app-2",
						},
					},
					Status: agentv2.O11yK8sAgentStatus{Phase: "Normal"},
				},
			},
			expectedCode: http.StatusOK,
			expectedResponse: []NodeGroup{
				{
					// No NodeGroup CR → version and count are empty.
					Kind:                  "o11y-k8s-nodes",
					Count:                 0,
					Version:               "",
					Ready:                 false,
					AgentType:             consts.AgentO11yK8s,
					AgentName:             "k8s-agent-1",
					KubernetesClusterName: "cluster-1",
					TenantID:              "tenant-1",
					ProjectID:             "project-1",
					ApplicationID:         "app-1",
					NodeGroupName:         "tidbcloud-o11y-k8s-project-1",
					Phase:                 "Normal",
				},
			},
		},
		{
			name:  "filter by tenantId and projectId",
			query: "?tenantId=tenant-1&projectId=project-1",
			setupResources: []client.Object{
				&agentv2.O11yK8sAgent{
					ObjectMeta: metav1.ObjectMeta{Name: "k8s-agent-1"},
					Spec: agentv2.O11yK8sAgentSpec{
						KubernetesClusterName: "cluster-1",
						O11y: agentv2.O11yConfig{
							TenantID:      "tenant-1",
							ProjectID:     "project-1",
							ApplicationID: "app-1",
						},
					},
					Status: agentv2.O11yK8sAgentStatus{Phase: "Normal"},
				},
				&agentv2.O11yK8sAgent{
					ObjectMeta: metav1.ObjectMeta{Name: "k8s-agent-2"},
					Spec: agentv2.O11yK8sAgentSpec{
						KubernetesClusterName: "cluster-2",
						O11y: agentv2.O11yConfig{
							TenantID:      "tenant-1",
							ProjectID:     "project-2",
							ApplicationID: "app-2",
						},
					},
					Status: agentv2.O11yK8sAgentStatus{Phase: "Normal"},
				},
			},
			expectedCode: http.StatusOK,
			expectedResponse: []NodeGroup{
				{
					Kind:                  "o11y-k8s-nodes",
					Count:                 0,
					Version:               "",
					Ready:                 false,
					AgentType:             consts.AgentO11yK8s,
					AgentName:             "k8s-agent-1",
					KubernetesClusterName: "cluster-1",
					TenantID:              "tenant-1",
					ProjectID:             "project-1",
					ApplicationID:         "app-1",
					NodeGroupName:         "tidbcloud-o11y-k8s-project-1",
					Phase:                 "Normal",
				},
			},
		},
		{
			name:             "empty list",
			query:            "",
			setupResources:   []client.Object{},
			expectedCode:     http.StatusOK,
			expectedResponse: []NodeGroup{},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s := makeTestServer(tt.setupResources...)
			path := fmt.Sprintf("/ops/nodegroups%s", tt.query)
			req := httptest.NewRequest(http.MethodGet, path, nil)

			w := httptest.NewRecorder()
			s.routeListNodeGroups(w, req)

			assert.Equal(t, tt.expectedCode, w.Code)
			rsp := parseResponse(w)
			data := parseResponseData[ListNodeGroupResponse](rsp)
			assert.Equal(t, len(tt.expectedResponse), data.Total)
			assert.ElementsMatch(t, tt.expectedResponse, data.NodeGroups)
		})
	}
}

func TestGetNodeGroupState(t *testing.T) {
	matchLabels := map[string]string{
		"o11y.tidbcloud.com/component": "o11y",
		"o11y.tidbcloud.com/project":   "project-1",
	}
	const baseName = "tidbcloud-o11y-k8s-project-1"

	tests := []struct {
		name            string
		labels          map[string]string
		baseName        string
		desiredSpec     string
		setupResources  []client.Object
		expectedVersion string
		expectedCount   int
		expectedReady   bool
	}{
		{
			name:        "version from status, count from spec, active",
			labels:      matchLabels,
			baseName:    baseName,
			desiredSpec: "medium",
			setupResources: []client.Object{
				&eksv1alpha1.NodeGroup{
					ObjectMeta: metav1.ObjectMeta{
						Name:   "tidbcloud-o11y-k8s-project-1-medium",
						Labels: matchLabels,
					},
					Spec: eksv1alpha1.NodeGroupSpec{
						ForProvider: eksv1alpha1.NodeGroupParameters{
							ScalingConfig: &eksv1alpha1.NodeGroupScalingConfig{
								DesiredSize: ptr.To[int32](3),
							},
							Version: ptr.To("1.31"),
						},
					},
					Status: eksv1alpha1.NodeGroupStatus{
						AtProvider: eksv1alpha1.NodeGroupObservation{
							Version: "1.32",
							Status:  eksv1alpha1.NodeGroupStatusActive,
						},
					},
				},
			},
			expectedVersion: "1.32", // status takes priority
			expectedCount:   3,
			expectedReady:   true,
		},
		{
			name:        "version from spec when status empty",
			labels:      matchLabels,
			baseName:    baseName,
			desiredSpec: "medium",
			setupResources: []client.Object{
				&eksv1alpha1.NodeGroup{
					ObjectMeta: metav1.ObjectMeta{
						Name:   "tidbcloud-o11y-k8s-project-1-medium",
						Labels: matchLabels,
					},
					Spec: eksv1alpha1.NodeGroupSpec{
						ForProvider: eksv1alpha1.NodeGroupParameters{
							Version: ptr.To("1.33"),
						},
					},
				},
			},
			expectedVersion: "1.33",
			expectedCount:   0,
			expectedReady:   false,
		},
		{
			name:        "count from status when spec empty, updating",
			labels:      matchLabels,
			baseName:    baseName,
			desiredSpec: "medium",
			setupResources: []client.Object{
				&eksv1alpha1.NodeGroup{
					ObjectMeta: metav1.ObjectMeta{
						Name:   "tidbcloud-o11y-k8s-project-1-medium",
						Labels: matchLabels,
					},
					Status: eksv1alpha1.NodeGroupStatus{
						AtProvider: eksv1alpha1.NodeGroupObservation{
							ScalingConfig: eksv1alpha1.NodeGroupScalingConfigStatus{
								DesiredSize: ptr.To[int32](5),
							},
							Status: eksv1alpha1.NodeGroupStatusUpdating,
						},
					},
				},
			},
			expectedVersion: "",
			expectedCount:   5,
			expectedReady:   false,
		},
		{
			// Transient autoresourcing spec-transition: both -medium (previous)
			// and -large (desired) exist. The desired-spec NodeGroup wins even
			// though it is not first alphabetically.
			name:        "multiple nodegroups prefers the desired-spec match",
			labels:      matchLabels,
			baseName:    baseName,
			desiredSpec: "large",
			setupResources: []client.Object{
				&eksv1alpha1.NodeGroup{
					ObjectMeta: metav1.ObjectMeta{
						Name:   "tidbcloud-o11y-k8s-project-1-large",
						Labels: matchLabels,
					},
					Spec: eksv1alpha1.NodeGroupSpec{
						ForProvider: eksv1alpha1.NodeGroupParameters{
							ScalingConfig: &eksv1alpha1.NodeGroupScalingConfig{
								DesiredSize: ptr.To[int32](5),
							},
							Version: ptr.To("1.32"),
						},
					},
					Status: eksv1alpha1.NodeGroupStatus{
						AtProvider: eksv1alpha1.NodeGroupObservation{
							Status: eksv1alpha1.NodeGroupStatusActive,
						},
					},
				},
				&eksv1alpha1.NodeGroup{
					ObjectMeta: metav1.ObjectMeta{
						Name:   "tidbcloud-o11y-k8s-project-1-medium",
						Labels: matchLabels,
					},
					Spec: eksv1alpha1.NodeGroupSpec{
						ForProvider: eksv1alpha1.NodeGroupParameters{
							ScalingConfig: &eksv1alpha1.NodeGroupScalingConfig{
								DesiredSize: ptr.To[int32](2),
							},
							Version: ptr.To("1.30"),
						},
					},
					Status: eksv1alpha1.NodeGroupStatus{
						AtProvider: eksv1alpha1.NodeGroupObservation{
							Status: eksv1alpha1.NodeGroupStatusUpdating,
						},
					},
				},
			},
			expectedVersion: "1.32",
			expectedCount:   5,
			expectedReady:   true,
		},
		{
			// desiredSpec not set or default: fall back to the bare-name
			// NodeGroup when present, otherwise the first item.
			name:        "multiple nodegroups with default spec uses bare name",
			labels:      matchLabels,
			baseName:    baseName,
			desiredSpec: "default",
			setupResources: []client.Object{
				&eksv1alpha1.NodeGroup{
					ObjectMeta: metav1.ObjectMeta{
						Name:   "tidbcloud-o11y-k8s-project-1",
						Labels: matchLabels,
					},
					Spec: eksv1alpha1.NodeGroupSpec{
						ForProvider: eksv1alpha1.NodeGroupParameters{
							ScalingConfig: &eksv1alpha1.NodeGroupScalingConfig{
								DesiredSize: ptr.To[int32](2),
							},
							Version: ptr.To("1.32"),
						},
					},
					Status: eksv1alpha1.NodeGroupStatus{
						AtProvider: eksv1alpha1.NodeGroupObservation{
							Status: eksv1alpha1.NodeGroupStatusActive,
						},
					},
				},
				&eksv1alpha1.NodeGroup{
					ObjectMeta: metav1.ObjectMeta{
						Name:   "tidbcloud-o11y-k8s-project-1-medium",
						Labels: matchLabels,
					},
					Spec: eksv1alpha1.NodeGroupSpec{
						ForProvider: eksv1alpha1.NodeGroupParameters{
							ScalingConfig: &eksv1alpha1.NodeGroupScalingConfig{
								DesiredSize: ptr.To[int32](7),
							},
							Version: ptr.To("1.30"),
						},
					},
					Status: eksv1alpha1.NodeGroupStatus{
						AtProvider: eksv1alpha1.NodeGroupObservation{
							Status: eksv1alpha1.NodeGroupStatusUpdating,
						},
					},
				},
			},
			expectedVersion: "1.32",
			expectedCount:   2,
			expectedReady:   true,
		},
		{
			// No exact name match: fall back to the first item rather than
			// failing the request.
			name:        "multiple nodegroups without match falls back to first",
			labels:      matchLabels,
			baseName:    baseName,
			desiredSpec: "large",
			setupResources: []client.Object{
				&eksv1alpha1.NodeGroup{
					ObjectMeta: metav1.ObjectMeta{
						Name:   "tidbcloud-o11y-k8s-project-1-medium",
						Labels: matchLabels,
					},
					Spec: eksv1alpha1.NodeGroupSpec{
						ForProvider: eksv1alpha1.NodeGroupParameters{
							ScalingConfig: &eksv1alpha1.NodeGroupScalingConfig{
								DesiredSize: ptr.To[int32](2),
							},
							Version: ptr.To("1.32"),
						},
					},
					Status: eksv1alpha1.NodeGroupStatus{
						AtProvider: eksv1alpha1.NodeGroupObservation{
							Status: eksv1alpha1.NodeGroupStatusActive,
						},
					},
				},
				&eksv1alpha1.NodeGroup{
					ObjectMeta: metav1.ObjectMeta{
						Name:   "tidbcloud-o11y-k8s-project-1-xlarge",
						Labels: matchLabels,
					},
					Spec: eksv1alpha1.NodeGroupSpec{
						ForProvider: eksv1alpha1.NodeGroupParameters{
							ScalingConfig: &eksv1alpha1.NodeGroupScalingConfig{
								DesiredSize: ptr.To[int32](7),
							},
							Version: ptr.To("1.30"),
						},
					},
					Status: eksv1alpha1.NodeGroupStatus{
						AtProvider: eksv1alpha1.NodeGroupObservation{
							Status: eksv1alpha1.NodeGroupStatusUpdating,
						},
					},
				},
			},
			expectedVersion: "1.32",
			expectedCount:   2,
			expectedReady:   true,
		},
		{
			name:            "nodegroup not found",
			labels:          matchLabels,
			baseName:        baseName,
			setupResources:  []client.Object{},
			expectedVersion: "",
			expectedCount:   0,
			expectedReady:   false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s := makeTestServer(tt.setupResources...)
			state := s.getNodeGroupState(context.Background(), tt.labels, tt.baseName, tt.desiredSpec)
			assert.Equal(t, tt.expectedVersion, state.Version)
			assert.Equal(t, tt.expectedCount, state.Count)
			assert.Equal(t, tt.expectedReady, state.Ready)
		})
	}
}

func TestGetNodeGroupStateGCP(t *testing.T) {
	labels := map[string]string{
		"o11y.tidbcloud.com/component": "o11y",
		"o11y.tidbcloud.com/project":   "project-1",
	}
	medium := &gkecontainerv1beta1.NodePool{
		ObjectMeta: metav1.ObjectMeta{
			Name:   "o11y-k8s-project-1-medium",
			Labels: labels,
		},
		Spec: gkecontainerv1beta1.NodePoolSpec{
			ForProvider: gkecontainerv1beta1.NodePoolParameters{
				InitialNodeCount: ptr.To[int64](2),
				Version:          ptr.To("1.31"),
			},
		},
	}
	large := &gkecontainerv1beta1.NodePool{
		ObjectMeta: metav1.ObjectMeta{
			Name:   "o11y-k8s-project-1-large",
			Labels: labels,
		},
		Spec: gkecontainerv1beta1.NodePoolSpec{
			ForProvider: gkecontainerv1beta1.NodePoolParameters{
				InitialNodeCount: ptr.To[int64](5),
				Version:          ptr.To("1.32"),
			},
		},
		Status: gkecontainerv1beta1.NodePoolStatus{
			AtProvider: gkecontainerv1beta1.NodePoolObservation{Status: gkecontainerv1beta1.NodePoolStateRunning},
		},
	}
	readyByCondition := &gkecontainerv1beta1.NodePool{
		ObjectMeta: metav1.ObjectMeta{
			Name:   "tidbcloud-o11y-k8s-project-1",
			Labels: labels,
		},
		Spec: gkecontainerv1beta1.NodePoolSpec{
			ForProvider: gkecontainerv1beta1.NodePoolParameters{
				InitialNodeCount: ptr.To[int64](3),
				Version:          ptr.To("1.33"),
			},
		},
	}
	readyByCondition.Status.SetConditions(xpv1.Available())
	defaultPool := &gkecontainerv1beta1.NodePool{
		ObjectMeta: metav1.ObjectMeta{
			Name:   "tidbcloud-o11y-k8s-project-1",
			Labels: labels,
		},
		Spec: gkecontainerv1beta1.NodePoolSpec{
			ForProvider: gkecontainerv1beta1.NodePoolParameters{
				InitialNodeCount: ptr.To[int64](1),
				Version:          ptr.To("1.30"),
			},
		},
		Status: gkecontainerv1beta1.NodePoolStatus{
			AtProvider: gkecontainerv1beta1.NodePoolObservation{Status: gkecontainerv1beta1.NodePoolStateRunning},
		},
	}
	notReady := &gkecontainerv1beta1.NodePool{
		ObjectMeta: metav1.ObjectMeta{
			Name:   "tidbcloud-o11y-k8s-project-1",
			Labels: labels,
		},
		Spec: gkecontainerv1beta1.NodePoolSpec{
			ForProvider: gkecontainerv1beta1.NodePoolParameters{
				InitialNodeCount: ptr.To[int64](1),
				Version:          ptr.To("1.30"),
			},
		},
		Status: gkecontainerv1beta1.NodePoolStatus{
			AtProvider: gkecontainerv1beta1.NodePoolObservation{Status: gkecontainerv1beta1.NodePoolStateReconciling},
		},
	}
	notReadyByCondition := &gkecontainerv1beta1.NodePool{
		ObjectMeta: metav1.ObjectMeta{
			Name:   "tidbcloud-o11y-k8s-project-1",
			Labels: labels,
		},
		Spec: gkecontainerv1beta1.NodePoolSpec{
			ForProvider: gkecontainerv1beta1.NodePoolParameters{
				InitialNodeCount: ptr.To[int64](1),
				Version:          ptr.To("1.30"),
			},
		},
	}
	notReadyByCondition.Status.SetConditions(xpv1.Creating())

	tests := []struct {
		name        string
		desiredSpec string
		resources   []client.Object
		version     string
		count       int
		ready       bool
	}{
		{
			name:        "prefers desired shortened name",
			desiredSpec: "large",
			resources:   []client.Object{medium, large},
			version:     "1.32",
			count:       5,
			ready:       true,
		},
		{
			name:        "uses Ready condition when provider status is empty",
			desiredSpec: "default",
			resources:   []client.Object{readyByCondition},
			version:     "1.33",
			count:       3,
			ready:       true,
		},
		{
			name:        "falls back to the default nodepool",
			desiredSpec: "large",
			resources:   []client.Object{defaultPool},
			version:     "1.30",
			count:       1,
			ready:       true,
		},
		{
			name:        "reports non-running nodepool as not ready",
			desiredSpec: "default",
			resources:   []client.Object{notReady},
			version:     "1.30",
			count:       1,
			ready:       false,
		},
		{
			name:        "reports false Ready condition as not ready",
			desiredSpec: "default",
			resources:   []client.Object{notReadyByCondition},
			version:     "1.30",
			count:       1,
			ready:       false,
		},
		{
			name:        "returns empty state when nodepool is absent",
			desiredSpec: "default",
			version:     "",
			count:       0,
			ready:       false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s := makeTestServer(tt.resources...)
			s.config.O11yConfig.Vendor = consts.VendorTypeGCP

			state := s.getNodeGroupState(context.Background(), labels, "tidbcloud-o11y-k8s-project-1", tt.desiredSpec)

			assert.Equal(t, tt.version, state.Version)
			assert.Equal(t, tt.count, state.Count)
			assert.Equal(t, tt.ready, state.Ready)
		})
	}

	t.Run("returns empty state when list fails", func(t *testing.T) {
		s := makeTestServer()
		s.config.O11yConfig.Vendor = consts.VendorTypeGCP
		s.client = listErrorClient{Client: s.client, err: errors.New("list nodepools")}

		state := s.getNodeGroupState(context.Background(), labels, "tidbcloud-o11y-k8s-project-1", "default")

		assert.Empty(t, state.Version)
		assert.Zero(t, state.Count)
		assert.False(t, state.Ready)
	})
}

func TestGetNodeGroupStateGCPTiDBAgent(t *testing.T) {
	labels := map[string]string{
		"o11y.tidbcloud.com/component": "o11y",
		"o11y.tidbcloud.com/cluster":   "application-1",
	}
	s := makeTestServer(
		&gkecontainerv1beta1.NodePool{
			ObjectMeta: metav1.ObjectMeta{
				Name:   "o11y-tidb-application-1-medium",
				Labels: labels,
			},
			Spec: gkecontainerv1beta1.NodePoolSpec{
				ForProvider: gkecontainerv1beta1.NodePoolParameters{
					InitialNodeCount: ptr.To[int64](2),
					Version:          ptr.To("1.31"),
				},
			},
		},
		&gkecontainerv1beta1.NodePool{
			ObjectMeta: metav1.ObjectMeta{
				Name:   "o11y-tidb-application-1-large",
				Labels: labels,
			},
			Spec: gkecontainerv1beta1.NodePoolSpec{
				ForProvider: gkecontainerv1beta1.NodePoolParameters{
					InitialNodeCount: ptr.To[int64](5),
					Version:          ptr.To("1.32"),
				},
			},
			Status: gkecontainerv1beta1.NodePoolStatus{
				AtProvider: gkecontainerv1beta1.NodePoolObservation{Status: "RUNNING"},
			},
		},
	)
	s.config.O11yConfig.Vendor = consts.VendorTypeGCP

	state := s.getNodeGroupState(context.Background(), labels, "tidbcloud-o11y-tidb-application-1", "large")

	assert.Equal(t, "1.32", state.Version)
	assert.Equal(t, 5, state.Count)
	assert.True(t, state.Ready)
}

func TestHandleListNodeGroupsGCP(t *testing.T) {
	labels := map[string]string{
		"o11y.tidbcloud.com/component": "o11y",
		"o11y.tidbcloud.com/project":   "project-1",
	}
	s := makeTestServer(
		&agentv2.O11yK8sAgent{
			ObjectMeta: metav1.ObjectMeta{
				Name: "k8s-agent-1",
				Annotations: map[string]string{
					consts.AnnotationKeyO11yDesiredSpec: "large",
				},
			},
			Spec: agentv2.O11yK8sAgentSpec{
				KubernetesClusterName: "cluster-1",
				O11y: agentv2.O11yConfig{
					TenantID:      "tenant-1",
					ProjectID:     "project-1",
					ApplicationID: "application-1",
				},
			},
			Status: agentv2.O11yK8sAgentStatus{Phase: agentv2.O11yAgentPhaseNormal},
		},
		&gkecontainerv1beta1.NodePool{
			ObjectMeta: metav1.ObjectMeta{Name: "o11y-k8s-project-1-large", Labels: labels},
			Spec: gkecontainerv1beta1.NodePoolSpec{
				ForProvider: gkecontainerv1beta1.NodePoolParameters{
					InitialNodeCount: ptr.To[int64](5),
					Version:          ptr.To("1.32"),
				},
			},
			Status: gkecontainerv1beta1.NodePoolStatus{
				AtProvider: gkecontainerv1beta1.NodePoolObservation{Status: gkecontainerv1beta1.NodePoolStateRunning},
			},
		},
	)
	s.config.O11yConfig.Vendor = consts.VendorTypeGCP

	w := httptest.NewRecorder()
	s.routeListNodeGroups(w, httptest.NewRequest(http.MethodGet, "/ops/nodegroups", nil))

	require.Equal(t, http.StatusOK, w.Code)
	data := parseResponseData[ListNodeGroupResponse](parseResponse(w))
	require.Len(t, data.NodeGroups, 1)
	assert.Equal(t, NodeGroup{
		Kind:                  "o11y-k8s-nodes",
		Count:                 5,
		Version:               "1.32",
		Ready:                 true,
		AgentType:             consts.AgentO11yK8s,
		AgentName:             "k8s-agent-1",
		KubernetesClusterName: "cluster-1",
		TenantID:              "tenant-1",
		ProjectID:             "project-1",
		ApplicationID:         "application-1",
		NodeGroupName:         "tidbcloud-o11y-k8s-project-1",
		Phase:                 agentv2.O11yAgentPhaseNormal,
	}, data.NodeGroups[0])
}

func TestGetNodeGroupStateAzure(t *testing.T) {
	labels := map[string]string{
		"o11y.tidbcloud.com/component": "o11y",
		"o11y.tidbcloud.com/project":   "project-1",
	}
	base := "tidbcloud-o11y-k8s-project-1"

	// Two spec variants during an autoresourcing transition.
	medium := &aksv1beta1.KubernetesClusterNodePool{
		ObjectMeta: metav1.ObjectMeta{
			Name:   commonazure.DedicatedNodePoolName(base, "medium"),
			Labels: labels,
		},
		Spec: aksv1beta1.KubernetesClusterNodePoolSpec{
			ForProvider: aksv1beta1.KubernetesClusterNodePoolParameters{
				NodeCount:           ptr.To[float64](2),
				OrchestratorVersion: ptr.To("1.31"),
			},
		},
	}
	large := &aksv1beta1.KubernetesClusterNodePool{
		ObjectMeta: metav1.ObjectMeta{
			Name:   commonazure.DedicatedNodePoolName(base, "large"),
			Labels: labels,
		},
		Spec: aksv1beta1.KubernetesClusterNodePoolSpec{
			ForProvider: aksv1beta1.KubernetesClusterNodePoolParameters{
				NodeCount:           ptr.To[float64](5),
				OrchestratorVersion: ptr.To("1.32"),
			},
		},
	}
	large.Status.SetConditions(xpv1.Available())

	// Status preferred over spec for version; spec preferred over status for count.
	statusPreferred := &aksv1beta1.KubernetesClusterNodePool{
		ObjectMeta: metav1.ObjectMeta{
			Name:   commonazure.DedicatedNodePoolName(base, "default"),
			Labels: labels,
		},
		Spec: aksv1beta1.KubernetesClusterNodePoolSpec{
			ForProvider: aksv1beta1.KubernetesClusterNodePoolParameters{
				NodeCount:           ptr.To[float64](3),
				OrchestratorVersion: ptr.To("1.30"),
			},
		},
		Status: aksv1beta1.KubernetesClusterNodePoolStatus{
			AtProvider: aksv1beta1.KubernetesClusterNodePoolObservation{
				OrchestratorVersion: ptr.To("1.33"),
				NodeCount:           ptr.To[float64](99),
			},
		},
	}
	statusPreferred.Status.SetConditions(xpv1.Available())

	// Only spec populated (e.g. before terraform sync observes the values).
	specOnly := &aksv1beta1.KubernetesClusterNodePool{
		ObjectMeta: metav1.ObjectMeta{
			Name:   commonazure.DedicatedNodePoolName(base, "default"),
			Labels: labels,
		},
		Spec: aksv1beta1.KubernetesClusterNodePoolSpec{
			ForProvider: aksv1beta1.KubernetesClusterNodePoolParameters{
				NodeCount:           ptr.To[float64](4),
				OrchestratorVersion: ptr.To("1.29"),
			},
		},
	}
	specOnly.Status.SetConditions(xpv1.Available())

	// Only status populated.
	statusOnly := &aksv1beta1.KubernetesClusterNodePool{
		ObjectMeta: metav1.ObjectMeta{
			Name:   commonazure.DedicatedNodePoolName(base, "default"),
			Labels: labels,
		},
		Status: aksv1beta1.KubernetesClusterNodePoolStatus{
			AtProvider: aksv1beta1.KubernetesClusterNodePoolObservation{
				OrchestratorVersion: ptr.To("1.28"),
				NodeCount:           ptr.To[float64](7),
			},
		},
	}
	statusOnly.Status.SetConditions(xpv1.Available())

	notReadyByCondition := &aksv1beta1.KubernetesClusterNodePool{
		ObjectMeta: metav1.ObjectMeta{
			Name:   commonazure.DedicatedNodePoolName(base, "default"),
			Labels: labels,
		},
		Spec: aksv1beta1.KubernetesClusterNodePoolSpec{
			ForProvider: aksv1beta1.KubernetesClusterNodePoolParameters{
				NodeCount:           ptr.To[float64](1),
				OrchestratorVersion: ptr.To("1.30"),
			},
		},
	}
	notReadyByCondition.Status.SetConditions(xpv1.Creating())

	tests := []struct {
		name        string
		desiredSpec string
		resources   []client.Object
		version     string
		count       int
		ready       bool
	}{
		{
			name:        "prefers desired-spec name during autoresourcing transition",
			desiredSpec: "large",
			resources:   []client.Object{medium, large},
			version:     "1.32",
			count:       5,
			ready:       true,
		},
		{
			name:        "version prefers observed status over spec; count prefers spec over status",
			desiredSpec: "default",
			resources:   []client.Object{statusPreferred},
			version:     "1.33",
			count:       3,
			ready:       true,
		},
		{
			name:        "falls back to spec when status fields are absent",
			desiredSpec: "default",
			resources:   []client.Object{specOnly},
			version:     "1.29",
			count:       4,
			ready:       true,
		},
		{
			name:        "falls back to status when spec fields are absent",
			desiredSpec: "default",
			resources:   []client.Object{statusOnly},
			version:     "1.28",
			count:       7,
			ready:       true,
		},
		{
			name:        "reports false Ready condition as not ready",
			desiredSpec: "default",
			resources:   []client.Object{notReadyByCondition},
			version:     "1.30",
			count:       1,
			ready:       false,
		},
		{
			name:        "returns empty state when nodepool is absent",
			desiredSpec: "default",
			version:     "",
			count:       0,
			ready:       false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s := makeTestServer(tt.resources...)
			s.config.O11yConfig.Vendor = consts.VendorTypeAzure

			state := s.getNodeGroupState(context.Background(), labels, base, tt.desiredSpec)

			assert.Equal(t, tt.version, state.Version)
			assert.Equal(t, tt.count, state.Count)
			assert.Equal(t, tt.ready, state.Ready)
		})
	}

	t.Run("returns empty state when list fails", func(t *testing.T) {
		s := makeTestServer()
		s.config.O11yConfig.Vendor = consts.VendorTypeAzure
		s.client = listErrorClient{Client: s.client, err: errors.New("list nodepools")}

		state := s.getNodeGroupState(context.Background(), labels, base, "default")

		assert.Empty(t, state.Version)
		assert.Zero(t, state.Count)
		assert.False(t, state.Ready)
	})
}

func TestGetNodeGroupStateAzureTiDBAgent(t *testing.T) {
	labels := map[string]string{
		"o11y.tidbcloud.com/component": "o11y",
		"o11y.tidbcloud.com/cluster":   "application-1",
	}
	base := "tidbcloud-o11y-tidb-application-1"
	s := makeTestServer(
		&aksv1beta1.KubernetesClusterNodePool{
			ObjectMeta: metav1.ObjectMeta{
				Name:   commonazure.DedicatedNodePoolName(base, "large"),
				Labels: labels,
			},
			Spec: aksv1beta1.KubernetesClusterNodePoolSpec{
				ForProvider: aksv1beta1.KubernetesClusterNodePoolParameters{
					NodeCount:           ptr.To[float64](5),
					OrchestratorVersion: ptr.To("1.32"),
				},
			},
		},
	)
	s.config.O11yConfig.Vendor = consts.VendorTypeAzure

	state := s.getNodeGroupState(context.Background(), labels, base, "large")

	// Without a Ready condition set, the AKS nodepool is treated as not ready.
	assert.Equal(t, "1.32", state.Version)
	assert.Equal(t, 5, state.Count)
	assert.False(t, state.Ready)
}

func TestHandleListNodeGroupsAzure(t *testing.T) {
	labels := map[string]string{
		"o11y.tidbcloud.com/component": "o11y",
		"o11y.tidbcloud.com/project":   "project-1",
	}
	base := "tidbcloud-o11y-k8s-project-1"
	nodePool := &aksv1beta1.KubernetesClusterNodePool{
		ObjectMeta: metav1.ObjectMeta{
			Name:   commonazure.DedicatedNodePoolName(base, "large"),
			Labels: labels,
		},
		Spec: aksv1beta1.KubernetesClusterNodePoolSpec{
			ForProvider: aksv1beta1.KubernetesClusterNodePoolParameters{
				NodeCount:           ptr.To[float64](5),
				OrchestratorVersion: ptr.To("1.32"),
			},
		},
	}
	nodePool.Status.SetConditions(xpv1.Available())

	s := makeTestServer(
		&agentv2.O11yK8sAgent{
			ObjectMeta: metav1.ObjectMeta{
				Name: "k8s-agent-1",
				Annotations: map[string]string{
					consts.AnnotationKeyO11yDesiredSpec: "large",
				},
			},
			Spec: agentv2.O11yK8sAgentSpec{
				KubernetesClusterName: "cluster-1",
				O11y: agentv2.O11yConfig{
					TenantID:      "tenant-1",
					ProjectID:     "project-1",
					ApplicationID: "application-1",
				},
			},
			Status: agentv2.O11yK8sAgentStatus{Phase: agentv2.O11yAgentPhaseNormal},
		},
		nodePool,
	)
	s.config.O11yConfig.Vendor = consts.VendorTypeAzure

	w := httptest.NewRecorder()
	s.routeListNodeGroups(w, httptest.NewRequest(http.MethodGet, "/ops/nodegroups", nil))

	require.Equal(t, http.StatusOK, w.Code)
	data := parseResponseData[ListNodeGroupResponse](parseResponse(w))
	require.Len(t, data.NodeGroups, 1)
	assert.Equal(t, NodeGroup{
		Kind:                  "o11y-k8s-nodes",
		Count:                 5,
		Version:               "1.32",
		Ready:                 true,
		AgentType:             consts.AgentO11yK8s,
		AgentName:             "k8s-agent-1",
		KubernetesClusterName: "cluster-1",
		TenantID:              "tenant-1",
		ProjectID:             "project-1",
		ApplicationID:         "application-1",
		NodeGroupName:         base,
		Phase:                 agentv2.O11yAgentPhaseNormal,
	}, data.NodeGroups[0])
}

func TestTargetToAgentType(t *testing.T) {
	tests := []struct {
		target   string
		expected string
	}{
		{"o11y-k8s-nodes", consts.AgentO11yK8s},
		{"o11y-tidb-nodes", consts.AgentO11yTidb},
		{"invalid", ""},
	}
	for _, tt := range tests {
		t.Run(tt.target, func(t *testing.T) {
			assert.Equal(t, tt.expected, targetToAgentType(tt.target))
		})
	}
}

func TestHandlePostNodeGroupsUpgrade(t *testing.T) {
	tests := []struct {
		name                   string
		requestBody            NodeGroupsUpgradeRequest
		setupResources         []client.Object
		expectedCode           int
		expectedCurrentVersion string
		expectedTargetVersion  string
	}{
		{
			name: "upgrade o11y-k8s-nodes",
			requestBody: NodeGroupsUpgradeRequest{
				Version:   "1.33",
				AgentName: "k8s-agent-1",
				Kind:      "o11y-k8s-nodes",
			},
			setupResources: []client.Object{
				&agentv2.O11yK8sAgent{
					ObjectMeta: metav1.ObjectMeta{
						Name: "k8s-agent-1",
						Annotations: map[string]string{
							consts.AnnotationKeyO11yNodeVersion: "1.32",
						},
					},
					Status: agentv2.O11yK8sAgentStatus{Phase: "Normal"},
				},
			},
			expectedCode:           http.StatusOK,
			expectedCurrentVersion: "1.32",
			expectedTargetVersion:  "1.33",
		},
		{
			name: "skip when version matches",
			requestBody: NodeGroupsUpgradeRequest{
				Version:   "1.32",
				AgentName: "k8s-agent-1",
				Kind:      "o11y-k8s-nodes",
			},
			setupResources: []client.Object{
				&agentv2.O11yK8sAgent{
					ObjectMeta: metav1.ObjectMeta{
						Name: "k8s-agent-1",
						Annotations: map[string]string{
							consts.AnnotationKeyO11yNodeVersion: "1.32",
						},
					},
					Status: agentv2.O11yK8sAgentStatus{Phase: "Normal"},
				},
			},
			expectedCode:           http.StatusOK,
			expectedCurrentVersion: "1.32",
			expectedTargetVersion:  "1.32",
		},
		{
			name: "upgrade o11y-tidb-nodes",
			requestBody: NodeGroupsUpgradeRequest{
				Version:   "1.33",
				AgentName: "tidb-agent-1",
				Kind:      "o11y-tidb-nodes",
			},
			setupResources: []client.Object{
				&agentv2.O11yTiDBAgent{
					ObjectMeta: metav1.ObjectMeta{
						Name: "tidb-agent-1",
						Annotations: map[string]string{
							consts.AnnotationKeyO11yNodeVersion: "1.31",
						},
					},
					Status: agentv2.O11yTiDBAgentStatus{Phase: "Normal"},
				},
			},
			expectedCode:           http.StatusOK,
			expectedCurrentVersion: "1.31",
			expectedTargetVersion:  "1.33",
		},
		{
			name: "Hibernated phase allowed",
			requestBody: NodeGroupsUpgradeRequest{
				Version:   "1.33",
				AgentName: "k8s-agent-1",
				Kind:      "o11y-k8s-nodes",
			},
			setupResources: []client.Object{
				&agentv2.O11yK8sAgent{
					ObjectMeta: metav1.ObjectMeta{
						Name: "k8s-agent-1",
						Annotations: map[string]string{
							consts.AnnotationKeyO11yNodeVersion: "1.32",
						},
					},
					Status: agentv2.O11yK8sAgentStatus{Phase: agentv2.O11yAgentPhaseHibernated},
				},
			},
			expectedCode:           http.StatusOK,
			expectedCurrentVersion: "1.32",
			expectedTargetVersion:  "1.33",
		},
		{
			name: "disallowed phase rejected",
			requestBody: NodeGroupsUpgradeRequest{
				Version:   "1.33",
				AgentName: "k8s-agent-1",
				Kind:      "o11y-k8s-nodes",
			},
			setupResources: []client.Object{
				&agentv2.O11yK8sAgent{
					ObjectMeta: metav1.ObjectMeta{
						Name: "k8s-agent-1",
						Annotations: map[string]string{
							consts.AnnotationKeyO11yNodeVersion: "1.32",
						},
					},
					Status: agentv2.O11yK8sAgentStatus{Phase: "Upgrading"},
				},
			},
			expectedCode: http.StatusConflict,
		},
		{
			name: "missing version",
			requestBody: NodeGroupsUpgradeRequest{
				AgentName: "k8s-agent-1",
				Kind:      "o11y-k8s-nodes",
			},
			setupResources: []client.Object{},
			expectedCode:   http.StatusBadRequest,
		},
		{
			name: "missing agentName",
			requestBody: NodeGroupsUpgradeRequest{
				Version: "1.33",
				Kind:    "o11y-k8s-nodes",
			},
			setupResources: []client.Object{},
			expectedCode:   http.StatusBadRequest,
		},
		{
			name: "missing target",
			requestBody: NodeGroupsUpgradeRequest{
				Version:   "1.33",
				AgentName: "k8s-agent-1",
			},
			setupResources: []client.Object{},
			expectedCode:   http.StatusBadRequest,
		},
		{
			name: "invalid target",
			requestBody: NodeGroupsUpgradeRequest{
				Version:   "1.33",
				AgentName: "k8s-agent-1",
				Kind:      "invalid",
			},
			setupResources: []client.Object{},
			expectedCode:   http.StatusBadRequest,
		},
		{
			name: "agent not found",
			requestBody: NodeGroupsUpgradeRequest{
				Version:   "1.33",
				AgentName: "non-existent",
				Kind:      "o11y-k8s-nodes",
			},
			setupResources: []client.Object{},
			expectedCode:   http.StatusNotFound,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s := makeTestServer(tt.setupResources...)
			bs, err := json.Marshal(tt.requestBody)
			require.NoError(t, err)
			req := httptest.NewRequest(http.MethodPost, "/ops/nodegroups/upgrade", bytes.NewReader(bs))

			w := httptest.NewRecorder()
			s.routePostNodeGroupsUpgrade(w, req)

			assert.Equal(t, tt.expectedCode, w.Code)
			if tt.expectedCode != http.StatusOK {
				if tt.expectedCode == http.StatusConflict {
					assert.Contains(t, w.Body.String(), tt.requestBody.Kind)
					assert.Contains(t, w.Body.String(), tt.requestBody.AgentName)
				}
				return
			}

			rsp := parseResponse(w)
			data := parseResponseData[NodeGroupsUpgradeResponse](rsp)
			assert.Equal(t, tt.expectedCurrentVersion, data.CurrentVersion)
			assert.Equal(t, tt.expectedTargetVersion, data.TargetVersion)

			if tt.expectedCurrentVersion != tt.expectedTargetVersion {
				obj, err := s.getAgentObject(context.Background(), targetToAgentType(tt.requestBody.Kind), tt.requestBody.AgentName)
				require.NoError(t, err)
				assert.Equal(t, tt.expectedTargetVersion, obj.GetAnnotations()[consts.AnnotationKeyO11yNodeVersion])
			}
		})
	}
}

func TestGetAgentPhase(t *testing.T) {
	tests := []struct {
		name     string
		agent    client.Object
		expected string
	}{
		{
			name:     "returns phase from O11yTiDBAgent",
			agent:    &agentv2.O11yTiDBAgent{Status: agentv2.O11yTiDBAgentStatus{Phase: "Running"}},
			expected: "Running",
		},
		{
			name:     "returns phase from O11yK8sAgent",
			agent:    &agentv2.O11yK8sAgent{Status: agentv2.O11yK8sAgentStatus{Phase: "Deleting"}},
			expected: "Deleting",
		},
		{
			name:     "returns empty for unknown agent type",
			agent:    &agentv2.KubernetesAgent{},
			expected: "",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert.Equal(t, tt.expected, getAgentPhase(tt.agent))
		})
	}
}

func TestPickNodeGroup(t *testing.T) {
	const base = "tidbcloud-o11y-k8s-project-1"
	ng := func(name string) eksv1alpha1.NodeGroup {
		return eksv1alpha1.NodeGroup{ObjectMeta: metav1.ObjectMeta{Name: name}}
	}
	tests := []struct {
		name        string
		items       []eksv1alpha1.NodeGroup
		desiredSpec string
		want        string
	}{
		{
			name:        "default spec unsuffixed",
			items:       []eksv1alpha1.NodeGroup{ng(base)},
			desiredSpec: "",
			want:        base,
		},
		{
			name:        "non-default spec picks spec suffix",
			items:       []eksv1alpha1.NodeGroup{ng(base + "-medium"), ng(base)},
			desiredSpec: "medium",
			want:        base + "-medium",
		},
		{
			name:        "bottlerocket suffix preferred over legacy during migration",
			items:       []eksv1alpha1.NodeGroup{ng(base), ng(base + "-bottlerocket")},
			desiredSpec: "",
			want:        base + "-bottlerocket",
		},
		{
			name:        "spec and bottlerocket suffix combined wins",
			items:       []eksv1alpha1.NodeGroup{ng(base + "-medium"), ng(base + "-medium-bottlerocket")},
			desiredSpec: "medium",
			want:        base + "-medium-bottlerocket",
		},
		{
			name:        "falls back to base when spec variant absent",
			items:       []eksv1alpha1.NodeGroup{ng(base)},
			desiredSpec: "large",
			want:        base,
		},
		{
			name:        "falls back to first item when nothing matches",
			items:       []eksv1alpha1.NodeGroup{ng("unrelated-1"), ng("unrelated-2")},
			desiredSpec: "medium",
			want:        "unrelated-1",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := pickNodeGroup(tt.items, base, tt.desiredSpec)
			assert.NotNil(t, got)
			assert.Equal(t, tt.want, got.Name)
		})
	}
}
