package server

import (
	"context"
	"errors"
	"strings"
	"testing"

	"buf.build/go/protovalidate"
	gkev1beta1 "github.com/crossplane-contrib/provider-gcp/apis/container/v1beta1"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	xpmeta "github.com/crossplane/crossplane-runtime/pkg/meta"
	"github.com/stretchr/testify/require"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/proto"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"

	internalutil "github.com/tidbcloud/cluster-controller/server/internal/util"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	apiv1 "github.com/tidbcloud/infra-provider/api/tidbcloud/cluster-server/v1"
	apicommon "github.com/tidbcloud/infra-provider/api/tidbcloud/common"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	shared "github.com/tidbcloud/infra-provider/pkg/controllers/common"
	"github.com/tidbcloud/infra-provider/pkg/gkeheterogeneous"
	"github.com/tidbcloud/infra-provider/pkg/namer"
	nodepoolutil "github.com/tidbcloud/infra-provider/pkg/nodepool"
	"github.com/tidbcloud/infra-provider/pkg/scheme"
)

const (
	testGKENodePoolMachineType16 = "n2-standard-16"
	testGKENodePoolMachineType64 = "n2-standard-64"
	testGKESharedNetworkName     = "shared-network"
	testGKEOtherNetworkName      = "other-network"
	testGKERegionName            = "us-west-2"
	testGKETenantID              = "tenant-1"
	testGKEProjectID             = "project-1"
)

func updateTestGKEComputeClassBinding(t *testing.T, cluster *cloudv1.Cluster, key, _ string, instanceTypes, nodePoolNames []string) string {
	t.Helper()
	require.NotEmpty(t, instanceTypes)
	require.Len(t, nodePoolNames, len(instanceTypes))
	annotation := cloudv1.GKEComputeClassBindingAnnotation{
		NodePools: make([]cloudv1.GKEComputeClassNodePoolBinding, 0, len(instanceTypes)),
	}
	for i, instanceType := range instanceTypes {
		role := cloudv1.GKEComputeClassNodePoolRoleExtra
		if i == 0 {
			role = cloudv1.GKEComputeClassNodePoolRoleBase
		}
		annotation.NodePools = append(annotation.NodePools, cloudv1.GKEComputeClassNodePoolBinding{
			Role:         role,
			InstanceType: instanceType,
			NodePoolName: nodePoolNames[i],
		})
	}
	computeClassName, err := cloudv1.GKEComputeClassNameForBindings(annotation.NodePools)
	require.NoError(t, err)
	annotation.ComputeClassName = computeClassName
	value, err := cloudv1.EncodeGKEComputeClassBindingAnnotation(annotation, key, instanceTypes[0], nodePoolNames[0])
	require.NoError(t, err)
	cluster.UpdateAnnotation(key, value)
	return computeClassName
}

func requireTestGKEComputeClassBinding(t *testing.T, cluster *cloudv1.Cluster, key, _ string, instanceTypes, nodePoolNames []string) {
	t.Helper()
	annotation, explicit, err := cloudv1.ClusterGKEComputeClassBindingAnnotation(cluster, key, instanceTypes[0], nodePoolNames[0])
	require.NoError(t, err)
	require.True(t, explicit)
	computeClassName, err := cloudv1.GKEComputeClassNameForBindings(annotation.NodePools)
	require.NoError(t, err)
	require.Equal(t, computeClassName, annotation.ComputeClassName)
	require.Len(t, annotation.NodePools, len(instanceTypes))
	for i, binding := range annotation.NodePools {
		require.Equal(t, instanceTypes[i], binding.InstanceType)
		require.Equal(t, nodePoolNames[i], binding.NodePoolName)
		if i == 0 {
			require.Equal(t, cloudv1.GKEComputeClassNodePoolRoleBase, binding.Role)
		} else {
			require.Equal(t, cloudv1.GKEComputeClassNodePoolRoleExtra, binding.Role)
		}
	}
}

func testGKEComputeClassName(t *testing.T, cluster *cloudv1.Cluster, componentType apicommon.ComponentType, instanceTypes ...string) string {
	t.Helper()
	switch componentType {
	case apicommon.ComponentType_TIKV, apicommon.ComponentType_TIDB:
	default:
		t.Fatalf("unsupported component type %s", componentType)
	}
	nodePoolNames := make([]string, 0, len(instanceTypes))
	for _, instanceType := range instanceTypes {
		nodePoolNames = append(nodePoolNames, gkeheterogeneous.NodePoolName(cluster, instanceType))
	}
	name, err := cloudv1.GKEComputeClassNameForBindings(testGKEComputeClassBindings(t, instanceTypes, nodePoolNames))
	require.NoError(t, err)
	return name
}

func testGKEComputeClassBindings(t *testing.T, instanceTypes, nodePoolNames []string) []cloudv1.GKEComputeClassNodePoolBinding {
	t.Helper()
	require.Len(t, instanceTypes, 2)
	require.Len(t, nodePoolNames, len(instanceTypes))
	bindings, err := cloudv1.NormalizeGKEComputeClassOrderedBindings([]cloudv1.GKEComputeClassNodePoolBinding{
		{
			Role:         cloudv1.GKEComputeClassNodePoolRoleBase,
			InstanceType: instanceTypes[0],
			NodePoolName: nodePoolNames[0],
		},
		{
			Role:         cloudv1.GKEComputeClassNodePoolRoleExtra,
			InstanceType: instanceTypes[1],
			NodePoolName: nodePoolNames[1],
		},
	})
	require.NoError(t, err)
	return bindings
}

type createAlreadyExistsNodePoolClient struct {
	client.Client
	existing *gkev1beta1.NodePool
}

func (c *createAlreadyExistsNodePoolClient) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error {
	if _, ok := obj.(*gkev1beta1.NodePool); ok && c.existing != nil {
		if err := c.Client.Create(ctx, c.existing.DeepCopy(), opts...); err != nil {
			return err
		}
		return apierrors.NewAlreadyExists(schema.GroupResource{Group: "container.gcp.crossplane.io", Resource: "nodepools"}, obj.GetName())
	}
	return c.Client.Create(ctx, obj, opts...)
}

type nodePoolForbiddenClient struct {
	client.Client
}

func (c *nodePoolForbiddenClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
	if _, ok := obj.(*gkev1beta1.NodePool); ok {
		return newNodePoolForbiddenError(key.Name)
	}
	return c.Client.Get(ctx, key, obj, opts...)
}

func (c *nodePoolForbiddenClient) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error {
	if _, ok := list.(*gkev1beta1.NodePoolList); ok {
		return newNodePoolForbiddenError("")
	}
	return c.Client.List(ctx, list, opts...)
}

func (c *nodePoolForbiddenClient) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error {
	if _, ok := obj.(*gkev1beta1.NodePool); ok {
		return newNodePoolForbiddenError(obj.GetName())
	}
	return c.Client.Create(ctx, obj, opts...)
}

func (c *nodePoolForbiddenClient) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error {
	if _, ok := obj.(*gkev1beta1.NodePool); ok {
		return newNodePoolForbiddenError(obj.GetName())
	}
	return c.Client.Update(ctx, obj, opts...)
}

func (c *nodePoolForbiddenClient) Delete(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error {
	if _, ok := obj.(*gkev1beta1.NodePool); ok {
		return newNodePoolForbiddenError(obj.GetName())
	}
	return c.Client.Delete(ctx, obj, opts...)
}

func newNodePoolForbiddenError(name string) error {
	return apierrors.NewForbidden(
		schema.GroupResource{Group: "container.gcp.crossplane.io", Resource: "nodepools"},
		name,
		errors.New("nodepool cache client must not be used"),
	)
}

func TestClusterServer_CreateGKEHeterogeneousDeployOnlySyncsIntent(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	baseNodePoolName := gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType16)
	extraNodePoolName := gkeheterogeneous.NodePoolName(cluster, "n2-standard-32")
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(
		cluster,
	).Build()
	s := newGKEHeterogeneousTestServer(cli)
	expectedNodePoolNames := []string{
		baseNodePoolName,
		extraNodePoolName,
	}
	computeClassName := testGKEComputeClassName(t, cluster, apicommon.ComponentType_TIKV, testGKENodePoolMachineType16, "n2-standard-32")

	_, err := s.CreateGKEHeterogeneousDeploy(context.Background(), &apiv1.CreateGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
		TidbGroupId:   "ignored",
		InstanceTypes: []string{"n2-standard-32"},
	})
	require.Error(t, err)
	require.Equal(t, codes.FailedPrecondition, status.Code(err))
	require.Contains(t, err.Error(), "must be prepared before enabling deploy")

	updated := &cloudv1.Cluster{}
	require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Namespace: cluster.Namespace, Name: cluster.Name}, updated))
	require.False(t, updated.TiKVGKEComputeClassIntentConfigured())

	prepareGKEHeterogeneousDeployNodePools(t, cli, cluster, apicommon.ComponentType_TIKV, "", "n2-standard-32")
	resp, err := s.CreateGKEHeterogeneousDeploy(context.Background(), &apiv1.CreateGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
		TidbGroupId:   "ignored",
		InstanceTypes: []string{"n2-standard-32"},
	})
	require.NoError(t, err)
	require.True(t, resp.Enabled)
	require.Empty(t, resp.TidbGroupId)
	require.Equal(t, computeClassName, resp.ComputeClassName)
	require.Equal(t, expectedNodePoolNames, resp.NodePoolNames)
	require.Equal(t, []string{testGKENodePoolMachineType16, "n2-standard-32"}, resp.InstanceTypes)

	require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Namespace: cluster.Namespace, Name: cluster.Name}, updated))
	require.True(t, updated.TiKVGKEComputeClassIntentConfigured())
	requireTestGKEComputeClassBinding(t, updated, cloudv1.AnnoKeyGKEComputeClassForTiKVBindings, computeClassName, []string{testGKENodePoolMachineType16, "n2-standard-32"}, expectedNodePoolNames)
	require.Equal(t, apiv1.GKEHeterogeneousDeployStatus_GKE_HETEROGENEOUS_DEPLOY_STATUS_INTENT_ENABLED_NOT_READY, resp.Status)
	markNodePoolComputeClass(t, cli, expectedNodePoolNames[0], computeClassName)
	markNodePoolComputeClass(t, cli, expectedNodePoolNames[1], computeClassName)
	notReadyResp, err := s.GetGKEHeterogeneousDeploy(context.Background(), &apiv1.GetGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
		TidbGroupId:   "ignored",
	})
	require.NoError(t, err)
	require.Empty(t, notReadyResp.TidbGroupId)
	require.Equal(t, apiv1.GKEHeterogeneousDeployStatus_GKE_HETEROGENEOUS_DEPLOY_STATUS_INTENT_ENABLED_NOT_READY, notReadyResp.Status)

	markNodePoolConditions(t, cli, expectedNodePoolNames[0], corev1.ConditionTrue, corev1.ConditionTrue)
	markNodePoolConditions(t, cli, expectedNodePoolNames[1], corev1.ConditionTrue, corev1.ConditionTrue)
	readyResp, err := s.GetGKEHeterogeneousDeploy(context.Background(), &apiv1.GetGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
	})
	require.NoError(t, err)
	require.Empty(t, readyResp.TidbGroupId)
	require.Equal(t, apiv1.GKEHeterogeneousDeployStatus_GKE_HETEROGENEOUS_DEPLOY_STATUS_READY_FOR_GATEKEEPER, readyResp.Status)
}

func TestClusterServer_GKEHeterogeneousNodePoolReadAllowsWarmupMissingComputeClass(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	baseNodePoolName := gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType16)
	extraNodePoolName := gkeheterogeneous.NodePoolName(cluster, "n2-standard-32")
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(
		cluster,
		newAPIManagedGKENodePool(cluster, baseNodePoolName, testGKENodePoolMachineType16),
		newAPIManagedGKENodePool(cluster, extraNodePoolName, "n2-standard-32"),
	).Build()
	s := newGKEHeterogeneousTestServer(cli)
	computeClassName := testGKEComputeClassName(t, cluster, apicommon.ComponentType_TIKV, testGKENodePoolMachineType16, "n2-standard-32")

	deployResp, err := s.CreateGKEHeterogeneousDeploy(context.Background(), &apiv1.CreateGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
		InstanceTypes: []string{"n2-standard-32"},
	})
	require.NoError(t, err)
	require.Equal(t, apiv1.GKEHeterogeneousDeployStatus_GKE_HETEROGENEOUS_DEPLOY_STATUS_INTENT_ENABLED_NOT_READY, deployResp.Status)
	requireNodePoolWithoutComputeClass(t, cli, baseNodePoolName)
	requireNodePoolWithoutComputeClass(t, cli, extraNodePoolName)

	getResp, err := s.GetGKEHeterogeneousNodePools(context.Background(), &apiv1.GetGKEHeterogeneousNodePoolsRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
		TidbGroupId:   "ignored",
	})
	require.NoError(t, err)
	require.Len(t, getResp.NodePools, 2)
	require.Empty(t, getResp.NodePools[0].TidbGroupId)
	require.Empty(t, getResp.NodePools[1].TidbGroupId)
	requireGKEHeterogeneousNodePool(t, getResp.NodePools[0], baseNodePoolName, testGKENodePoolMachineType16, computeClassName, true, true, true, int64(100))
	requireGKEHeterogeneousNodePool(t, getResp.NodePools[1], extraNodePoolName, "n2-standard-32", computeClassName, false, true, true, int64(100))

	listResp, err := s.ListGKEHeterogeneousNodePools(context.Background(), &apiv1.ListGKEHeterogeneousNodePoolsRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
		TidbGroupId:   "ignored",
	})
	require.NoError(t, err)
	require.Len(t, listResp.NodePools, 2)
	require.Empty(t, listResp.NodePools[0].TidbGroupId)
	require.Empty(t, listResp.NodePools[1].TidbGroupId)
	requireGKEHeterogeneousNodePool(t, listResp.NodePools[0], baseNodePoolName, testGKENodePoolMachineType16, computeClassName, true, true, true, int64(100))
	requireGKEHeterogeneousNodePool(t, listResp.NodePools[1], extraNodePoolName, "n2-standard-32", computeClassName, false, true, true, int64(100))
}

func TestClusterServer_CreateAndUpdateGKEHeterogeneousDeployValidateLiveNodePools(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster).Build()
	s := newGKEHeterogeneousTestServer(cli)
	computeClassName := testGKEComputeClassName(t, cluster, apicommon.ComponentType_TIKV, testGKENodePoolMachineType16, testGKENodePoolMachineType64)
	expectedNodePoolNames := []string{
		gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType16),
		gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType64),
	}
	expectedInstanceTypes := []string{testGKENodePoolMachineType16, testGKENodePoolMachineType64}
	prepareGKEHeterogeneousDeployNodePools(t, cli, cluster, apicommon.ComponentType_TIKV, "", testGKENodePoolMachineType64)

	createResp, err := s.CreateGKEHeterogeneousDeploy(context.Background(), &apiv1.CreateGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
		InstanceTypes: []string{testGKENodePoolMachineType64},
	})
	require.NoError(t, err)
	require.True(t, createResp.Enabled)
	require.Equal(t, computeClassName, createResp.ComputeClassName)
	require.Equal(t, expectedNodePoolNames, createResp.NodePoolNames)
	require.Equal(t, expectedInstanceTypes, createResp.InstanceTypes)
	require.Equal(t, apiv1.GKEHeterogeneousDeployStatus_GKE_HETEROGENEOUS_DEPLOY_STATUS_INTENT_ENABLED_NOT_READY, createResp.Status)
	require.NotEmpty(t, createResp.ClusterAnnotations[cloudv1.AnnoKeyGKEComputeClassForTiKVBindings])

	updateResp, err := s.UpdateGKEHeterogeneousDeploy(context.Background(), &apiv1.UpdateGKEHeterogeneousDeployRequest{
		ClusterId:        cluster.Name,
		ComponentType:    apicommon.ComponentType_TIKV,
		ComputeClassName: computeClassName,
		InstanceTypes:    []string{testGKENodePoolMachineType64},
	})
	require.NoError(t, err)
	require.True(t, updateResp.Enabled)
	require.Equal(t, computeClassName, updateResp.ComputeClassName)
	require.Equal(t, expectedNodePoolNames, updateResp.NodePoolNames)
	require.Equal(t, expectedInstanceTypes, updateResp.InstanceTypes)
	require.Equal(t, apiv1.GKEHeterogeneousDeployStatus_GKE_HETEROGENEOUS_DEPLOY_STATUS_INTENT_ENABLED_NOT_READY, updateResp.Status)
	require.NotEmpty(t, updateResp.ClusterAnnotations[cloudv1.AnnoKeyGKEComputeClassForTiKVBindings])

	updated := &cloudv1.Cluster{}
	require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Namespace: cluster.Namespace, Name: cluster.Name}, updated))
	requireTestGKEComputeClassBinding(t, updated, cloudv1.AnnoKeyGKEComputeClassForTiKVBindings, computeClassName, expectedInstanceTypes, expectedNodePoolNames)
}

func TestClusterServer_CreateGKEHeterogeneousDeployPreservesMainNetworkNodePoolNames(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cluster.Spec.NetworkName = "network-ns/shared-network"
	cluster.UpdateAnnotation(consts.AnnoKeyControlPlaneVersion, "1.30")
	baseNodePoolName := gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType16)
	extraNodePoolName := gkeheterogeneous.NodePoolName(cluster, "n2-standard-32")
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster).Build()
	s := newGKEHeterogeneousTestServer(cli)
	prepareGKEHeterogeneousDeployNodePools(t, cli, cluster, apicommon.ComponentType_TIKV, "", "n2-standard-32")

	resp, err := s.CreateGKEHeterogeneousDeploy(context.Background(), &apiv1.CreateGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
		InstanceTypes: []string{"n2-standard-32"},
	})
	require.NoError(t, err)
	require.Equal(t, []string{baseNodePoolName, extraNodePoolName}, resp.NodePoolNames)

	updated := &cloudv1.Cluster{}
	require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Namespace: cluster.Namespace, Name: cluster.Name}, updated))
	requireTestGKEComputeClassBinding(t, updated, cloudv1.AnnoKeyGKEComputeClassForTiKVBindings, resp.ComputeClassName, []string{testGKENodePoolMachineType16, "n2-standard-32"}, []string{baseNodePoolName, extraNodePoolName})
}

func TestClusterServer_CreateGKEHeterogeneousNodePoolMatchesOrdinaryLegacyScope(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cluster.Spec.NetworkName = "network-ns/shared-network"
	cluster.Spec.Kubernetes.Account = "gcp-project"
	cluster.Labels[cloudv1.RegionLabel] = "asia-east1"
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster).Build()
	s := newGKEHeterogeneousTestServer(cli)

	resp, err := s.CreateGKEHeterogeneousNodePool(context.Background(), newCreateGKEHeterogeneousNodePoolRequest(cluster.Name, "n2-standard-32"))
	require.NoError(t, err)

	require.Equal(t, gkeheterogeneous.NodePoolName(cluster, "n2-standard-32"), resp.NodePoolName)

	nodePool := &gkev1beta1.NodePool{}
	require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Name: resp.NodePoolName}, nodePool))
	networkScope := metav1.ObjectMeta{Name: cloudv1.ProviderTypeGKE + "-asia-east1", Namespace: cluster.Namespace}
	require.NotNil(t, nodePool.Spec.ProviderConfigReference)
	require.Equal(t, namer.UniqueIdentifier(networkScope), nodePool.Spec.ProviderConfigReference.Name)
	expectedServiceAccountName := namer.UniqueIdentifier(networkScope, namer.GCPNodePoolServiceAccountNameOptions()...)
	require.NotNil(t, nodePool.Spec.ForProvider.Config)
	require.NotNil(t, nodePool.Spec.ForProvider.Config.ServiceAccount)
	require.Equal(t, expectedServiceAccountName+"@gcp-project.iam.gserviceaccount.com", *nodePool.Spec.ForProvider.Config.ServiceAccount)
	require.Equal(t, networkScope.Name, nodePool.Labels[namer.LegacyNetworkSelectorKey])
	require.Equal(t, networkScope.Namespace, nodePool.Labels[namer.LegacyNetworkNamespaceSelectorKey])
	expectedClusterRef := "projects/" + cluster.Spec.Kubernetes.Account + "/locations/asia-east1/clusters/" + cluster.Spec.Kubernetes.Name
	require.Equal(t, expectedClusterRef, nodePool.Spec.ForProvider.Cluster)
}

func TestBuildGKEHeterogeneousNodePoolMatchesOrdinaryVersionLabelFallback(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cluster.UpdateAnnotation(consts.AnnoKeyControlPlaneVersion, "1.30")
	nodePoolName := gkeheterogeneous.NodePoolName(cluster, "n2-standard-32")

	nodePool := buildGKEHeterogeneousNodePool(cluster, nodePoolName, "n2-standard-32", 0, 100)
	require.NotContains(t, nodePool.Spec.ForProvider.Config.Labels, consts.K8sVersionLabelKey)

	cluster.Status.FromProvider.Raw = []byte(`{"nodeVersion":"1.34"}`)
	nodePool = buildGKEHeterogeneousNodePool(cluster, nodePoolName, "n2-standard-32", 0, 100)
	require.Equal(t, "1.34", nodePool.Spec.ForProvider.Config.Labels[consts.K8sVersionLabelKey])
}

func TestClusterServer_CreateGKEHeterogeneousNodePoolFallsBackToTiKVOrdinaryLocations(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cluster.Spec.Kubernetes.Cloud.GCP = nil
	const (
		baseInstanceType  = "n2-standard-16"
		extraInstanceType = "n2-standard-32"
	)
	cluster.Spec.Cluster.TiKV.InstanceType = baseInstanceType
	ordinaryNodePool := &gkev1beta1.NodePool{
		ObjectMeta: metav1.ObjectMeta{
			Name: namer.GKENodePoolNameForCluster(cluster, baseInstanceType),
		},
		Spec: gkev1beta1.NodePoolSpec{
			ForProvider: gkev1beta1.NodePoolParameters{
				Locations: []string{"us-west-2a", "us-west-2b", "us-west-2c"},
			},
		},
	}
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster, ordinaryNodePool).Build()
	s := newGKEHeterogeneousTestServer(cli)

	resp, err := s.CreateGKEHeterogeneousNodePool(context.Background(), newCreateGKEHeterogeneousNodePoolRequest(cluster.Name, extraInstanceType))
	require.NoError(t, err)

	nodePool := &gkev1beta1.NodePool{}
	require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Name: resp.NodePoolName}, nodePool))
	require.Equal(t, extraInstanceType, *nodePool.Spec.ForProvider.Config.MachineType)
	require.Equal(t, ordinaryNodePool.Spec.ForProvider.Locations, nodePool.Spec.ForProvider.Locations)
}

func TestClusterServer_CreateGKEHeterogeneousNodePoolRejectsMissingTiKVOrdinaryLocationSource(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cluster.Spec.Kubernetes.Cloud.GCP = nil
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster).Build()
	s := newGKEHeterogeneousTestServer(cli)

	_, err := s.CreateGKEHeterogeneousNodePool(context.Background(), newCreateGKEHeterogeneousNodePoolRequest(cluster.Name, "n2-standard-32"))
	require.Equal(t, codes.FailedPrecondition, status.Code(err))
	require.Contains(t, err.Error(), "ordinary nodepool")
	require.Contains(t, err.Error(), namer.GKENodePoolNameForCluster(cluster, cluster.Spec.Cluster.TiKV.InstanceType))
}

func TestClusterServer_CreateGKEHeterogeneousNodePoolUsesDirectNodePoolClient(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cachedClient := &nodePoolForbiddenClient{
		Client: fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster).Build(),
	}
	directNodePoolClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).Build()
	s := &ClusterServer{
		KubeClient:     cachedClient,
		NodePoolClient: directNodePoolClient,
		clusterReader:  &internalutil.LabelSelectorReader{},
	}

	resp, err := s.CreateGKEHeterogeneousNodePool(context.Background(), newCreateGKEHeterogeneousNodePoolRequest(cluster.Name, "n2-standard-32"))
	require.NoError(t, err)

	nodePool := &gkev1beta1.NodePool{}
	require.NoError(t, directNodePoolClient.Get(context.Background(), client.ObjectKey{Name: resp.NodePoolName}, nodePool))
	require.Equal(t, "n2-standard-32", *nodePool.Spec.ForProvider.Config.MachineType)
	require.Equal(t, resp.NodePoolName, nodePool.GetAnnotations()[xpmeta.AnnotationKeyExternalName])
}

func TestClusterServer_GKEHeterogeneousNodePoolResponsesUsePublicClusterID(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	publicClusterID := "public-cluster-id"
	cluster.Labels[cloudv1.ClusterTagKey] = publicClusterID
	extraNodePoolName := gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType64)
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster).Build()
	s := newGKEHeterogeneousTestServer(cli)

	createResp, err := s.CreateGKEHeterogeneousNodePool(context.Background(), newCreateGKEHeterogeneousNodePoolRequest(publicClusterID, testGKENodePoolMachineType64))
	require.NoError(t, err)
	require.NotEqual(t, cluster.Name, createResp.ClusterId)
	require.Equal(t, publicClusterID, createResp.ClusterId)

	preparedResp, err := s.ListGKEHeterogeneousNodePools(context.Background(), &apiv1.ListGKEHeterogeneousNodePoolsRequest{
		ClusterId: publicClusterID,
	})
	require.NoError(t, err)
	require.Len(t, preparedResp.NodePools, 1)
	require.Equal(t, publicClusterID, preparedResp.NodePools[0].ClusterId)

	prepareGKEHeterogeneousDeployNodePools(t, cli, cluster, apicommon.ComponentType_TIKV, "", testGKENodePoolMachineType64)
	_, err = s.CreateGKEHeterogeneousDeploy(context.Background(), &apiv1.CreateGKEHeterogeneousDeployRequest{
		ClusterId:     publicClusterID,
		ComponentType: apicommon.ComponentType_TIKV,
		InstanceTypes: []string{testGKENodePoolMachineType64},
	})
	require.NoError(t, err)
	computeClassName := testGKEComputeClassName(t, cluster, apicommon.ComponentType_TIKV, testGKENodePoolMachineType16, testGKENodePoolMachineType64)
	markNodePoolComputeClass(t, cli, extraNodePoolName, computeClassName)

	listResp, err := s.ListGKEHeterogeneousNodePools(context.Background(), &apiv1.ListGKEHeterogeneousNodePoolsRequest{
		ClusterId:     publicClusterID,
		ComponentType: apicommon.ComponentType_TIKV,
	})
	require.NoError(t, err)
	require.Len(t, listResp.NodePools, 2)
	for _, nodePool := range listResp.NodePools {
		require.Equal(t, publicClusterID, nodePool.ClusterId)
	}
}

func TestClusterServer_ListGKEHeterogeneousNodePoolsReturnsPreparedNodePoolWithStaleComputeClassMetadata(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	nodePoolName := gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType64)
	staleComputeClass := testGKEComputeClassName(t, cluster, apicommon.ComponentType_TIKV, testGKENodePoolMachineType16, testGKENodePoolMachineType64)
	nodePool := newHeterogeneousGKENodePool(cluster, nodePoolName, testGKENodePoolMachineType64, staleComputeClass)
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster, nodePool).Build()
	s := newGKEHeterogeneousTestServer(cli)

	resp, err := s.ListGKEHeterogeneousNodePools(context.Background(), &apiv1.ListGKEHeterogeneousNodePoolsRequest{
		ClusterId: cluster.Name,
	})
	require.NoError(t, err)
	require.Len(t, resp.NodePools, 1)
	require.Equal(t, cluster.Name, resp.NodePools[0].ClusterId)
	require.Equal(t, apicommon.ComponentType_COMPONENT_TYPE_UNSPECIFIED, resp.NodePools[0].ComponentType)
	require.Equal(t, nodePoolName, resp.NodePools[0].NodePoolName)
	require.Equal(t, testGKENodePoolMachineType64, resp.NodePools[0].InstanceType)
	require.Equal(t, staleComputeClass, resp.NodePools[0].ComputeClassName)
	require.False(t, resp.NodePools[0].IsBaseNodePool)
	require.True(t, resp.NodePools[0].ApiManaged)
	require.True(t, resp.NodePools[0].Exists)
}

func TestClusterServer_ListGKEHeterogeneousNodePoolsIgnoresComponentFilter(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	nodePoolName := gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType64)
	computeClassName := testGKEComputeClassName(t, cluster, apicommon.ComponentType_TIKV, testGKENodePoolMachineType16, testGKENodePoolMachineType64)
	nodePool := newHeterogeneousGKENodePool(cluster, nodePoolName, testGKENodePoolMachineType64, computeClassName)
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster, nodePool).Build()
	s := newGKEHeterogeneousTestServer(cli)

	resp, err := s.ListGKEHeterogeneousNodePools(context.Background(), &apiv1.ListGKEHeterogeneousNodePoolsRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
	})
	require.NoError(t, err)
	require.Len(t, resp.NodePools, 1)
	require.Equal(t, nodePoolName, resp.NodePools[0].NodePoolName)
	require.Equal(t, testGKENodePoolMachineType64, resp.NodePools[0].InstanceType)
	require.Equal(t, computeClassName, resp.NodePools[0].ComputeClassName)
}

func TestClusterServer_ListGKEHeterogeneousNodePoolsReturnsPreparedNodePoolWithAppliedOnlyComputeClassMetadata(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	nodePoolName := gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType64)
	staleComputeClass := testGKEComputeClassName(t, cluster, apicommon.ComponentType_TIKV, testGKENodePoolMachineType16, testGKENodePoolMachineType64)
	nodePool := newAPIManagedGKENodePool(cluster, nodePoolName, testGKENodePoolMachineType64)
	nodepoolutil.SetGKEAppliedComputeClassForNodePool(nodePool, staleComputeClass)
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster, nodePool).Build()
	s := newGKEHeterogeneousTestServer(cli)

	resp, err := s.ListGKEHeterogeneousNodePools(context.Background(), &apiv1.ListGKEHeterogeneousNodePoolsRequest{
		ClusterId: cluster.Name,
	})
	require.NoError(t, err)
	require.Len(t, resp.NodePools, 1)
	require.Equal(t, nodePoolName, resp.NodePools[0].NodePoolName)
	require.Equal(t, staleComputeClass, resp.NodePools[0].ComputeClassName)
}

func TestClusterServer_ListGKEHeterogeneousNodePoolsRejectsPreparedScanWithoutClusterScope(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	nodePoolName := gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType64)
	nodePool := newHeterogeneousGKENodePool(cluster, nodePoolName, testGKENodePoolMachineType64, "stale-priority")
	delete(cluster.Labels, cloudv1.ProjectTagKey)
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster, nodePool).Build()
	s := newGKEHeterogeneousTestServer(cli)

	_, err := s.ListGKEHeterogeneousNodePools(context.Background(), &apiv1.ListGKEHeterogeneousNodePoolsRequest{
		ClusterId: cluster.Name,
	})
	require.Error(t, err)
	require.Equal(t, codes.FailedPrecondition, status.Code(err))
	require.Contains(t, err.Error(), "missing required GKE heterogeneous nodepool scope labels")
	require.Contains(t, err.Error(), cloudv1.ProjectTagKey)
}

func TestValidateGKEHeterogeneousNodePoolComputeClassChecksAllMetadataSources(t *testing.T) {
	const (
		expectedComputeClass = "new-priority"
		oldComputeClass      = "old-priority"
	)

	tests := []struct {
		name       string
		mutate     func(*gkev1beta1.NodePool)
		wantErrMsg string
	}{
		{
			name: "all empty",
		},
		{
			name: "desired matches",
			mutate: func(nodePool *gkev1beta1.NodePool) {
				nodepoolutil.SetGKEDesiredComputeClassForNodePool(nodePool, expectedComputeClass)
			},
		},
		{
			name: "applied only mismatch",
			mutate: func(nodePool *gkev1beta1.NodePool) {
				nodepoolutil.SetGKEAppliedComputeClassForNodePool(nodePool, oldComputeClass)
			},
			wantErrMsg: "applied compute class old-priority",
		},
		{
			name: "desired matches but applied mismatches",
			mutate: func(nodePool *gkev1beta1.NodePool) {
				nodepoolutil.SetGKEDesiredComputeClassForNodePool(nodePool, expectedComputeClass)
				nodepoolutil.SetGKEAppliedComputeClassForNodePool(nodePool, oldComputeClass)
			},
			wantErrMsg: "applied compute class old-priority",
		},
		{
			name: "desired matches but spec label mismatches",
			mutate: func(nodePool *gkev1beta1.NodePool) {
				nodepoolutil.SetGKEDesiredComputeClassForNodePool(nodePool, expectedComputeClass)
				nodepoolutil.SetGKEComputeClassSpecLabelForNodePool(nodePool, oldComputeClass)
			},
			wantErrMsg: "compute class spec label old-priority",
		},
		{
			name: "desired matches but spec taint mismatches",
			mutate: func(nodePool *gkev1beta1.NodePool) {
				nodepoolutil.SetGKEDesiredComputeClassForNodePool(nodePool, expectedComputeClass)
				nodepoolutil.SetGKEComputeClassSpecTaintForNodePool(nodePool, oldComputeClass)
			},
			wantErrMsg: "compute class spec taint old-priority",
		},
		{
			name: "all non empty match",
			mutate: func(nodePool *gkev1beta1.NodePool) {
				nodepoolutil.SetGKEComputeClassForNodePool(nodePool, expectedComputeClass)
				nodepoolutil.SetGKEAppliedComputeClassForNodePool(nodePool, expectedComputeClass)
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			nodePool := &gkev1beta1.NodePool{
				ObjectMeta: metav1.ObjectMeta{Name: "test-nodepool"},
			}
			if tt.mutate != nil {
				tt.mutate(nodePool)
			}

			err := validateGKEHeterogeneousNodePoolComputeClass(nodePool, expectedComputeClass)
			if tt.wantErrMsg == "" {
				require.NoError(t, err)
				return
			}
			require.Error(t, err)
			require.Equal(t, codes.FailedPrecondition, status.Code(err))
			require.Contains(t, err.Error(), tt.wantErrMsg)
		})
	}
}

func TestClusterServer_CreateGKEHeterogeneousNodePoolBackfillsExternalName(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	nodePoolName := gkeheterogeneous.NodePoolName(cluster, "n2-standard-32")
	nodePool := newAPIManagedGKENodePool(cluster, nodePoolName, "n2-standard-32")
	delete(nodePool.Annotations, xpmeta.AnnotationKeyExternalName)
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster, nodePool).Build()
	s := newGKEHeterogeneousTestServer(cli)

	resp, err := s.CreateGKEHeterogeneousNodePool(context.Background(), newCreateGKEHeterogeneousNodePoolRequest(cluster.Name, "n2-standard-32"))
	require.NoError(t, err)

	got := &gkev1beta1.NodePool{}
	require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Name: resp.NodePoolName}, got))
	require.Equal(t, nodePoolName, got.GetAnnotations()[xpmeta.AnnotationKeyExternalName])
}

func TestClusterServer_CreateGKEHeterogeneousNodePoolDoesNotBackfillExternalNameBeforeOwnershipValidation(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	nodePoolName := gkeheterogeneous.NodePoolName(cluster, "n2-standard-32")
	nodePool := newOrdinarySharedGKENodePool(cluster, nodePoolName, "n2-standard-32")
	delete(nodePool.Annotations, xpmeta.AnnotationKeyExternalName)
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster, nodePool).Build()
	s := newGKEHeterogeneousTestServer(cli)

	_, err := s.CreateGKEHeterogeneousNodePool(context.Background(), newCreateGKEHeterogeneousNodePoolRequest(cluster.Name, "n2-standard-32"))
	require.Error(t, err)
	require.Equal(t, codes.FailedPrecondition, status.Code(err))
	require.Contains(t, err.Error(), "not managed by heterogeneous nodepool API")

	got := &gkev1beta1.NodePool{}
	require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Name: nodePoolName}, got))
	require.NotContains(t, got.GetAnnotations(), xpmeta.AnnotationKeyExternalName)
}

func TestClusterServer_CreateGKEHeterogeneousNodePoolRejectsExternalNameDrift(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	nodePoolName := gkeheterogeneous.NodePoolName(cluster, "n2-standard-32")
	nodePool := newAPIManagedGKENodePool(cluster, nodePoolName, "n2-standard-32")
	nodePool.Annotations[xpmeta.AnnotationKeyExternalName] = "other-nodepool"
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster, nodePool).Build()
	s := newGKEHeterogeneousTestServer(cli)

	_, err := s.CreateGKEHeterogeneousNodePool(context.Background(), newCreateGKEHeterogeneousNodePoolRequest(cluster.Name, "n2-standard-32"))
	require.Error(t, err)
	require.Equal(t, codes.FailedPrecondition, status.Code(err))
	require.Contains(t, err.Error(), "external name")
}

func TestClusterServer_CreateAndUpdateGKEHeterogeneousDeployIntentLifecycle(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster).Build()
	s := newGKEHeterogeneousTestServer(cli)
	computeClassName := testGKEComputeClassName(t, cluster, apicommon.ComponentType_TIKV, testGKENodePoolMachineType16, testGKENodePoolMachineType64)
	prepareGKEHeterogeneousDeployNodePools(t, cli, cluster, apicommon.ComponentType_TIKV, "", testGKENodePoolMachineType64)
	createReq := &apiv1.CreateGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
		InstanceTypes: []string{testGKENodePoolMachineType64},
	}

	_, err := s.UpdateGKEHeterogeneousDeploy(context.Background(), &apiv1.UpdateGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
		InstanceTypes: []string{testGKENodePoolMachineType64},
	})
	require.Error(t, err)
	require.Equal(t, codes.FailedPrecondition, status.Code(err))

	createResp, err := s.CreateGKEHeterogeneousDeploy(context.Background(), createReq)
	require.NoError(t, err)

	_, err = s.CreateGKEHeterogeneousDeploy(context.Background(), createReq)
	require.Error(t, err)
	require.Equal(t, codes.AlreadyExists, status.Code(err))

	updateResp, err := s.UpdateGKEHeterogeneousDeploy(context.Background(), &apiv1.UpdateGKEHeterogeneousDeployRequest{
		ClusterId:        cluster.Name,
		ComponentType:    apicommon.ComponentType_TIKV,
		TidbGroupId:      "ignored",
		ComputeClassName: computeClassName,
		InstanceTypes:    []string{testGKENodePoolMachineType64},
	})
	require.NoError(t, err)
	require.Empty(t, updateResp.TidbGroupId)
	require.Equal(t, createResp.NodePoolNames, updateResp.NodePoolNames)
	require.Equal(t, computeClassName, updateResp.ComputeClassName)

	updated := &cloudv1.Cluster{}
	require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Namespace: cluster.Namespace, Name: cluster.Name}, updated))
	requireTestGKEComputeClassBinding(t, updated, cloudv1.AnnoKeyGKEComputeClassForTiKVBindings, computeClassName, updateResp.InstanceTypes, updateResp.NodePoolNames)
}

func TestClusterServer_GKEHeterogeneousDeployRequiresKubernetesClusterScopeLabels(t *testing.T) {
	tests := []struct {
		name         string
		missingLabel string
		seedIntent   bool
	}{
		{
			name:         "create",
			missingLabel: cloudv1.TenantTagKey,
		},
		{
			name:         "update",
			missingLabel: cloudv1.ProjectTagKey,
			seedIntent:   true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
			if tt.seedIntent {
				updateTestGKEComputeClassBinding(
					t,
					cluster,
					cloudv1.AnnoKeyGKEComputeClassForTiKVBindings,
					"tikv-priority",
					[]string{testGKENodePoolMachineType16, testGKENodePoolMachineType64},
					[]string{
						gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType16),
						gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType64),
					},
				)
			}
			beforeBinding := cluster.GetAnnotations()[cloudv1.AnnoKeyGKEComputeClassForTiKVBindings]
			delete(cluster.Labels, tt.missingLabel)
			cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster).Build()
			s := newGKEHeterogeneousTestServer(cli)

			var err error
			if tt.seedIntent {
				_, err = s.UpdateGKEHeterogeneousDeploy(context.Background(), &apiv1.UpdateGKEHeterogeneousDeployRequest{
					ClusterId:     cluster.Name,
					ComponentType: apicommon.ComponentType_TIKV,
					InstanceTypes: []string{testGKENodePoolMachineType64},
				})
			} else {
				_, err = s.CreateGKEHeterogeneousDeploy(context.Background(), &apiv1.CreateGKEHeterogeneousDeployRequest{
					ClusterId:     cluster.Name,
					ComponentType: apicommon.ComponentType_TIKV,
					InstanceTypes: []string{testGKENodePoolMachineType64},
				})
			}
			require.Error(t, err)
			require.Equal(t, codes.FailedPrecondition, status.Code(err))
			require.Contains(t, err.Error(), "missing required GKE heterogeneous nodepool scope labels")
			require.Contains(t, err.Error(), tt.missingLabel)

			updated := &cloudv1.Cluster{}
			require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Namespace: cluster.Namespace, Name: cluster.Name}, updated))
			if tt.seedIntent {
				require.Equal(t, beforeBinding, updated.GetAnnotations()[cloudv1.AnnoKeyGKEComputeClassForTiKVBindings])
			} else {
				require.NotContains(t, updated.GetAnnotations(), cloudv1.AnnoKeyGKEComputeClassForTiKVBindings)
			}
		})
	}
}

func TestClusterServer_GKEHeterogeneousTiKVRequestsPassProtoValidationWithoutTiDBGroupID(t *testing.T) {
	validator, err := protovalidate.New()
	require.NoError(t, err)

	tests := []struct {
		name string
		req  proto.Message
	}{
		{
			name: "get deploy",
			req: &apiv1.GetGKEHeterogeneousDeployRequest{
				ClusterId:     "cluster-1",
				ComponentType: apicommon.ComponentType_TIKV,
			},
		},
		{
			name: "get nodepools",
			req: &apiv1.GetGKEHeterogeneousNodePoolsRequest{
				ClusterId:     "cluster-1",
				ComponentType: apicommon.ComponentType_TIKV,
			},
		},
		{
			name: "create deploy",
			req: &apiv1.CreateGKEHeterogeneousDeployRequest{
				ClusterId:     "cluster-1",
				ComponentType: apicommon.ComponentType_TIKV,
				InstanceTypes: []string{"n2-standard-32"},
			},
		},
		{
			name: "update deploy",
			req: &apiv1.UpdateGKEHeterogeneousDeployRequest{
				ClusterId:     "cluster-1",
				ComponentType: apicommon.ComponentType_TIKV,
				InstanceTypes: []string{"n2-standard-32"},
			},
		},
		{
			name: "delete deploy",
			req: &apiv1.DeleteGKEHeterogeneousDeployRequest{
				ClusterId:     "cluster-1",
				ComponentType: apicommon.ComponentType_TIKV,
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			require.NoError(t, validator.Validate(tt.req))
		})
	}
}

func TestClusterServer_GKEHeterogeneousDeployChecksGeneratedComputeClassName(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster).Build()
	s := newGKEHeterogeneousTestServer(cli)
	computeClassName := testGKEComputeClassName(t, cluster, apicommon.ComponentType_TIKV, testGKENodePoolMachineType16, testGKENodePoolMachineType64)
	prepareGKEHeterogeneousDeployNodePools(t, cli, cluster, apicommon.ComponentType_TIKV, "", testGKENodePoolMachineType64)

	resp, err := s.CreateGKEHeterogeneousDeploy(context.Background(), &apiv1.CreateGKEHeterogeneousDeployRequest{
		ClusterId:        cluster.Name,
		ComponentType:    apicommon.ComponentType_TIKV,
		ComputeClassName: computeClassName,
		InstanceTypes:    []string{testGKENodePoolMachineType64},
	})
	require.NoError(t, err)
	require.Equal(t, computeClassName, resp.ComputeClassName)

	mismatchCluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	setGKEHeterogeneousTestClusterID(mismatchCluster, cluster.Namespace, "mismatch-cluster")
	mismatchCluster.ResourceVersion = ""
	require.NoError(t, cli.Create(context.Background(), mismatchCluster))
	_, err = s.CreateGKEHeterogeneousDeploy(context.Background(), &apiv1.CreateGKEHeterogeneousDeployRequest{
		ClusterId:        mismatchCluster.Name,
		ComponentType:    apicommon.ComponentType_TIKV,
		ComputeClassName: "custom-priority",
		InstanceTypes:    []string{testGKENodePoolMachineType64},
	})
	require.Error(t, err)
	require.Equal(t, codes.InvalidArgument, status.Code(err))
	require.Contains(t, err.Error(), "does not match generated compute class name")

	_, err = s.UpdateGKEHeterogeneousDeploy(context.Background(), &apiv1.UpdateGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
		InstanceTypes: []string{testGKENodePoolMachineType64},
	})
	require.NoError(t, err)

	_, err = s.UpdateGKEHeterogeneousDeploy(context.Background(), &apiv1.UpdateGKEHeterogeneousDeployRequest{
		ClusterId:        cluster.Name,
		ComponentType:    apicommon.ComponentType_TIKV,
		ComputeClassName: "custom-priority",
		InstanceTypes:    []string{testGKENodePoolMachineType64},
	})
	require.Error(t, err)
	require.Equal(t, codes.InvalidArgument, status.Code(err))
	require.Contains(t, err.Error(), "does not match generated compute class name")
}

func TestClusterServer_GKEHeterogeneousDeployStatusUsesNodePoolReadiness(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	baseNodePoolName := gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType16)
	extraNodePoolName := gkeheterogeneous.NodePoolName(cluster, "n2-standard-32")
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(
		cluster,
		newAPIManagedGKENodePool(cluster, baseNodePoolName, testGKENodePoolMachineType16),
		newAPIManagedGKENodePool(cluster, extraNodePoolName, "n2-standard-32"),
	).Build()
	s := newGKEHeterogeneousTestServer(cli)
	computeClassName := testGKEComputeClassName(t, cluster, apicommon.ComponentType_TIKV, testGKENodePoolMachineType16, "n2-standard-32")

	_, err := s.CreateGKEHeterogeneousDeploy(context.Background(), &apiv1.CreateGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
		InstanceTypes: []string{"n2-standard-32"},
	})
	require.NoError(t, err)

	markNodePoolDesiredComputeClass(t, cli, baseNodePoolName, computeClassName)
	markNodePoolDesiredComputeClass(t, cli, extraNodePoolName, computeClassName)
	markNodePoolConditions(t, cli, baseNodePoolName, corev1.ConditionTrue, corev1.ConditionTrue)
	markNodePoolConditions(t, cli, extraNodePoolName, corev1.ConditionTrue, corev1.ConditionTrue)
	notReadyResp, err := s.GetGKEHeterogeneousDeploy(context.Background(), &apiv1.GetGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
	})
	require.NoError(t, err)
	require.Equal(t, apiv1.GKEHeterogeneousDeployStatus_GKE_HETEROGENEOUS_DEPLOY_STATUS_INTENT_ENABLED_NOT_READY, notReadyResp.Status)

	markNodePoolComputeClass(t, cli, baseNodePoolName, computeClassName)
	markNodePoolComputeClass(t, cli, extraNodePoolName, computeClassName)
	resp, err := s.GetGKEHeterogeneousDeploy(context.Background(), &apiv1.GetGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
	})
	require.NoError(t, err)
	require.Equal(t, apiv1.GKEHeterogeneousDeployStatus_GKE_HETEROGENEOUS_DEPLOY_STATUS_READY_FOR_GATEKEEPER, resp.Status)
}

func TestClusterServer_GKEHeterogeneousDeployStatusRequiresReadyAndSyncedNodePools(t *testing.T) {
	tests := []struct {
		name       string
		ready      corev1.ConditionStatus
		synced     corev1.ConditionStatus
		wantStatus apiv1.GKEHeterogeneousDeployStatus
	}{
		{
			name:       "ready without synced is not ready",
			ready:      corev1.ConditionTrue,
			synced:     corev1.ConditionFalse,
			wantStatus: apiv1.GKEHeterogeneousDeployStatus_GKE_HETEROGENEOUS_DEPLOY_STATUS_INTENT_ENABLED_NOT_READY,
		},
		{
			name:       "synced without ready is not ready",
			ready:      corev1.ConditionFalse,
			synced:     corev1.ConditionTrue,
			wantStatus: apiv1.GKEHeterogeneousDeployStatus_GKE_HETEROGENEOUS_DEPLOY_STATUS_INTENT_ENABLED_NOT_READY,
		},
		{
			name:       "ready and synced is ready for gatekeeper",
			ready:      corev1.ConditionTrue,
			synced:     corev1.ConditionTrue,
			wantStatus: apiv1.GKEHeterogeneousDeployStatus_GKE_HETEROGENEOUS_DEPLOY_STATUS_READY_FOR_GATEKEEPER,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
			baseNodePoolName := gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType16)
			extraNodePoolName := gkeheterogeneous.NodePoolName(cluster, "n2-standard-32")
			cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(
				cluster,
				newAPIManagedGKENodePool(cluster, baseNodePoolName, testGKENodePoolMachineType16),
				newAPIManagedGKENodePool(cluster, extraNodePoolName, "n2-standard-32"),
			).Build()
			s := newGKEHeterogeneousTestServer(cli)
			computeClassName := testGKEComputeClassName(t, cluster, apicommon.ComponentType_TIKV, testGKENodePoolMachineType16, "n2-standard-32")

			_, err := s.CreateGKEHeterogeneousDeploy(context.Background(), &apiv1.CreateGKEHeterogeneousDeployRequest{
				ClusterId:     cluster.Name,
				ComponentType: apicommon.ComponentType_TIKV,
				InstanceTypes: []string{"n2-standard-32"},
			})
			require.NoError(t, err)
			markNodePoolComputeClass(t, cli, baseNodePoolName, computeClassName)
			markNodePoolComputeClass(t, cli, extraNodePoolName, computeClassName)
			markNodePoolConditions(t, cli, baseNodePoolName, tt.ready, tt.synced)
			markNodePoolConditions(t, cli, extraNodePoolName, tt.ready, tt.synced)

			resp, err := s.GetGKEHeterogeneousDeploy(context.Background(), &apiv1.GetGKEHeterogeneousDeployRequest{
				ClusterId:     cluster.Name,
				ComponentType: apicommon.ComponentType_TIKV,
			})

			require.NoError(t, err)
			require.Equal(t, tt.wantStatus, resp.Status)
		})
	}
}

func TestClusterServer_CreateGKEHeterogeneousDeployRejectsInvalidComputeClassName(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster).Build()
	s := newGKEHeterogeneousTestServer(cli)

	_, err := s.CreateGKEHeterogeneousDeploy(context.Background(), &apiv1.CreateGKEHeterogeneousDeployRequest{
		ClusterId:        cluster.Name,
		ComponentType:    apicommon.ComponentType_TIKV,
		ComputeClassName: "Bad_Name",
		InstanceTypes:    []string{"n2-standard-32"},
	})
	require.Error(t, err)
	require.Equal(t, codes.InvalidArgument, status.Code(err))
	require.Contains(t, err.Error(), `invalid compute_class_name "Bad_Name"`)

	updated := &cloudv1.Cluster{}
	require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Namespace: cluster.Namespace, Name: cluster.Name}, updated))
	require.NotContains(t, updated.GetAnnotations(), cloudv1.AnnoKeyGKEComputeClassForTiKVBindings)
}

func TestClusterServer_CreateGKEHeterogeneousDeployRejectsEmptyBaseInstanceType(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cluster.Spec.Cluster.TiKV.InstanceType = ""
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster).Build()
	s := newGKEHeterogeneousTestServer(cli)

	_, err := s.CreateGKEHeterogeneousDeploy(context.Background(), &apiv1.CreateGKEHeterogeneousDeployRequest{
		ClusterId:        cluster.Name,
		ComponentType:    apicommon.ComponentType_TIKV,
		ComputeClassName: "tikv-priority",
		InstanceTypes:    []string{testGKENodePoolMachineType64},
	})
	require.Error(t, err)
	require.Equal(t, codes.FailedPrecondition, status.Code(err))
	require.Contains(t, err.Error(), "base instance type is empty")
}

func TestClusterServer_CreateGKEHeterogeneousDeployRequiresCompleteIntent(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster).Build()
	s := newGKEHeterogeneousTestServer(cli)

	_, err := s.CreateGKEHeterogeneousDeploy(context.Background(), &apiv1.CreateGKEHeterogeneousDeployRequest{
		ClusterId:        cluster.Name,
		ComponentType:    apicommon.ComponentType_TIKV,
		ComputeClassName: "tikv-priority",
	})
	require.Error(t, err)
	require.Equal(t, codes.InvalidArgument, status.Code(err))
	require.Contains(t, err.Error(), "instance_types must contain exactly one extra instance type")

	_, err = s.CreateGKEHeterogeneousDeploy(context.Background(), &apiv1.CreateGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
		InstanceTypes: []string{testGKENodePoolMachineType16},
	})
	require.Error(t, err)
	require.Equal(t, codes.InvalidArgument, status.Code(err))
	require.Contains(t, err.Error(), "instance_types must contain exactly one extra instance type")
}

func TestClusterServer_CreateGKEHeterogeneousDeployRejectsBaseOnlyIntent(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster).Build()
	s := newGKEHeterogeneousTestServer(cli)

	_, err := s.CreateGKEHeterogeneousDeploy(context.Background(), &apiv1.CreateGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
		InstanceTypes: []string{testGKENodePoolMachineType16},
	})
	require.Error(t, err)
	require.Equal(t, codes.InvalidArgument, status.Code(err))
	require.Contains(t, err.Error(), "instance_types must contain exactly one extra instance type")
}

func TestClusterServer_CreateGKEHeterogeneousDeployAllowsOrdinaryBaseInstanceTypes(t *testing.T) {
	tests := []struct {
		name              string
		componentType     apicommon.ComponentType
		tidbGroupID       string
		instanceTypes     []string
		wantInstanceTypes []string
	}{
		{
			name:              "tikv extra uses tidb default base",
			componentType:     apicommon.ComponentType_TIKV,
			instanceTypes:     []string{"n2-standard-8"},
			wantInstanceTypes: []string{testGKENodePoolMachineType16, "n2-standard-8"},
		},
		{
			name:              "tikv extra uses tidb extra group base",
			componentType:     apicommon.ComponentType_TIKV,
			instanceTypes:     []string{"n2-standard-32"},
			wantInstanceTypes: []string{testGKENodePoolMachineType16, "n2-standard-32"},
		},
		{
			name:              "tidb extra uses tikv base",
			componentType:     apicommon.ComponentType_TIDB,
			tidbGroupID:       "default-group",
			instanceTypes:     []string{testGKENodePoolMachineType16},
			wantInstanceTypes: []string{"n2-standard-8", testGKENodePoolMachineType16},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
			cluster.Spec.ExtraGroups = []cloudv1.ExtraGroupSpec{
				{
					Name: "group1",
					TiDB: cloudv1.TiDB{
						Replicas:     2,
						InstanceType: "n2-standard-32",
					},
				},
			}
			cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster).Build()
			s := newGKEHeterogeneousTestServer(cli)
			prepareGKEHeterogeneousDeployNodePools(t, cli, cluster, tt.componentType, tt.tidbGroupID, tt.instanceTypes[0])

			resp, err := s.CreateGKEHeterogeneousDeploy(context.Background(), &apiv1.CreateGKEHeterogeneousDeployRequest{
				ClusterId:     cluster.Name,
				ComponentType: tt.componentType,
				TidbGroupId:   tt.tidbGroupID,
				InstanceTypes: tt.instanceTypes,
			})
			require.NoError(t, err)
			require.Equal(t, tt.wantInstanceTypes, resp.InstanceTypes)
			require.Len(t, resp.NodePoolNames, len(tt.wantInstanceTypes))
			for i, instanceType := range tt.wantInstanceTypes {
				require.Equal(t, gkeheterogeneous.NodePoolName(cluster, instanceType), resp.NodePoolNames[i])
			}
		})
	}
}

func TestClusterServer_CreateGKEHeterogeneousDeployRejectsCurrentClusterComputeClassConflict(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	extraNodePoolName := gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType64)
	tikvComputeClassName := testGKEComputeClassName(t, cluster, apicommon.ComponentType_TIKV, testGKENodePoolMachineType16, testGKENodePoolMachineType64)
	tidbComputeClassName := testGKEComputeClassName(t, cluster, apicommon.ComponentType_TIDB, "n2-standard-8", testGKENodePoolMachineType64)
	updateTestGKEComputeClassBinding(
		t,
		cluster,
		cloudv1.AnnoKeyGKEComputeClassForTiKVBindings,
		tikvComputeClassName,
		[]string{testGKENodePoolMachineType16, testGKENodePoolMachineType64},
		[]string{gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType16), extraNodePoolName},
	)
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster).Build()
	s := newGKEHeterogeneousTestServer(cli)

	_, err := s.CreateGKEHeterogeneousDeploy(context.Background(), &apiv1.CreateGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIDB,
		TidbGroupId:   cluster.GetDefaultGroupID(),
		InstanceTypes: []string{testGKENodePoolMachineType64},
	})
	require.Error(t, err)
	require.Equal(t, codes.FailedPrecondition, status.Code(err))
	require.Contains(t, err.Error(), "gke shared nodepool")
	require.Contains(t, err.Error(), "conflicting compute class")
	require.Contains(t, err.Error(), extraNodePoolName)
	require.Contains(t, err.Error(), tikvComputeClassName)
	require.Contains(t, err.Error(), tidbComputeClassName)
}

func TestClusterServer_CreateGKEHeterogeneousDeployRejectsCurrentClusterInstanceTypeConflict(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	conflictingNodePoolName := gkeheterogeneous.NodePoolName(cluster, "n2-standard-128")
	updateTestGKEComputeClassBinding(
		t,
		cluster,
		cloudv1.AnnoKeyGKEComputeClassForTiKVBindings,
		"shared-priority",
		[]string{testGKENodePoolMachineType16, testGKENodePoolMachineType64},
		[]string{gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType16), conflictingNodePoolName},
	)
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster).Build()
	s := newGKEHeterogeneousTestServer(cli)

	_, err := s.CreateGKEHeterogeneousDeploy(context.Background(), &apiv1.CreateGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIDB,
		TidbGroupId:   cluster.GetDefaultGroupID(),
		InstanceTypes: []string{"n2-standard-128"},
	})
	require.Error(t, err)
	require.Equal(t, codes.FailedPrecondition, status.Code(err))
	require.Contains(t, err.Error(), "conflicting instance type")
	require.Contains(t, err.Error(), conflictingNodePoolName)
	require.Contains(t, err.Error(), testGKENodePoolMachineType64)
	require.Contains(t, err.Error(), "n2-standard-128")
}

func TestClusterServer_CreateGKEHeterogeneousDeployRejectsPeerClusterConflict(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cluster.Spec.NetworkName = testGKESharedNetworkName
	peer := newGKEHeterogeneousTestCluster("http://gateway.example")
	setGKEHeterogeneousTestClusterID(peer, cluster.Namespace, "peer-cluster")
	peer.Spec.NetworkName = cluster.Spec.NetworkName
	peer.Spec.Cluster.TiKV.InstanceType = "n2-standard-48"
	sharedNodePoolName := gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType64)
	peerComputeClassName := testGKEComputeClassName(t, peer, apicommon.ComponentType_TIKV, "n2-standard-48", testGKENodePoolMachineType64)
	currentComputeClassName := testGKEComputeClassName(t, cluster, apicommon.ComponentType_TIKV, testGKENodePoolMachineType16, testGKENodePoolMachineType64)
	updateTestGKEComputeClassBinding(
		t,
		peer,
		cloudv1.AnnoKeyGKEComputeClassForTiKVBindings,
		peerComputeClassName,
		[]string{"n2-standard-48", testGKENodePoolMachineType64},
		[]string{gkeheterogeneous.NodePoolName(peer, "n2-standard-48"), sharedNodePoolName},
	)
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster, peer).Build()
	s := newGKEHeterogeneousTestServer(cli)

	_, err := s.CreateGKEHeterogeneousDeploy(context.Background(), &apiv1.CreateGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
		InstanceTypes: []string{testGKENodePoolMachineType64},
	})
	require.Error(t, err)
	require.Equal(t, codes.FailedPrecondition, status.Code(err))
	require.Contains(t, err.Error(), "gke shared nodepool")
	require.Contains(t, err.Error(), "conflicting compute class")
	require.Contains(t, err.Error(), sharedNodePoolName)
	require.Contains(t, err.Error(), peerComputeClassName)
	require.Contains(t, err.Error(), currentComputeClassName)
}

func TestClusterServer_CreateGKEHeterogeneousDeployRejectsSharedNodePoolRoleConflict(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cluster.Spec.NetworkName = testGKESharedNetworkName
	peer := newGKEHeterogeneousTestCluster("http://gateway.example")
	setGKEHeterogeneousTestClusterID(peer, cluster.Namespace, "peer-cluster")
	peer.Spec.NetworkName = cluster.Spec.NetworkName
	peer.Spec.Cluster.TiKV.InstanceType = testGKENodePoolMachineType64
	sharedNodePoolName := gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType64)
	computeClassName := testGKEComputeClassName(t, cluster, apicommon.ComponentType_TIKV, testGKENodePoolMachineType16, testGKENodePoolMachineType64)
	updateTestGKEComputeClassBinding(
		t,
		peer,
		cloudv1.AnnoKeyGKEComputeClassForTiKVBindings,
		computeClassName,
		[]string{testGKENodePoolMachineType64, "n2-standard-96"},
		[]string{sharedNodePoolName, gkeheterogeneous.NodePoolName(cluster, "n2-standard-96")},
	)
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster, peer).Build()
	s := newGKEHeterogeneousTestServer(cli)

	_, err := s.CreateGKEHeterogeneousDeploy(context.Background(), &apiv1.CreateGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
		InstanceTypes: []string{testGKENodePoolMachineType64},
	})
	require.Error(t, err)
	require.Equal(t, codes.FailedPrecondition, status.Code(err))
	require.Contains(t, err.Error(), "gke shared nodepool")
	require.Contains(t, err.Error(), "conflicting compute class")
	require.Contains(t, err.Error(), sharedNodePoolName)
}

func TestValidateSharedGKEHeterogeneousNodePoolIntentRefConflictsAllowsSameInstanceTypeOnDifferentNodePools(t *testing.T) {
	refs := []gkeHeterogeneousDeployIntentRef{
		{
			nodePoolName:     "legacy-np",
			instanceType:     testGKENodePoolMachineType64,
			computeClassName: "legacy-priority",
			role:             cloudv1.GKEComputeClassNodePoolRoleExtra,
		},
		{
			nodePoolName:     "three-layer-np",
			instanceType:     testGKENodePoolMachineType64,
			computeClassName: "three-layer-priority",
			role:             cloudv1.GKEComputeClassNodePoolRoleExtra,
		},
	}

	require.NoError(t, validateSharedGKEHeterogeneousNodePoolIntentRefConflicts(refs))
}

func TestClusterServer_CreateGKEHeterogeneousDeployAllowsMatchingSharedNodePoolIntent(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cluster.Spec.NetworkName = testGKESharedNetworkName
	peer := newGKEHeterogeneousTestCluster("http://gateway.example")
	setGKEHeterogeneousTestClusterID(peer, cluster.Namespace, "peer-cluster")
	peer.Spec.NetworkName = cluster.Spec.NetworkName
	peer.Spec.Cluster.TiKV.InstanceType = testGKENodePoolMachineType16
	sharedNodePoolName := gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType64)
	computeClassName := testGKEComputeClassName(t, cluster, apicommon.ComponentType_TIKV, testGKENodePoolMachineType16, testGKENodePoolMachineType64)
	updateTestGKEComputeClassBinding(
		t,
		peer,
		cloudv1.AnnoKeyGKEComputeClassForTiKVBindings,
		computeClassName,
		[]string{testGKENodePoolMachineType16, testGKENodePoolMachineType64},
		[]string{gkeheterogeneous.NodePoolName(peer, testGKENodePoolMachineType16), sharedNodePoolName},
	)
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster, peer).Build()
	s := newGKEHeterogeneousTestServer(cli)
	prepareGKEHeterogeneousDeployNodePools(t, cli, cluster, apicommon.ComponentType_TIKV, "", testGKENodePoolMachineType64)

	resp, err := s.CreateGKEHeterogeneousDeploy(context.Background(), &apiv1.CreateGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
		InstanceTypes: []string{testGKENodePoolMachineType64},
	})
	require.NoError(t, err)
	require.Contains(t, resp.NodePoolNames, sharedNodePoolName)
	require.Equal(t, computeClassName, resp.ComputeClassName)
}

func TestClusterServer_ValidateSharedGKEHeterogeneousNodePoolIntentConflictsScopesMalformedPeer(t *testing.T) {
	tests := []struct {
		name          string
		configurePeer func(*cloudv1.Cluster)
		wantErr       bool
	}{
		{
			name: "different APPBox does not block",
			configurePeer: func(peer *cloudv1.Cluster) {
				setGKEHeterogeneousTestClusterAPPBox(peer, testGKERegionName, "tenant-2", "project-2")
			},
		},
		{
			name: "same APPBox different heterogeneous nodepool scope does not block",
			configurePeer: func(peer *cloudv1.Cluster) {
				peer.Spec.NetworkName = testGKEOtherNetworkName
			},
		},
		{
			name:    "same heterogeneous nodepool scope fails closed",
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
			cluster.Spec.NetworkName = testGKESharedNetworkName
			peer := newGKEHeterogeneousTestCluster("http://gateway.example")
			setGKEHeterogeneousTestClusterID(peer, cluster.Namespace, "peer-cluster")
			peer.Spec.NetworkName = cluster.Spec.NetworkName
			peer.UpdateAnnotation(cloudv1.AnnoKeyGKEComputeClassForTiKVBindings, `{"computeClassName":`)
			if tt.configurePeer != nil {
				tt.configurePeer(peer)
			}

			cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster, peer).Build()
			s := newGKEHeterogeneousTestServer(cli)

			err := s.validateSharedGKEHeterogeneousNodePoolIntentConflicts(context.Background(), nil, cluster)
			if !tt.wantErr {
				require.NoError(t, err)
				return
			}
			require.Error(t, err)
			require.Equal(t, codes.FailedPrecondition, status.Code(err))
			require.Contains(t, err.Error(), "cannot inspect gke heterogeneous deploy intent for cluster "+peer.Namespace+"/"+peer.Name)
		})
	}
}

func TestClusterServer_ValidateSharedGKEHeterogeneousNodePoolIntentConflicts3LayerDifferentProjectDoesNotBlock(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cluster.Spec.NetworkName = ""
	cluster.EnableFeature(consts.ClusterFeatureGCP3LayerNodePool)
	peer := newGKEHeterogeneousTestCluster("http://gateway.example")
	setGKEHeterogeneousTestClusterID(peer, cluster.Namespace, "peer-cluster")
	peer.Spec.NetworkName = ""
	peer.EnableFeature(consts.ClusterFeatureGCP3LayerNodePool)
	setGKEHeterogeneousTestClusterAPPBox(peer, "us-east1", testGKETenantID, "other-project")
	peer.UpdateAnnotation(cloudv1.AnnoKeyGKEComputeClassForTiKVBindings, `{"computeClassName":`)

	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster, peer).Build()
	s := newGKEHeterogeneousTestServer(cli)

	err := s.validateSharedGKEHeterogeneousNodePoolIntentConflicts(context.Background(), nil, cluster)
	require.NoError(t, err)
}

func TestClusterServer_ValidateSharedGKEHeterogeneousNodePoolIntentConflictsChecksOriginalAPPBox(t *testing.T) {
	original := newGKEHeterogeneousTestCluster("http://gateway.example")
	original.Spec.NetworkName = testGKESharedNetworkName
	setGKEHeterogeneousTestClusterAPPBox(original, testGKERegionName, "tenant-old", "project-old")
	updated := original.DeepCopy()
	setGKEHeterogeneousTestClusterAPPBox(updated, testGKERegionName, "tenant-new", "project-new")

	peer := newGKEHeterogeneousTestCluster("http://gateway.example")
	setGKEHeterogeneousTestClusterID(peer, original.Namespace, "peer-cluster")
	peer.Spec.NetworkName = original.Spec.NetworkName
	setGKEHeterogeneousTestClusterAPPBox(peer, testGKERegionName, "tenant-old", "project-old")
	peer.UpdateAnnotation(cloudv1.AnnoKeyGKEComputeClassForTiKVBindings, `{"computeClassName":`)

	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(original, peer).Build()
	s := newGKEHeterogeneousTestServer(cli)

	err := s.validateSharedGKEHeterogeneousNodePoolIntentConflicts(context.Background(), original, updated)
	require.Error(t, err)
	require.Equal(t, codes.FailedPrecondition, status.Code(err))
	require.Contains(t, err.Error(), "cannot inspect gke heterogeneous deploy intent for cluster "+peer.Namespace+"/"+peer.Name)
}

func TestClusterServer_ListGKEHeterogeneousClustersInSameKubernetesClusterScopeForSharedNodePoolUsesKubernetesClusterScopeLabels(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cluster.Spec.NetworkName = ""
	baseClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster).Build()
	recordingClient := &recordingClusterListClient{Client: baseClient}
	s := &ClusterServer{
		KubeClient:    recordingClient,
		clusterReader: &internalutil.LabelSelectorReader{},
	}

	peers, err := s.listGKEClustersInSameKubernetesClusterScopeForSharedNodePool(context.Background(), cluster)
	require.NoError(t, err)
	require.Len(t, peers, 1)
	require.Equal(t, cluster.Name, peers[0].Name)
	require.NotEmpty(t, recordingClient.clusterListSelectors)
}

func TestClusterServer_ListGKEHeterogeneousClustersInSameKubernetesClusterScopeForSharedNodePoolMissingTenantProjectLabelsFailsClosed(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cluster.Spec.NetworkName = ""
	cluster.EnableFeature(consts.ClusterFeatureGCP3LayerNodePool)
	delete(cluster.Labels, cloudv1.TenantTagKey)
	delete(cluster.Labels, cloudv1.ProjectTagKey)

	baseClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster).Build()
	recordingClient := &recordingClusterListClient{Client: baseClient}
	s := &ClusterServer{
		KubeClient:    recordingClient,
		clusterReader: &internalutil.LabelSelectorReader{},
	}

	peers, err := s.listGKEClustersInSameKubernetesClusterScopeForSharedNodePool(context.Background(), cluster)
	require.Error(t, err)
	require.Equal(t, codes.FailedPrecondition, status.Code(err))
	require.Contains(t, err.Error(), "missing required GKE heterogeneous nodepool scope labels")
	require.Nil(t, peers)
	require.Empty(t, recordingClient.clusterListSelectors)
}

func TestClusterServer_CreateGKEHeterogeneousDeployAllowsPeerOrdinaryBaseInstanceType(t *testing.T) {
	tests := []struct {
		name          string
		configurePeer func(*cloudv1.Cluster)
	}{
		{
			name: "tikv base",
			configurePeer: func(peer *cloudv1.Cluster) {
				peer.Spec.Cluster.TiKV.InstanceType = testGKENodePoolMachineType64
			},
		},
		{
			name: "tidb base",
			configurePeer: func(peer *cloudv1.Cluster) {
				peer.Spec.Cluster.TiDB.InstanceType = testGKENodePoolMachineType64
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
			cluster.Spec.NetworkName = testGKESharedNetworkName
			peer := newGKEHeterogeneousTestCluster("http://gateway.example")
			setGKEHeterogeneousTestClusterID(peer, cluster.Namespace, "peer-cluster")
			peer.Spec.NetworkName = cluster.Spec.NetworkName
			tt.configurePeer(peer)
			cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster, peer).Build()
			s := newGKEHeterogeneousTestServer(cli)
			prepareGKEHeterogeneousDeployNodePools(t, cli, cluster, apicommon.ComponentType_TIKV, "", testGKENodePoolMachineType64)

			resp, err := s.CreateGKEHeterogeneousDeploy(context.Background(), &apiv1.CreateGKEHeterogeneousDeployRequest{
				ClusterId:     cluster.Name,
				ComponentType: apicommon.ComponentType_TIKV,
				InstanceTypes: []string{testGKENodePoolMachineType64},
			})
			require.NoError(t, err)
			require.Equal(t, []string{testGKENodePoolMachineType16, testGKENodePoolMachineType64}, resp.InstanceTypes)
			require.Equal(t, []string{
				gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType16),
				gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType64),
			}, resp.NodePoolNames)

			updated := &cloudv1.Cluster{}
			require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Namespace: cluster.Namespace, Name: cluster.Name}, updated))
			requireTestGKEComputeClassBinding(t, updated, cloudv1.AnnoKeyGKEComputeClassForTiKVBindings, resp.ComputeClassName, resp.InstanceTypes, resp.NodePoolNames)
		})
	}
}

func TestClusterServer_UpdateGKEHeterogeneousDeployAllowsPeerOrdinaryBaseInstanceType(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cluster.Spec.NetworkName = testGKESharedNetworkName
	peer := newGKEHeterogeneousTestCluster("http://gateway.example")
	setGKEHeterogeneousTestClusterID(peer, cluster.Namespace, "peer-cluster")
	peer.Spec.NetworkName = cluster.Spec.NetworkName
	peer.Spec.Cluster.TiKV.InstanceType = testGKENodePoolMachineType64
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster, peer).Build()
	s := newGKEHeterogeneousTestServer(cli)
	prepareGKEHeterogeneousDeployNodePools(t, cli, cluster, apicommon.ComponentType_TIKV, "", "n2-standard-32")

	createResp, err := s.CreateGKEHeterogeneousDeploy(context.Background(), &apiv1.CreateGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
		InstanceTypes: []string{"n2-standard-32"},
	})
	require.NoError(t, err)
	require.Equal(t, []string{testGKENodePoolMachineType16, "n2-standard-32"}, createResp.InstanceTypes)
	before := &cloudv1.Cluster{}
	require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Namespace: cluster.Namespace, Name: cluster.Name}, before))
	beforeBinding := before.GetAnnotations()[cloudv1.AnnoKeyGKEComputeClassForTiKVBindings]
	require.NotEmpty(t, beforeBinding)

	prepareGKEHeterogeneousDeployNodePools(t, cli, cluster, apicommon.ComponentType_TIKV, "", testGKENodePoolMachineType64)
	_, err = s.UpdateGKEHeterogeneousDeploy(context.Background(), &apiv1.UpdateGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
		InstanceTypes: []string{testGKENodePoolMachineType64},
	})
	require.NoError(t, err)

	updated := &cloudv1.Cluster{}
	require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Namespace: cluster.Namespace, Name: cluster.Name}, updated))
	require.NotEqual(t, beforeBinding, updated.GetAnnotations()[cloudv1.AnnoKeyGKEComputeClassForTiKVBindings])
}

func TestClusterServer_CreateGKEHeterogeneousDeployAllowsDifferentPeerOrdinaryBaseInstanceType(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cluster.Spec.NetworkName = testGKESharedNetworkName
	peer := newGKEHeterogeneousTestCluster("http://gateway.example")
	setGKEHeterogeneousTestClusterID(peer, cluster.Namespace, "peer-cluster")
	peer.Spec.NetworkName = cluster.Spec.NetworkName
	peer.Spec.Cluster.TiKV.InstanceType = "n2-standard-32"
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster, peer).Build()
	s := newGKEHeterogeneousTestServer(cli)
	prepareGKEHeterogeneousDeployNodePools(t, cli, cluster, apicommon.ComponentType_TIKV, "", testGKENodePoolMachineType64)

	resp, err := s.CreateGKEHeterogeneousDeploy(context.Background(), &apiv1.CreateGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
		InstanceTypes: []string{testGKENodePoolMachineType64},
	})
	require.NoError(t, err)
	require.Equal(t, []string{testGKENodePoolMachineType16, testGKENodePoolMachineType64}, resp.InstanceTypes)
	updated := &cloudv1.Cluster{}
	require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Namespace: cluster.Namespace, Name: cluster.Name}, updated))
	requireTestGKEComputeClassBinding(t, updated, cloudv1.AnnoKeyGKEComputeClassForTiKVBindings, resp.ComputeClassName, resp.InstanceTypes, resp.NodePoolNames)
}

func TestClusterServer_UpdateGKEHeterogeneousDeployRequiresCompleteIntent(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	updateTestGKEComputeClassBinding(t, cluster, cloudv1.AnnoKeyGKEComputeClassForTiKVBindings, "tikv-priority", []string{testGKENodePoolMachineType16, "n2-standard-32"}, []string{gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType16), gkeheterogeneous.NodePoolName(cluster, "n2-standard-32")})
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster).Build()
	s := newGKEHeterogeneousTestServer(cli)

	_, err := s.UpdateGKEHeterogeneousDeploy(context.Background(), &apiv1.UpdateGKEHeterogeneousDeployRequest{
		ClusterId:        cluster.Name,
		ComponentType:    apicommon.ComponentType_TIKV,
		ComputeClassName: "tikv-priority",
	})
	require.Error(t, err)
	require.Equal(t, codes.InvalidArgument, status.Code(err))
	require.Contains(t, err.Error(), "instance_types must contain exactly one extra instance type")

	_, err = s.UpdateGKEHeterogeneousDeploy(context.Background(), &apiv1.UpdateGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
		InstanceTypes: []string{testGKENodePoolMachineType16},
	})
	require.Error(t, err)
	require.Equal(t, codes.InvalidArgument, status.Code(err))
	require.Contains(t, err.Error(), "instance_types must contain exactly one extra instance type")
}

func TestClusterServer_GKEHeterogeneousDeployStatusRequiresSameScopeNodePool(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cluster.Spec.NetworkName = testGKESharedNetworkName
	baseNodePoolName := gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType16)
	extraNodePoolName := gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType64)
	computeClassName := testGKEComputeClassName(t, cluster, apicommon.ComponentType_TIKV, testGKENodePoolMachineType16, testGKENodePoolMachineType64)
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(
		cluster,
		newAPIManagedGKENodePool(cluster, baseNodePoolName, testGKENodePoolMachineType16),
		newAPIManagedGKENodePool(cluster, extraNodePoolName, testGKENodePoolMachineType64),
	).Build()
	s := newGKEHeterogeneousTestServer(cli)

	_, err := s.CreateGKEHeterogeneousDeploy(context.Background(), &apiv1.CreateGKEHeterogeneousDeployRequest{
		ClusterId:        cluster.Name,
		ComponentType:    apicommon.ComponentType_TIKV,
		ComputeClassName: computeClassName,
		InstanceTypes:    []string{testGKENodePoolMachineType64},
	})
	require.NoError(t, err)
	markNodePoolComputeClass(t, cli, baseNodePoolName, computeClassName)
	markNodePoolComputeClass(t, cli, extraNodePoolName, computeClassName)
	extraNodePool := &gkev1beta1.NodePool{}
	require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Name: extraNodePoolName}, extraNodePool))
	extraNodePool.Labels[cloudv1.TenantTagKey] = "other-tenant"
	require.NoError(t, cli.Update(context.Background(), extraNodePool))
	markNodePoolConditions(t, cli, baseNodePoolName, corev1.ConditionTrue, corev1.ConditionTrue)
	markNodePoolConditions(t, cli, extraNodePoolName, corev1.ConditionTrue, corev1.ConditionTrue)

	_, err = s.GetGKEHeterogeneousDeploy(context.Background(), &apiv1.GetGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
	})
	require.Error(t, err)
	require.Equal(t, codes.FailedPrecondition, status.Code(err))
	require.Contains(t, err.Error(), "does not belong to cluster")
}

func TestClusterServer_GKEHeterogeneousDeployStatusRequiresMatchingMachineTypeNodePool(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	baseNodePoolName := gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType16)
	extraNodePoolName := gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType64)
	computeClassName := testGKEComputeClassName(t, cluster, apicommon.ComponentType_TIKV, testGKENodePoolMachineType16, testGKENodePoolMachineType64)
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(
		cluster,
		newAPIManagedGKENodePool(cluster, baseNodePoolName, testGKENodePoolMachineType16),
		newAPIManagedGKENodePool(cluster, extraNodePoolName, testGKENodePoolMachineType64),
	).Build()
	s := newGKEHeterogeneousTestServer(cli)

	_, err := s.CreateGKEHeterogeneousDeploy(context.Background(), &apiv1.CreateGKEHeterogeneousDeployRequest{
		ClusterId:        cluster.Name,
		ComponentType:    apicommon.ComponentType_TIKV,
		ComputeClassName: computeClassName,
		InstanceTypes:    []string{testGKENodePoolMachineType64},
	})
	require.NoError(t, err)
	markNodePoolComputeClass(t, cli, baseNodePoolName, computeClassName)
	markNodePoolComputeClass(t, cli, extraNodePoolName, computeClassName)
	extraNodePool := &gkev1beta1.NodePool{}
	require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Name: extraNodePoolName}, extraNodePool))
	extraNodePool.Spec.ForProvider.Config.MachineType = ptr.To("n2-standard-32")
	require.NoError(t, cli.Update(context.Background(), extraNodePool))
	markNodePoolConditions(t, cli, baseNodePoolName, corev1.ConditionTrue, corev1.ConditionTrue)
	markNodePoolConditions(t, cli, extraNodePoolName, corev1.ConditionTrue, corev1.ConditionTrue)

	_, err = s.GetGKEHeterogeneousDeploy(context.Background(), &apiv1.GetGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
	})
	require.Error(t, err)
	require.Equal(t, codes.FailedPrecondition, status.Code(err))
	require.Contains(t, err.Error(), "has machine type n2-standard-32, want n2-standard-64")
}

func TestClusterServer_GKEHeterogeneousDeployStatusRequiresManagedExtraNodePool(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	baseNodePoolName := gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType16)
	extraNodePoolName := gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType64)
	computeClassName := testGKEComputeClassName(t, cluster, apicommon.ComponentType_TIKV, testGKENodePoolMachineType16, testGKENodePoolMachineType64)
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(
		cluster,
		newAPIManagedGKENodePool(cluster, baseNodePoolName, testGKENodePoolMachineType16),
		newAPIManagedGKENodePool(cluster, extraNodePoolName, testGKENodePoolMachineType64),
	).Build()
	s := newGKEHeterogeneousTestServer(cli)

	_, err := s.CreateGKEHeterogeneousDeploy(context.Background(), &apiv1.CreateGKEHeterogeneousDeployRequest{
		ClusterId:        cluster.Name,
		ComponentType:    apicommon.ComponentType_TIKV,
		ComputeClassName: computeClassName,
		InstanceTypes:    []string{testGKENodePoolMachineType64},
	})
	require.NoError(t, err)
	markNodePoolComputeClass(t, cli, baseNodePoolName, computeClassName)
	markNodePoolComputeClass(t, cli, extraNodePoolName, computeClassName)
	extraNodePool := &gkev1beta1.NodePool{}
	require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Name: extraNodePoolName}, extraNodePool))
	delete(extraNodePool.Labels, consts.GKEHeterogeneousNodePoolManagedByLabelKey)
	require.NoError(t, cli.Update(context.Background(), extraNodePool))
	markNodePoolConditions(t, cli, baseNodePoolName, corev1.ConditionTrue, corev1.ConditionTrue)
	markNodePoolConditions(t, cli, extraNodePoolName, corev1.ConditionTrue, corev1.ConditionTrue)

	_, err = s.GetGKEHeterogeneousDeploy(context.Background(), &apiv1.GetGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
	})
	require.Error(t, err)
	require.Equal(t, codes.FailedPrecondition, status.Code(err))
	require.Contains(t, err.Error(), "not managed by heterogeneous nodepool API")
}

func TestClusterServer_GKEHeterogeneousDeployStatusWaitsForMissingComputeClassBinding(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	baseNodePoolName := gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType16)
	extraNodePoolName := gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType64)
	computeClassName := testGKEComputeClassName(t, cluster, apicommon.ComponentType_TIKV, testGKENodePoolMachineType16, testGKENodePoolMachineType64)
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(
		cluster,
		newHeterogeneousGKENodePool(cluster, baseNodePoolName, testGKENodePoolMachineType16, computeClassName),
		newAPIManagedGKENodePool(cluster, extraNodePoolName, testGKENodePoolMachineType64),
	).Build()
	s := newGKEHeterogeneousTestServer(cli)

	_, err := s.CreateGKEHeterogeneousDeploy(context.Background(), &apiv1.CreateGKEHeterogeneousDeployRequest{
		ClusterId:        cluster.Name,
		ComponentType:    apicommon.ComponentType_TIKV,
		ComputeClassName: computeClassName,
		InstanceTypes:    []string{testGKENodePoolMachineType64},
	})
	require.NoError(t, err)
	markNodePoolConditions(t, cli, baseNodePoolName, corev1.ConditionTrue, corev1.ConditionTrue)
	markNodePoolConditions(t, cli, extraNodePoolName, corev1.ConditionTrue, corev1.ConditionTrue)

	resp, err := s.GetGKEHeterogeneousDeploy(context.Background(), &apiv1.GetGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
	})
	require.NoError(t, err)
	require.Equal(t, apiv1.GKEHeterogeneousDeployStatus_GKE_HETEROGENEOUS_DEPLOY_STATUS_INTENT_ENABLED_NOT_READY, resp.Status)
}

func TestClusterServer_GKEHeterogeneousDeployStatusRequiresTargetComputeClassBinding(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	baseNodePoolName := gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType16)
	extraNodePoolName := gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType64)
	computeClassName := testGKEComputeClassName(t, cluster, apicommon.ComponentType_TIKV, testGKENodePoolMachineType16, testGKENodePoolMachineType64)
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(
		cluster,
		newHeterogeneousGKENodePool(cluster, baseNodePoolName, testGKENodePoolMachineType16, computeClassName),
		newHeterogeneousGKENodePool(cluster, extraNodePoolName, testGKENodePoolMachineType64, "other-priority"),
	).Build()
	s := newGKEHeterogeneousTestServer(cli)

	_, err := s.CreateGKEHeterogeneousDeploy(context.Background(), &apiv1.CreateGKEHeterogeneousDeployRequest{
		ClusterId:        cluster.Name,
		ComponentType:    apicommon.ComponentType_TIKV,
		ComputeClassName: computeClassName,
		InstanceTypes:    []string{testGKENodePoolMachineType64},
	})
	require.Error(t, err)
	require.Equal(t, codes.FailedPrecondition, status.Code(err))
	require.Contains(t, err.Error(), "bound to desired compute class other-priority, want")
}

func TestClusterServer_DeleteGKEHeterogeneousDeployOnlyClearsIntent(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	nodePoolNames := []string{gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType16), gkeheterogeneous.NodePoolName(cluster, "n2-standard-32")}
	computeClassName := updateTestGKEComputeClassBinding(t, cluster, cloudv1.AnnoKeyGKEComputeClassForTiKVBindings, "tikv-priority", []string{testGKENodePoolMachineType16, "n2-standard-32"}, nodePoolNames)
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(
		cluster,
		newHeterogeneousGKENodePool(cluster, nodePoolNames[0], testGKENodePoolMachineType16, computeClassName),
		newHeterogeneousGKENodePool(cluster, nodePoolNames[1], "n2-standard-32", computeClassName),
	).Build()

	s := newGKEHeterogeneousTestServer(cli)

	_, err := s.DeleteGKEHeterogeneousDeploy(context.Background(), &apiv1.DeleteGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
		TidbGroupId:   "ignored",
	})
	require.NoError(t, err)

	updated := &cloudv1.Cluster{}
	require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Namespace: cluster.Namespace, Name: cluster.Name}, updated))
	require.False(t, updated.TiKVGKEComputeClassIntentConfigured())
	require.NotContains(t, updated.GetAnnotations(), cloudv1.AnnoKeyGKEComputeClassForTiKVBindings)
	requireNodePoolComputeClass(t, cli, nodePoolNames[0], computeClassName)
	requireNodePoolComputeClass(t, cli, nodePoolNames[1], computeClassName)
}

func TestClusterServer_CreateGKEHeterogeneousDeployDoesNotRequireGateway(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("")
	cluster.Status.Gateway = nil
	baseNodePoolName := gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType16)
	extraNodePoolName := gkeheterogeneous.NodePoolName(cluster, "n2-standard-32")
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(
		cluster,
		newAPIManagedGKENodePool(cluster, baseNodePoolName, testGKENodePoolMachineType16),
		newAPIManagedGKENodePool(cluster, extraNodePoolName, "n2-standard-32"),
	).Build()
	s := newGKEHeterogeneousTestServer(cli)
	computeClassName := testGKEComputeClassName(t, cluster, apicommon.ComponentType_TIKV, testGKENodePoolMachineType16, "n2-standard-32")

	resp, err := s.CreateGKEHeterogeneousDeploy(context.Background(), &apiv1.CreateGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
		InstanceTypes: []string{"n2-standard-32"},
	})
	require.NoError(t, err)
	require.True(t, resp.Enabled)
	require.Equal(t, computeClassName, resp.ComputeClassName)
	require.Equal(t, []string{baseNodePoolName, extraNodePoolName}, resp.NodePoolNames)
	require.Equal(t, apiv1.GKEHeterogeneousDeployStatus_GKE_HETEROGENEOUS_DEPLOY_STATUS_INTENT_ENABLED_NOT_READY, resp.Status)
	requireNodePoolWithoutComputeClass(t, cli, baseNodePoolName)
}

func TestClusterServer_CreateGKEHeterogeneousDeployRejectsInvalidNodePoolName(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster).Build()
	s := newGKEHeterogeneousTestServer(cli)

	_, err := s.CreateGKEHeterogeneousDeploy(context.Background(), &apiv1.CreateGKEHeterogeneousDeployRequest{
		ClusterId:        cluster.Name,
		ComponentType:    apicommon.ComponentType_TIKV,
		ComputeClassName: "tikv-priority",
		NodePoolNames:    []string{"Bad_Name"},
		InstanceTypes:    []string{testGKENodePoolMachineType16},
	})
	require.Error(t, err)
	require.Equal(t, codes.InvalidArgument, status.Code(err))
}

func TestClusterServer_CreateGKEHeterogeneousDeployAutoGeneratesNodePoolNames(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	expectedNodePoolNames := []string{
		gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType16),
		gkeheterogeneous.NodePoolName(cluster, "n2-standard-32"),
	}
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(
		cluster,
		newAPIManagedGKENodePool(cluster, gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType16), testGKENodePoolMachineType16),
		newAPIManagedGKENodePool(cluster, gkeheterogeneous.NodePoolName(cluster, "n2-standard-32"), "n2-standard-32"),
	).Build()
	s := newGKEHeterogeneousTestServer(cli)

	resp, err := s.CreateGKEHeterogeneousDeploy(context.Background(), &apiv1.CreateGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
		InstanceTypes: []string{"n2-standard-32"},
	})
	require.NoError(t, err)
	require.Equal(t, expectedNodePoolNames, resp.NodePoolNames)
}

func TestClusterServer_CreateGKEHeterogeneousDeployRejectsMismatchedNodePoolNames(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster).Build()
	s := newGKEHeterogeneousTestServer(cli)

	_, err := s.CreateGKEHeterogeneousDeploy(context.Background(), &apiv1.CreateGKEHeterogeneousDeployRequest{
		ClusterId:        cluster.Name,
		ComponentType:    apicommon.ComponentType_TIKV,
		ComputeClassName: "tikv-priority",
		NodePoolNames:    []string{"np-a", gkeheterogeneous.NodePoolName(cluster, "n2-standard-32")},
		InstanceTypes:    []string{"n2-standard-32"},
	})
	require.Error(t, err)
	require.Equal(t, codes.InvalidArgument, status.Code(err))
	require.Contains(t, err.Error(), "does not match generated nodepool name")
}

func TestClusterServer_CreateGKEHeterogeneousDeployForTiDBExtraGroupUsesGroupAnnotations(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cluster.Spec.ExtraGroups = []cloudv1.ExtraGroupSpec{
		{
			Name: "group1",
			TiDB: cloudv1.TiDB{
				Replicas:     2,
				InstanceType: "n2-standard-32",
			},
		},
	}
	expectedNodePoolNames := []string{
		gkeheterogeneous.NodePoolName(cluster, "n2-standard-32"),
		gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType64),
	}
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(
		cluster,
		newAPIManagedGKENodePool(cluster, expectedNodePoolNames[0], "n2-standard-32"),
		newAPIManagedGKENodePool(cluster, expectedNodePoolNames[1], testGKENodePoolMachineType64),
	).Build()
	s := newGKEHeterogeneousTestServer(cli)
	computeClassName := testGKEComputeClassName(t, cluster, apicommon.ComponentType_TIDB, "n2-standard-32", testGKENodePoolMachineType64)

	resp, err := s.CreateGKEHeterogeneousDeploy(context.Background(), &apiv1.CreateGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIDB,
		TidbGroupId:   "group1",
		InstanceTypes: []string{testGKENodePoolMachineType64},
	})
	require.NoError(t, err)
	require.True(t, resp.Enabled)
	require.Equal(t, "group1", resp.TidbGroupId)
	require.Equal(t, computeClassName, resp.ComputeClassName)
	require.Equal(t, expectedNodePoolNames, resp.NodePoolNames)

	updated := &cloudv1.Cluster{}
	require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Namespace: cluster.Namespace, Name: cluster.Name}, updated))
	requireTestGKEComputeClassBinding(t, updated, updated.TiDBGKEComputeClassBindingsAnnoKey("group1"), computeClassName, []string{"n2-standard-32", testGKENodePoolMachineType64}, expectedNodePoolNames)
	require.Empty(t, updated.GetAnnotations()[cloudv1.AnnoKeyGKEComputeClassForTiDBBindings])
	require.True(t, updated.GKEComputeClassForTiDBExtraGroupEnabled("group1"))
	require.False(t, updated.GKEComputeClassForTiDBDefaultGroupEnabled())
	require.Equal(t, apiv1.GKEHeterogeneousDeployStatus_GKE_HETEROGENEOUS_DEPLOY_STATUS_INTENT_ENABLED_NOT_READY, resp.Status)
	requireNodePoolWithoutComputeClass(t, cli, expectedNodePoolNames[0])
	requireNodePoolWithoutComputeClass(t, cli, expectedNodePoolNames[1])
}

func TestClusterServer_CreateGKEHeterogeneousDeployForTiDBDefaultGroupUsesDefaultAnnotations(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	expectedNodePoolNames := []string{
		gkeheterogeneous.NodePoolName(cluster, "n2-standard-8"),
		gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType64),
	}
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(
		cluster,
		newAPIManagedGKENodePool(cluster, expectedNodePoolNames[0], "n2-standard-8"),
		newAPIManagedGKENodePool(cluster, expectedNodePoolNames[1], testGKENodePoolMachineType64),
	).Build()
	s := newGKEHeterogeneousTestServer(cli)
	computeClassName := testGKEComputeClassName(t, cluster, apicommon.ComponentType_TIDB, "n2-standard-8", testGKENodePoolMachineType64)

	resp, err := s.CreateGKEHeterogeneousDeploy(context.Background(), &apiv1.CreateGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIDB,
		TidbGroupId:   cluster.GetDefaultGroupID(),
		InstanceTypes: []string{testGKENodePoolMachineType64},
	})
	require.NoError(t, err)
	require.True(t, resp.Enabled)
	require.Equal(t, strings.TrimSpace(cluster.GetDefaultGroupID()), resp.TidbGroupId)
	require.Equal(t, expectedNodePoolNames, resp.NodePoolNames)

	updated := &cloudv1.Cluster{}
	require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Namespace: cluster.Namespace, Name: cluster.Name}, updated))
	requireTestGKEComputeClassBinding(t, updated, cloudv1.AnnoKeyGKEComputeClassForTiDBBindings, computeClassName, []string{"n2-standard-8", testGKENodePoolMachineType64}, expectedNodePoolNames)
	require.Empty(t, updated.GetAnnotations()[updated.TiDBGKEComputeClassBindingsAnnoKey("group1")])
	require.Equal(t, apiv1.GKEHeterogeneousDeployStatus_GKE_HETEROGENEOUS_DEPLOY_STATUS_INTENT_ENABLED_NOT_READY, resp.Status)
	requireNodePoolWithoutComputeClass(t, cli, expectedNodePoolNames[0])
	requireNodePoolWithoutComputeClass(t, cli, expectedNodePoolNames[1])
}

func TestClusterServer_CreateAndUpdateTiDBGroupIntentPreservesOtherGroupAnnotations(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cluster.Spec.ExtraGroups = []cloudv1.ExtraGroupSpec{
		{
			Name: "group1",
			TiDB: cloudv1.TiDB{
				Replicas:     2,
				InstanceType: "n2-standard-32",
			},
		},
		{
			Name: "group2",
			TiDB: cloudv1.TiDB{
				Replicas:     2,
				InstanceType: "n2-standard-48",
			},
		},
	}
	group2Key := cluster.TiDBGKEComputeClassBindingsAnnoKey("group2")
	group2NodePoolNames := []string{
		gkeheterogeneous.NodePoolName(cluster, "n2-standard-48"),
		gkeheterogeneous.NodePoolName(cluster, "n2-standard-96"),
	}
	updateTestGKEComputeClassBinding(t, cluster, group2Key, "tidb-group2-priority", []string{"n2-standard-48", "n2-standard-96"}, group2NodePoolNames)
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster).Build()
	s := newGKEHeterogeneousTestServer(cli)

	prepareGKEHeterogeneousDeployNodePools(t, cli, cluster, apicommon.ComponentType_TIDB, "group1", testGKENodePoolMachineType64)
	_, err := s.CreateGKEHeterogeneousDeploy(context.Background(), &apiv1.CreateGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIDB,
		TidbGroupId:   "group1",
		InstanceTypes: []string{testGKENodePoolMachineType64},
	})
	require.NoError(t, err)

	updated := &cloudv1.Cluster{}
	require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Namespace: cluster.Namespace, Name: cluster.Name}, updated))
	requireTestGKEComputeClassBinding(t, updated, group2Key, "tidb-group2-priority", []string{"n2-standard-48", "n2-standard-96"}, group2NodePoolNames)

	prepareGKEHeterogeneousDeployNodePools(t, cli, cluster, apicommon.ComponentType_TIDB, "group1", "n2-standard-80")
	_, err = s.UpdateGKEHeterogeneousDeploy(context.Background(), &apiv1.UpdateGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIDB,
		TidbGroupId:   "group1",
		InstanceTypes: []string{"n2-standard-80"},
	})
	require.NoError(t, err)

	require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Namespace: cluster.Namespace, Name: cluster.Name}, updated))
	requireTestGKEComputeClassBinding(t, updated, group2Key, "tidb-group2-priority", []string{"n2-standard-48", "n2-standard-96"}, group2NodePoolNames)
}

func TestClusterServer_CreateGKEHeterogeneousDeployForTiDBRejectsMissingGroupID(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster).Build()
	s := newGKEHeterogeneousTestServer(cli)

	_, err := s.CreateGKEHeterogeneousDeploy(context.Background(), &apiv1.CreateGKEHeterogeneousDeployRequest{
		ClusterId:        cluster.Name,
		ComponentType:    apicommon.ComponentType_TIDB,
		InstanceTypes:    []string{"n2-standard-8"},
		ComputeClassName: "tidb-default-priority",
	})
	require.Error(t, err)
	require.Equal(t, codes.InvalidArgument, status.Code(err))
	require.Contains(t, err.Error(), "tidb_group_id is required")
}

func TestClusterServer_DeleteTiDBExtraGroupIntentPreservesDefaultGroupIntent(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cluster.Spec.ExtraGroups = []cloudv1.ExtraGroupSpec{
		{
			Name: "group1",
			TiDB: cloudv1.TiDB{
				Replicas:     2,
				InstanceType: "n2-standard-32",
			},
		},
	}
	updateTestGKEComputeClassBinding(t, cluster, cloudv1.AnnoKeyGKEComputeClassForTiDBBindings, "tidb-default-priority", []string{"n2-standard-8", testGKENodePoolMachineType16}, []string{gkeheterogeneous.NodePoolName(cluster, "n2-standard-8"), gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType16)})
	updateTestGKEComputeClassBinding(t, cluster, cluster.TiDBGKEComputeClassBindingsAnnoKey("group1"), "tidb-group1-priority", []string{"n2-standard-32", testGKENodePoolMachineType64}, []string{gkeheterogeneous.NodePoolName(cluster, "n2-standard-32"), gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType64)})
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster).Build()
	s := newGKEHeterogeneousTestServer(cli)

	_, err := s.DeleteGKEHeterogeneousDeploy(context.Background(), &apiv1.DeleteGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIDB,
		TidbGroupId:   "group1",
	})
	require.NoError(t, err)

	updated := &cloudv1.Cluster{}
	require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Namespace: cluster.Namespace, Name: cluster.Name}, updated))
	requireTestGKEComputeClassBinding(t, updated, cloudv1.AnnoKeyGKEComputeClassForTiDBBindings, "tidb-default-priority", []string{"n2-standard-8", testGKENodePoolMachineType16}, []string{gkeheterogeneous.NodePoolName(updated, "n2-standard-8"), gkeheterogeneous.NodePoolName(updated, testGKENodePoolMachineType16)})
	require.NotContains(t, updated.GetAnnotations(), updated.TiDBGKEComputeClassBindingsAnnoKey("group1"))
}

func TestClusterServer_DeleteOnlyTiDBExtraGroupIntentClearsGroupAnnotation(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cluster.Spec.ExtraGroups = []cloudv1.ExtraGroupSpec{
		{
			Name: "group1",
			TiDB: cloudv1.TiDB{
				Replicas:     2,
				InstanceType: "n2-standard-32",
			},
		},
	}
	updateTestGKEComputeClassBinding(t, cluster, cluster.TiDBGKEComputeClassBindingsAnnoKey("group1"), "tidb-group1-priority", []string{"n2-standard-32", testGKENodePoolMachineType64}, []string{gkeheterogeneous.NodePoolName(cluster, "n2-standard-32"), gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType64)})
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster).Build()
	s := newGKEHeterogeneousTestServer(cli)

	_, err := s.DeleteGKEHeterogeneousDeploy(context.Background(), &apiv1.DeleteGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIDB,
		TidbGroupId:   "group1",
	})
	require.NoError(t, err)

	updated := &cloudv1.Cluster{}
	require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Namespace: cluster.Namespace, Name: cluster.Name}, updated))
	require.NotContains(t, updated.GetAnnotations(), updated.TiDBGKEComputeClassBindingsAnnoKey("group1"))
}

func TestClusterServer_DeleteOrphanedTiDBGroupIntentClearsGroupAnnotation(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	updateTestGKEComputeClassBinding(t, cluster, cluster.TiDBGKEComputeClassBindingsAnnoKey("orphan"), "tidb-orphan-priority", []string{"n2-standard-32", testGKENodePoolMachineType64}, []string{gkeheterogeneous.NodePoolName(cluster, "n2-standard-32"), gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType64)})
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster).Build()
	s := newGKEHeterogeneousTestServer(cli)

	_, err := s.DeleteGKEHeterogeneousDeploy(context.Background(), &apiv1.DeleteGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIDB,
		TidbGroupId:   "orphan",
	})
	require.NoError(t, err)

	updated := &cloudv1.Cluster{}
	require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Namespace: cluster.Namespace, Name: cluster.Name}, updated))
	require.NotContains(t, updated.GetAnnotations(), updated.TiDBGKEComputeClassBindingsAnnoKey("orphan"))
}

func TestClusterServer_DeleteMissingTiDBGroupIntentWithoutAnnotationReturnsNotFound(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster).Build()
	s := newGKEHeterogeneousTestServer(cli)

	_, err := s.DeleteGKEHeterogeneousDeploy(context.Background(), &apiv1.DeleteGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIDB,
		TidbGroupId:   "missing",
	})
	require.Error(t, err)
	require.Equal(t, codes.NotFound, status.Code(err))
	require.Contains(t, err.Error(), "TiDB group missing not found")
}

func TestClusterServer_DeleteTiDBDefaultGroupIntentPreservesExtraGroupIntent(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cluster.Spec.ExtraGroups = []cloudv1.ExtraGroupSpec{
		{
			Name: "group1",
			TiDB: cloudv1.TiDB{
				Replicas:     2,
				InstanceType: "n2-standard-32",
			},
		},
	}
	updateTestGKEComputeClassBinding(t, cluster, cloudv1.AnnoKeyGKEComputeClassForTiDBBindings, "tidb-default-priority", []string{"n2-standard-8", testGKENodePoolMachineType16}, []string{gkeheterogeneous.NodePoolName(cluster, "n2-standard-8"), gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType16)})
	updateTestGKEComputeClassBinding(t, cluster, cluster.TiDBGKEComputeClassBindingsAnnoKey("group1"), "tidb-group1-priority", []string{"n2-standard-32", testGKENodePoolMachineType64}, []string{gkeheterogeneous.NodePoolName(cluster, "n2-standard-32"), gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType64)})
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster).Build()
	s := newGKEHeterogeneousTestServer(cli)

	_, err := s.DeleteGKEHeterogeneousDeploy(context.Background(), &apiv1.DeleteGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIDB,
		TidbGroupId:   cluster.GetDefaultGroupID(),
	})
	require.NoError(t, err)

	updated := &cloudv1.Cluster{}
	require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Namespace: cluster.Namespace, Name: cluster.Name}, updated))
	require.False(t, updated.GKEComputeClassForTiDBDefaultGroupEnabled())
	require.True(t, updated.GKEComputeClassForTiDBExtraGroupEnabled("group1"))
	require.Empty(t, updated.GetAnnotations()[cloudv1.AnnoKeyGKEComputeClassForTiDBBindings])
	requireTestGKEComputeClassBinding(t, updated, updated.TiDBGKEComputeClassBindingsAnnoKey("group1"), "tidb-group1-priority", []string{"n2-standard-32", testGKENodePoolMachineType64}, []string{gkeheterogeneous.NodePoolName(updated, "n2-standard-32"), gkeheterogeneous.NodePoolName(updated, testGKENodePoolMachineType64)})
}

func TestClusterServer_ListGKEHeterogeneousDeploysIncludesActiveTiDBExtraGroups(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cluster.Spec.ExtraGroups = []cloudv1.ExtraGroupSpec{
		{
			Name: "group1",
			TiDB: cloudv1.TiDB{
				Replicas:     2,
				InstanceType: "n2-standard-32",
			},
		},
	}
	updateTestGKEComputeClassBinding(t, cluster, cluster.TiDBGKEComputeClassBindingsAnnoKey("group1"), "tidb-group1-priority", []string{"n2-standard-32", testGKENodePoolMachineType64}, []string{gkeheterogeneous.NodePoolName(cluster, "n2-standard-32"), gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType64)})
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster).Build()
	s := newGKEHeterogeneousTestServer(cli)

	resp, err := s.ListGKEHeterogeneousDeploys(context.Background(), &apiv1.ListGKEHeterogeneousDeploysRequest{
		ClusterId: cluster.Name,
	})
	require.NoError(t, err)
	require.Len(t, resp.Deploys, 2)
	require.Equal(t, apicommon.ComponentType_TIKV, resp.Deploys[0].ComponentType)
	require.Equal(t, apicommon.ComponentType_TIDB, resp.Deploys[1].ComponentType)
	require.Equal(t, "group1", resp.Deploys[1].TidbGroupId)
}

func TestClusterServer_ListGKEHeterogeneousDeploysDoesNotAppendDefaultTiDBDeployWhenDefaultGroupIDMissing(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	delete(cluster.Annotations, cloudv1.DefaultLoadBalancerIDAnnotationKey)
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster).Build()
	s := newGKEHeterogeneousTestServer(cli)

	resp, err := s.ListGKEHeterogeneousDeploys(context.Background(), &apiv1.ListGKEHeterogeneousDeploysRequest{
		ClusterId: cluster.Name,
	})
	require.NoError(t, err)
	require.Len(t, resp.Deploys, 1)
	require.Equal(t, apicommon.ComponentType_TIKV, resp.Deploys[0].ComponentType)
	for _, deploy := range resp.Deploys {
		require.NotEqual(t, apicommon.ComponentType_TIDB, deploy.ComponentType)
	}
}

func TestClusterServer_ListGKEHeterogeneousDeploysRejectsDefaultTiDBIntentWhenDefaultGroupIDMissing(t *testing.T) {
	tests := []struct {
		name      string
		configure func(*cloudv1.Cluster)
	}{
		{
			name: "missing default group id annotation",
			configure: func(cluster *cloudv1.Cluster) {
				delete(cluster.Annotations, cloudv1.DefaultLoadBalancerIDAnnotationKey)
			},
		},
		{
			name: "blank default group id annotation",
			configure: func(cluster *cloudv1.Cluster) {
				cluster.UpdateAnnotation(cloudv1.DefaultLoadBalancerIDAnnotationKey, "   ")
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
			baseInstanceType := cluster.Spec.Cluster.TiDB.InstanceType
			updateTestGKEComputeClassBinding(
				t,
				cluster,
				cloudv1.AnnoKeyGKEComputeClassForTiDBBindings,
				"tidb-default-priority",
				[]string{baseInstanceType, testGKENodePoolMachineType64},
				[]string{
					gkeheterogeneous.NodePoolName(cluster, baseInstanceType),
					gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType64),
				},
			)
			tt.configure(cluster)
			cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster).Build()
			s := newGKEHeterogeneousTestServer(cli)

			resp, err := s.ListGKEHeterogeneousDeploys(context.Background(), &apiv1.ListGKEHeterogeneousDeploysRequest{
				ClusterId: cluster.Name,
			})
			require.Nil(t, resp)
			require.Error(t, err)
			require.Equal(t, codes.FailedPrecondition, status.Code(err))
			require.Contains(t, err.Error(), cloudv1.DefaultLoadBalancerIDAnnotationKey)
		})
	}
}

func TestClusterServer_ListGKEHeterogeneousDeploysRejectsMalformedTiDBExtraGroupBindings(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cluster.Spec.ExtraGroups = []cloudv1.ExtraGroupSpec{
		{
			Name: "group1",
			TiDB: cloudv1.TiDB{
				Replicas:     2,
				InstanceType: "n2-standard-32",
			},
		},
	}
	cluster.UpdateAnnotation(cluster.TiDBGKEComputeClassBindingsAnnoKey("group1"), `{"computeClassName":"tidb-group1-priority","nodePools":[{"role":"extra","instanceType":"`+testGKENodePoolMachineType64+`","nodePoolName":"np-a"}]}`)
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster).Build()
	s := newGKEHeterogeneousTestServer(cli)

	resp, err := s.ListGKEHeterogeneousDeploys(context.Background(), &apiv1.ListGKEHeterogeneousDeploysRequest{
		ClusterId: cluster.Name,
	})
	require.Nil(t, resp)
	require.Error(t, err)
	require.Equal(t, codes.FailedPrecondition, status.Code(err))
	require.Contains(t, err.Error(), "must contain exactly 2 nodePools")
}

func TestClusterServer_GetGKEHeterogeneousDeployRejectsMalformedNodePoolBindings(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cluster.UpdateAnnotation(cloudv1.AnnoKeyGKEComputeClassForTiKVBindings, `{"computeClassName":"tikv-priority","nodePools":[{"role":"extra","instanceType":"n2-standard-32","nodePoolName":"np-a"}]}`)
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster).Build()
	s := newGKEHeterogeneousTestServer(cli)

	_, err := s.GetGKEHeterogeneousDeploy(context.Background(), &apiv1.GetGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
	})
	require.Error(t, err)
	require.Equal(t, codes.FailedPrecondition, status.Code(err))
	require.Contains(t, err.Error(), "must contain exactly 2 nodePools")
}

func TestClusterServer_GetGKEHeterogeneousDeployWithoutIntentReturnsDisabledDeploy(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster).Build()
	s := newGKEHeterogeneousTestServer(cli)

	resp, err := s.GetGKEHeterogeneousDeploy(context.Background(), &apiv1.GetGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
	})
	require.NoError(t, err)
	require.False(t, resp.Enabled)
	require.Equal(t, apiv1.GKEHeterogeneousDeployStatus_GKE_HETEROGENEOUS_DEPLOY_STATUS_PREPARED, resp.Status)
	require.Empty(t, resp.ClusterAnnotations)
}

func TestClusterServer_GetGKEHeterogeneousDeployRequiresExplicitComputeClassName(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cluster.UpdateAnnotation(cloudv1.AnnoKeyGKEComputeClassForTiKVBindings, `{"nodePools":[{"role":"base","instanceType":"`+testGKENodePoolMachineType16+`","nodePoolName":"`+gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType16)+`"}]}`)
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster).Build()
	s := newGKEHeterogeneousTestServer(cli)

	_, err := s.GetGKEHeterogeneousDeploy(context.Background(), &apiv1.GetGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
	})
	require.Error(t, err)
	require.Equal(t, codes.FailedPrecondition, status.Code(err))
	require.Contains(t, err.Error(), "compute_class_name is required")
}

func TestClusterServer_GetGKEHeterogeneousDeployRequiresExplicitInstanceTypes(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cluster.UpdateAnnotation(cloudv1.AnnoKeyGKEComputeClassForTiKVBindings, `{"computeClassName":"tikv-priority","nodePools":[{"role":"base","nodePoolName":"`+gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType16)+`"},{"role":"extra","instanceType":"n2-standard-32","nodePoolName":"`+gkeheterogeneous.NodePoolName(cluster, "n2-standard-32")+`"}]}`)
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster).Build()
	s := newGKEHeterogeneousTestServer(cli)

	_, err := s.GetGKEHeterogeneousDeploy(context.Background(), &apiv1.GetGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
	})
	require.Error(t, err)
	require.Equal(t, codes.FailedPrecondition, status.Code(err))
	require.Contains(t, err.Error(), "empty instanceType")
}

func TestClusterServer_GKEHeterogeneousNodePoolCRUDForTiKV(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cluster.Spec.NetworkName = testGKESharedNetworkName
	baseNodePoolName := gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType16)
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(
		cluster,
		newAPIManagedGKENodePool(cluster, baseNodePoolName, testGKENodePoolMachineType16),
	).Build()
	s := newGKEHeterogeneousTestServer(cli)
	expectedName := gkeheterogeneous.NodePoolName(cluster, "n2-standard-32")
	createResp, err := s.CreateGKEHeterogeneousNodePool(context.Background(), &apiv1.CreateGKEHeterogeneousNodePoolRequest{
		ClusterId:    cluster.Name,
		InstanceType: "n2-standard-32",
	})
	require.NoError(t, err)
	require.Equal(t, expectedName, createResp.NodePoolName)
	require.Equal(t, "n2-standard-32", createResp.InstanceType)
	require.Equal(t, int64(0), createResp.MinNodeCount)
	require.Equal(t, defaultGKEHeterogeneousNodePoolMaxNodeCount, createResp.MaxNodeCount)

	nodePool := &gkev1beta1.NodePool{}
	require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Name: expectedName}, nodePool))
	require.NotNil(t, nodePool.Spec.ForProvider.Config)
	require.NotNil(t, nodePool.Spec.ForProvider.Config.MachineType)
	require.Equal(t, "n2-standard-32", *nodePool.Spec.ForProvider.Config.MachineType)
	require.Equal(t, int64(0), *nodePool.Spec.ForProvider.Autoscaling.MinNodeCount)
	require.Equal(t, defaultGKEHeterogeneousNodePoolMaxNodeCount, *nodePool.Spec.ForProvider.Autoscaling.MaxNodeCount)
	configLabels := nodePool.Spec.ForProvider.Config.Labels
	require.Equal(t, shared.CustomerTiDB, configLabels[cloudv1.TagLabelUsedBy])
	require.Equal(t, shared.ClustersShared, configLabels[cloudv1.TagLabelCluster])
	require.Equal(t, shared.Dedicated, configLabels[cloudv1.TagLabelServiceType])
	require.Equal(t, cluster.GetProjectID(), configLabels[cloudv1.TagLabelProject])
	require.Equal(t, cluster.GetTenantID(), configLabels[cloudv1.TagLabelTenant])
	require.True(t, nodepoolutil.HasGKEHeterogeneousNodePoolIsolationTaint(nodePool))
	require.NotContains(t, configLabels, cloudv1.TagLabelPrefix+shared.UsedBy)
	require.NotContains(t, configLabels, cloudv1.TagLabelPrefix+shared.Cluster)
	require.NotContains(t, configLabels, cloudv1.TagLabelPrefix+shared.ServiceType)
	require.NotContains(t, configLabels, cloudv1.TagLabelPrefix+shared.Project)
	require.NotContains(t, configLabels, cloudv1.TagLabelPrefix+shared.Tenant)
	require.Equal(t, shared.ComponentShared, nodePool.Labels[namer.ComponentSelectorKey])
	require.Equal(t, shared.ClustersShared, nodePool.Labels[namer.ClusterSelectorKey])
	requireNodePoolWithoutComputeClass(t, cli, expectedName)
	annotationsBefore := getClusterAnnotations(t, cli, cluster)

	_, err = s.CreateGKEHeterogeneousDeploy(context.Background(), &apiv1.CreateGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
		InstanceTypes: []string{"n2-standard-32"},
	})
	require.NoError(t, err)
	computeClassName := testGKEComputeClassName(t, cluster, apicommon.ComponentType_TIKV, testGKENodePoolMachineType16, "n2-standard-32")
	requireNodePoolWithoutComputeClass(t, cli, baseNodePoolName)
	requireNodePoolWithoutComputeClass(t, cli, expectedName)
	markNodePoolComputeClass(t, cli, baseNodePoolName, computeClassName)
	markNodePoolComputeClass(t, cli, expectedName, computeClassName)
	require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Name: expectedName}, nodePool))
	require.NotNil(t, nodePool.Spec.ForProvider.Autoscaling)
	require.NotNil(t, nodePool.Spec.ForProvider.Autoscaling.MinNodeCount)
	require.NotNil(t, nodePool.Spec.ForProvider.Autoscaling.MaxNodeCount)
	nodePool.Spec.ForProvider.Autoscaling.MinNodeCount = ptr.To(int64(3))
	nodePool.Spec.ForProvider.Autoscaling.MaxNodeCount = ptr.To(int64(9))
	require.NoError(t, cli.Update(context.Background(), nodePool))

	getResp, err := s.GetGKEHeterogeneousNodePools(context.Background(), &apiv1.GetGKEHeterogeneousNodePoolsRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
	})
	require.NoError(t, err)
	require.Len(t, getResp.NodePools, 2)
	requireGKEHeterogeneousNodePool(t, getResp.NodePools[0], baseNodePoolName, testGKENodePoolMachineType16, computeClassName, true, true, true, int64(100))
	requireGKEHeterogeneousNodePool(t, getResp.NodePools[1], expectedName, "n2-standard-32", computeClassName, false, true, true, int64(9), int64(3))

	listResp, err := s.ListGKEHeterogeneousNodePools(context.Background(), &apiv1.ListGKEHeterogeneousNodePoolsRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
	})
	require.NoError(t, err)
	require.Len(t, listResp.NodePools, 2)
	requireGKEHeterogeneousNodePool(t, listResp.NodePools[0], baseNodePoolName, testGKENodePoolMachineType16, computeClassName, true, true, true, int64(100))
	requireGKEHeterogeneousNodePool(t, listResp.NodePools[1], expectedName, "n2-standard-32", computeClassName, false, true, true, int64(9), int64(3))

	_, err = s.DeleteGKEHeterogeneousDeploy(context.Background(), &apiv1.DeleteGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
		TidbGroupId:   "ignored",
	})
	require.NoError(t, err)
	require.Equal(t, annotationsBefore, getClusterAnnotations(t, cli, cluster))

	getResp, err = s.GetGKEHeterogeneousNodePools(context.Background(), &apiv1.GetGKEHeterogeneousNodePoolsRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
	})
	require.NoError(t, err)
	require.Empty(t, getResp.NodePools)

	listResp, err = s.ListGKEHeterogeneousNodePools(context.Background(), &apiv1.ListGKEHeterogeneousNodePoolsRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
	})
	require.NoError(t, err)
	require.Len(t, listResp.NodePools, 2)
	inventoryNodePools := map[string]struct{}{}
	for _, np := range listResp.NodePools {
		inventoryNodePools[np.GetNodePoolName()] = struct{}{}
	}
	require.Contains(t, inventoryNodePools, baseNodePoolName)
	require.Contains(t, inventoryNodePools, expectedName)
}

func TestClusterServer_CreateGKEHeterogeneousNodePoolReusesAlreadyExistsResourceWithDifferentAutoscaling(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	nodePoolName := gkeheterogeneous.NodePoolName(cluster, "n2-standard-32")
	existing := newAPIManagedGKENodePool(cluster, nodePoolName, "n2-standard-32")
	existing.Spec.ForProvider.Autoscaling.MaxNodeCount = ptr.To(int64(9))
	base := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster).Build()
	cli := &createAlreadyExistsNodePoolClient{Client: base, existing: existing}
	s := newGKEHeterogeneousTestServer(cli)

	resp, err := s.CreateGKEHeterogeneousNodePool(context.Background(), newCreateGKEHeterogeneousNodePoolRequest(cluster.Name, "n2-standard-32"))
	require.NoError(t, err)
	require.Equal(t, nodePoolName, resp.NodePoolName)
	require.Equal(t, int64(0), resp.MinNodeCount)
	require.Equal(t, int64(9), resp.MaxNodeCount)

	current := &gkev1beta1.NodePool{}
	require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Name: nodePoolName}, current))
	require.NotNil(t, current.Spec.ForProvider.Autoscaling)
	require.NotNil(t, current.Spec.ForProvider.Autoscaling.MaxNodeCount)
	require.Equal(t, int64(9), *current.Spec.ForProvider.Autoscaling.MaxNodeCount)
}

func TestClusterServer_CreateGKEHeterogeneousNodePoolRejectsMissingScopeLabels(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cluster.EnableFeature(consts.ClusterFeatureGCP3LayerNodePool)
	delete(cluster.Labels, cloudv1.ProjectTagKey)
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster).Build()
	s := newGKEHeterogeneousTestServer(cli)

	_, err := s.CreateGKEHeterogeneousNodePool(context.Background(), newCreateGKEHeterogeneousNodePoolRequest(cluster.Name, "n2-standard-32"))
	require.Error(t, err)
	require.Equal(t, codes.FailedPrecondition, status.Code(err))
	require.Contains(t, err.Error(), "missing required GKE heterogeneous nodepool scope labels")
	require.Contains(t, err.Error(), cloudv1.ProjectTagKey)
}

func TestClusterServer_CreateGKEHeterogeneousNodePoolRejects3LayerMissingKubernetesName(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cluster.EnableFeature(consts.ClusterFeatureGCP3LayerNodePool)
	cluster.Spec.Kubernetes.Name = " \t "
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster).Build()
	s := newGKEHeterogeneousTestServer(cli)

	_, err := s.CreateGKEHeterogeneousNodePool(context.Background(), newCreateGKEHeterogeneousNodePoolRequest(cluster.Name, "n2-standard-32"))
	require.Error(t, err)
	require.Equal(t, codes.FailedPrecondition, status.Code(err))
	require.Contains(t, err.Error(), "missing required GKE heterogeneous nodepool scope labels")
	require.Contains(t, err.Error(), consts.GKEHeterogeneousNodePoolScopeLabelKey)
}

func TestClusterServer_CreateGKEHeterogeneousNodePoolUsesClusterRegionForClusterRef(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cluster.EnableFeature(consts.ClusterFeatureGCP3LayerNodePool)
	cluster.Spec.Kubernetes.Name = "gke-main"
	cluster.Labels[cloudv1.RegionLabel] = "asia-east1"
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster).Build()
	s := newGKEHeterogeneousTestServer(cli)

	resp, err := s.CreateGKEHeterogeneousNodePool(context.Background(), newCreateGKEHeterogeneousNodePoolRequest(cluster.Name, "n2-standard-32"))
	require.NoError(t, err)

	nodePool := &gkev1beta1.NodePool{}
	require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Name: resp.NodePoolName}, nodePool))
	expectedClusterRef := "projects/" + cluster.Spec.Kubernetes.Account + "/locations/asia-east1/clusters/" + cluster.Spec.Kubernetes.Name
	require.Equal(t, expectedClusterRef, nodePool.Spec.ForProvider.Cluster)
}

func TestClusterServer_CreateGKEHeterogeneousNodePoolUsesSharedDiskTypeRules(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster).Build()
	s := newGKEHeterogeneousTestServer(cli)

	resp, err := s.CreateGKEHeterogeneousNodePool(context.Background(), newCreateGKEHeterogeneousNodePoolRequest(cluster.Name, "c4a-standard-16"))
	require.NoError(t, err)

	nodePool := &gkev1beta1.NodePool{}
	require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Name: resp.NodePoolName}, nodePool))
	require.NotNil(t, nodePool.Spec.ForProvider.Config)
	require.NotNil(t, nodePool.Spec.ForProvider.Config.DiskType)
	require.Equal(t, "hyperdisk-balanced", *nodePool.Spec.ForProvider.Config.DiskType)
}

func TestGKEHeterogeneousSharedNodePoolLabelsSkipPartialScopeMetadata(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cluster.EnableFeature(consts.ClusterFeatureGCP3LayerNodePool)
	delete(cluster.Labels, cloudv1.ProjectTagKey)

	labels := gkeHeterogeneousSharedNodePoolLabels(cluster)

	require.NotContains(t, labels, cloudv1.RegionLabel)
	require.NotContains(t, labels, cloudv1.ProjectTagKey)
	require.NotContains(t, labels, cloudv1.TenantTagKey)
}

func TestClusterServer_GetGKEHeterogeneousNodePoolsReturnsEmptyBeforeDeploy(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	baseNodePoolName := gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType16)
	baseNodePool := newAPIManagedGKENodePool(cluster, baseNodePoolName, testGKENodePoolMachineType16)
	delete(baseNodePool.Labels, cloudv1.RegionLabel)
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(
		cluster,
		baseNodePool,
	).Build()
	s := newGKEHeterogeneousTestServer(cli)

	resp, err := s.GetGKEHeterogeneousNodePools(context.Background(), &apiv1.GetGKEHeterogeneousNodePoolsRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
	})
	require.NoError(t, err)
	require.Empty(t, resp.NodePools)
}

func TestClusterServer_GetGKEHeterogeneousNodePoolsRequiresComponentType(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster).Build()
	s := newGKEHeterogeneousTestServer(cli)

	_, err := s.GetGKEHeterogeneousNodePools(context.Background(), &apiv1.GetGKEHeterogeneousNodePoolsRequest{
		ClusterId: cluster.Name,
	})
	require.Error(t, err)
	require.Equal(t, codes.InvalidArgument, status.Code(err))
	require.Contains(t, err.Error(), "component_type is required")
}

func TestClusterServer_GetGKEHeterogeneousNodePoolsUsesAnnotatedNodePoolName(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	nodePoolNames := []string{gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType16), gkeheterogeneous.NodePoolName(cluster, "n2-standard-32")}
	computeClassName := updateTestGKEComputeClassBinding(t, cluster, cloudv1.AnnoKeyGKEComputeClassForTiKVBindings, "tikv-priority", []string{testGKENodePoolMachineType16, "n2-standard-32"}, nodePoolNames)
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(
		cluster,
		newAPIManagedGKENodePool(cluster, nodePoolNames[0], testGKENodePoolMachineType16),
		newAPIManagedGKENodePool(cluster, nodePoolNames[1], "n2-standard-32"),
	).Build()
	s := newGKEHeterogeneousTestServer(cli)
	markNodePoolComputeClass(t, cli, nodePoolNames[0], computeClassName)
	markNodePoolComputeClass(t, cli, nodePoolNames[1], computeClassName)

	resp, err := s.GetGKEHeterogeneousNodePools(context.Background(), &apiv1.GetGKEHeterogeneousNodePoolsRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
	})
	require.NoError(t, err)
	require.Len(t, resp.NodePools, 2)
	requireGKEHeterogeneousNodePool(t, resp.NodePools[0], nodePoolNames[0], testGKENodePoolMachineType16, computeClassName, true, true, true, int64(100))
	requireGKEHeterogeneousNodePool(t, resp.NodePools[1], nodePoolNames[1], "n2-standard-32", computeClassName, false, true, true, int64(100))
}

func TestClusterServer_CreateGKEHeterogeneousNodePoolDoesNotRequireDeployEnabled(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cluster.Spec.NetworkName = testGKESharedNetworkName
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster).Build()
	s := newGKEHeterogeneousTestServer(cli)
	resp, err := s.CreateGKEHeterogeneousNodePool(context.Background(), newCreateGKEHeterogeneousNodePoolRequest(cluster.Name, "n2-standard-32"))
	require.NoError(t, err)
	require.Equal(t, gkeheterogeneous.NodePoolName(cluster, "n2-standard-32"), resp.NodePoolName)

	listResp, err := s.ListGKEHeterogeneousNodePools(context.Background(), &apiv1.ListGKEHeterogeneousNodePoolsRequest{
		ClusterId: cluster.Name,
	})
	require.NoError(t, err)
	require.Len(t, listResp.NodePools, 1)
	require.Equal(t, apicommon.ComponentType_COMPONENT_TYPE_UNSPECIFIED, listResp.NodePools[0].ComponentType)
	requireGKEHeterogeneousNodePool(t, listResp.NodePools[0], resp.NodePoolName, "n2-standard-32", "", false, true, true, int64(100))

	getResp, err := s.GetGKEHeterogeneousNodePools(context.Background(), &apiv1.GetGKEHeterogeneousNodePoolsRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
	})
	require.NoError(t, err)
	require.Empty(t, getResp.NodePools)

	baseResp, err := s.CreateGKEHeterogeneousNodePool(context.Background(), newCreateGKEHeterogeneousNodePoolRequest(cluster.Name, testGKENodePoolMachineType16))
	require.NoError(t, err)

	inventoryResp, err := s.ListGKEHeterogeneousNodePools(context.Background(), &apiv1.ListGKEHeterogeneousNodePoolsRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
	})
	require.NoError(t, err)
	require.Len(t, inventoryResp.NodePools, 2)
	nodePoolsByName := map[string]*apiv1.GKEHeterogeneousNodePool{}
	for _, np := range inventoryResp.NodePools {
		nodePoolsByName[np.GetNodePoolName()] = np
	}
	requireGKEHeterogeneousNodePool(t, nodePoolsByName[baseResp.NodePoolName], baseResp.NodePoolName, testGKENodePoolMachineType16, "", false, true, true, int64(100))
	requireGKEHeterogeneousNodePool(t, nodePoolsByName[resp.NodePoolName], resp.NodePoolName, "n2-standard-32", "", false, true, true, int64(100))

	requireNodePoolWithoutComputeClass(t, cli, resp.NodePoolName)
	requireNodePoolWithoutComputeClass(t, cli, baseResp.NodePoolName)
}

func TestClusterServer_CreateGKEHeterogeneousNodePoolAllowsOrdinaryBaseInstanceType(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cluster.Spec.ExtraGroups = []cloudv1.ExtraGroupSpec{
		{
			Name: "group1",
			TiDB: cloudv1.TiDB{
				Replicas:     2,
				InstanceType: "n2-standard-32",
			},
		},
	}
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster).Build()
	s := newGKEHeterogeneousTestServer(cli)

	tests := []struct {
		name         string
		instanceType string
	}{
		{
			name:         "tikv base",
			instanceType: testGKENodePoolMachineType16,
		},
		{
			name:         "default tidb base",
			instanceType: "n2-standard-8",
		},
		{
			name:         "extra tidb group base",
			instanceType: "n2-standard-32",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			resp, err := s.CreateGKEHeterogeneousNodePool(context.Background(), newCreateGKEHeterogeneousNodePoolRequest(cluster.Name, tt.instanceType))
			require.NoError(t, err)
			require.Equal(t, gkeheterogeneous.NodePoolName(cluster, tt.instanceType), resp.NodePoolName)
			requireNodePoolWithoutComputeClass(t, cli, resp.NodePoolName)
		})
	}
}

func TestClusterServer_CreateGKEHeterogeneousNodePoolAllowsPeerOrdinaryBaseInstanceType(t *testing.T) {
	tests := []struct {
		name          string
		configurePeer func(*cloudv1.Cluster)
	}{
		{
			name: "tikv base",
			configurePeer: func(peer *cloudv1.Cluster) {
				peer.Spec.Cluster.TiKV.InstanceType = testGKENodePoolMachineType64
			},
		},
		{
			name: "tidb default base",
			configurePeer: func(peer *cloudv1.Cluster) {
				peer.Spec.Cluster.TiDB.InstanceType = testGKENodePoolMachineType64
			},
		},
		{
			name: "tidb extra group base",
			configurePeer: func(peer *cloudv1.Cluster) {
				peer.Spec.ExtraGroups = []cloudv1.ExtraGroupSpec{
					{
						Name: "group1",
						TiDB: cloudv1.TiDB{
							Replicas:     2,
							InstanceType: testGKENodePoolMachineType64,
						},
					},
				}
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
			cluster.Spec.NetworkName = testGKESharedNetworkName
			peer := newGKEHeterogeneousTestCluster("http://gateway.example")
			setGKEHeterogeneousTestClusterID(peer, cluster.Namespace, "peer-cluster")
			peer.Spec.NetworkName = cluster.Spec.NetworkName
			tt.configurePeer(peer)
			cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster, peer).Build()
			s := newGKEHeterogeneousTestServer(cli)

			resp, err := s.CreateGKEHeterogeneousNodePool(context.Background(), newCreateGKEHeterogeneousNodePoolRequest(cluster.Name, testGKENodePoolMachineType64))
			require.NoError(t, err)
			require.Equal(t, gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType64), resp.NodePoolName)
			requireNodePoolWithoutComputeClass(t, cli, resp.NodePoolName)
		})
	}
}

func TestClusterServer_CreateGKEHeterogeneousNodePoolAllowsDifferentPeerOrdinaryBaseInstanceType(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cluster.Spec.NetworkName = testGKESharedNetworkName
	peer := newGKEHeterogeneousTestCluster("http://gateway.example")
	setGKEHeterogeneousTestClusterID(peer, cluster.Namespace, "peer-cluster")
	peer.Spec.NetworkName = cluster.Spec.NetworkName
	peer.Spec.Cluster.TiKV.InstanceType = "n2-standard-32"
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster, peer).Build()
	s := newGKEHeterogeneousTestServer(cli)

	resp, err := s.CreateGKEHeterogeneousNodePool(context.Background(), newCreateGKEHeterogeneousNodePoolRequest(cluster.Name, testGKENodePoolMachineType64))
	require.NoError(t, err)
	require.Equal(t, gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType64), resp.NodePoolName)
	requireNodePoolWithoutComputeClass(t, cli, resp.NodePoolName)
}

func TestClusterServer_CreateGKEHeterogeneousNodePoolAllowsPeerOrdinaryBaseInstanceTypeInSameKubernetesClusterScopeWithDifferentNetwork(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cluster.Spec.NetworkName = testGKESharedNetworkName
	peer := newGKEHeterogeneousTestCluster("http://gateway.example")
	setGKEHeterogeneousTestClusterID(peer, cluster.Namespace, "peer-cluster")
	peer.Spec.NetworkName = testGKEOtherNetworkName
	peer.Spec.Cluster.TiKV.InstanceType = testGKENodePoolMachineType64
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster, peer).Build()
	s := newGKEHeterogeneousTestServer(cli)

	resp, err := s.CreateGKEHeterogeneousNodePool(context.Background(), newCreateGKEHeterogeneousNodePoolRequest(cluster.Name, testGKENodePoolMachineType64))
	require.NoError(t, err)
	require.Equal(t, gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType64), resp.NodePoolName)
	requireNodePoolWithoutComputeClass(t, cli, resp.NodePoolName)
}

func TestClusterServer_CreateGKEHeterogeneousNodePoolRejectsExistingPoolWithWrongMachineType(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	wrongExisting := newAPIManagedGKENodePool(cluster, gkeheterogeneous.NodePoolName(cluster, "n2-standard-32"), "n2-standard-80")
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster, wrongExisting).Build()
	s := newGKEHeterogeneousTestServer(cli)
	_, err := s.CreateGKEHeterogeneousNodePool(context.Background(), newCreateGKEHeterogeneousNodePoolRequest(cluster.Name, "n2-standard-32"))
	require.Error(t, err)
	require.Equal(t, codes.FailedPrecondition, status.Code(err))
	require.Contains(t, err.Error(), "want n2-standard-32")
}

func TestClusterServer_CreateGKEHeterogeneousNodePoolReusesExistingPoolWithDifferentAutoscaling(t *testing.T) {
	tests := []struct {
		name         string
		minNodeCount int64
		maxNodeCount int64
	}{
		{
			name:         "min zero max one",
			minNodeCount: 0,
			maxNodeCount: 1,
		},
		{
			name:         "non default min and max",
			minNodeCount: 1,
			maxNodeCount: 4,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
			existing := newAPIManagedGKENodePool(cluster, gkeheterogeneous.NodePoolName(cluster, "n2-standard-32"), "n2-standard-32")
			existing.Spec.ForProvider.Autoscaling.MinNodeCount = ptr.To(tt.minNodeCount)
			existing.Spec.ForProvider.Autoscaling.MaxNodeCount = ptr.To(tt.maxNodeCount)
			cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster, existing).Build()
			s := newGKEHeterogeneousTestServer(cli)

			resp, err := s.CreateGKEHeterogeneousNodePool(context.Background(), newCreateGKEHeterogeneousNodePoolRequest(cluster.Name, "n2-standard-32"))
			require.NoError(t, err)
			require.Equal(t, existing.Name, resp.NodePoolName)
			require.Equal(t, tt.minNodeCount, resp.MinNodeCount)
			require.Equal(t, tt.maxNodeCount, resp.MaxNodeCount)

			listResp, err := s.ListGKEHeterogeneousNodePools(context.Background(), &apiv1.ListGKEHeterogeneousNodePoolsRequest{
				ClusterId: cluster.Name,
			})
			require.NoError(t, err)
			require.Len(t, listResp.NodePools, 1)
			requireGKEHeterogeneousNodePool(t, listResp.NodePools[0], existing.Name, "n2-standard-32", "", false, true, true, tt.maxNodeCount, tt.minNodeCount)
		})
	}
}

func TestClusterServer_CreateGKEHeterogeneousNodePoolRejectsExistingPoolWithInvalidAutoscalingShape(t *testing.T) {
	tests := []struct {
		name    string
		mutate  func(*gkev1beta1.NodePool)
		wantMsg string
	}{
		{
			name: "missing autoscaling",
			mutate: func(nodePool *gkev1beta1.NodePool) {
				nodePool.Spec.ForProvider.Autoscaling = nil
			},
			wantMsg: "autoscaling configured",
		},
		{
			name: "autoscaling disabled",
			mutate: func(nodePool *gkev1beta1.NodePool) {
				nodePool.Spec.ForProvider.Autoscaling.Enabled = ptr.To(false)
			},
			wantMsg: "autoscaling enabled",
		},
		{
			name: "missing min",
			mutate: func(nodePool *gkev1beta1.NodePool) {
				nodePool.Spec.ForProvider.Autoscaling.MinNodeCount = nil
			},
			wantMsg: "autoscaling min/max configured",
		},
		{
			name: "missing max",
			mutate: func(nodePool *gkev1beta1.NodePool) {
				nodePool.Spec.ForProvider.Autoscaling.MaxNodeCount = nil
			},
			wantMsg: "autoscaling min/max configured",
		},
		{
			name: "min greater than max",
			mutate: func(nodePool *gkev1beta1.NodePool) {
				nodePool.Spec.ForProvider.Autoscaling.MinNodeCount = ptr.To(int64(5))
				nodePool.Spec.ForProvider.Autoscaling.MaxNodeCount = ptr.To(int64(4))
			},
			wantMsg: "invalid autoscaling min/max 5/4",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
			existing := newAPIManagedGKENodePool(cluster, gkeheterogeneous.NodePoolName(cluster, "n2-standard-32"), "n2-standard-32")
			tt.mutate(existing)
			cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster, existing).Build()
			s := newGKEHeterogeneousTestServer(cli)

			_, err := s.CreateGKEHeterogeneousNodePool(context.Background(), newCreateGKEHeterogeneousNodePoolRequest(cluster.Name, "n2-standard-32"))
			require.Error(t, err)
			require.Equal(t, codes.FailedPrecondition, status.Code(err))
			require.Contains(t, err.Error(), tt.wantMsg)
		})
	}
}

func TestClusterServer_CreateGKEHeterogeneousNodePoolRejectsExistingOrdinarySharedPool(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	nodePoolName := gkeheterogeneous.NodePoolName(cluster, "n2-standard-32")
	ordinaryShared := newOrdinarySharedGKENodePool(cluster, nodePoolName, "n2-standard-32")
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster, ordinaryShared).Build()
	s := newGKEHeterogeneousTestServer(cli)

	_, err := s.CreateGKEHeterogeneousNodePool(context.Background(), newCreateGKEHeterogeneousNodePoolRequest(cluster.Name, "n2-standard-32"))
	require.Error(t, err)
	require.Equal(t, codes.FailedPrecondition, status.Code(err))
	require.Contains(t, err.Error(), "not managed by heterogeneous nodepool API")
}

func TestClusterServer_ListGKEHeterogeneousNodePoolsIgnoresUnannotatedTiDBGroups(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cluster.Spec.ExtraGroups = []cloudv1.ExtraGroupSpec{
		{
			Name: "group1",
			TiDB: cloudv1.TiDB{
				Replicas:     2,
				InstanceType: "n2-standard-32",
			},
		},
		{
			Name: "group2",
			TiDB: cloudv1.TiDB{
				Replicas:     2,
				InstanceType: testGKENodePoolMachineType64,
			},
		},
	}
	groupNodePool := &gkev1beta1.NodePool{ObjectMeta: metav1.ObjectMeta{Name: "group1-np-a"}}
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster, groupNodePool).Build()
	s := newGKEHeterogeneousTestServer(cli)

	listResp, err := s.ListGKEHeterogeneousNodePools(context.Background(), &apiv1.ListGKEHeterogeneousNodePoolsRequest{
		ClusterId: cluster.Name,
	})
	require.NoError(t, err)
	require.Empty(t, listResp.NodePools)
}

func TestClusterServer_ListGKEHeterogeneousNodePoolsIgnoresOrphanedTiDBGroups(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cluster.Spec.ExtraGroups = []cloudv1.ExtraGroupSpec{
		{
			Name: "group1",
			TiDB: cloudv1.TiDB{
				Replicas:     2,
				InstanceType: "n2-standard-32",
			},
		},
	}
	cluster.UpdateAnnotation(cloudv1.AnnoKeyGKEComputeClassForTiDBBindings+"-orphan", `{"computeClassName":"tidb-orphan-priority","nodePools":[{"role":"base","instanceType":"`+testGKENodePoolMachineType64+`","nodePoolName":"orphan-np-a"}]}`)
	groupNodePool := &gkev1beta1.NodePool{ObjectMeta: metav1.ObjectMeta{Name: "group1-np-a"}}
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster, groupNodePool).Build()
	s := newGKEHeterogeneousTestServer(cli)

	listResp, err := s.ListGKEHeterogeneousNodePools(context.Background(), &apiv1.ListGKEHeterogeneousNodePoolsRequest{
		ClusterId: cluster.Name,
	})
	require.NoError(t, err)
	require.Empty(t, listResp.NodePools)
}

func TestClusterServer_ListGKEHeterogeneousDeploysIgnoresOrphanedTiDBAnnotations(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cluster.UpdateAnnotation(cluster.TiDBGKEComputeClassBindingsAnnoKey("orphan"), `{"computeClassName":"tidb-orphan-priority","nodePools":[{"role":"base","instanceType":"`+testGKENodePoolMachineType64+`","nodePoolName":"orphan-np-a"}]}`)
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster).Build()
	s := newGKEHeterogeneousTestServer(cli)

	resp, err := s.ListGKEHeterogeneousDeploys(context.Background(), &apiv1.ListGKEHeterogeneousDeploysRequest{
		ClusterId: cluster.Name,
	})
	require.NoError(t, err)
	require.Len(t, resp.Deploys, 2)
	require.Equal(t, apicommon.ComponentType_TIKV, resp.Deploys[0].ComponentType)
	require.Equal(t, apicommon.ComponentType_TIDB, resp.Deploys[1].ComponentType)
	require.Equal(t, strings.TrimSpace(cluster.GetDefaultGroupID()), resp.Deploys[1].TidbGroupId)
	require.False(t, resp.Deploys[1].Enabled)
}

func TestClusterServer_CreateGKEHeterogeneousNodePoolRejectsMissingRequiredFields(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster).Build()
	s := newGKEHeterogeneousTestServer(cli)

	tests := []struct {
		name    string
		req     *apiv1.CreateGKEHeterogeneousNodePoolRequest
		wantMsg string
	}{
		{
			name: "missing cluster id",
			req: &apiv1.CreateGKEHeterogeneousNodePoolRequest{
				InstanceType: testGKENodePoolMachineType16,
			},
			wantMsg: "cluster_id is required",
		},
		{
			name: "missing instance type",
			req: &apiv1.CreateGKEHeterogeneousNodePoolRequest{
				ClusterId: cluster.Name,
			},
			wantMsg: "instance_type is required",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			_, err := s.CreateGKEHeterogeneousNodePool(context.Background(), tt.req)
			require.Error(t, err)
			require.Equal(t, codes.InvalidArgument, status.Code(err))
			require.Contains(t, err.Error(), tt.wantMsg)
		})
	}
}

func TestClusterServer_GKEHeterogeneousNodePoolCRUDForTiDBExtraGroup(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cluster.Spec.NetworkName = testGKESharedNetworkName
	cluster.Spec.ExtraGroups = []cloudv1.ExtraGroupSpec{
		{
			Name: "group1",
			TiDB: cloudv1.TiDB{
				Replicas:     2,
				InstanceType: "n2-standard-32",
			},
		},
	}
	baseNodePoolName := gkeheterogeneous.NodePoolName(cluster, "n2-standard-32")
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(
		cluster,
		newAPIManagedGKENodePool(cluster, baseNodePoolName, "n2-standard-32"),
	).Build()
	s := newGKEHeterogeneousTestServer(cli)
	expectedName := gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType64)
	createResp, err := s.CreateGKEHeterogeneousNodePool(context.Background(), newCreateGKEHeterogeneousNodePoolRequest(cluster.Name, testGKENodePoolMachineType64))
	require.NoError(t, err)
	require.Equal(t, expectedName, createResp.NodePoolName)
	requireNodePoolWithoutComputeClass(t, cli, expectedName)

	annotationsBefore := getClusterAnnotations(t, cli, cluster)
	_, err = s.CreateGKEHeterogeneousDeploy(context.Background(), &apiv1.CreateGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIDB,
		TidbGroupId:   "group1",
		InstanceTypes: []string{testGKENodePoolMachineType64},
	})
	require.NoError(t, err)
	computeClassName := testGKEComputeClassName(t, cluster, apicommon.ComponentType_TIDB, "n2-standard-32", testGKENodePoolMachineType64)
	requireNodePoolWithoutComputeClass(t, cli, baseNodePoolName)
	requireNodePoolWithoutComputeClass(t, cli, expectedName)
	markNodePoolComputeClass(t, cli, baseNodePoolName, computeClassName)
	markNodePoolComputeClass(t, cli, expectedName, computeClassName)

	getResp, err := s.GetGKEHeterogeneousNodePools(context.Background(), &apiv1.GetGKEHeterogeneousNodePoolsRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIDB,
		TidbGroupId:   "group1",
	})
	require.NoError(t, err)
	require.Len(t, getResp.NodePools, 2)
	require.Equal(t, "group1", getResp.NodePools[0].TidbGroupId)
	require.Equal(t, "group1", getResp.NodePools[1].TidbGroupId)
	requireGKEHeterogeneousNodePool(t, getResp.NodePools[0], baseNodePoolName, "n2-standard-32", computeClassName, true, true, true, int64(100))
	requireGKEHeterogeneousNodePool(t, getResp.NodePools[1], expectedName, testGKENodePoolMachineType64, computeClassName, false, true, true, int64(100))

	listResp, err := s.ListGKEHeterogeneousNodePools(context.Background(), &apiv1.ListGKEHeterogeneousNodePoolsRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIDB,
		TidbGroupId:   "group1",
	})
	require.NoError(t, err)
	require.Len(t, listResp.NodePools, 2)
	require.Equal(t, "group1", listResp.NodePools[0].TidbGroupId)
	require.Equal(t, "group1", listResp.NodePools[1].TidbGroupId)
	requireGKEHeterogeneousNodePool(t, listResp.NodePools[0], baseNodePoolName, "n2-standard-32", computeClassName, true, true, true, int64(100))
	requireGKEHeterogeneousNodePool(t, listResp.NodePools[1], expectedName, testGKENodePoolMachineType64, computeClassName, false, true, true, int64(100))

	_, err = s.DeleteGKEHeterogeneousDeploy(context.Background(), &apiv1.DeleteGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIDB,
		TidbGroupId:   "group1",
	})
	require.NoError(t, err)
	require.Equal(t, annotationsBefore, getClusterAnnotations(t, cli, cluster))

	getResp, err = s.GetGKEHeterogeneousNodePools(context.Background(), &apiv1.GetGKEHeterogeneousNodePoolsRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIDB,
		TidbGroupId:   "group1",
	})
	require.NoError(t, err)
	require.Empty(t, getResp.NodePools)

	listResp, err = s.ListGKEHeterogeneousNodePools(context.Background(), &apiv1.ListGKEHeterogeneousNodePoolsRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIDB,
		TidbGroupId:   "group1",
	})
	require.NoError(t, err)
	require.Len(t, listResp.NodePools, 2)
	inventoryNodePools := map[string]struct{}{}
	for _, np := range listResp.NodePools {
		inventoryNodePools[np.GetNodePoolName()] = struct{}{}
	}
	require.Contains(t, inventoryNodePools, baseNodePoolName)
	require.Contains(t, inventoryNodePools, expectedName)
}

func TestClusterServer_GetGKEHeterogeneousNodePoolsFailsOnMachineTypeDrift(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cluster.Spec.NetworkName = testGKESharedNetworkName
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster).Build()
	s := newGKEHeterogeneousTestServer(cli)
	expectedName := gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType64)
	_, err := s.CreateGKEHeterogeneousNodePool(context.Background(), newCreateGKEHeterogeneousNodePoolRequest(cluster.Name, testGKENodePoolMachineType64))
	require.NoError(t, err)
	prepareGKEHeterogeneousDeployNodePools(t, cli, cluster, apicommon.ComponentType_TIKV, "", testGKENodePoolMachineType64)
	_, err = s.CreateGKEHeterogeneousDeploy(context.Background(), &apiv1.CreateGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
		InstanceTypes: []string{testGKENodePoolMachineType64},
	})
	require.NoError(t, err)

	nodePool := &gkev1beta1.NodePool{}
	require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Name: expectedName}, nodePool))
	nodePool.Spec.ForProvider.Config.MachineType = ptr.To("n2-standard-32")
	require.NoError(t, cli.Update(context.Background(), nodePool))

	_, err = s.GetGKEHeterogeneousNodePools(context.Background(), &apiv1.GetGKEHeterogeneousNodePoolsRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
	})
	require.Error(t, err)
	require.Equal(t, codes.FailedPrecondition, status.Code(err))
	require.Contains(t, err.Error(), "has machine type n2-standard-32, want n2-standard-64")
}

func TestClusterServer_GetGKEHeterogeneousNodePoolsValidatesExistingResourceIdentity(t *testing.T) {
	const computeClassName = "tikv-priority"

	tests := []struct {
		name       string
		nodePool   func(cluster *cloudv1.Cluster, nodePoolName string) *gkev1beta1.NodePool
		wantCode   codes.Code
		wantErrMsg string
	}{
		{
			name: "unmanaged extra nodepool",
			nodePool: func(cluster *cloudv1.Cluster, nodePoolName string) *gkev1beta1.NodePool {
				nodePool := newOrdinarySharedGKENodePool(cluster, nodePoolName, testGKENodePoolMachineType64)
				nodepoolutil.SetGKEComputeClassForNodePool(nodePool, computeClassName)
				return nodePool
			},
			wantCode:   codes.FailedPrecondition,
			wantErrMsg: "not managed by heterogeneous nodepool API",
		},
		{
			name: "different heterogeneous nodepool scope",
			nodePool: func(cluster *cloudv1.Cluster, nodePoolName string) *gkev1beta1.NodePool {
				otherCluster := cluster.DeepCopy()
				setGKEHeterogeneousTestClusterAPPBox(otherCluster, testGKERegionName, "other-tenant", "other-project")
				otherCluster.Spec.NetworkName = testGKEOtherNetworkName
				nodePool := newAPIManagedGKENodePool(otherCluster, nodePoolName, testGKENodePoolMachineType64)
				nodepoolutil.SetGKEComputeClassForNodePool(nodePool, computeClassName)
				return nodePool
			},
			wantCode:   codes.FailedPrecondition,
			wantErrMsg: "does not belong to cluster",
		},
		{
			name: "different GKE cluster ref",
			nodePool: func(cluster *cloudv1.Cluster, nodePoolName string) *gkev1beta1.NodePool {
				nodePool := newAPIManagedGKENodePool(cluster, nodePoolName, testGKENodePoolMachineType64)
				nodePool.Spec.ForProvider.Cluster = "projects/other-project/locations/us-central1/clusters/other-gke"
				nodepoolutil.SetGKEComputeClassForNodePool(nodePool, computeClassName)
				return nodePool
			},
			wantCode:   codes.PermissionDenied,
			wantErrMsg: "GKE cluster ref",
		},
		{
			name: "different compute-class metadata",
			nodePool: func(cluster *cloudv1.Cluster, nodePoolName string) *gkev1beta1.NodePool {
				nodePool := newAPIManagedGKENodePool(cluster, nodePoolName, testGKENodePoolMachineType64)
				nodepoolutil.SetGKEComputeClassForNodePool(nodePool, "other-priority")
				return nodePool
			},
			wantCode:   codes.FailedPrecondition,
			wantErrMsg: "bound to desired compute class other-priority, want",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
			cluster.Spec.NetworkName = testGKESharedNetworkName
			baseNodePoolName := gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType16)
			extraNodePoolName := gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType64)
			updateTestGKEComputeClassBinding(t, cluster, cloudv1.AnnoKeyGKEComputeClassForTiKVBindings, computeClassName, []string{testGKENodePoolMachineType16, testGKENodePoolMachineType64}, []string{baseNodePoolName, extraNodePoolName})
			cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(
				cluster,
				tt.nodePool(cluster, extraNodePoolName),
			).Build()
			s := newGKEHeterogeneousTestServer(cli)

			_, err := s.GetGKEHeterogeneousNodePools(context.Background(), &apiv1.GetGKEHeterogeneousNodePoolsRequest{
				ClusterId:     cluster.Name,
				ComponentType: apicommon.ComponentType_TIKV,
			})
			require.Error(t, err)
			require.Equal(t, tt.wantCode, status.Code(err))
			require.Contains(t, err.Error(), tt.wantErrMsg)
		})
	}
}

func TestClusterServer_GetGKEHeterogeneousNodePoolsReturnsEmptyForTiDBGroupBeforeDeploy(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cluster.Spec.ExtraGroups = []cloudv1.ExtraGroupSpec{
		{
			Name: "group1",
			TiDB: cloudv1.TiDB{
				Replicas:     2,
				InstanceType: "n2-standard-32",
			},
		},
	}
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(
		cluster,
	).Build()
	s := newGKEHeterogeneousTestServer(cli)

	resp, err := s.GetGKEHeterogeneousNodePools(context.Background(), &apiv1.GetGKEHeterogeneousNodePoolsRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIDB,
		TidbGroupId:   "group1",
	})
	require.NoError(t, err)
	require.Empty(t, resp.NodePools)
}

func TestClusterServer_ListGKEHeterogeneousNodePoolsIgnoresTiDBGroupFilterBeforeDeploy(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cluster.Spec.ExtraGroups = []cloudv1.ExtraGroupSpec{
		{
			Name: "group1",
			TiDB: cloudv1.TiDB{
				Replicas:     2,
				InstanceType: "n2-standard-32",
			},
		},
	}
	baseNodePoolName := gkeheterogeneous.NodePoolName(cluster, "n2-standard-32")
	extraNodePoolName := gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType64)
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(
		cluster,
		newAPIManagedGKENodePool(cluster, baseNodePoolName, "n2-standard-32"),
		newAPIManagedGKENodePool(cluster, extraNodePoolName, testGKENodePoolMachineType64),
	).Build()
	s := newGKEHeterogeneousTestServer(cli)

	getResp, err := s.GetGKEHeterogeneousNodePools(context.Background(), &apiv1.GetGKEHeterogeneousNodePoolsRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIDB,
		TidbGroupId:   "group1",
	})
	require.NoError(t, err)
	require.Empty(t, getResp.NodePools)

	listResp, err := s.ListGKEHeterogeneousNodePools(context.Background(), &apiv1.ListGKEHeterogeneousNodePoolsRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIDB,
		TidbGroupId:   "group1",
	})
	require.NoError(t, err)
	require.Len(t, listResp.NodePools, 2)
	nodePoolsByName := map[string]*apiv1.GKEHeterogeneousNodePool{}
	for _, np := range listResp.NodePools {
		nodePoolsByName[np.GetNodePoolName()] = np
		require.Equal(t, apicommon.ComponentType_COMPONENT_TYPE_UNSPECIFIED, np.GetComponentType())
		require.Empty(t, np.GetTidbGroupId())
	}
	requireGKEHeterogeneousNodePool(t, nodePoolsByName[baseNodePoolName], baseNodePoolName, "n2-standard-32", "", false, true, true, int64(100))
	requireGKEHeterogeneousNodePool(t, nodePoolsByName[extraNodePoolName], extraNodePoolName, testGKENodePoolMachineType64, "", false, true, true, int64(100))
}

func TestClusterServer_GKEHeterogeneousNodePoolReusesSharedNameByInstanceType(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cluster.Spec.Cluster.TiDB.InstanceType = testGKENodePoolMachineType16
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cluster).Build()
	s := newGKEHeterogeneousTestServer(cli)
	tikvResp, err := s.CreateGKEHeterogeneousNodePool(context.Background(), newCreateGKEHeterogeneousNodePoolRequest(cluster.Name, testGKENodePoolMachineType64))
	require.NoError(t, err)

	tidbResp, err := s.CreateGKEHeterogeneousNodePool(context.Background(), newCreateGKEHeterogeneousNodePoolRequest(cluster.Name, testGKENodePoolMachineType64))
	require.NoError(t, err)

	require.NotEmpty(t, tikvResp.NodePoolName)
	require.Equal(t, tikvResp.NodePoolName, tidbResp.NodePoolName)
	require.Equal(t, gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType64), tikvResp.NodePoolName)
}

func TestClusterServer_GKEHeterogeneousNodePoolRefsIgnoreMalformedIntentOutsideKubernetesClusterScope(t *testing.T) {
	cluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	cluster.Spec.NetworkName = testGKESharedNetworkName
	otherCluster := newGKEHeterogeneousTestCluster("http://gateway.example")
	setGKEHeterogeneousTestClusterID(otherCluster, cluster.Namespace, "789")
	setGKEHeterogeneousTestClusterAPPBox(otherCluster, testGKERegionName, "other-tenant", "other-project")
	otherCluster.Spec.NetworkName = testGKEOtherNetworkName

	baseNodePoolName := gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType16)
	extraNodePoolName := gkeheterogeneous.NodePoolName(cluster, testGKENodePoolMachineType64)
	otherCluster.UpdateAnnotation(cloudv1.AnnoKeyGKEComputeClassForTiKVBindings, `{"computeClassName":"tikv-priority","nodePools":[{"role":"extra","instanceType":"`+testGKENodePoolMachineType64+`","nodePoolName":"`+extraNodePoolName+`"}]}`)
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(
		cluster,
		otherCluster,
		newAPIManagedGKENodePool(cluster, baseNodePoolName, testGKENodePoolMachineType16),
		newAPIManagedGKENodePool(cluster, extraNodePoolName, testGKENodePoolMachineType64),
	).Build()
	s := newGKEHeterogeneousTestServer(cli)

	_, err := s.CreateGKEHeterogeneousDeploy(context.Background(), &apiv1.CreateGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
		InstanceTypes: []string{testGKENodePoolMachineType64},
	})
	require.NoError(t, err)
	computeClassName := testGKEComputeClassName(t, cluster, apicommon.ComponentType_TIKV, testGKENodePoolMachineType16, testGKENodePoolMachineType64)
	markNodePoolComputeClass(t, cli, baseNodePoolName, computeClassName)
	markNodePoolComputeClass(t, cli, extraNodePoolName, computeClassName)

	listResp, err := s.ListGKEHeterogeneousNodePools(context.Background(), &apiv1.ListGKEHeterogeneousNodePoolsRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
	})
	require.NoError(t, err)
	require.Len(t, listResp.NodePools, 2)

	_, err = s.DeleteGKEHeterogeneousDeploy(context.Background(), &apiv1.DeleteGKEHeterogeneousDeployRequest{
		ClusterId:     cluster.Name,
		ComponentType: apicommon.ComponentType_TIKV,
	})
	require.NoError(t, err)
}

func newGKEHeterogeneousTestCluster(gatewayEndpoint string) *cloudv1.Cluster {
	cluster := newCluster("")
	cluster.Labels[cloudv1.ProviderTypeLabel] = cloudv1.ProviderTypeGKE
	setGKEHeterogeneousTestClusterAPPBox(cluster, testGKERegionName, testGKETenantID, testGKEProjectID)
	cluster.Spec.Kubernetes.Name = "gke-main"
	cluster.Spec.Kubernetes.Cloud.GCP = &cloudv1.CloudGCP{
		NodeZones: []string{"us-west-2a", "us-west-2b", "us-west-2c"},
	}
	cluster.Spec.Cluster.TiKV.InstanceType = testGKENodePoolMachineType16
	cluster.Spec.Cluster.TiDB.InstanceType = "n2-standard-8"
	cluster.UpdateAnnotation(cloudv1.DefaultLoadBalancerIDAnnotationKey, "default-group")
	cluster.Status.Gateway = &cloudv1.GatewayStatus{Endpoint: gatewayEndpoint}
	cluster.ObjectMeta.ManagedFields = []metav1.ManagedFieldsEntry{}
	return cluster
}

func setGKEHeterogeneousTestClusterAPPBox(cluster *cloudv1.Cluster, region, tenantID, projectID string) {
	if cluster.Labels == nil {
		cluster.Labels = map[string]string{}
	}
	cluster.Labels[cloudv1.RegionLabel] = region
	cluster.Labels[cloudv1.TenantTagKey] = tenantID
	cluster.Labels[cloudv1.ProjectTagKey] = projectID
}

func setGKEHeterogeneousTestClusterID(cluster *cloudv1.Cluster, namespace, name string) {
	cluster.Namespace = namespace
	cluster.Name = name
	cluster.Labels[cloudv1.ClusterTagKey] = name
}

type recordingClusterListClient struct {
	client.Client
	clusterListSelectors []string
}

func (c *recordingClusterListClient) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error {
	if _, ok := list.(*cloudv1.ClusterList); ok {
		listOptions := &client.ListOptions{}
		listOptions.ApplyOptions(opts)
		if listOptions.LabelSelector == nil {
			c.clusterListSelectors = append(c.clusterListSelectors, "")
		} else {
			c.clusterListSelectors = append(c.clusterListSelectors, listOptions.LabelSelector.String())
		}
	}
	return c.Client.List(ctx, list, opts...)
}

func newGKEHeterogeneousTestServer(cli client.Client) *ClusterServer {
	return &ClusterServer{
		KubeClient:    cli,
		clusterReader: &internalutil.LabelSelectorReader{},
	}
}

func newCreateGKEHeterogeneousNodePoolRequest(clusterID, instanceType string) *apiv1.CreateGKEHeterogeneousNodePoolRequest {
	return &apiv1.CreateGKEHeterogeneousNodePoolRequest{
		ClusterId:    clusterID,
		InstanceType: instanceType,
	}
}

func newAPIManagedGKENodePool(cluster *cloudv1.Cluster, nodePoolName, instanceType string) *gkev1beta1.NodePool {
	return buildGKEHeterogeneousNodePool(cluster, nodePoolName, instanceType, 0, 100)
}

func newOrdinarySharedGKENodePool(cluster *cloudv1.Cluster, nodePoolName, instanceType string) *gkev1beta1.NodePool {
	nodePool := newAPIManagedGKENodePool(cluster, nodePoolName, instanceType)
	delete(nodePool.Labels, consts.GKEHeterogeneousNodePoolManagedByLabelKey)
	nodepoolutil.ClearGKEHeterogeneousNodePoolIsolationTaint(nodePool)
	return nodePool
}

func newHeterogeneousGKENodePool(cluster *cloudv1.Cluster, nodePoolName, instanceType, computeClass string) *gkev1beta1.NodePool {
	nodePool := buildGKEHeterogeneousNodePool(cluster, nodePoolName, instanceType, 0, 100)
	nodepoolutil.SetGKEComputeClassForNodePool(nodePool, computeClass)
	nodepoolutil.SetGKEAppliedComputeClassForNodePool(nodePool, computeClass)
	nodepoolutil.ClearGKEHeterogeneousNodePoolIsolationTaint(nodePool)
	return nodePool
}

func prepareGKEHeterogeneousDeployNodePools(t *testing.T, cli client.Client, cluster *cloudv1.Cluster, componentType apicommon.ComponentType, tidbGroupID, extraInstanceType string) []string {
	t.Helper()

	instanceTypes := []string{
		baseGKEHeterogeneousInstanceType(cluster, componentType, tidbGroupID),
		extraInstanceType,
	}
	nodePoolNames := make([]string, 0, len(instanceTypes))
	seen := map[string]struct{}{}
	for _, instanceType := range instanceTypes {
		instanceType = strings.TrimSpace(instanceType)
		require.NotEmpty(t, instanceType)
		nodePoolName := gkeheterogeneous.NodePoolName(cluster, instanceType)
		nodePoolNames = append(nodePoolNames, nodePoolName)
		if _, ok := seen[nodePoolName]; ok {
			continue
		}
		seen[nodePoolName] = struct{}{}
		err := cli.Create(context.Background(), newAPIManagedGKENodePool(cluster, nodePoolName, instanceType))
		if err != nil {
			require.True(t, apierrors.IsAlreadyExists(err), "create prepared nodepool %s: %v", nodePoolName, err)
		}
	}
	return nodePoolNames
}

func markNodePoolComputeClass(t *testing.T, cli client.Client, nodePoolName, computeClass string) {
	t.Helper()

	nodePool := &gkev1beta1.NodePool{}
	require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Name: nodePoolName}, nodePool))
	nodepoolutil.SetGKEComputeClassForNodePool(nodePool, computeClass)
	nodepoolutil.SetGKEAppliedComputeClassForNodePool(nodePool, computeClass)
	nodepoolutil.ClearGKEHeterogeneousNodePoolIsolationTaint(nodePool)
	require.NoError(t, cli.Update(context.Background(), nodePool))
}

func markNodePoolDesiredComputeClass(t *testing.T, cli client.Client, nodePoolName, computeClass string) {
	t.Helper()

	nodePool := &gkev1beta1.NodePool{}
	require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Name: nodePoolName}, nodePool))
	nodepoolutil.SetGKEComputeClassForNodePool(nodePool, computeClass)
	require.NoError(t, cli.Update(context.Background(), nodePool))
}

func markNodePoolConditions(t *testing.T, cli client.Client, nodePoolName string, ready, synced corev1.ConditionStatus) {
	t.Helper()

	nodePool := &gkev1beta1.NodePool{}
	require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Name: nodePoolName}, nodePool))
	nodePool.Status.Conditions = []xpv1.Condition{
		{
			Type:   xpv1.TypeReady,
			Status: ready,
		},
		{
			Type:   xpv1.TypeSynced,
			Status: synced,
		},
	}
	require.NoError(t, cli.Update(context.Background(), nodePool))
}

func requireNodePoolComputeClass(t *testing.T, cli client.Client, nodePoolName, computeClass string) {
	t.Helper()

	nodePool := &gkev1beta1.NodePool{}
	require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Name: nodePoolName}, nodePool))
	require.Equal(t, computeClass, strings.TrimSpace(nodePool.Annotations[consts.AnnoKeyGKENodePoolComputeClass]))
	require.Equal(t, computeClass, strings.TrimSpace(nodePool.Annotations[consts.AnnoKeyGKENodePoolComputeClassApplied]))
	require.NotNil(t, nodePool.Spec.ForProvider.Config)
	require.Equal(t, computeClass, strings.TrimSpace(nodePool.Spec.ForProvider.Config.Labels[consts.GKEComputeClassLabelKey]))
	require.Equal(t, computeClass, nodepoolutil.GKEComputeClassSpecTaintForNodePool(nodePool))
	require.False(t, nodepoolutil.HasGKEHeterogeneousNodePoolIsolationTaint(nodePool))
}

func requireNodePoolWithoutComputeClass(t *testing.T, cli client.Client, nodePoolName string) {
	t.Helper()

	nodePool := &gkev1beta1.NodePool{}
	require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Name: nodePoolName}, nodePool))
	require.Empty(t, strings.TrimSpace(nodePool.Annotations[consts.AnnoKeyGKENodePoolComputeClass]))
	require.Empty(t, strings.TrimSpace(nodePool.Annotations[consts.AnnoKeyGKENodePoolComputeClassApplied]))
	if nodePool.Spec.ForProvider.Config == nil {
		return
	}
	require.Empty(t, strings.TrimSpace(nodePool.Spec.ForProvider.Config.Labels[consts.GKEComputeClassLabelKey]))
}

func requireGKEHeterogeneousNodePool(
	t *testing.T,
	nodePool *apiv1.GKEHeterogeneousNodePool,
	nodePoolName string,
	instanceType string,
	computeClassName string,
	isBase bool,
	apiManaged bool,
	exists bool,
	maxNodeCount int64,
	minNodeCount ...int64,
) {
	t.Helper()

	expectedMinNodeCount := int64(0)
	if len(minNodeCount) > 0 {
		expectedMinNodeCount = minNodeCount[0]
	}

	require.Equal(t, nodePoolName, nodePool.NodePoolName)
	require.Equal(t, instanceType, nodePool.InstanceType)
	require.Equal(t, computeClassName, nodePool.ComputeClassName)
	require.Equal(t, isBase, nodePool.IsBaseNodePool)
	require.Equal(t, apiManaged, nodePool.ApiManaged)
	require.Equal(t, exists, nodePool.Exists)
	require.Equal(t, expectedMinNodeCount, nodePool.MinNodeCount)
	require.Equal(t, maxNodeCount, nodePool.MaxNodeCount)
}

func getClusterAnnotations(t *testing.T, cli client.Client, cluster *cloudv1.Cluster) map[string]string {
	t.Helper()

	updated := &cloudv1.Cluster{}
	require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Namespace: cluster.Namespace, Name: cluster.Name}, updated))

	out := make(map[string]string, len(updated.GetAnnotations()))
	for key, value := range updated.GetAnnotations() {
		out[key] = value
	}
	return out
}
