package service

import (
	"context"
	"errors"
	"sort"
	"testing"

	"github.com/stretchr/testify/require"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	k8smeta "k8s.io/apimachinery/pkg/api/meta"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	apiv1 "github.com/tidbcloud/infra-provider/api/tidbcloud/cluster-agent/v1"
	apicommon "github.com/tidbcloud/infra-provider/api/tidbcloud/common"
	"github.com/tidbcloud/infra-provider/pkg/consts"
)

func newGKEHeterogeneousTestScheme(t *testing.T) *runtime.Scheme {
	t.Helper()
	scheme := runtime.NewScheme()
	require.NoError(t, clientgoscheme.AddToScheme(scheme))
	return scheme
}

func newScopedGKEHeterogeneousTestService(cli client.Client) *AgentService {
	return NewAgentService(cli, WithClusterNamespace(buildNamespace("123")), WithProvider(consts.ProviderTypeGKE))
}

func testAgentGKEComputeClassName(instanceTypes ...string) string {
	if len(instanceTypes) == 1 {
		instanceTypes = append(instanceTypes, testAgentExtraInstanceType(instanceTypes[0]))
	}
	name, _, err := gkeComputeClassNameAndBindingsFromInput([]string{"np-a", "np-b"}, instanceTypes)
	if err != nil {
		panic(err)
	}
	return name
}

func testAgentGKEComputeClassNameForBindings(t *testing.T, nodePoolNames []string, instanceTypes ...string) string {
	t.Helper()
	name, _, err := gkeComputeClassNameAndBindingsFromInput(nodePoolNames, instanceTypes)
	require.NoError(t, err)
	return name
}

func testBuildGKEComputeClass(nodePoolNames []string, instanceTypes ...[]string) *unstructured.Unstructured {
	resolvedNodePoolNames := append([]string{}, nodePoolNames...)
	if len(resolvedNodePoolNames) == 0 {
		resolvedNodePoolNames = []string{"np-a", "np-b"}
	}
	if len(resolvedNodePoolNames) == 1 {
		resolvedNodePoolNames = append(resolvedNodePoolNames, resolvedNodePoolNames[0]+"-extra")
	}

	resolvedInstanceTypes := []string{"n2-standard-16", "n2-standard-64"}
	if len(instanceTypes) > 0 {
		resolvedInstanceTypes = append([]string{}, instanceTypes[0]...)
	}
	if len(resolvedInstanceTypes) == 1 {
		resolvedInstanceTypes = append(resolvedInstanceTypes, testAgentExtraInstanceType(resolvedInstanceTypes[0]))
	}
	name, bindings, err := gkeComputeClassNameAndBindingsFromInput(resolvedNodePoolNames, resolvedInstanceTypes)
	if err != nil {
		panic(err)
	}
	obj, err := buildGKEComputeClass(name, bindings)
	if err != nil {
		panic(err)
	}
	return obj
}

func responseComputeClassNames(resp *apiv1.GetGKEHeterogeneousComputeClassResponse) []string {
	names := make([]string, 0, len(resp.GetComputeClasses()))
	for _, computeClass := range resp.GetComputeClasses() {
		names = append(names, computeClass.GetName())
	}
	return names
}

func testAgentExtraInstanceType(instanceType string) string {
	if instanceType == "n2-standard-64" {
		return "n2-standard-96"
	}
	return "n2-standard-64"
}

type createConflictClient struct {
	client.Client
	conflictObject client.Object
}

func (c *createConflictClient) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error {
	copied := obj.DeepCopyObject().(client.Object)
	if c.conflictObject != nil {
		copied = c.conflictObject.DeepCopyObject().(client.Object)
	}
	if err := c.Client.Create(ctx, copied, opts...); err != nil {
		return err
	}
	return apierrors.NewAlreadyExists(schema.GroupResource{Group: obj.GetObjectKind().GroupVersionKind().Group, Resource: obj.GetObjectKind().GroupVersionKind().Kind}, obj.GetName())
}

type noMatchClient struct {
	client.Client
}

type deleteErrorClient struct {
	client.Client
	failName string
	err      error
}

type createErrorForNameClient struct {
	client.Client
	failName string
	err      error
}

func (c *createErrorForNameClient) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error {
	if c.err != nil && obj.GetName() == c.failName {
		return c.err
	}
	return c.Client.Create(ctx, obj, opts...)
}

func (c *deleteErrorClient) Delete(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error {
	if c.err != nil && obj.GetName() == c.failName {
		return c.err
	}
	return c.Client.Delete(ctx, obj, opts...)
}

func (c *noMatchClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
	return noKindMatchForObject(obj)
}

func (c *noMatchClient) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error {
	return noKindMatchForObject(list)
}

func (c *noMatchClient) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error {
	return noKindMatchForObject(obj)
}

func noKindMatchForObject(obj runtime.Object) error {
	gvk := obj.GetObjectKind().GroupVersionKind()
	return &k8smeta.NoKindMatchError{
		GroupKind:        gvk.GroupKind(),
		SearchedVersions: []string{gvk.Version},
	}
}

func requireGatekeeperAssignNoMatchExpression(t *testing.T, obj *unstructured.Unstructured, key, operator string) {
	t.Helper()
	expressions, found, err := unstructured.NestedSlice(obj.Object, "spec", "match", "labelSelector", "matchExpressions")
	require.NoError(t, err)
	if !found {
		return
	}
	require.False(t, hasGatekeeperAssignMatchExpression(expressions, key, operator))
}

func hasGatekeeperAssignMatchExpression(expressions []interface{}, key, operator string) bool {
	for _, expression := range expressions {
		expressionMap, ok := expression.(map[string]interface{})
		if !ok {
			continue
		}
		expressionKey, _ := expressionMap["key"].(string)
		expressionOperator, _ := expressionMap["operator"].(string)
		if expressionKey == key && expressionOperator == operator {
			return true
		}
	}
	return false
}

func requireGatekeeperAssignExemption(t *testing.T, obj *unstructured.Unstructured) string {
	t.Helper()
	token := gatekeeperAssignExemptToken(obj.GetName())
	requireGatekeeperAssignExemptionToken(t, obj, token)
	return token
}

func requireGatekeeperAssignExemptionToken(t *testing.T, obj *unstructured.Unstructured, token string) {
	t.Helper()
	expressions, found, err := unstructured.NestedSlice(obj.Object, "spec", "match", "labelSelector", "matchExpressions")
	require.NoError(t, err)
	require.True(t, found)
	for _, expression := range expressions {
		expressionMap, ok := expression.(map[string]interface{})
		if !ok || expressionMap["key"] != gkeHeterogeneousGatekeeperExemptLabelKey || expressionMap["operator"] != "NotIn" {
			continue
		}
		require.Equal(t, []interface{}{token}, expressionMap["values"])
		return
	}
	require.Fail(t, "missing gatekeeper live pod exemption expression")
}

func requireBuildGatekeeperAssign(t *testing.T, name, clusterID string, component *gkeHeterogeneousDataplaneComponent, computeClassName string, settings *gkeHeterogeneousAssignSettings) *unstructured.Unstructured {
	t.Helper()
	obj, err := buildGatekeeperAssign(name, clusterID, component, computeClassName, settings)
	require.NoError(t, err)
	return obj
}

func requireBuildGatekeeperTopologySpreadAssign(t *testing.T, name, clusterID string, component *gkeHeterogeneousDataplaneComponent, settings *gkeHeterogeneousAssignSettings, token string) *unstructured.Unstructured {
	t.Helper()
	obj, err := buildGatekeeperTopologySpreadAssign(name, clusterID, component, settings, token)
	require.NoError(t, err)
	return obj
}

func testGatekeeperAssignMatchLabels(t *testing.T, component *gkeHeterogeneousDataplaneComponent) map[string]string {
	t.Helper()
	require.NotNil(t, component)

	matchLabels := gkeHeterogeneousPodMatchLabels(component)
	if !component.isTiDB() {
		return matchLabels
	}

	switch component.tidbGroupKind {
	case apiv1.TiDBGroupKind_TIDB_GROUP_KIND_DEFAULT:
		matchLabels[consts.InstanceLabelKey] = consts.ClusterPrefix
	case apiv1.TiDBGroupKind_TIDB_GROUP_KIND_EXTRA:
		matchLabels[consts.InstanceLabelKey] = gkeHeterogeneousTiDBGroupInstanceLabel(component.tidbGroupID)
	default:
		require.Fail(t, "tidb group kind is required for TiDB gatekeeper assign match labels")
	}
	return matchLabels
}

func nodeInclusionPolicyPtr(policy corev1.NodeInclusionPolicy) *corev1.NodeInclusionPolicy {
	return &policy
}

func testPodTopologySpreadConstraints(policy *corev1.NodeInclusionPolicy) []corev1.TopologySpreadConstraint {
	return []corev1.TopologySpreadConstraint{
		{
			TopologyKey:        consts.ZoneLabelKey,
			MaxSkew:            1,
			NodeAffinityPolicy: policy,
		},
	}
}

func TestAgentService_CreateAndUpdateGKEHeterogeneousComputeClass(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)
	cli := fake.NewClientBuilder().WithScheme(scheme).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	ctx := context.Background()
	resp, err := svc.CreateGKEHeterogeneousComputeClass(ctx, &apiv1.CreateGKEHeterogeneousComputeClassRequest{
		ClusterId:     "123",
		ComponentType: apicommon.ComponentType_TIKV,
		NodePoolNames: []string{"np-a", "np-b"},
		InstanceTypes: []string{"n2-standard-16", "n2-standard-64"},
	})
	require.NoError(t, err)
	require.Equal(t, testAgentGKEComputeClassNameForBindings(t, []string{"np-a", "np-b"}, "n2-standard-16", "n2-standard-64"), resp.Name)
	require.Equal(t, apicommon.ComponentType_TIKV, resp.ComponentType)
	require.Equal(t, []string{"np-a", "np-b"}, resp.NodePoolNames)
	require.Equal(t, []string{"n2-standard-16", "n2-standard-64"}, resp.InstanceTypes)
	require.Equal(t, gkeWhenUnsatisfiableDoNotScaleUp, resp.WhenUnsatisfiable)
	obj, err := getGKEHeterogeneousResource(ctx, cli, gkeComputeClassGVK, resp.Name, gkeComputeClassLabel)
	require.NoError(t, err)
	require.Equal(t, gkeComputeClassDeployLabelValue, obj.GetLabels()[gkeHeterogeneousDeployLabelKey])
	require.JSONEq(t, `[{"index":0,"role":"base","instanceType":"n2-standard-16","nodePoolName":"np-a"},{"index":1,"role":"extra","instanceType":"n2-standard-64","nodePoolName":"np-b"}]`, obj.GetAnnotations()[gkeHeterogeneousBindingsAnnotationKey])
	require.NotEmpty(t, obj.GetLabels()[gkeHeterogeneousBindingsHashLabelKey])
	require.Equal(t, "n2-standard-16", obj.GetLabels()[gkeHeterogeneousBaseInstanceTypeLabelKey])
	require.Equal(t, "n2-standard-64", obj.GetLabels()[gkeHeterogeneousExtraInstanceTypeLabelKey])
	require.Equal(t, "np-a", obj.GetLabels()[gkeHeterogeneousBaseNodePoolLabelKey])
	require.Equal(t, "np-b", obj.GetLabels()[gkeHeterogeneousExtraNodePoolLabelKey])
	autoCreationEnabled, found, err := unstructured.NestedBool(obj.Object, "spec", "nodePoolAutoCreation", "enabled")
	require.NoError(t, err)
	require.True(t, found)
	require.False(t, autoCreationEnabled)

	getResp, err := svc.GetGKEHeterogeneousComputeClass(ctx, &apiv1.GetGKEHeterogeneousComputeClassRequest{
		ClusterId: "123",
		Name:      resp.Name,
	})
	require.NoError(t, err)
	require.Len(t, getResp.ComputeClasses, 1)
	require.Equal(t, resp.Name, getResp.ComputeClasses[0].Name)
	require.Equal(t, []string{"n2-standard-16", "n2-standard-64"}, getResp.ComputeClasses[0].InstanceTypes)

	resp, err = svc.UpdateGKEHeterogeneousComputeClass(ctx, &apiv1.UpdateGKEHeterogeneousComputeClassRequest{
		ClusterId:     "123",
		Name:          " " + resp.Name + " ",
		NodePoolNames: []string{"np-a", "np-b"},
	})
	require.NoError(t, err)
	require.Equal(t, []string{"np-a", "np-b"}, resp.NodePoolNames)
	require.Equal(t, []string{"n2-standard-16", "n2-standard-64"}, resp.InstanceTypes)
	require.Equal(t, gkeWhenUnsatisfiableDoNotScaleUp, resp.WhenUnsatisfiable)

	_, err = svc.UpdateGKEHeterogeneousComputeClass(ctx, &apiv1.UpdateGKEHeterogeneousComputeClassRequest{
		ClusterId:     "123",
		Name:          resp.Name,
		NodePoolNames: []string{"np-c", "np-d"},
	})
	require.Error(t, err)
	require.Equal(t, codes.FailedPrecondition, status.Code(err))
	require.Contains(t, err.Error(), "bindings are immutable")

	_, err = svc.UpdateGKEHeterogeneousComputeClass(ctx, &apiv1.UpdateGKEHeterogeneousComputeClassRequest{
		ClusterId:     "123",
		Name:          resp.Name,
		NodePoolNames: []string{"np-a"},
	})
	require.Error(t, err)
	require.Equal(t, codes.InvalidArgument, status.Code(err))
	require.Contains(t, err.Error(), "match existing compute class priorities")

	_, err = svc.DeleteGKEHeterogeneousComputeClass(ctx, &apiv1.DeleteGKEHeterogeneousComputeClassRequest{ClusterId: "123", Name: " " + resp.Name + " "})
	require.NoError(t, err)
}

func TestCreateManagedUnstructuredTreatsCreateConflictWithSameSpecAsSuccess(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)
	base := fake.NewClientBuilder().WithScheme(scheme).Build()
	cli := &createConflictClient{Client: base}
	desired := testBuildGKEComputeClass([]string{"np-a", "np-b"}, []string{"n2-standard-16", "n2-standard-64"})

	got, err := createManagedUnstructured(context.Background(), cli, desired, validateSharedManagedGKEHeterogeneousComputeClass)

	require.NoError(t, err)
	require.Equal(t, desired.GetName(), got.GetName())
	require.Equal(t, desired.Object["spec"], got.Object["spec"])
}

func TestCreateManagedUnstructuredRejectsCreateConflictWithDifferentComputeClassAnnotation(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)
	desired := testBuildGKEComputeClass([]string{"np-a", "np-b"}, []string{"n2-standard-16", "n2-standard-64"})
	conflict := desired.DeepCopy()
	annotations := conflict.GetAnnotations()
	annotations[gkeHeterogeneousBindingsAnnotationKey] = `not-json`
	conflict.SetAnnotations(annotations)

	base := fake.NewClientBuilder().WithScheme(scheme).Build()
	cli := &createConflictClient{Client: base, conflictObject: conflict}

	_, err := createManagedUnstructured(context.Background(), cli, desired, validateSharedManagedGKEHeterogeneousComputeClass)

	require.Error(t, err)
	require.Equal(t, codes.AlreadyExists, status.Code(err))
	require.Contains(t, err.Error(), "invalid bindings")
}

func TestCreateManagedUnstructuredReturnsFailedPreconditionForMissingCRD(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)
	base := fake.NewClientBuilder().WithScheme(scheme).Build()
	cli := &noMatchClient{Client: base}
	desired := testBuildGKEComputeClass([]string{"np-a", "np-b"}, []string{"n2-standard-16", "n2-standard-64"})

	_, err := createManagedUnstructured(context.Background(), cli, desired, validateSharedManagedGKEHeterogeneousComputeClass)

	require.Error(t, err)
	require.Equal(t, codes.FailedPrecondition, status.Code(err))
	require.Contains(t, err.Error(), "ComputeClass API is not ready")
	require.Contains(t, err.Error(), "ensure the CRD is installed/enabled")
}

func TestAgentService_CreateGKEHeterogeneousComputeClassRejectsNodePoolOwnedByDifferentComputeClass(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)
	existing := testBuildGKEComputeClass([]string{"np-a", "np-b"}, []string{"n2-standard-16", "n2-standard-64"})
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(existing).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	_, err := svc.CreateGKEHeterogeneousComputeClass(context.Background(), &apiv1.CreateGKEHeterogeneousComputeClassRequest{
		ClusterId:     "123",
		ComponentType: apicommon.ComponentType_TIDB,
		NodePoolNames: []string{"np-a", "np-c"},
		InstanceTypes: []string{"n2-standard-16", "n2-standard-96"},
	})

	require.Error(t, err)
	require.Equal(t, codes.AlreadyExists, status.Code(err))
	require.Contains(t, err.Error(), "different bindings")
}

func TestAgentService_CreateGKEHeterogeneousComputeClassRejectsNodePoolOwnedByDifferentBinding(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)
	existing := testBuildGKEComputeClass([]string{"np-a", "np-b"}, []string{"n2-standard-16", "n2-standard-64"})
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(existing).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	_, err := svc.CreateGKEHeterogeneousComputeClass(context.Background(), &apiv1.CreateGKEHeterogeneousComputeClassRequest{
		ClusterId:     "123",
		ComponentType: apicommon.ComponentType_TIDB,
		NodePoolNames: []string{"np-c", "np-a"},
		InstanceTypes: []string{"n2-standard-96", "n2-standard-128"},
	})

	require.Error(t, err)
	require.Equal(t, codes.AlreadyExists, status.Code(err))
	require.Contains(t, err.Error(), "different bindings")
}

func TestAgentService_CreateGKEHeterogeneousComputeClassAllowsSameInstanceTypesWithDifferentNodePools(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)
	existing := testBuildGKEComputeClass([]string{"np-a", "np-b"}, []string{"n2-standard-16", "n2-standard-64"})
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(existing).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	resp, err := svc.CreateGKEHeterogeneousComputeClass(context.Background(), &apiv1.CreateGKEHeterogeneousComputeClassRequest{
		ClusterId:     "123",
		ComponentType: apicommon.ComponentType_TIKV,
		NodePoolNames: []string{"np-c", "np-d"},
		InstanceTypes: []string{"n2-standard-16", "n2-standard-64"},
	})
	require.NoError(t, err)
	require.Equal(t, []string{"np-c", "np-d"}, resp.NodePoolNames)

	computeClasses := &unstructured.UnstructuredList{}
	computeClasses.SetGroupVersionKind(gkeComputeClassListGVK)
	require.NoError(t, cli.List(context.Background(), computeClasses))
	require.Len(t, computeClasses.Items, 2)
}

func TestAgentService_GKEHeterogeneousRejectsMismatchedClusterNamespace(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)
	cli := fake.NewClientBuilder().WithScheme(scheme).Build()
	svc := NewAgentService(cli, WithClusterNamespace(buildNamespace("123")))

	_, err := svc.CreateGKEHeterogeneousComputeClass(context.Background(), &apiv1.CreateGKEHeterogeneousComputeClassRequest{
		ClusterId:     "456",
		ComponentType: apicommon.ComponentType_TIKV,
		NodePoolNames: []string{"np-a"},
		InstanceTypes: []string{"n2-standard-16"},
	})
	require.Error(t, err)
	require.Equal(t, codes.PermissionDenied, status.Code(err))
}

func TestAgentService_GKEHeterogeneousRequiresClusterNamespace(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)
	cli := fake.NewClientBuilder().WithScheme(scheme).Build()
	svc := NewAgentService(cli)

	_, err := svc.CreateGKEHeterogeneousComputeClass(context.Background(), &apiv1.CreateGKEHeterogeneousComputeClassRequest{
		ClusterId:     "123",
		ComponentType: apicommon.ComponentType_TIKV,
		NodePoolNames: []string{"np-a"},
		InstanceTypes: []string{"n2-standard-16"},
	})
	require.Error(t, err)
	require.Equal(t, codes.FailedPrecondition, status.Code(err))
	require.Contains(t, err.Error(), "cluster-agent namespace is required")
}

func TestAgentService_GKEHeterogeneousRequiresGKEProvider(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)
	cli := fake.NewClientBuilder().WithScheme(scheme).Build()
	svc := NewAgentService(cli, WithClusterNamespace(buildNamespace("123")), WithProvider(consts.ProviderTypeEKS))

	_, err := svc.CreateGKEHeterogeneousComputeClass(context.Background(), &apiv1.CreateGKEHeterogeneousComputeClassRequest{
		ClusterId:     "123",
		ComponentType: apicommon.ComponentType_TIKV,
		NodePoolNames: []string{"np-a"},
		InstanceTypes: []string{"n2-standard-16"},
	})
	require.Error(t, err)
	require.Equal(t, codes.FailedPrecondition, status.Code(err))
	require.Contains(t, err.Error(), "gke heterogeneous APIs require gke/gcp cluster-agent provider")
}

func TestAgentService_CreateGKEHeterogeneousComputeClassRejectsInvalidNodePoolName(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)
	cli := fake.NewClientBuilder().WithScheme(scheme).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	_, err := svc.CreateGKEHeterogeneousComputeClass(context.Background(), &apiv1.CreateGKEHeterogeneousComputeClassRequest{
		ClusterId:     "123",
		ComponentType: apicommon.ComponentType_TIKV,
		NodePoolNames: []string{"Bad_Name"},
		InstanceTypes: []string{"n2-standard-16"},
	})
	require.Error(t, err)
	require.Equal(t, codes.InvalidArgument, status.Code(err))
}

func TestAgentService_CreateGKEHeterogeneousComputeClassRequiresMatchingInstanceTypes(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)
	cli := fake.NewClientBuilder().WithScheme(scheme).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	_, err := svc.CreateGKEHeterogeneousComputeClass(context.Background(), &apiv1.CreateGKEHeterogeneousComputeClassRequest{
		ClusterId:     "123",
		ComponentType: apicommon.ComponentType_TIKV,
		NodePoolNames: []string{"np-a", "np-b"},
		InstanceTypes: []string{"n2-standard-16"},
	})
	require.Error(t, err)
	require.Equal(t, codes.InvalidArgument, status.Code(err))
	require.Contains(t, err.Error(), "instance_types must contain exactly 2 items")
}

func TestAgentService_CreateGKEHeterogeneousComputeClassRejectsEmptyInstanceTypeItem(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)
	cli := fake.NewClientBuilder().WithScheme(scheme).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	_, err := svc.CreateGKEHeterogeneousComputeClass(context.Background(), &apiv1.CreateGKEHeterogeneousComputeClassRequest{
		ClusterId:     "123",
		ComponentType: apicommon.ComponentType_TIKV,
		NodePoolNames: []string{"np-a", "np-b"},
		InstanceTypes: []string{"n2-standard-16", "", "n2-standard-64"},
	})
	require.Error(t, err)
	require.Equal(t, codes.InvalidArgument, status.Code(err))
}

func TestAgentService_GetGKEHeterogeneousComputeClassReturnsNamedResource(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)

	computeClass := testBuildGKEComputeClass([]string{"np-a", "np-b"}, []string{"n2-standard-16", "n2-standard-64"})
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(computeClass).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	resp, err := svc.GetGKEHeterogeneousComputeClass(context.Background(), &apiv1.GetGKEHeterogeneousComputeClassRequest{
		ClusterId: "123",
		Name:      " " + computeClass.GetName() + " ",
	})
	require.NoError(t, err)
	require.Len(t, resp.ComputeClasses, 1)
	require.Equal(t, computeClass.GetName(), resp.ComputeClasses[0].Name)
	require.Equal(t, []string{"np-a", "np-b"}, resp.ComputeClasses[0].NodePoolNames)
}

func TestAgentService_GetGKEHeterogeneousComputeClassReturnsNotFoundForMissingName(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)
	cli := fake.NewClientBuilder().WithScheme(scheme).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	_, err := svc.GetGKEHeterogeneousComputeClass(context.Background(), &apiv1.GetGKEHeterogeneousComputeClassRequest{
		ClusterId: "123",
		Name:      "compute-class-missing-priority",
	})
	require.Error(t, err)
	require.Equal(t, codes.NotFound, status.Code(err))
}

func TestAgentService_GetGKEHeterogeneousComputeClassReturnsFailedPreconditionForMissingCRD(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)
	base := fake.NewClientBuilder().WithScheme(scheme).Build()
	cli := &noMatchClient{Client: base}
	svc := newScopedGKEHeterogeneousTestService(cli)

	_, err := svc.GetGKEHeterogeneousComputeClass(context.Background(), &apiv1.GetGKEHeterogeneousComputeClassRequest{
		ClusterId: "123",
		Name:      "compute-class-priority",
	})

	require.Error(t, err)
	require.Equal(t, codes.FailedPrecondition, status.Code(err))
	require.Contains(t, err.Error(), "compute class API is not ready")
	require.Contains(t, err.Error(), "ensure the CRD is installed/enabled")
}

func TestAgentService_GetGKEHeterogeneousComputeClassRejectsNameWithFilters(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)
	cli := fake.NewClientBuilder().WithScheme(scheme).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	_, err := svc.GetGKEHeterogeneousComputeClass(context.Background(), &apiv1.GetGKEHeterogeneousComputeClassRequest{
		ClusterId:        "123",
		Name:             "compute-class-a-priority",
		BaseInstanceType: "n2-standard-16",
	})
	require.Error(t, err)
	require.Equal(t, codes.InvalidArgument, status.Code(err))
	require.Contains(t, err.Error(), "name cannot be used with instance type filters")
}

func TestAgentService_GetGKEHeterogeneousComputeClassFiltersByBaseInstanceType(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)

	smallComputeClass := testBuildGKEComputeClass([]string{"np-small-a", "np-small-b"}, []string{"n2-standard-16", "n2-standard-32"})
	largeComputeClass := testBuildGKEComputeClass([]string{"np-large-a", "np-large-b"}, []string{"n2-standard-64", "n2-standard-96"})
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(smallComputeClass, largeComputeClass).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	resp, err := svc.GetGKEHeterogeneousComputeClass(context.Background(), &apiv1.GetGKEHeterogeneousComputeClassRequest{
		ClusterId:        "123",
		BaseInstanceType: " n2-standard-64 ",
	})
	require.NoError(t, err)
	require.Equal(t, []string{largeComputeClass.GetName()}, responseComputeClassNames(resp))
	require.Equal(t, []string{"np-large-a", "np-large-b"}, resp.ComputeClasses[0].NodePoolNames)
}

func TestAgentService_GetGKEHeterogeneousComputeClassFiltersByHeterogeneousInstanceType(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)

	smallComputeClass := testBuildGKEComputeClass([]string{"np-small-a", "np-small-b"}, []string{"n2-standard-16", "n2-standard-32"})
	largeComputeClass := testBuildGKEComputeClass([]string{"np-large-a", "np-large-b"}, []string{"n2-standard-64", "n2-standard-96"})
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(smallComputeClass, largeComputeClass).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	resp, err := svc.GetGKEHeterogeneousComputeClass(context.Background(), &apiv1.GetGKEHeterogeneousComputeClassRequest{
		ClusterId:                 "123",
		HeterogeneousInstanceType: " n2-standard-96 ",
	})
	require.NoError(t, err)
	require.Equal(t, []string{largeComputeClass.GetName()}, responseComputeClassNames(resp))
}

func TestAgentService_GetGKEHeterogeneousComputeClassFiltersByBaseAndHeterogeneousInstanceTypes(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)

	smallComputeClass := testBuildGKEComputeClass([]string{"np-small-a", "np-small-b"}, []string{"n2-standard-16", "n2-standard-32"})
	largeComputeClass := testBuildGKEComputeClass([]string{"np-large-a", "np-large-b"}, []string{"n2-standard-64", "n2-standard-96"})
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(smallComputeClass, largeComputeClass).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	resp, err := svc.GetGKEHeterogeneousComputeClass(context.Background(), &apiv1.GetGKEHeterogeneousComputeClassRequest{
		ClusterId:                 "123",
		BaseInstanceType:          "n2-standard-16",
		HeterogeneousInstanceType: "n2-standard-32",
	})
	require.NoError(t, err)
	require.Equal(t, []string{smallComputeClass.GetName()}, responseComputeClassNames(resp))
}

func TestAgentService_GetGKEHeterogeneousComputeClassListsAllSorted(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)

	first := testBuildGKEComputeClass([]string{"np-b-a", "np-b-b"}, []string{"n2-standard-64", "n2-standard-96"})
	second := testBuildGKEComputeClass([]string{"np-a-a", "np-a-b"}, []string{"n2-standard-16", "n2-standard-32"})
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(first, second).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	resp, err := svc.GetGKEHeterogeneousComputeClass(context.Background(), &apiv1.GetGKEHeterogeneousComputeClassRequest{
		ClusterId: "123",
	})
	require.NoError(t, err)
	expectedNames := []string{first.GetName(), second.GetName()}
	sort.Strings(expectedNames)
	require.Equal(t, expectedNames, responseComputeClassNames(resp))
}

func TestAgentService_GetGKEHeterogeneousComputeClassListReturnsFailedPreconditionForMissingCRD(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)
	base := fake.NewClientBuilder().WithScheme(scheme).Build()
	cli := &noMatchClient{Client: base}
	svc := newScopedGKEHeterogeneousTestService(cli)

	_, err := svc.GetGKEHeterogeneousComputeClass(context.Background(), &apiv1.GetGKEHeterogeneousComputeClassRequest{
		ClusterId: "123",
	})

	require.Error(t, err)
	require.Equal(t, codes.FailedPrecondition, status.Code(err))
	require.Contains(t, err.Error(), "compute class API is not ready")
	require.Contains(t, err.Error(), "ensure the CRD is installed/enabled")
}

func TestAgentService_GetGKEHeterogeneousComputeClassReturnsEmptyForUnmatchedFilters(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)
	cli := fake.NewClientBuilder().WithScheme(scheme).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	resp, err := svc.GetGKEHeterogeneousComputeClass(context.Background(), &apiv1.GetGKEHeterogeneousComputeClassRequest{
		ClusterId:        "123",
		BaseInstanceType: "n2-standard-128",
	})
	require.NoError(t, err)
	require.Empty(t, resp.ComputeClasses)
}

func TestAgentService_GetGKEHeterogeneousComputeClassDoesNotReturnPartialInstanceTypeHit(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)

	computeClass := testBuildGKEComputeClass([]string{"np-a", "np-b"}, []string{"n2-standard-16", "n2-standard-64"})
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(computeClass).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	resp, err := svc.GetGKEHeterogeneousComputeClass(context.Background(), &apiv1.GetGKEHeterogeneousComputeClassRequest{
		ClusterId:                 "123",
		BaseInstanceType:          "n2-standard-16",
		HeterogeneousInstanceType: "n2-standard-96",
	})
	require.NoError(t, err)
	require.Empty(t, resp.ComputeClasses)
}

func TestAgentService_GetGKEHeterogeneousComputeClassFailsClosedForDuplicateLabelMatch(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)

	first := testBuildGKEComputeClass([]string{"np-a", "np-b"}, []string{"n2-standard-16", "n2-standard-64"})
	second := testBuildGKEComputeClass([]string{"np-c", "np-d"}, []string{"n2-standard-16", "n2-standard-64"})
	second.SetName("compute-class-invalid-duplicate-priority")
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(first, second).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	_, err := svc.GetGKEHeterogeneousComputeClass(context.Background(), &apiv1.GetGKEHeterogeneousComputeClassRequest{
		ClusterId:                 "123",
		BaseInstanceType:          "n2-standard-16",
		HeterogeneousInstanceType: "n2-standard-64",
	})
	require.Error(t, err)
	require.Equal(t, codes.FailedPrecondition, status.Code(err))
	require.Contains(t, err.Error(), "name does not match bindings hash")
}

func TestAgentService_GetGKEHeterogeneousComputeClassRejectsMalformedManagedComputeClass(t *testing.T) {
	tests := []struct {
		name     string
		mutate   func(*unstructured.Unstructured)
		wantErr  string
		wantCode codes.Code
		useName  bool
	}{
		{
			name: "missing-bindings-annotation",
			mutate: func(obj *unstructured.Unstructured) {
				annotations := obj.GetAnnotations()
				delete(annotations, gkeHeterogeneousBindingsAnnotationKey)
				obj.SetAnnotations(annotations)
			},
			wantErr: "missing bindings annotation",
		},
		{
			name: "invalid-bindings-json",
			mutate: func(obj *unstructured.Unstructured) {
				annotations := obj.GetAnnotations()
				annotations[gkeHeterogeneousBindingsAnnotationKey] = "not-json"
				obj.SetAnnotations(annotations)
			},
			wantErr: "invalid bindings annotation",
		},
		{
			name: "invalid-priority-type",
			mutate: func(obj *unstructured.Unstructured) {
				spec := obj.Object["spec"].(map[string]interface{})
				spec["priorities"] = []interface{}{"not-a-priority"}
			},
			wantErr: "priority 0 is invalid",
		},
		{
			name: "base-instance-type-label-mismatch",
			mutate: func(obj *unstructured.Unstructured) {
				labels := obj.GetLabels()
				labels[gkeHeterogeneousBaseInstanceTypeLabelKey] = "n2-standard-32"
				obj.SetLabels(labels)
			},
			wantErr:  "base instance type label does not match bindings annotation",
			wantCode: codes.PermissionDenied,
			useName:  true,
		},
		{
			name: "heterogeneous-nodepool-label-mismatch",
			mutate: func(obj *unstructured.Unstructured) {
				labels := obj.GetLabels()
				labels[gkeHeterogeneousExtraNodePoolLabelKey] = "np-c"
				obj.SetLabels(labels)
			},
			wantErr:  "heterogeneous nodepool label does not match bindings annotation",
			wantCode: codes.PermissionDenied,
			useName:  true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			scheme := newGKEHeterogeneousTestScheme(t)
			computeClass := testBuildGKEComputeClass([]string{"np-a", "np-b"}, []string{"n2-standard-16", "n2-standard-64"})
			tt.mutate(computeClass)
			cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(computeClass).Build()
			svc := newScopedGKEHeterogeneousTestService(cli)

			req := &apiv1.GetGKEHeterogeneousComputeClassRequest{
				ClusterId:                 "123",
				BaseInstanceType:          "n2-standard-16",
				HeterogeneousInstanceType: "n2-standard-64",
			}
			if tt.useName {
				req.Name = computeClass.GetName()
				req.BaseInstanceType = ""
				req.HeterogeneousInstanceType = ""
			}
			_, err := svc.GetGKEHeterogeneousComputeClass(context.Background(), req)
			require.Error(t, err)
			wantCode := tt.wantCode
			if wantCode == codes.OK {
				wantCode = codes.FailedPrecondition
			}
			require.Equal(t, wantCode, status.Code(err))
			require.Contains(t, err.Error(), tt.wantErr)
		})
	}
}

func TestAgentService_CreateGKEHeterogeneousComputeClassDoesNotOverwriteDifferentSpec(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)
	cli := fake.NewClientBuilder().WithScheme(scheme).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	ctx := context.Background()
	_, err := svc.CreateGKEHeterogeneousComputeClass(ctx, &apiv1.CreateGKEHeterogeneousComputeClassRequest{
		ClusterId:     "123",
		ComponentType: apicommon.ComponentType_TIKV,
		NodePoolNames: []string{"np-a", "np-b"},
		InstanceTypes: []string{"n2-standard-16", "n2-standard-64"},
	})
	require.NoError(t, err)

	resp, err := svc.CreateGKEHeterogeneousComputeClass(ctx, &apiv1.CreateGKEHeterogeneousComputeClassRequest{
		ClusterId:     "123",
		ComponentType: apicommon.ComponentType_TIKV,
		NodePoolNames: []string{"np-a", "np-b"},
		InstanceTypes: []string{"n2-standard-16", "n2-standard-64"},
	})
	require.NoError(t, err)
	require.Equal(t, []string{"np-a", "np-b"}, resp.NodePoolNames)

	_, err = svc.CreateGKEHeterogeneousComputeClass(ctx, &apiv1.CreateGKEHeterogeneousComputeClassRequest{
		ClusterId:     "123",
		ComponentType: apicommon.ComponentType_TIKV,
		NodePoolNames: []string{"np-a", "np-d"},
		InstanceTypes: []string{"n2-standard-16", "n2-standard-96"},
	})
	require.Error(t, err)
	require.Equal(t, codes.AlreadyExists, status.Code(err))
}

func TestAgentService_CreateGKEHeterogeneousComputeClassIsSharedAcrossClusters(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)
	cli := fake.NewClientBuilder().WithScheme(scheme).Build()
	svc123 := newScopedGKEHeterogeneousTestService(cli)
	svc456 := NewAgentService(cli, WithClusterNamespace(buildNamespace("456")), WithProvider(consts.ProviderTypeGKE))

	req := &apiv1.CreateGKEHeterogeneousComputeClassRequest{
		ComponentType: apicommon.ComponentType_TIKV,
		NodePoolNames: []string{"np-a", "np-b"},
		InstanceTypes: []string{"n2-standard-16", "n2-standard-64"},
	}
	req.ClusterId = "123"
	first, err := svc123.CreateGKEHeterogeneousComputeClass(context.Background(), req)
	require.NoError(t, err)

	req.ClusterId = "456"
	second, err := svc456.CreateGKEHeterogeneousComputeClass(context.Background(), req)
	require.NoError(t, err)
	require.Equal(t, first.Name, second.Name)
	require.Equal(t, first.NodePoolNames, second.NodePoolNames)
}

func TestAgentService_CreateGKEHeterogeneousComputeClassRejectsSharedNameDifferentSpec(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)
	cli := fake.NewClientBuilder().WithScheme(scheme).Build()
	svc123 := newScopedGKEHeterogeneousTestService(cli)
	svc456 := NewAgentService(cli, WithClusterNamespace(buildNamespace("456")), WithProvider(consts.ProviderTypeGKE))

	_, err := svc123.CreateGKEHeterogeneousComputeClass(context.Background(), &apiv1.CreateGKEHeterogeneousComputeClassRequest{
		ClusterId:     "123",
		ComponentType: apicommon.ComponentType_TIKV,
		NodePoolNames: []string{"np-a", "np-b"},
		InstanceTypes: []string{"n2-standard-16", "n2-standard-64"},
	})
	require.NoError(t, err)

	_, err = svc456.CreateGKEHeterogeneousComputeClass(context.Background(), &apiv1.CreateGKEHeterogeneousComputeClassRequest{
		ClusterId:     "456",
		ComponentType: apicommon.ComponentType_TIKV,
		NodePoolNames: []string{"np-a", "np-c"},
		InstanceTypes: []string{"n2-standard-16", "n2-standard-64"},
	})
	require.Error(t, err)
	require.Equal(t, codes.AlreadyExists, status.Code(err))
	require.Contains(t, err.Error(), "different bindings")
}

func TestAgentService_CreateGKEHeterogeneousComputeClassRejectsNodePoolAutoCreationEnabled(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)
	existing := testBuildGKEComputeClass([]string{"np-a", "np-b"}, []string{"n2-standard-16", "n2-standard-64"})
	require.NoError(t, unstructured.SetNestedField(existing.Object, true, "spec", "nodePoolAutoCreation", "enabled"))
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(existing).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	_, err := svc.CreateGKEHeterogeneousComputeClass(context.Background(), &apiv1.CreateGKEHeterogeneousComputeClassRequest{
		ClusterId:     "123",
		ComponentType: apicommon.ComponentType_TIKV,
		NodePoolNames: []string{"np-a", "np-b"},
		InstanceTypes: []string{"n2-standard-16", "n2-standard-64"},
	})

	require.Error(t, err)
	require.Equal(t, codes.AlreadyExists, status.Code(err))
	require.Contains(t, err.Error(), "nodePoolAutoCreation must be disabled")
}

func TestAgentService_CreateGKEHeterogeneousComputeClassRejectsForeignOwner(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)

	foreignComputeClass := testBuildGKEComputeClass([]string{"np-a", "np-b"}, []string{"n2-standard-16", "n2-standard-64"})
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(foreignComputeClass).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	_, err := svc.CreateGKEHeterogeneousComputeClass(context.Background(), &apiv1.CreateGKEHeterogeneousComputeClassRequest{
		ClusterId:     "123",
		ComponentType: apicommon.ComponentType_TIKV,
		NodePoolNames: []string{"np-a", "np-d"},
		InstanceTypes: []string{"n2-standard-16", "n2-standard-96"},
	})
	require.Error(t, err)
	require.Equal(t, codes.AlreadyExists, status.Code(err))
	require.Contains(t, err.Error(), "different bindings")
}

func TestAgentService_CreateGKEHeterogeneousComputeClassRejectsUnmanagedResource(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)

	unmanagedComputeClass := testBuildGKEComputeClass([]string{"np-a", "np-b"}, []string{"n2-standard-16", "n2-standard-64"})
	unmanagedComputeClass.SetLabels(map[string]string{consts.ClusterLabelKey: "123"})
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(unmanagedComputeClass).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	_, err := svc.CreateGKEHeterogeneousComputeClass(context.Background(), &apiv1.CreateGKEHeterogeneousComputeClassRequest{
		ClusterId:     "123",
		ComponentType: apicommon.ComponentType_TIKV,
		NodePoolNames: []string{"np-a", "np-b"},
		InstanceTypes: []string{"n2-standard-16", "n2-standard-64"},
	})
	require.Error(t, err)
	require.Contains(t, err.Error(), "not a valid binding-scoped gke heterogeneous compute class")
}

func TestAgentService_CreateGKEHeterogeneousComputeClassRejectsMissingScopeLabel(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)

	missingScopeComputeClass := testBuildGKEComputeClass([]string{"np-a", "np-b"}, []string{"n2-standard-16", "n2-standard-64"})
	labels := missingScopeComputeClass.GetLabels()
	delete(labels, gkeHeterogeneousComputeClassScopeLabelKey)
	missingScopeComputeClass.SetLabels(labels)
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(missingScopeComputeClass).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	_, err := svc.CreateGKEHeterogeneousComputeClass(context.Background(), &apiv1.CreateGKEHeterogeneousComputeClassRequest{
		ClusterId:     "123",
		ComponentType: apicommon.ComponentType_TIKV,
		NodePoolNames: []string{"np-a", "np-b"},
		InstanceTypes: []string{"n2-standard-16", "n2-standard-64"},
	})
	require.Error(t, err)
	require.Equal(t, codes.AlreadyExists, status.Code(err))
	require.Contains(t, err.Error(), "not a valid binding-scoped gke heterogeneous compute class")
}

func TestAgentService_CreateGKEHeterogeneousComputeClassAllowsComponentReuseForSameBinding(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)

	tikvComputeClass := testBuildGKEComputeClass([]string{"np-a", "np-b"}, []string{"n2-standard-16", "n2-standard-64"})
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(tikvComputeClass).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	resp, err := svc.CreateGKEHeterogeneousComputeClass(context.Background(), &apiv1.CreateGKEHeterogeneousComputeClassRequest{
		ClusterId:     "123",
		ComponentType: apicommon.ComponentType_TIDB,
		NodePoolNames: []string{"np-a", "np-b"},
		InstanceTypes: []string{"n2-standard-16", "n2-standard-64"},
	})
	require.NoError(t, err)
	require.Equal(t, tikvComputeClass.GetName(), resp.Name)
	require.Equal(t, apicommon.ComponentType_TIDB, resp.ComponentType)
}

func TestAgentService_CreateTiDBComputeClassDoesNotRequireReferencePods(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)

	cli := fake.NewClientBuilder().WithScheme(scheme).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	resp, err := svc.CreateGKEHeterogeneousComputeClass(context.Background(), &apiv1.CreateGKEHeterogeneousComputeClassRequest{
		ClusterId:     "123",
		ComponentType: apicommon.ComponentType_TIDB,
		NodePoolNames: []string{"np-a", "np-b"},
		InstanceTypes: []string{"n2-standard-16", "n2-standard-64"},
	})
	require.NoError(t, err)
	require.Equal(t, testAgentGKEComputeClassName("n2-standard-16", "n2-standard-64"), resp.Name)
	require.Equal(t, []string{"n2-standard-16", "n2-standard-64"}, resp.InstanceTypes)
}

func TestAgentService_CreateTiDBComputeClassAllowsMissingGroupID(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)

	cli := fake.NewClientBuilder().WithScheme(scheme).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	resp, err := svc.CreateGKEHeterogeneousComputeClass(context.Background(), &apiv1.CreateGKEHeterogeneousComputeClassRequest{
		ClusterId:     "123",
		ComponentType: apicommon.ComponentType_TIDB,
		NodePoolNames: []string{"np-a", "np-b"},
		InstanceTypes: []string{"n2-standard-16", "n2-standard-64"},
	})
	require.NoError(t, err)
	require.Equal(t, testAgentGKEComputeClassName("n2-standard-16", "n2-standard-64"), resp.Name)
	require.Equal(t, []string{"n2-standard-16", "n2-standard-64"}, resp.InstanceTypes)
}

func TestAgentService_CreateTiDBGatekeeperAssignAllowsSharedComputeClassWithoutGroupLabel(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)

	defaultPod := &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "db-tidb-0",
			Namespace: buildNamespace("123"),
			Labels: map[string]string{
				consts.ComponentLabelKey: string(cloudv1.ComponentTiDB),
				consts.InstanceLabelKey:  consts.ClusterPrefix,
			},
		},
	}
	extraPod := defaultPod.DeepCopy()
	extraPod.Name = "extra-tidb-0"
	extraPod.Labels = map[string]string{
		consts.ComponentLabelKey: string(cloudv1.ComponentTiDB),
		consts.InstanceLabelKey:  gkeHeterogeneousTiDBGroupInstanceLabel("extra-group"),
	}
	computeClass := testBuildGKEComputeClass([]string{"np-a"})
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(defaultPod, extraPod, computeClass).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	resp, err := svc.CreateGKEHeterogeneousGatekeeperAssign(context.Background(), &apiv1.CreateGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId:        "123",
		ComponentType:    apicommon.ComponentType_TIDB,
		TidbGroupId:      "extra-group",
		TidbGroupKind:    apiv1.TiDBGroupKind_TIDB_GROUP_KIND_EXTRA,
		ComputeClassName: computeClass.GetName(),
	})
	require.NoError(t, err)
	require.Equal(t, "extra-group", resp.TidbGroupId)
}

func TestAgentService_CreateTiDBGatekeeperAssignSupportsExtraGroupID(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)

	defaultPod := &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "db-tidb-0",
			Namespace: buildNamespace("123"),
			Labels: map[string]string{
				consts.ComponentLabelKey: string(cloudv1.ComponentTiDB),
				consts.InstanceLabelKey:  consts.ClusterPrefix,
			},
		},
	}
	extraPod := defaultPod.DeepCopy()
	extraPod.Name = "extra-tidb-0"
	extraPod.Labels = map[string]string{
		consts.ComponentLabelKey: string(cloudv1.ComponentTiDB),
		consts.InstanceLabelKey:  gkeHeterogeneousTiDBGroupInstanceLabel("extra-group"),
	}
	extraPod.Spec.TopologySpreadConstraints = testPodTopologySpreadConstraints(nil)
	computeClass := testBuildGKEComputeClass([]string{"np-a"})
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(defaultPod, extraPod, computeClass).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	resp, err := svc.CreateGKEHeterogeneousGatekeeperAssign(context.Background(), &apiv1.CreateGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId:        "123",
		ComponentType:    apicommon.ComponentType_TIDB,
		TidbGroupId:      "extra-group",
		TidbGroupKind:    apiv1.TiDBGroupKind_TIDB_GROUP_KIND_EXTRA,
		ComputeClassName: computeClass.GetName(),
	})
	require.NoError(t, err)
	require.Equal(t, "extra-group", resp.TidbGroupId)
	require.Equal(t, apiv1.TiDBGroupKind_TIDB_GROUP_KIND_EXTRA, resp.TidbGroupKind)
	require.Equal(t, map[string]string{
		consts.ComponentLabelKey: string(cloudv1.ComponentTiDB),
		consts.InstanceLabelKey:  gkeHeterogeneousTiDBGroupInstanceLabel("extra-group"),
	}, resp.MatchLabels)
	getResp, err := svc.GetGKEHeterogeneousGatekeeperAssign(context.Background(), &apiv1.GetGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId:     "123",
		ComponentType: apicommon.ComponentType_TIDB,
		TidbGroupId:   "extra-group",
	})
	require.NoError(t, err)
	require.Equal(t, apiv1.TiDBGroupKind_TIDB_GROUP_KIND_EXTRA, getResp.TidbGroupKind)
	assign, err := getGKEHeterogeneousResource(context.Background(), cli, gatekeeperAssignGVK, resp.Name, gatekeeperAssignLabel)
	require.NoError(t, err)
	topologySpreadName := defaultGKEHeterogeneousResourceName("123", &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiDB, tidbGroupID: "extra-group", tidbGroupKind: apiv1.TiDBGroupKind_TIDB_GROUP_KIND_EXTRA}, "topology-spread-assign")
	_, err = getGKEHeterogeneousResource(context.Background(), cli, gatekeeperAssignGVK, topologySpreadName, gatekeeperAssignLabel)
	require.NoError(t, err)
	require.NotContains(t, assign.GetLabels(), "tidbcloud.com/tidb-group-kind")
	token := requireGatekeeperAssignExemption(t, assign)
	updatedExtraPod := &corev1.Pod{}
	require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Namespace: extraPod.Namespace, Name: extraPod.Name}, updatedExtraPod))
	require.Equal(t, token, updatedExtraPod.GetLabels()[gkeHeterogeneousGatekeeperExemptLabelKey])
	updatedDefaultPod := &corev1.Pod{}
	require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Namespace: defaultPod.Namespace, Name: defaultPod.Name}, updatedDefaultPod))
	require.Empty(t, updatedDefaultPod.GetLabels()[gkeHeterogeneousGatekeeperExemptLabelKey])
}

func TestAgentService_CreateAndUpdateTiDBGatekeeperAssignSupportsDefaultGroupID(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)

	defaultPod := &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "db-tidb-0",
			Namespace: buildNamespace("123"),
			Labels: map[string]string{
				consts.ComponentLabelKey: string(cloudv1.ComponentTiDB),
				consts.InstanceLabelKey:  consts.ClusterPrefix,
			},
		},
		Spec: corev1.PodSpec{
			NodeSelector: map[string]string{
				consts.LabelKeyNodeGroupType: consts.LabelValNodeGroupTypeDataPlane,
				consts.K8sVersionLabelKey:    "1.31",
			},
			TopologySpreadConstraints: testPodTopologySpreadConstraints(nil),
		},
	}
	extraPod := defaultPod.DeepCopy()
	extraPod.Name = "extra-tidb-0"
	extraPod.Labels = map[string]string{
		consts.ComponentLabelKey: string(cloudv1.ComponentTiDB),
		consts.InstanceLabelKey:  gkeHeterogeneousTiDBGroupInstanceLabel("default-group"),
	}
	computeClass := testBuildGKEComputeClass([]string{"np-a"})
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(defaultPod, extraPod, computeClass).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	ctx := context.Background()
	resp, err := svc.CreateGKEHeterogeneousGatekeeperAssign(ctx, &apiv1.CreateGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId:        "123",
		ComponentType:    apicommon.ComponentType_TIDB,
		TidbGroupId:      "default-group",
		TidbGroupKind:    apiv1.TiDBGroupKind_TIDB_GROUP_KIND_DEFAULT,
		ComputeClassName: computeClass.GetName(),
	})
	require.NoError(t, err)
	require.Equal(t, "default-group", resp.TidbGroupId)
	require.Equal(t, apiv1.TiDBGroupKind_TIDB_GROUP_KIND_DEFAULT, resp.TidbGroupKind)
	require.Equal(t, map[string]string{
		consts.ComponentLabelKey: string(cloudv1.ComponentTiDB),
		consts.InstanceLabelKey:  consts.ClusterPrefix,
	}, resp.MatchLabels)
	topologySpreadName := defaultGKEHeterogeneousResourceName("123", &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiDB, tidbGroupID: "default-group", tidbGroupKind: apiv1.TiDBGroupKind_TIDB_GROUP_KIND_DEFAULT}, "topology-spread-assign")
	_, err = getGKEHeterogeneousResource(ctx, cli, gatekeeperAssignGVK, topologySpreadName, gatekeeperAssignLabel)
	require.NoError(t, err)

	resp, err = svc.UpdateGKEHeterogeneousGatekeeperAssign(ctx, &apiv1.UpdateGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId:        "123",
		Name:             resp.Name,
		ComputeClassName: computeClass.GetName(),
	})
	require.NoError(t, err)
	require.Equal(t, apiv1.TiDBGroupKind_TIDB_GROUP_KIND_DEFAULT, resp.TidbGroupKind)
	require.Equal(t, map[string]string{
		consts.ComponentLabelKey: string(cloudv1.ComponentTiDB),
		consts.InstanceLabelKey:  consts.ClusterPrefix,
	}, resp.MatchLabels)
}

func TestAgentService_CreateTiDBGatekeeperAssignRejectsMissingGroupReferencePod(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)

	defaultPod := &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "db-tidb-0",
			Namespace: buildNamespace("123"),
			Labels: map[string]string{
				consts.ComponentLabelKey: string(cloudv1.ComponentTiDB),
				consts.InstanceLabelKey:  consts.ClusterPrefix,
			},
		},
		Spec: corev1.PodSpec{NodeSelector: map[string]string{
			consts.LabelKeyNodeGroupType: consts.LabelValNodeGroupTypeDataPlane,
			consts.K8sVersionLabelKey:    "1.31",
		}},
	}

	computeClass := testBuildGKEComputeClass([]string{"np-a"})
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(defaultPod, computeClass).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	_, err := svc.CreateGKEHeterogeneousGatekeeperAssign(context.Background(), &apiv1.CreateGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId:        "123",
		ComponentType:    apicommon.ComponentType_TIDB,
		TidbGroupId:      "extra-group",
		TidbGroupKind:    apiv1.TiDBGroupKind_TIDB_GROUP_KIND_EXTRA,
		ComputeClassName: computeClass.GetName(),
	})
	require.Error(t, err)
	require.Equal(t, codes.FailedPrecondition, status.Code(err))
	require.Contains(t, err.Error(), "no tidb pod found")
}

func TestAgentService_CreateTiDBGatekeeperAssignRejectsMissingGroupKind(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)

	defaultPod := &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "db-tidb-0",
			Namespace: buildNamespace("123"),
			Labels: map[string]string{
				consts.ComponentLabelKey: string(cloudv1.ComponentTiDB),
				consts.InstanceLabelKey:  consts.ClusterPrefix,
			},
		},
		Spec: corev1.PodSpec{NodeSelector: map[string]string{
			consts.LabelKeyNodeGroupType: consts.LabelValNodeGroupTypeDataPlane,
			consts.K8sVersionLabelKey:    "1.31",
		}},
	}
	computeClass := testBuildGKEComputeClass([]string{"np-a"})
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(defaultPod, computeClass).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	_, err := svc.CreateGKEHeterogeneousGatekeeperAssign(context.Background(), &apiv1.CreateGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId:        "123",
		ComponentType:    apicommon.ComponentType_TIDB,
		TidbGroupId:      "default-group",
		ComputeClassName: computeClass.GetName(),
	})
	require.Error(t, err)
	require.Equal(t, codes.InvalidArgument, status.Code(err))
	require.Contains(t, err.Error(), "tidb_group_kind is required")
}

func TestAgentService_CreateTiDBComputeClassAllowsDifferentGroupSameSharedSpec(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)

	group1Pod := &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "group1-tidb-0",
			Namespace: buildNamespace("123"),
			Labels: map[string]string{
				consts.ComponentLabelKey: string(cloudv1.ComponentTiDB),
				consts.InstanceLabelKey:  gkeHeterogeneousTiDBGroupInstanceLabel("group1"),
			},
		},
	}
	group2Pod := group1Pod.DeepCopy()
	group2Pod.Name = "group2-tidb-0"
	group2Pod.Labels[consts.InstanceLabelKey] = gkeHeterogeneousTiDBGroupInstanceLabel("group2")

	existing := testBuildGKEComputeClass([]string{"np-a", "np-b"}, []string{"n2-standard-16", "n2-standard-64"})
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(group1Pod, group2Pod, existing).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	resp, err := svc.CreateGKEHeterogeneousComputeClass(context.Background(), &apiv1.CreateGKEHeterogeneousComputeClassRequest{
		ClusterId:     "123",
		ComponentType: apicommon.ComponentType_TIDB,
		NodePoolNames: []string{"np-a", "np-b"},
		InstanceTypes: []string{"n2-standard-16", "n2-standard-64"},
	})
	require.NoError(t, err)
	require.Equal(t, existing.GetName(), resp.Name)
}

func TestAgentService_CreateTiDBGatekeeperAssignRejectsMissingGroupID(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)

	defaultPod := &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "db-tidb-0",
			Namespace: buildNamespace("123"),
			Labels: map[string]string{
				consts.ComponentLabelKey: string(cloudv1.ComponentTiDB),
				consts.InstanceLabelKey:  consts.ClusterPrefix,
			},
		},
		Spec: corev1.PodSpec{NodeSelector: map[string]string{
			consts.LabelKeyNodeGroupType: consts.LabelValNodeGroupTypeDataPlane,
			consts.K8sVersionLabelKey:    "1.31",
		}},
	}
	extraPod := defaultPod.DeepCopy()
	extraPod.Name = "extra-tidb-0"
	extraPod.Labels = map[string]string{
		consts.ComponentLabelKey: string(cloudv1.ComponentTiDB),
		consts.InstanceLabelKey:  gkeHeterogeneousTiDBGroupInstanceLabel("extra-group"),
	}
	computeClass := testBuildGKEComputeClass([]string{"np-a"})
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(defaultPod, extraPod, computeClass).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	_, err := svc.CreateGKEHeterogeneousGatekeeperAssign(context.Background(), &apiv1.CreateGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId:        "123",
		ComponentType:    apicommon.ComponentType_TIDB,
		ComputeClassName: computeClass.GetName(),
	})
	require.Error(t, err)
	require.Equal(t, codes.InvalidArgument, status.Code(err))
	require.Contains(t, err.Error(), "tidb_group_id is required")
}

func TestAgentService_CreateAndUpdateGKEHeterogeneousGatekeeperAssign(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)

	refPod := &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "db-tikv-0",
			Namespace: buildNamespace("123"),
			Labels: map[string]string{
				consts.ComponentLabelKey: string(cloudv1.ComponentTiKV),
				consts.InstanceLabelKey:  consts.ClusterPrefix,
			},
		},
		Spec: corev1.PodSpec{
			NodeSelector: map[string]string{
				consts.LabelKeyNodeGroupType: consts.LabelValNodeGroupTypeDataPlane,
				consts.K8sVersionLabelKey:    "1.31",
				consts.InstanceTypeLabelKey:  "n2-standard-8",
				"tidbcloud.com/custom":       "keep",
			},
		},
	}

	computeClass := testBuildGKEComputeClass([]string{"np-a", "np-b"})
	computeClassName := computeClass.GetName()
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(refPod, computeClass).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	ctx := context.Background()
	resp, err := svc.CreateGKEHeterogeneousGatekeeperAssign(ctx, &apiv1.CreateGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId:        "123",
		ComponentType:    apicommon.ComponentType_TIKV,
		TidbGroupId:      "ignored",
		ComputeClassName: computeClassName,
	})
	require.NoError(t, err)
	require.Empty(t, resp.TidbGroupId)
	require.Equal(t, computeClassName, resp.ComputeClassName)
	require.Equal(t, "1.31", resp.SelectorVersion)
	require.Equal(t, buildNamespace("123"), resp.Namespace)
	require.Equal(t, map[string]string{
		consts.GKEComputeClassLabelKey: computeClassName,
		consts.LabelKeyNodeGroupType:   consts.LabelValNodeGroupTypeDataPlane,
		consts.K8sVersionLabelKey:      "1.31",
		"tidbcloud.com/custom":         "keep",
	}, resp.NodeSelector)
	require.NotContains(t, resp.NodeSelector, consts.InstanceTypeLabelKey)
	assign, err := getGKEHeterogeneousResource(ctx, cli, gatekeeperAssignGVK, resp.Name, gatekeeperAssignLabel)
	require.NoError(t, err)
	require.Equal(t, gatekeeperAssignDeployLabelValue, assign.GetLabels()[gkeHeterogeneousDeployLabelKey])
	token := requireGatekeeperAssignExemption(t, assign)
	updatedPod := &corev1.Pod{}
	require.NoError(t, cli.Get(ctx, client.ObjectKey{Namespace: refPod.Namespace, Name: refPod.Name}, updatedPod))
	require.Equal(t, token, updatedPod.GetLabels()[gkeHeterogeneousGatekeeperExemptLabelKey])

	getResp, err := svc.GetGKEHeterogeneousGatekeeperAssign(ctx, &apiv1.GetGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId:     "123",
		ComponentType: apicommon.ComponentType_TIKV,
		TidbGroupId:   "ignored",
	})
	require.NoError(t, err)
	require.Empty(t, getResp.TidbGroupId)
	require.Equal(t, resp.Name, getResp.Name)
	require.Equal(t, computeClassName, getResp.ComputeClassName)

	resp, err = svc.UpdateGKEHeterogeneousGatekeeperAssign(ctx, &apiv1.UpdateGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId:        "123",
		Name:             resp.Name,
		ComputeClassName: computeClassName,
	})
	require.NoError(t, err)
	require.Equal(t, computeClassName, resp.ComputeClassName)
	require.Equal(t, map[string]string{
		consts.GKEComputeClassLabelKey: computeClassName,
		consts.LabelKeyNodeGroupType:   consts.LabelValNodeGroupTypeDataPlane,
		consts.K8sVersionLabelKey:      "1.31",
		"tidbcloud.com/custom":         "keep",
	}, resp.NodeSelector)
	updatedAssign, err := getGKEHeterogeneousResource(ctx, cli, gatekeeperAssignGVK, resp.Name, gatekeeperAssignLabel)
	require.NoError(t, err)
	requireGatekeeperAssignExemption(t, updatedAssign)

	_, err = svc.DeleteGKEHeterogeneousGatekeeperAssign(ctx, &apiv1.DeleteGKEHeterogeneousGatekeeperAssignRequest{ClusterId: "123", Name: resp.Name})
	require.NoError(t, err)
}

func TestAgentService_CreateGKEHeterogeneousGatekeeperAssignCreatesTopologySpreadCompanion(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)
	component := &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiKV}

	refPod := &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "db-tikv-0",
			Namespace: buildNamespace("123"),
			Labels: map[string]string{
				consts.ComponentLabelKey: string(cloudv1.ComponentTiKV),
				consts.InstanceLabelKey:  consts.ClusterPrefix,
			},
		},
		Spec: corev1.PodSpec{
			NodeSelector: map[string]string{
				consts.LabelKeyNodeGroupType: consts.LabelValNodeGroupTypeDataPlane,
				consts.K8sVersionLabelKey:    "1.31",
			},
			TopologySpreadConstraints: testPodTopologySpreadConstraints(nil),
		},
	}
	computeClass := testBuildGKEComputeClass([]string{"np-a", "np-b"})
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(refPod, computeClass).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	ctx := context.Background()
	resp, err := svc.CreateGKEHeterogeneousGatekeeperAssign(ctx, &apiv1.CreateGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId:        "123",
		ComponentType:    apicommon.ComponentType_TIKV,
		ComputeClassName: computeClass.GetName(),
	})
	require.NoError(t, err)

	assign, err := getGKEHeterogeneousResource(ctx, cli, gatekeeperAssignGVK, resp.Name, gatekeeperAssignLabel)
	require.NoError(t, err)
	token := requireGatekeeperAssignExemption(t, assign)
	require.Equal(t, gatekeeperAssignPurposeNodeSelector, assign.GetLabels()[gkeHeterogeneousGatekeeperAssignPurposeKey])

	topologySpreadName := defaultGKEHeterogeneousResourceName("123", component, "topology-spread-assign")
	topologySpreadAssign, err := getGKEHeterogeneousResource(ctx, cli, gatekeeperAssignGVK, topologySpreadName, gatekeeperAssignLabel)
	require.NoError(t, err)
	require.Equal(t, gatekeeperAssignPurposeTopologySpread, topologySpreadAssign.GetLabels()[gkeHeterogeneousGatekeeperAssignPurposeKey])
	require.Empty(t, topologySpreadAssign.GetLabels()[gkeHeterogeneousComputeClassLabelKey])
	requireGatekeeperAssignExemptionToken(t, topologySpreadAssign, token)

	location, found, err := unstructured.NestedString(topologySpreadAssign.Object, "spec", "location")
	require.NoError(t, err)
	require.True(t, found)
	require.Equal(t, "spec.topologySpreadConstraints[topologyKey:'topology.kubernetes.io/zone'].nodeAffinityPolicy", location)
	require.NotContains(t, location, "topologyKey:topology.kubernetes.io/zone")
	value, found, err := unstructured.NestedString(topologySpreadAssign.Object, "spec", "parameters", "assign", "value")
	require.NoError(t, err)
	require.True(t, found)
	require.Equal(t, string(corev1.NodeInclusionPolicyIgnore), value)
	pathTests, found, err := unstructured.NestedSlice(topologySpreadAssign.Object, "spec", "parameters", "pathTests")
	require.NoError(t, err)
	require.True(t, found)
	require.Equal(t, []interface{}{
		map[string]interface{}{
			"subPath":   "spec.topologySpreadConstraints[topologyKey:'topology.kubernetes.io/zone']",
			"condition": "MustExist",
		},
	}, pathTests)
	for _, pathTest := range pathTests {
		pathTestMap, ok := pathTest.(map[string]interface{})
		require.True(t, ok)
		subPath, ok := pathTestMap["subPath"].(string)
		require.True(t, ok)
		require.NotContains(t, subPath, "topologyKey:topology.kubernetes.io/zone")
	}

	getResp, err := svc.GetGKEHeterogeneousGatekeeperAssign(ctx, &apiv1.GetGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId:     "123",
		ComponentType: apicommon.ComponentType_TIKV,
	})
	require.NoError(t, err)
	require.Equal(t, resp.Name, getResp.Name)
	require.Equal(t, computeClass.GetName(), getResp.ComputeClassName)

	_, err = svc.UpdateGKEHeterogeneousGatekeeperAssign(ctx, &apiv1.UpdateGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId:        "123",
		Name:             topologySpreadName,
		ComputeClassName: computeClass.GetName(),
	})
	require.Error(t, err)
	require.Equal(t, codes.FailedPrecondition, status.Code(err))
	require.Contains(t, err.Error(), "not a node selector assign")

	_, err = svc.DeleteGKEHeterogeneousGatekeeperAssign(ctx, &apiv1.DeleteGKEHeterogeneousGatekeeperAssignRequest{ClusterId: "123", Name: resp.Name})
	require.NoError(t, err)
	deleted := &unstructured.Unstructured{}
	deleted.SetGroupVersionKind(gatekeeperAssignGVK)
	require.True(t, apierrors.IsNotFound(cli.Get(ctx, client.ObjectKey{Name: resp.Name}, deleted)))
	deletedTopologySpread := &unstructured.Unstructured{}
	deletedTopologySpread.SetGroupVersionKind(gatekeeperAssignGVK)
	require.True(t, apierrors.IsNotFound(cli.Get(ctx, client.ObjectKey{Name: topologySpreadName}, deletedTopologySpread)))
}

func TestAgentService_GetGKEHeterogeneousGatekeeperAssignRequiresTopologySpreadCompanion(t *testing.T) {
	tests := []struct {
		name             string
		includeCompanion bool
		mutateCompanion  func(t *testing.T, companion *unstructured.Unstructured)
		wantCode         codes.Code
		wantErrMsg       string
	}{
		{
			name:       "missing companion",
			wantCode:   codes.FailedPrecondition,
			wantErrMsg: "topology spread gatekeeper assign",
		},
		{
			name:             "malformed companion",
			includeCompanion: true,
			mutateCompanion: func(t *testing.T, companion *unstructured.Unstructured) {
				t.Helper()
				require.NoError(t, unstructured.SetNestedField(companion.Object, string(corev1.NodeInclusionPolicyHonor), "spec", "parameters", "assign", "value"))
			},
			wantCode:   codes.FailedPrecondition,
			wantErrMsg: "must assign Ignore",
		},
		{
			name:             "companion match mismatch",
			includeCompanion: true,
			mutateCompanion: func(t *testing.T, companion *unstructured.Unstructured) {
				t.Helper()
				require.NoError(t, unstructured.SetNestedField(
					companion.Object,
					"1.32",
					"spec", "match", "labelSelector", "matchLabels", consts.K8sVersionLabelKey,
				))
			},
			wantCode:   codes.FailedPrecondition,
			wantErrMsg: "match does not match primary",
		},
		{
			name:             "valid companion",
			includeCompanion: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			scheme := newGKEHeterogeneousTestScheme(t)
			component := &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiKV}
			settings := &gkeHeterogeneousAssignSettings{
				namespace: buildNamespace("123"),
				nodeSelector: map[string]string{
					consts.LabelKeyNodeGroupType: consts.LabelValNodeGroupTypeDataPlane,
					consts.K8sVersionLabelKey:    "1.31",
				},
				matchLabels: testGatekeeperAssignMatchLabels(t, component),
			}
			primaryName := defaultGKEHeterogeneousResourceName("123", component, "compute-class-assign")
			primary := requireBuildGatekeeperAssign(t, primaryName, "123", component, "tikv-priority", settings)
			topologySpreadName := defaultGKEHeterogeneousResourceName("123", component, "topology-spread-assign")
			companion := requireBuildGatekeeperTopologySpreadAssign(t, topologySpreadName, "123", component, settings, gatekeeperAssignExemptToken(primaryName))
			if tt.mutateCompanion != nil {
				tt.mutateCompanion(t, companion)
			}

			objects := []runtime.Object{primary}
			if tt.includeCompanion {
				objects = append(objects, companion)
			}
			cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objects...).Build()
			svc := newScopedGKEHeterogeneousTestService(cli)

			resp, err := svc.GetGKEHeterogeneousGatekeeperAssign(context.Background(), &apiv1.GetGKEHeterogeneousGatekeeperAssignRequest{
				ClusterId:     "123",
				ComponentType: apicommon.ComponentType_TIKV,
			})
			if tt.wantErrMsg != "" {
				require.Error(t, err)
				require.Equal(t, tt.wantCode, status.Code(err))
				require.Contains(t, err.Error(), tt.wantErrMsg)
				return
			}
			require.NoError(t, err)
			require.Equal(t, primaryName, resp.GetName())
			require.Equal(t, "tikv-priority", resp.GetComputeClassName())
		})
	}
}

func TestAgentService_DeleteGKEHeterogeneousGatekeeperAssignDeletesOrphanTopologySpreadCompanionWhenPrimaryMissing(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)
	component := &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiKV}
	settings := &gkeHeterogeneousAssignSettings{
		namespace:   buildNamespace("123"),
		matchLabels: testGatekeeperAssignMatchLabels(t, component),
	}
	name := defaultGKEHeterogeneousResourceName("123", component, "compute-class-assign")
	topologySpreadName := defaultGKEHeterogeneousResourceName("123", component, "topology-spread-assign")
	topologySpreadAssign, err := buildGatekeeperTopologySpreadAssign(topologySpreadName, "123", component, settings, gatekeeperAssignExemptToken(name))
	require.NoError(t, err)
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(topologySpreadAssign).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	ctx := context.Background()
	_, err = svc.DeleteGKEHeterogeneousGatekeeperAssign(ctx, &apiv1.DeleteGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId: "123",
		Name:      name,
	})
	require.NoError(t, err)

	got := &unstructured.Unstructured{}
	got.SetGroupVersionKind(gatekeeperAssignGVK)
	require.True(t, apierrors.IsNotFound(cli.Get(ctx, client.ObjectKey{Name: topologySpreadName}, got)))
}

func TestAgentService_DeleteGKEHeterogeneousGatekeeperAssignDoesNotDeleteOtherComponentOrphanTopologySpreadCompanion(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)
	component := &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiKV}
	otherComponent := &gkeHeterogeneousDataplaneComponent{
		componentType: cloudv1.ComponentTiDB,
		tidbGroupID:   "extra-group",
		tidbGroupKind: apiv1.TiDBGroupKind_TIDB_GROUP_KIND_EXTRA,
	}
	otherSettings := &gkeHeterogeneousAssignSettings{
		namespace:   buildNamespace("123"),
		matchLabels: testGatekeeperAssignMatchLabels(t, otherComponent),
	}
	name := defaultGKEHeterogeneousResourceName("123", component, "compute-class-assign")
	otherPrimaryName := defaultGKEHeterogeneousResourceName("123", otherComponent, "compute-class-assign")
	otherTopologySpreadName := defaultGKEHeterogeneousResourceName("123", otherComponent, "topology-spread-assign")
	otherTopologySpreadAssign, err := buildGatekeeperTopologySpreadAssign(otherTopologySpreadName, "123", otherComponent, otherSettings, gatekeeperAssignExemptToken(otherPrimaryName))
	require.NoError(t, err)
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(otherTopologySpreadAssign).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	ctx := context.Background()
	_, err = svc.DeleteGKEHeterogeneousGatekeeperAssign(ctx, &apiv1.DeleteGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId: "123",
		Name:      name,
	})
	require.NoError(t, err)

	got := &unstructured.Unstructured{}
	got.SetGroupVersionKind(gatekeeperAssignGVK)
	require.NoError(t, cli.Get(ctx, client.ObjectKey{Name: otherTopologySpreadName}, got))
}

func TestAgentService_DeleteGKEHeterogeneousGatekeeperAssignDoesNotDeleteMismatchedTopologySpreadCompanionName(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)
	component := &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiKV}
	settings := &gkeHeterogeneousAssignSettings{
		namespace:   buildNamespace("123"),
		matchLabels: testGatekeeperAssignMatchLabels(t, component),
	}
	name := defaultGKEHeterogeneousResourceName("123", component, "compute-class-assign")
	topologySpreadAssign, err := buildGatekeeperTopologySpreadAssign("manual-topology-spread-assign", "123", component, settings, gatekeeperAssignExemptToken(name))
	require.NoError(t, err)
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(topologySpreadAssign).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	ctx := context.Background()
	_, err = svc.DeleteGKEHeterogeneousGatekeeperAssign(ctx, &apiv1.DeleteGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId: "123",
		Name:      name,
	})
	require.NoError(t, err)

	got := &unstructured.Unstructured{}
	got.SetGroupVersionKind(gatekeeperAssignGVK)
	require.NoError(t, cli.Get(ctx, client.ObjectKey{Name: topologySpreadAssign.GetName()}, got))
}

func TestAgentService_DeleteGKEHeterogeneousGatekeeperAssignDeletesMalformedOrphanTopologySpreadCompanionForPrimaryName(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)
	component := &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiKV}
	settings := &gkeHeterogeneousAssignSettings{
		namespace:   buildNamespace("123"),
		matchLabels: testGatekeeperAssignMatchLabels(t, component),
	}
	name := defaultGKEHeterogeneousResourceName("123", component, "compute-class-assign")
	topologySpreadName := defaultGKEHeterogeneousResourceName("123", component, "topology-spread-assign")
	topologySpreadAssign, err := buildGatekeeperTopologySpreadAssign(topologySpreadName, "123", component, settings, gatekeeperAssignExemptToken(name))
	require.NoError(t, err)
	require.NoError(t, unstructured.SetNestedField(topologySpreadAssign.Object, "spec.nodeSelector", "spec", "location"))
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(topologySpreadAssign).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	ctx := context.Background()
	_, err = svc.DeleteGKEHeterogeneousGatekeeperAssign(ctx, &apiv1.DeleteGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId: "123",
		Name:      name,
	})
	require.NoError(t, err)

	got := &unstructured.Unstructured{}
	got.SetGroupVersionKind(gatekeeperAssignGVK)
	require.True(t, apierrors.IsNotFound(cli.Get(ctx, client.ObjectKey{Name: topologySpreadName}, got)))
}

func TestAgentService_DeleteGKEHeterogeneousGatekeeperAssignDeletesMalformedTopologySpreadCompanion(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)
	component := &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiKV}
	settings := &gkeHeterogeneousAssignSettings{
		namespace: buildNamespace("123"),
		nodeSelector: map[string]string{
			consts.LabelKeyNodeGroupType: consts.LabelValNodeGroupTypeDataPlane,
			consts.K8sVersionLabelKey:    "1.31",
		},
		matchLabels: testGatekeeperAssignMatchLabels(t, component),
	}
	name := defaultGKEHeterogeneousResourceName("123", component, "compute-class-assign")
	topologySpreadName := defaultGKEHeterogeneousResourceName("123", component, "topology-spread-assign")
	assign := requireBuildGatekeeperAssign(t, name, "123", component, "tikv-priority", settings)
	topologySpreadAssign, err := buildGatekeeperTopologySpreadAssign(topologySpreadName, "123", component, settings, gatekeeperAssignExemptToken(name))
	require.NoError(t, err)
	require.NoError(t, unstructured.SetNestedField(topologySpreadAssign.Object, "spec.nodeSelector", "spec", "location"))
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(assign, topologySpreadAssign).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	ctx := context.Background()
	_, err = svc.DeleteGKEHeterogeneousGatekeeperAssign(ctx, &apiv1.DeleteGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId: "123",
		Name:      name,
	})
	require.NoError(t, err)

	got := &unstructured.Unstructured{}
	got.SetGroupVersionKind(gatekeeperAssignGVK)
	require.True(t, apierrors.IsNotFound(cli.Get(ctx, client.ObjectKey{Name: name}, got)))
	gotTopologySpread := &unstructured.Unstructured{}
	gotTopologySpread.SetGroupVersionKind(gatekeeperAssignGVK)
	require.True(t, apierrors.IsNotFound(cli.Get(ctx, client.ObjectKey{Name: topologySpreadName}, gotTopologySpread)))
}

func TestAgentService_CreateGKEHeterogeneousGatekeeperAssignPreflightRejectsExistingPrimaryBeforeTopologySpreadSideEffects(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)
	component := &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiKV}

	refPod := &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "db-tikv-0",
			Namespace: buildNamespace("123"),
			Labels: map[string]string{
				consts.ComponentLabelKey: string(cloudv1.ComponentTiKV),
				consts.InstanceLabelKey:  consts.ClusterPrefix,
			},
		},
		Spec: corev1.PodSpec{
			NodeSelector: map[string]string{
				consts.LabelKeyNodeGroupType: consts.LabelValNodeGroupTypeDataPlane,
				consts.K8sVersionLabelKey:    "1.31",
			},
			TopologySpreadConstraints: testPodTopologySpreadConstraints(nil),
		},
	}
	computeClass := testBuildGKEComputeClass([]string{"np-a", "np-b"})
	name := defaultGKEHeterogeneousResourceName("123", component, "compute-class-assign")
	topologySpreadName := defaultGKEHeterogeneousResourceName("123", component, "topology-spread-assign")
	existing := requireBuildGatekeeperAssign(t, name, "123", component, computeClass.GetName(), &gkeHeterogeneousAssignSettings{
		namespace: buildNamespace("123"),
		nodeSelector: map[string]string{
			consts.LabelKeyNodeGroupType: consts.LabelValNodeGroupTypeDataPlane,
			consts.K8sVersionLabelKey:    "1.31",
		},
		matchLabels: testGatekeeperAssignMatchLabels(t, component),
	})
	unstructured.RemoveNestedField(existing.Object, "spec", "match", "labelSelector", "matchExpressions")
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(refPod, computeClass, existing).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	ctx := context.Background()
	_, err := svc.CreateGKEHeterogeneousGatekeeperAssign(ctx, &apiv1.CreateGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId:        "123",
		ComponentType:    apicommon.ComponentType_TIKV,
		ComputeClassName: computeClass.GetName(),
	})
	require.Error(t, err)
	require.Equal(t, codes.FailedPrecondition, status.Code(err))
	require.Contains(t, err.Error(), "different spec")

	topologySpread := &unstructured.Unstructured{}
	topologySpread.SetGroupVersionKind(gatekeeperAssignGVK)
	require.True(t, apierrors.IsNotFound(cli.Get(ctx, client.ObjectKey{Name: topologySpreadName}, topologySpread)))

	gotPod := &corev1.Pod{}
	require.NoError(t, cli.Get(ctx, client.ObjectKey{Namespace: refPod.Namespace, Name: refPod.Name}, gotPod))
	require.Empty(t, gotPod.GetLabels()[gkeHeterogeneousGatekeeperExemptLabelKey])
}

func TestAgentService_CreateGKEHeterogeneousGatekeeperAssignRollsBackNewTopologySpreadCompanionWhenPrimaryCreateFails(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)
	component := &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiKV}

	refPod := &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "db-tikv-0",
			Namespace: buildNamespace("123"),
			Labels: map[string]string{
				consts.ComponentLabelKey: string(cloudv1.ComponentTiKV),
				consts.InstanceLabelKey:  consts.ClusterPrefix,
			},
		},
		Spec: corev1.PodSpec{
			NodeSelector: map[string]string{
				consts.LabelKeyNodeGroupType: consts.LabelValNodeGroupTypeDataPlane,
				consts.K8sVersionLabelKey:    "1.31",
			},
			TopologySpreadConstraints: testPodTopologySpreadConstraints(nil),
		},
	}
	computeClass := testBuildGKEComputeClass([]string{"np-a", "np-b"})
	name := defaultGKEHeterogeneousResourceName("123", component, "compute-class-assign")
	topologySpreadName := defaultGKEHeterogeneousResourceName("123", component, "topology-spread-assign")
	base := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(refPod, computeClass).Build()
	cli := &createErrorForNameClient{
		Client:   base,
		failName: name,
		err:      apierrors.NewInternalError(errors.New("injected primary create failure")),
	}
	svc := newScopedGKEHeterogeneousTestService(cli)

	ctx := context.Background()
	_, err := svc.CreateGKEHeterogeneousGatekeeperAssign(ctx, &apiv1.CreateGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId:        "123",
		ComponentType:    apicommon.ComponentType_TIKV,
		ComputeClassName: computeClass.GetName(),
	})
	require.Error(t, err)
	require.Equal(t, codes.Internal, status.Code(err))
	require.Contains(t, err.Error(), "injected primary create failure")

	topologySpread := &unstructured.Unstructured{}
	topologySpread.SetGroupVersionKind(gatekeeperAssignGVK)
	require.True(t, apierrors.IsNotFound(cli.Get(ctx, client.ObjectKey{Name: topologySpreadName}, topologySpread)))
	primary := &unstructured.Unstructured{}
	primary.SetGroupVersionKind(gatekeeperAssignGVK)
	require.True(t, apierrors.IsNotFound(cli.Get(ctx, client.ObjectKey{Name: name}, primary)))
}

func TestAgentService_CreateGKEHeterogeneousGatekeeperAssignKeepsPreexistingTopologySpreadCompanionWhenPrimaryCreateFails(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)
	component := &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiKV}

	refPod := &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "db-tikv-0",
			Namespace: buildNamespace("123"),
			Labels: map[string]string{
				consts.ComponentLabelKey: string(cloudv1.ComponentTiKV),
				consts.InstanceLabelKey:  consts.ClusterPrefix,
			},
		},
		Spec: corev1.PodSpec{
			NodeSelector: map[string]string{
				consts.LabelKeyNodeGroupType: consts.LabelValNodeGroupTypeDataPlane,
				consts.K8sVersionLabelKey:    "1.31",
			},
			TopologySpreadConstraints: testPodTopologySpreadConstraints(nil),
		},
	}
	computeClass := testBuildGKEComputeClass([]string{"np-a", "np-b"})
	name := defaultGKEHeterogeneousResourceName("123", component, "compute-class-assign")
	topologySpreadName := defaultGKEHeterogeneousResourceName("123", component, "topology-spread-assign")
	settings := &gkeHeterogeneousAssignSettings{
		namespace:   buildNamespace("123"),
		matchLabels: testGatekeeperAssignMatchLabels(t, component),
	}
	topologySpreadAssign, err := buildGatekeeperTopologySpreadAssign(topologySpreadName, "123", component, settings, gatekeeperAssignExemptToken(name))
	require.NoError(t, err)
	base := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(refPod, computeClass, topologySpreadAssign).Build()
	cli := &createErrorForNameClient{
		Client:   base,
		failName: name,
		err:      apierrors.NewInternalError(errors.New("injected primary create failure")),
	}
	svc := newScopedGKEHeterogeneousTestService(cli)

	ctx := context.Background()
	_, err = svc.CreateGKEHeterogeneousGatekeeperAssign(ctx, &apiv1.CreateGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId:        "123",
		ComponentType:    apicommon.ComponentType_TIKV,
		ComputeClassName: computeClass.GetName(),
	})
	require.Error(t, err)
	require.Equal(t, codes.Internal, status.Code(err))
	require.Contains(t, err.Error(), "injected primary create failure")

	gotTopologySpread := &unstructured.Unstructured{}
	gotTopologySpread.SetGroupVersionKind(gatekeeperAssignGVK)
	require.NoError(t, cli.Get(ctx, client.ObjectKey{Name: topologySpreadName}, gotTopologySpread))
	primary := &unstructured.Unstructured{}
	primary.SetGroupVersionKind(gatekeeperAssignGVK)
	require.True(t, apierrors.IsNotFound(cli.Get(ctx, client.ObjectKey{Name: name}, primary)))
}

func TestAgentService_DeleteGKEHeterogeneousGatekeeperAssignKeepsNodeSelectorWhenCompanionDeleteFails(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)
	component := &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiKV}
	settings := &gkeHeterogeneousAssignSettings{
		namespace:   buildNamespace("123"),
		matchLabels: testGatekeeperAssignMatchLabels(t, component),
	}
	name := defaultGKEHeterogeneousResourceName("123", component, "compute-class-assign")
	topologySpreadName := defaultGKEHeterogeneousResourceName("123", component, "topology-spread-assign")
	assign := requireBuildGatekeeperAssign(t, name, "123", component, "tikv-priority", settings)
	topologySpreadAssign, err := buildGatekeeperTopologySpreadAssign(topologySpreadName, "123", component, settings, gatekeeperAssignExemptToken(name))
	require.NoError(t, err)
	base := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(assign, topologySpreadAssign).Build()
	cli := &deleteErrorClient{
		Client:   base,
		failName: topologySpreadName,
		err:      apierrors.NewInternalError(errors.New("injected delete failure")),
	}
	svc := newScopedGKEHeterogeneousTestService(cli)

	ctx := context.Background()
	_, err = svc.DeleteGKEHeterogeneousGatekeeperAssign(ctx, &apiv1.DeleteGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId: "123",
		Name:      name,
	})
	require.Error(t, err)
	require.Equal(t, codes.Internal, status.Code(err))
	require.Contains(t, err.Error(), "delete topology spread gatekeeper assign")

	got := &unstructured.Unstructured{}
	got.SetGroupVersionKind(gatekeeperAssignGVK)
	require.NoError(t, cli.Get(ctx, client.ObjectKey{Name: name}, got))
	gotTopologySpread := &unstructured.Unstructured{}
	gotTopologySpread.SetGroupVersionKind(gatekeeperAssignGVK)
	require.NoError(t, cli.Get(ctx, client.ObjectKey{Name: topologySpreadName}, gotTopologySpread))

	cli.err = nil
	_, err = svc.DeleteGKEHeterogeneousGatekeeperAssign(ctx, &apiv1.DeleteGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId: "123",
		Name:      name,
	})
	require.NoError(t, err)
	deleted := &unstructured.Unstructured{}
	deleted.SetGroupVersionKind(gatekeeperAssignGVK)
	require.True(t, apierrors.IsNotFound(cli.Get(ctx, client.ObjectKey{Name: name}, deleted)))
	deletedTopologySpread := &unstructured.Unstructured{}
	deletedTopologySpread.SetGroupVersionKind(gatekeeperAssignGVK)
	require.True(t, apierrors.IsNotFound(cli.Get(ctx, client.ObjectKey{Name: topologySpreadName}, deletedTopologySpread)))
}

func TestAgentService_CreateGKEHeterogeneousGatekeeperAssignCreatesTopologySpreadCompanionWhenLivePodAlreadyIgnoresNodeAffinity(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)
	component := &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiKV}

	refPod := &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "db-tikv-0",
			Namespace: buildNamespace("123"),
			Labels: map[string]string{
				consts.ComponentLabelKey: string(cloudv1.ComponentTiKV),
				consts.InstanceLabelKey:  consts.ClusterPrefix,
			},
		},
		Spec: corev1.PodSpec{
			NodeSelector: map[string]string{
				consts.LabelKeyNodeGroupType: consts.LabelValNodeGroupTypeDataPlane,
				consts.K8sVersionLabelKey:    "1.31",
			},
			TopologySpreadConstraints: testPodTopologySpreadConstraints(nodeInclusionPolicyPtr(corev1.NodeInclusionPolicyIgnore)),
		},
	}
	computeClass := testBuildGKEComputeClass([]string{"np-a", "np-b"})
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(refPod, computeClass).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	resp, err := svc.CreateGKEHeterogeneousGatekeeperAssign(context.Background(), &apiv1.CreateGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId:        "123",
		ComponentType:    apicommon.ComponentType_TIKV,
		ComputeClassName: computeClass.GetName(),
	})
	require.NoError(t, err)
	require.NotEmpty(t, resp.Name)

	topologySpread := &unstructured.Unstructured{}
	topologySpread.SetGroupVersionKind(gatekeeperAssignGVK)
	err = cli.Get(context.Background(), client.ObjectKey{Name: defaultGKEHeterogeneousResourceName("123", component, "topology-spread-assign")}, topologySpread)
	require.NoError(t, err)
}

func TestAgentService_CreateGKEHeterogeneousGatekeeperAssignCreatesTopologySpreadCompanionForMixedEmptyAndIgnoreNodeAffinity(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)
	component := &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiKV}

	podWithoutPolicy := &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "db-tikv-0",
			Namespace: buildNamespace("123"),
			Labels: map[string]string{
				consts.ComponentLabelKey: string(cloudv1.ComponentTiKV),
				consts.InstanceLabelKey:  consts.ClusterPrefix,
			},
		},
		Spec: corev1.PodSpec{
			NodeSelector: map[string]string{
				consts.LabelKeyNodeGroupType: consts.LabelValNodeGroupTypeDataPlane,
				consts.K8sVersionLabelKey:    "1.31",
			},
			TopologySpreadConstraints: testPodTopologySpreadConstraints(nil),
		},
	}
	podWithIgnore := podWithoutPolicy.DeepCopy()
	podWithIgnore.Name = "db-tikv-1"
	podWithIgnore.Spec.TopologySpreadConstraints = testPodTopologySpreadConstraints(nodeInclusionPolicyPtr(corev1.NodeInclusionPolicyIgnore))
	computeClass := testBuildGKEComputeClass([]string{"np-a", "np-b"})
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(podWithoutPolicy, podWithIgnore, computeClass).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	resp, err := svc.CreateGKEHeterogeneousGatekeeperAssign(context.Background(), &apiv1.CreateGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId:        "123",
		ComponentType:    apicommon.ComponentType_TIKV,
		ComputeClassName: computeClass.GetName(),
	})
	require.NoError(t, err)
	require.NotEmpty(t, resp.Name)

	topologySpread := &unstructured.Unstructured{}
	topologySpread.SetGroupVersionKind(gatekeeperAssignGVK)
	err = cli.Get(context.Background(), client.ObjectKey{Name: defaultGKEHeterogeneousResourceName("123", component, "topology-spread-assign")}, topologySpread)
	require.NoError(t, err)
}

func TestAgentService_CreateGKEHeterogeneousGatekeeperAssignCreatesTopologySpreadCompanionWhenLivePodHasHonorNodeAffinityPolicy(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)
	component := &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiKV}

	refPod := &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "db-tikv-0",
			Namespace: buildNamespace("123"),
			Labels: map[string]string{
				consts.ComponentLabelKey: string(cloudv1.ComponentTiKV),
				consts.InstanceLabelKey:  consts.ClusterPrefix,
			},
		},
		Spec: corev1.PodSpec{
			NodeSelector:              map[string]string{consts.K8sVersionLabelKey: "1.31"},
			TopologySpreadConstraints: testPodTopologySpreadConstraints(nodeInclusionPolicyPtr(corev1.NodeInclusionPolicyHonor)),
		},
	}
	computeClass := testBuildGKEComputeClass([]string{"np-a", "np-b"})
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(refPod, computeClass).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	resp, err := svc.CreateGKEHeterogeneousGatekeeperAssign(context.Background(), &apiv1.CreateGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId:        "123",
		ComponentType:    apicommon.ComponentType_TIKV,
		ComputeClassName: computeClass.GetName(),
	})
	require.NoError(t, err)
	require.NotEmpty(t, resp.Name)

	topologySpread := &unstructured.Unstructured{}
	topologySpread.SetGroupVersionKind(gatekeeperAssignGVK)
	err = cli.Get(context.Background(), client.ObjectKey{Name: defaultGKEHeterogeneousResourceName("123", component, "topology-spread-assign")}, topologySpread)
	require.NoError(t, err)

	value, found, err := unstructured.NestedString(topologySpread.Object, "spec", "parameters", "assign", "value")
	require.NoError(t, err)
	require.True(t, found)
	require.Equal(t, string(corev1.NodeInclusionPolicyIgnore), value)
}

func TestAgentService_CreateGKEHeterogeneousGatekeeperAssignCreatesTopologySpreadCompanionWhenLivePodHasMultipleZoneConstraints(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)
	component := &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiKV}

	refPod := &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "db-tikv-0",
			Namespace: buildNamespace("123"),
			Labels: map[string]string{
				consts.ComponentLabelKey: string(cloudv1.ComponentTiKV),
				consts.InstanceLabelKey:  consts.ClusterPrefix,
			},
		},
		Spec: corev1.PodSpec{
			NodeSelector: map[string]string{consts.K8sVersionLabelKey: "1.31"},
			TopologySpreadConstraints: []corev1.TopologySpreadConstraint{
				{
					TopologyKey: consts.ZoneLabelKey,
					MaxSkew:     1,
				},
				{
					TopologyKey: consts.ZoneLabelKey,
					MaxSkew:     2,
				},
			},
		},
	}
	computeClass := testBuildGKEComputeClass([]string{"np-a", "np-b"})
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(refPod, computeClass).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	resp, err := svc.CreateGKEHeterogeneousGatekeeperAssign(context.Background(), &apiv1.CreateGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId:        "123",
		ComponentType:    apicommon.ComponentType_TIKV,
		ComputeClassName: computeClass.GetName(),
	})
	require.NoError(t, err)
	require.NotEmpty(t, resp.Name)

	assign := &unstructured.Unstructured{}
	assign.SetGroupVersionKind(gatekeeperAssignGVK)
	assignName := defaultGKEHeterogeneousResourceName("123", component, "compute-class-assign")
	require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Name: assignName}, assign))

	topologySpreadAssign := &unstructured.Unstructured{}
	topologySpreadAssign.SetGroupVersionKind(gatekeeperAssignGVK)
	topologySpreadName := defaultGKEHeterogeneousResourceName("123", component, "topology-spread-assign")
	require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Name: topologySpreadName}, topologySpreadAssign))

	value, found, err := unstructured.NestedString(topologySpreadAssign.Object, "spec", "parameters", "assign", "value")
	require.NoError(t, err)
	require.True(t, found)
	require.Equal(t, string(corev1.NodeInclusionPolicyIgnore), value)
}

func TestAgentService_CreateGKEHeterogeneousGatekeeperAssignCreatesTopologySpreadCompanionWithoutLivePodZoneConstraint(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)
	component := &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiKV}

	pod0 := &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "db-tikv-0",
			Namespace: buildNamespace("123"),
			Labels: map[string]string{
				consts.ComponentLabelKey: string(cloudv1.ComponentTiKV),
				consts.InstanceLabelKey:  consts.ClusterPrefix,
			},
		},
		Spec: corev1.PodSpec{
			NodeSelector: map[string]string{consts.K8sVersionLabelKey: "1.31"},
		},
	}
	computeClass := testBuildGKEComputeClass([]string{"np-a", "np-b"})
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(pod0, computeClass).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	resp, err := svc.CreateGKEHeterogeneousGatekeeperAssign(context.Background(), &apiv1.CreateGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId:        "123",
		ComponentType:    apicommon.ComponentType_TIKV,
		ComputeClassName: computeClass.GetName(),
	})
	require.NoError(t, err)
	require.NotEmpty(t, resp.Name)

	ctx := context.Background()
	assign, err := getGKEHeterogeneousResource(ctx, cli, gatekeeperAssignGVK, resp.Name, gatekeeperAssignLabel)
	require.NoError(t, err)
	token := requireGatekeeperAssignExemption(t, assign)

	topologySpreadName := defaultGKEHeterogeneousResourceName("123", component, "topology-spread-assign")
	topologySpread, err := getGKEHeterogeneousResource(ctx, cli, gatekeeperAssignGVK, topologySpreadName, gatekeeperAssignLabel)
	require.NoError(t, err)
	require.Equal(t, gatekeeperAssignPurposeTopologySpread, topologySpread.GetLabels()[gkeHeterogeneousGatekeeperAssignPurposeKey])
	require.Empty(t, topologySpread.GetLabels()[gkeHeterogeneousComputeClassLabelKey])
	requireGatekeeperAssignExemptionToken(t, topologySpread, token)

	location, found, err := unstructured.NestedString(topologySpread.Object, "spec", "location")
	require.NoError(t, err)
	require.True(t, found)
	require.Equal(t, gatekeeperTopologySpreadNodeAffinityLocation, location)
	value, found, err := unstructured.NestedString(topologySpread.Object, "spec", "parameters", "assign", "value")
	require.NoError(t, err)
	require.True(t, found)
	require.Equal(t, string(corev1.NodeInclusionPolicyIgnore), value)
	pathTests, found, err := unstructured.NestedSlice(topologySpread.Object, "spec", "parameters", "pathTests")
	require.NoError(t, err)
	require.True(t, found)
	require.Equal(t, []interface{}{
		map[string]interface{}{
			"subPath":   gatekeeperTopologySpreadConstraintLocation,
			"condition": "MustExist",
		},
	}, pathTests)
}

func TestAgentService_CreateGKEHeterogeneousGatekeeperAssignRejectsTiKVGroupKind(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)
	cli := fake.NewClientBuilder().WithScheme(scheme).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	_, err := svc.CreateGKEHeterogeneousGatekeeperAssign(context.Background(), &apiv1.CreateGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId:        "123",
		ComponentType:    apicommon.ComponentType_TIKV,
		TidbGroupId:      "ignored",
		TidbGroupKind:    apiv1.TiDBGroupKind_TIDB_GROUP_KIND_EXTRA,
		ComputeClassName: "unused",
	})
	require.Error(t, err)
	require.Equal(t, codes.InvalidArgument, status.Code(err))
	require.Contains(t, err.Error(), "tidb_group_kind is only supported for TiDB heterogeneous deploy")
}

func TestAgentService_CreateGKEHeterogeneousGatekeeperAssignUsesSharedComputeClass(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)

	refPod := &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "db-tikv-0",
			Namespace: buildNamespace("456"),
			Labels: map[string]string{
				consts.ComponentLabelKey: string(cloudv1.ComponentTiKV),
				consts.InstanceLabelKey:  consts.ClusterPrefix,
			},
		},
		Spec: corev1.PodSpec{NodeSelector: map[string]string{
			consts.LabelKeyNodeGroupType: consts.LabelValNodeGroupTypeDataPlane,
			consts.K8sVersionLabelKey:    "1.31",
		}},
	}
	computeClass := testBuildGKEComputeClass([]string{"np-a"})
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(refPod, computeClass).Build()
	svc := NewAgentService(cli, WithClusterNamespace(buildNamespace("456")), WithProvider(consts.ProviderTypeGKE))

	resp, err := svc.CreateGKEHeterogeneousGatekeeperAssign(context.Background(), &apiv1.CreateGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId:        "456",
		ComponentType:    apicommon.ComponentType_TIKV,
		ComputeClassName: computeClass.GetName(),
	})
	require.NoError(t, err)
	require.Equal(t, "456", resp.ClusterId)
	require.Equal(t, computeClass.GetName(), resp.ComputeClassName)
}

func TestAgentService_CreateGKEHeterogeneousGatekeeperAssignRejectsExistingTiKVAssignWithDifferentSpec(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)

	component := &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiKV}
	refPod := &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "db-tikv-0",
			Namespace: buildNamespace("123"),
			Labels: map[string]string{
				consts.ComponentLabelKey: string(cloudv1.ComponentTiKV),
				consts.InstanceLabelKey:  consts.ClusterPrefix,
			},
		},
		Spec: corev1.PodSpec{NodeSelector: map[string]string{
			consts.LabelKeyNodeGroupType: consts.LabelValNodeGroupTypeDataPlane,
			consts.K8sVersionLabelKey:    "1.31",
		}},
	}
	computeClass := testBuildGKEComputeClass([]string{"np-a"})
	computeClassName := computeClass.GetName()
	existing := requireBuildGatekeeperAssign(t, defaultGKEHeterogeneousResourceName("123", component, "compute-class-assign"), "123", component, computeClassName, &gkeHeterogeneousAssignSettings{
		namespace: buildNamespace("123"),
		nodeSelector: map[string]string{
			consts.LabelKeyNodeGroupType: consts.LabelValNodeGroupTypeDataPlane,
			consts.K8sVersionLabelKey:    "1.31",
		},
		matchLabels: testGatekeeperAssignMatchLabels(t, component),
	})
	unstructured.RemoveNestedField(existing.Object, "spec", "match", "labelSelector", "matchExpressions")

	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(refPod, computeClass, existing).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	_, err := svc.CreateGKEHeterogeneousGatekeeperAssign(context.Background(), &apiv1.CreateGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId:        "123",
		ComponentType:    apicommon.ComponentType_TIKV,
		ComputeClassName: computeClassName,
	})
	require.Error(t, err)
	require.Equal(t, codes.FailedPrecondition, status.Code(err))
	require.Contains(t, err.Error(), "different spec")

	updated := &unstructured.Unstructured{}
	updated.SetGroupVersionKind(gatekeeperAssignGVK)
	require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Name: defaultGKEHeterogeneousResourceName("123", component, "compute-class-assign")}, updated))
	requireGatekeeperAssignNoMatchExpression(t, updated, gkeHeterogeneousGatekeeperExemptLabelKey, "NotIn")
}

func TestAgentService_CreateGKEHeterogeneousGatekeeperAssignRejectsExistingTiKVMismatchedComputeClass(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)

	component := &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiKV}
	refPod := &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "db-tikv-0",
			Namespace: buildNamespace("123"),
			Labels: map[string]string{
				consts.ComponentLabelKey: string(cloudv1.ComponentTiKV),
				consts.InstanceLabelKey:  consts.ClusterPrefix,
			},
		},
		Spec: corev1.PodSpec{NodeSelector: map[string]string{
			consts.LabelKeyNodeGroupType: consts.LabelValNodeGroupTypeDataPlane,
			consts.K8sVersionLabelKey:    "1.31",
		}},
	}
	oldComputeClass := testBuildGKEComputeClass([]string{"np-a"})
	newComputeClass := testBuildGKEComputeClass([]string{"np-b"})
	oldComputeClassName := oldComputeClass.GetName()
	newComputeClassName := newComputeClass.GetName()
	existing := requireBuildGatekeeperAssign(t, defaultGKEHeterogeneousResourceName("123", component, "compute-class-assign"), "123", component, oldComputeClassName, &gkeHeterogeneousAssignSettings{
		namespace: buildNamespace("123"),
		nodeSelector: map[string]string{
			consts.LabelKeyNodeGroupType: consts.LabelValNodeGroupTypeDataPlane,
			consts.K8sVersionLabelKey:    "1.31",
		},
		matchLabels: testGatekeeperAssignMatchLabels(t, component),
	})
	unstructured.RemoveNestedField(existing.Object, "spec", "match", "labelSelector", "matchExpressions")
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(refPod, oldComputeClass, newComputeClass, existing).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	_, err := svc.CreateGKEHeterogeneousGatekeeperAssign(context.Background(), &apiv1.CreateGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId:        "123",
		ComponentType:    apicommon.ComponentType_TIKV,
		ComputeClassName: newComputeClassName,
	})
	require.Error(t, err)
	require.Equal(t, codes.FailedPrecondition, status.Code(err))
	require.Contains(t, err.Error(), "different compute class label")

	updated := &unstructured.Unstructured{}
	updated.SetGroupVersionKind(gatekeeperAssignGVK)
	require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Name: defaultGKEHeterogeneousResourceName("123", component, "compute-class-assign")}, updated))
	requireGatekeeperAssignNoMatchExpression(t, updated, gkeHeterogeneousGatekeeperExemptLabelKey, "NotIn")
}

func TestBuildGatekeeperAssignSetsLivePodExemption(t *testing.T) {
	assign := requireBuildGatekeeperAssign(t, "tikv-assign", "123", &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiKV}, "tikv-priority", &gkeHeterogeneousAssignSettings{
		namespace:   buildNamespace("123"),
		matchLabels: testGatekeeperAssignMatchLabels(t, &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiKV}),
	})

	requireGatekeeperAssignExemption(t, assign)
}

func TestBuildGatekeeperAssignRejectsMissingMatchLabels(t *testing.T) {
	_, err := buildGatekeeperAssign("tikv-assign", "123", &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiKV}, "tikv-priority", &gkeHeterogeneousAssignSettings{
		namespace: buildNamespace("123"),
	})

	require.Error(t, err)
	require.Equal(t, codes.Internal, status.Code(err))
	require.Contains(t, err.Error(), "match labels are required")
}

func TestAgentService_GetGKEHeterogeneousGatekeeperAssignRejectsMislabeledNamespaceScope(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)

	assign := requireBuildGatekeeperAssign(t, "tikv-assign", "123", &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiKV}, "tikv-priority", &gkeHeterogeneousAssignSettings{
		namespace:   buildNamespace("456"),
		matchLabels: testGatekeeperAssignMatchLabels(t, &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiKV}),
	})
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(assign).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	_, err := svc.GetGKEHeterogeneousGatekeeperAssign(context.Background(), &apiv1.GetGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId:     "123",
		ComponentType: apicommon.ComponentType_TIKV,
	})
	require.Error(t, err)
	require.Equal(t, codes.PermissionDenied, status.Code(err))
	require.Contains(t, err.Error(), "does not target cluster namespace "+buildNamespace("123"))
}

func TestAgentService_GetGKEHeterogeneousGatekeeperAssignReturnsNotFoundWhenMissing(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)
	cli := fake.NewClientBuilder().WithScheme(scheme).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	_, err := svc.GetGKEHeterogeneousGatekeeperAssign(context.Background(), &apiv1.GetGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId:     "123",
		ComponentType: apicommon.ComponentType_TIKV,
	})

	require.Error(t, err)
	require.Equal(t, codes.NotFound, status.Code(err))
	require.Contains(t, err.Error(), "gatekeeper assign for component tikv not found")
}

func TestAgentService_GetGKEHeterogeneousGatekeeperAssignReturnsNotFoundForMissingCRD(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)
	base := fake.NewClientBuilder().WithScheme(scheme).Build()
	cli := &noMatchClient{Client: base}
	svc := newScopedGKEHeterogeneousTestService(cli)

	_, err := svc.GetGKEHeterogeneousGatekeeperAssign(context.Background(), &apiv1.GetGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId:     "123",
		ComponentType: apicommon.ComponentType_TIKV,
	})

	require.Error(t, err)
	require.Equal(t, codes.NotFound, status.Code(err))
	require.Contains(t, err.Error(), "gatekeeper assign API is not installed")
}

func TestAgentService_GetGKEHeterogeneousGatekeeperAssignRejectsMalformedAssignValue(t *testing.T) {
	tests := []struct {
		name       string
		mutate     func(t *testing.T, assign *unstructured.Unstructured)
		wantErrMsg string
	}{
		{
			name: "assign value is not map",
			mutate: func(t *testing.T, assign *unstructured.Unstructured) {
				t.Helper()
				require.NoError(t, unstructured.SetNestedField(assign.Object, "not-a-map", "spec", "parameters", "assign", "value"))
			},
			wantErrMsg: "compute class assign value",
		},
		{
			name: "compute class value is not string",
			mutate: func(t *testing.T, assign *unstructured.Unstructured) {
				t.Helper()
				require.NoError(t, unstructured.SetNestedField(assign.Object, int64(123), "spec", "parameters", "assign", "value", consts.GKEComputeClassLabelKey))
			},
			wantErrMsg: "compute class assign value",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			scheme := newGKEHeterogeneousTestScheme(t)
			assign := requireBuildGatekeeperAssign(t, "tikv-assign", "123", &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiKV}, "tikv-priority", &gkeHeterogeneousAssignSettings{
				namespace: buildNamespace("123"),
				nodeSelector: map[string]string{
					consts.LabelKeyNodeGroupType: consts.LabelValNodeGroupTypeDataPlane,
				},
				matchLabels: testGatekeeperAssignMatchLabels(t, &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiKV}),
			})
			tt.mutate(t, assign)
			cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(assign).Build()
			svc := newScopedGKEHeterogeneousTestService(cli)

			_, err := svc.GetGKEHeterogeneousGatekeeperAssign(context.Background(), &apiv1.GetGKEHeterogeneousGatekeeperAssignRequest{
				ClusterId:     "123",
				ComponentType: apicommon.ComponentType_TIKV,
			})
			require.Error(t, err)
			require.Equal(t, codes.FailedPrecondition, status.Code(err))
			require.Contains(t, err.Error(), tt.wantErrMsg)
		})
	}
}

func TestAgentService_UpdateGKEHeterogeneousGatekeeperAssignRejectsMislabeledNamespaceScope(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)

	assign := requireBuildGatekeeperAssign(t, "tikv-assign", "123", &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiKV}, "tikv-priority-old", &gkeHeterogeneousAssignSettings{
		namespace:   buildNamespace("456"),
		matchLabels: testGatekeeperAssignMatchLabels(t, &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiKV}),
	})
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(assign).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	_, err := svc.UpdateGKEHeterogeneousGatekeeperAssign(context.Background(), &apiv1.UpdateGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId:        "123",
		Name:             "tikv-assign",
		ComputeClassName: "tikv-priority-new",
	})
	require.Error(t, err)
	require.Equal(t, codes.PermissionDenied, status.Code(err))
	require.Contains(t, err.Error(), "does not target cluster namespace "+buildNamespace("123"))

	got := &unstructured.Unstructured{}
	got.SetGroupVersionKind(gatekeeperAssignGVK)
	require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Name: "tikv-assign"}, got))
	computeClassName, found, err := unstructured.NestedString(got.Object, "spec", "parameters", "assign", "value", consts.GKEComputeClassLabelKey)
	require.NoError(t, err)
	require.True(t, found)
	require.Equal(t, "tikv-priority-old", computeClassName)
}

func TestAgentService_DeleteGKEHeterogeneousGatekeeperAssignRejectsMislabeledNamespaceScope(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)

	assign := requireBuildGatekeeperAssign(t, "tikv-assign", "123", &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiKV}, "tikv-priority", &gkeHeterogeneousAssignSettings{
		namespace:   buildNamespace("456"),
		matchLabels: testGatekeeperAssignMatchLabels(t, &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiKV}),
	})
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(assign).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	_, err := svc.DeleteGKEHeterogeneousGatekeeperAssign(context.Background(), &apiv1.DeleteGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId: "123",
		Name:      "tikv-assign",
	})
	require.Error(t, err)
	require.Equal(t, codes.PermissionDenied, status.Code(err))
	require.Contains(t, err.Error(), "does not target cluster namespace "+buildNamespace("123"))

	got := &unstructured.Unstructured{}
	got.SetGroupVersionKind(gatekeeperAssignGVK)
	require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Name: "tikv-assign"}, got))
}

func TestAgentService_UpdateGKEHeterogeneousGatekeeperAssignRejectsInvalidComponentOwnership(t *testing.T) {
	tests := []struct {
		name       string
		mutate     func(labels map[string]string)
		wantErrMsg string
	}{
		{
			name: "missing component label",
			mutate: func(labels map[string]string) {
				delete(labels, gkeHeterogeneousComponentLabelKey)
			},
			wantErrMsg: "missing component ownership",
		},
		{
			name: "unsupported component label",
			mutate: func(labels map[string]string) {
				labels[gkeHeterogeneousComponentLabelKey] = "pd"
			},
			wantErrMsg: "unsupported component ownership",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			scheme := newGKEHeterogeneousTestScheme(t)

			newComputeClass := testBuildGKEComputeClass([]string{"np-a"})
			newComputeClassName := newComputeClass.GetName()
			assign := requireBuildGatekeeperAssign(t, "tikv-assign", "123", &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiKV}, "tikv-priority-old", &gkeHeterogeneousAssignSettings{
				namespace:   buildNamespace("123"),
				matchLabels: testGatekeeperAssignMatchLabels(t, &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiKV}),
			})
			labels := assign.GetLabels()
			tt.mutate(labels)
			assign.SetLabels(labels)
			cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(newComputeClass, assign).Build()
			svc := newScopedGKEHeterogeneousTestService(cli)

			_, err := svc.UpdateGKEHeterogeneousGatekeeperAssign(context.Background(), &apiv1.UpdateGKEHeterogeneousGatekeeperAssignRequest{
				ClusterId:        "123",
				Name:             "tikv-assign",
				ComputeClassName: newComputeClassName,
			})
			require.Error(t, err)
			require.Equal(t, codes.FailedPrecondition, status.Code(err))
			require.Contains(t, err.Error(), tt.wantErrMsg)

			got := &unstructured.Unstructured{}
			got.SetGroupVersionKind(gatekeeperAssignGVK)
			require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Name: "tikv-assign"}, got))
			computeClassName, found, err := unstructured.NestedString(got.Object, "spec", "parameters", "assign", "value", consts.GKEComputeClassLabelKey)
			require.NoError(t, err)
			require.True(t, found)
			require.Equal(t, "tikv-priority-old", computeClassName)
			require.Equal(t, "tikv-priority-old", got.GetLabels()[gkeHeterogeneousComputeClassLabelKey])
		})
	}
}

func TestAgentService_DeleteGKEHeterogeneousGatekeeperAssignRejectsInvalidComponentOwnership(t *testing.T) {
	tests := []struct {
		name       string
		mutate     func(labels map[string]string)
		wantErrMsg string
	}{
		{
			name: "missing component label",
			mutate: func(labels map[string]string) {
				delete(labels, gkeHeterogeneousComponentLabelKey)
			},
			wantErrMsg: "missing component ownership",
		},
		{
			name: "unsupported component label",
			mutate: func(labels map[string]string) {
				labels[gkeHeterogeneousComponentLabelKey] = "pd"
			},
			wantErrMsg: "unsupported component ownership",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			scheme := newGKEHeterogeneousTestScheme(t)

			assign := requireBuildGatekeeperAssign(t, "tikv-assign", "123", &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiKV}, "tikv-priority", &gkeHeterogeneousAssignSettings{
				namespace:   buildNamespace("123"),
				matchLabels: testGatekeeperAssignMatchLabels(t, &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiKV}),
			})
			labels := assign.GetLabels()
			tt.mutate(labels)
			assign.SetLabels(labels)
			cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(assign).Build()
			svc := newScopedGKEHeterogeneousTestService(cli)

			_, err := svc.DeleteGKEHeterogeneousGatekeeperAssign(context.Background(), &apiv1.DeleteGKEHeterogeneousGatekeeperAssignRequest{
				ClusterId: "123",
				Name:      "tikv-assign",
			})
			require.Error(t, err)
			require.Equal(t, codes.FailedPrecondition, status.Code(err))
			require.Contains(t, err.Error(), tt.wantErrMsg)

			got := &unstructured.Unstructured{}
			got.SetGroupVersionKind(gatekeeperAssignGVK)
			require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Name: "tikv-assign"}, got))
		})
	}
}

func TestAgentService_CreateGKEHeterogeneousGatekeeperAssignRejectsMislabeledExistingNamespaceScope(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)

	component := &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiKV}
	refPod := &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "db-tikv-0",
			Namespace: buildNamespace("123"),
			Labels: map[string]string{
				consts.ComponentLabelKey: string(cloudv1.ComponentTiKV),
				consts.InstanceLabelKey:  consts.ClusterPrefix,
			},
		},
		Spec: corev1.PodSpec{NodeSelector: map[string]string{
			consts.LabelKeyNodeGroupType: consts.LabelValNodeGroupTypeDataPlane,
			consts.K8sVersionLabelKey:    "1.31",
		}},
	}
	computeClass := testBuildGKEComputeClass([]string{"np-a"})
	computeClassName := computeClass.GetName()
	existing := requireBuildGatekeeperAssign(t, defaultGKEHeterogeneousResourceName("123", component, "compute-class-assign"), "123", component, computeClassName, &gkeHeterogeneousAssignSettings{
		namespace:   buildNamespace("456"),
		matchLabels: testGatekeeperAssignMatchLabels(t, component),
	})
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(refPod, computeClass, existing).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	_, err := svc.CreateGKEHeterogeneousGatekeeperAssign(context.Background(), &apiv1.CreateGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId:        "123",
		ComponentType:    apicommon.ComponentType_TIKV,
		ComputeClassName: computeClassName,
	})
	require.Error(t, err)
	require.Equal(t, codes.PermissionDenied, status.Code(err))
	require.Contains(t, err.Error(), "does not target cluster namespace "+buildNamespace("123"))
}

func TestAgentService_CreateGKEHeterogeneousGatekeeperAssignRejectsExistingMismatchedComputeClassReference(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)

	component := &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiKV}
	refPod := &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "db-tikv-0",
			Namespace: buildNamespace("123"),
			Labels: map[string]string{
				consts.ComponentLabelKey: string(cloudv1.ComponentTiKV),
				consts.InstanceLabelKey:  consts.ClusterPrefix,
			},
		},
		Spec: corev1.PodSpec{NodeSelector: map[string]string{
			consts.LabelKeyNodeGroupType: consts.LabelValNodeGroupTypeDataPlane,
			consts.K8sVersionLabelKey:    "1.31",
		}},
	}
	computeClass := testBuildGKEComputeClass([]string{"np-a"})
	computeClassName := computeClass.GetName()
	existing := requireBuildGatekeeperAssign(t, defaultGKEHeterogeneousResourceName("123", component, "compute-class-assign"), "123", component, computeClassName, &gkeHeterogeneousAssignSettings{
		namespace: buildNamespace("123"),
		nodeSelector: map[string]string{
			consts.LabelKeyNodeGroupType: consts.LabelValNodeGroupTypeDataPlane,
			consts.K8sVersionLabelKey:    "1.31",
		},
		matchLabels: testGatekeeperAssignMatchLabels(t, component),
	})
	labels := existing.GetLabels()
	labels[gkeHeterogeneousComputeClassLabelKey] = "other-priority"
	existing.SetLabels(labels)
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(refPod, computeClass, existing).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	_, err := svc.CreateGKEHeterogeneousGatekeeperAssign(context.Background(), &apiv1.CreateGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId:        "123",
		ComponentType:    apicommon.ComponentType_TIKV,
		ComputeClassName: computeClassName,
	})
	require.Error(t, err)
	require.Equal(t, codes.FailedPrecondition, status.Code(err))
	require.Contains(t, err.Error(), "different compute class label")
}

func TestAgentService_CreateGKEHeterogeneousGatekeeperAssignRejectsCreateConflictMismatchedComputeClassReference(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)

	component := &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiKV}
	refPod := &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "db-tikv-0",
			Namespace: buildNamespace("123"),
			Labels: map[string]string{
				consts.ComponentLabelKey: string(cloudv1.ComponentTiKV),
				consts.InstanceLabelKey:  consts.ClusterPrefix,
			},
		},
		Spec: corev1.PodSpec{NodeSelector: map[string]string{
			consts.LabelKeyNodeGroupType: consts.LabelValNodeGroupTypeDataPlane,
			consts.K8sVersionLabelKey:    "1.31",
		}},
	}
	computeClass := testBuildGKEComputeClass([]string{"np-a"})
	computeClassName := computeClass.GetName()
	name := defaultGKEHeterogeneousResourceName("123", component, "compute-class-assign")
	settings := &gkeHeterogeneousAssignSettings{
		namespace: buildNamespace("123"),
		nodeSelector: map[string]string{
			consts.LabelKeyNodeGroupType: consts.LabelValNodeGroupTypeDataPlane,
			consts.K8sVersionLabelKey:    "1.31",
		},
		matchLabels: testGatekeeperAssignMatchLabels(t, component),
	}
	conflict := requireBuildGatekeeperAssign(t, name, "123", component, computeClassName, settings)
	labels := conflict.GetLabels()
	labels[gkeHeterogeneousComputeClassLabelKey] = "other-priority"
	conflict.SetLabels(labels)
	topologySpreadName := defaultGKEHeterogeneousResourceName("123", component, "topology-spread-assign")
	topologySpreadAssign, err := buildGatekeeperTopologySpreadAssign(topologySpreadName, "123", component, settings, gatekeeperAssignExemptToken(name))
	require.NoError(t, err)
	base := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(refPod, computeClass, topologySpreadAssign).Build()
	cli := &createConflictClient{Client: base, conflictObject: conflict}
	svc := newScopedGKEHeterogeneousTestService(cli)

	_, err = svc.CreateGKEHeterogeneousGatekeeperAssign(context.Background(), &apiv1.CreateGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId:        "123",
		ComponentType:    apicommon.ComponentType_TIKV,
		ComputeClassName: computeClassName,
	})
	require.Error(t, err)
	require.Equal(t, codes.AlreadyExists, status.Code(err))
	require.Contains(t, err.Error(), "different compute class label")
}

func TestAgentService_UpdateGKEHeterogeneousGatekeeperAssignOnlyChangesComputeClass(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)

	oldComputeClass := testBuildGKEComputeClass([]string{"np-a"})
	newComputeClass := testBuildGKEComputeClass([]string{"np-b"})
	oldComputeClassName := oldComputeClass.GetName()
	newComputeClassName := newComputeClass.GetName()
	assign := requireBuildGatekeeperAssign(t, "tikv-assign", "123", &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiKV}, oldComputeClassName, &gkeHeterogeneousAssignSettings{
		namespace: buildNamespace("123"),
		nodeSelector: map[string]string{
			consts.LabelKeyNodeGroupType: consts.LabelValNodeGroupTypeDataPlane,
			consts.K8sVersionLabelKey:    "1.31",
			"tidbcloud.com/custom":       "keep",
		},
		matchLabels: testGatekeeperAssignMatchLabels(t, &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiKV}),
	})
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(oldComputeClass, newComputeClass, assign).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	resp, err := svc.UpdateGKEHeterogeneousGatekeeperAssign(context.Background(), &apiv1.UpdateGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId:        "123",
		Name:             "tikv-assign",
		ComputeClassName: newComputeClassName,
	})
	require.NoError(t, err)
	require.Equal(t, newComputeClassName, resp.ComputeClassName)
	require.Equal(t, buildNamespace("123"), resp.Namespace)
	require.Equal(t, map[string]string{
		consts.GKEComputeClassLabelKey: newComputeClassName,
		consts.LabelKeyNodeGroupType:   consts.LabelValNodeGroupTypeDataPlane,
		consts.K8sVersionLabelKey:      "1.31",
		"tidbcloud.com/custom":         "keep",
	}, resp.NodeSelector)
	require.Equal(t, map[string]string{
		consts.ComponentLabelKey: string(cloudv1.ComponentTiKV),
		consts.InstanceLabelKey:  consts.ClusterPrefix,
	}, resp.MatchLabels)

	updated := &unstructured.Unstructured{}
	updated.SetGroupVersionKind(gatekeeperAssignGVK)
	require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Name: "tikv-assign"}, updated))
	require.Equal(t, newComputeClassName, updated.GetLabels()[gkeHeterogeneousComputeClassLabelKey])
	requireGatekeeperAssignExemption(t, updated)
}

func TestAgentService_UpdateGKEHeterogeneousGatekeeperAssignDoesNotBackfillExemption(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)

	oldComputeClass := testBuildGKEComputeClass([]string{"np-a"})
	newComputeClass := testBuildGKEComputeClass([]string{"np-b"})
	oldComputeClassName := oldComputeClass.GetName()
	newComputeClassName := newComputeClass.GetName()
	assign := requireBuildGatekeeperAssign(t, "tikv-assign", "123", &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiKV}, oldComputeClassName, &gkeHeterogeneousAssignSettings{
		namespace: buildNamespace("123"),
		nodeSelector: map[string]string{
			consts.LabelKeyNodeGroupType: consts.LabelValNodeGroupTypeDataPlane,
			consts.K8sVersionLabelKey:    "1.31",
		},
		matchLabels: testGatekeeperAssignMatchLabels(t, &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiKV}),
	})
	unstructured.RemoveNestedField(assign.Object, "spec", "match", "labelSelector", "matchExpressions")
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(oldComputeClass, newComputeClass, assign).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	resp, err := svc.UpdateGKEHeterogeneousGatekeeperAssign(context.Background(), &apiv1.UpdateGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId:        "123",
		Name:             "tikv-assign",
		ComputeClassName: newComputeClassName,
	})
	require.NoError(t, err)
	require.Equal(t, newComputeClassName, resp.ComputeClassName)

	updated := &unstructured.Unstructured{}
	updated.SetGroupVersionKind(gatekeeperAssignGVK)
	require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Name: "tikv-assign"}, updated))
	require.Equal(t, newComputeClassName, updated.GetLabels()[gkeHeterogeneousComputeClassLabelKey])
	requireGatekeeperAssignNoMatchExpression(t, updated, gkeHeterogeneousGatekeeperExemptLabelKey, "NotIn")
}

func TestAgentService_CreateGKEHeterogeneousGatekeeperAssignAllowsDifferentInstanceTypeSelectors(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)

	podA := &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "db-tikv-0",
			Namespace: buildNamespace("123"),
			Labels: map[string]string{
				consts.ComponentLabelKey: string(cloudv1.ComponentTiKV),
				consts.InstanceLabelKey:  consts.ClusterPrefix,
			},
		},
		Spec: corev1.PodSpec{NodeSelector: map[string]string{
			consts.LabelKeyNodeGroupType: consts.LabelValNodeGroupTypeDataPlane,
			consts.K8sVersionLabelKey:    "1.31",
			consts.InstanceTypeLabelKey:  "n2-standard-8",
		}},
	}
	podB := podA.DeepCopy()
	podB.Name = "db-tikv-1"
	podB.Spec.NodeSelector[consts.InstanceTypeLabelKey] = "n2-standard-16"

	computeClass := testBuildGKEComputeClass([]string{"np-a", "np-b"})
	computeClassName := computeClass.GetName()
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(podA, podB, computeClass).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	resp, err := svc.CreateGKEHeterogeneousGatekeeperAssign(context.Background(), &apiv1.CreateGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId:        "123",
		ComponentType:    apicommon.ComponentType_TIKV,
		ComputeClassName: computeClassName,
	})
	require.NoError(t, err)
	require.Equal(t, map[string]string{
		consts.GKEComputeClassLabelKey: computeClassName,
		consts.LabelKeyNodeGroupType:   consts.LabelValNodeGroupTypeDataPlane,
		consts.K8sVersionLabelKey:      "1.31",
	}, resp.NodeSelector)
	require.NotContains(t, resp.NodeSelector, consts.InstanceTypeLabelKey)
}

func TestAgentService_CreateGKEHeterogeneousGatekeeperAssignAllowsDifferentComputeClassSelectors(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)

	podA := &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "db-tikv-0",
			Namespace: buildNamespace("123"),
			Labels: map[string]string{
				consts.ComponentLabelKey: string(cloudv1.ComponentTiKV),
				consts.InstanceLabelKey:  consts.ClusterPrefix,
			},
		},
		Spec: corev1.PodSpec{NodeSelector: map[string]string{
			consts.LabelKeyNodeGroupType:   consts.LabelValNodeGroupTypeDataPlane,
			consts.K8sVersionLabelKey:      "1.31",
			consts.GKEComputeClassLabelKey: "old-priority",
		}},
	}
	podB := podA.DeepCopy()
	podB.Name = "db-tikv-1"
	delete(podB.Spec.NodeSelector, consts.GKEComputeClassLabelKey)

	computeClass := testBuildGKEComputeClass([]string{"np-a", "np-b"})
	computeClassName := computeClass.GetName()
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(podA, podB, computeClass).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	resp, err := svc.CreateGKEHeterogeneousGatekeeperAssign(context.Background(), &apiv1.CreateGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId:        "123",
		ComponentType:    apicommon.ComponentType_TIKV,
		ComputeClassName: computeClassName,
	})
	require.NoError(t, err)
	require.Equal(t, map[string]string{
		consts.GKEComputeClassLabelKey: computeClassName,
		consts.LabelKeyNodeGroupType:   consts.LabelValNodeGroupTypeDataPlane,
		consts.K8sVersionLabelKey:      "1.31",
	}, resp.NodeSelector)
}

func TestAgentService_CreateGKEHeterogeneousGatekeeperAssignRejectsInconsistentSelectors(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)

	pod131 := &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "db-tikv-0",
			Namespace: buildNamespace("123"),
			Labels: map[string]string{
				consts.ComponentLabelKey: string(cloudv1.ComponentTiKV),
				consts.InstanceLabelKey:  consts.ClusterPrefix,
			},
		},
		Spec: corev1.PodSpec{NodeSelector: map[string]string{
			consts.LabelKeyNodeGroupType: consts.LabelValNodeGroupTypeDataPlane,
			consts.K8sVersionLabelKey:    "1.31",
		}},
	}
	pod132 := pod131.DeepCopy()
	pod132.Name = "db-tikv-1"
	pod132.Spec.NodeSelector[consts.K8sVersionLabelKey] = "1.32"

	computeClass := testBuildGKEComputeClass([]string{"np-a", "np-b"})
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(pod131, pod132, computeClass).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	_, err := svc.CreateGKEHeterogeneousGatekeeperAssign(context.Background(), &apiv1.CreateGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId:        "123",
		ComponentType:    apicommon.ComponentType_TIKV,
		ComputeClassName: computeClass.GetName(),
	})
	require.Error(t, err)
	require.Contains(t, err.Error(), "inconsistent node selectors")
}

func TestAgentService_GKEHeterogeneousGatekeeperAssignAllowsComputeClassSharedAcrossComponents(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)

	tidbPod := &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "db-tidb-0",
			Namespace: buildNamespace("123"),
			Labels: map[string]string{
				consts.ComponentLabelKey: string(cloudv1.ComponentTiDB),
				consts.InstanceLabelKey:  consts.ClusterPrefix,
			},
		},
		Spec: corev1.PodSpec{NodeSelector: map[string]string{
			consts.LabelKeyNodeGroupType: consts.LabelValNodeGroupTypeDataPlane,
			consts.K8sVersionLabelKey:    "1.31",
		}},
	}
	tikvComputeClass := testBuildGKEComputeClass([]string{"np-a"})
	tidbComponent := &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiDB, tidbGroupID: "default-group", tidbGroupKind: apiv1.TiDBGroupKind_TIDB_GROUP_KIND_DEFAULT}
	tidbAssign := requireBuildGatekeeperAssign(t, "tidb-assign", "123", tidbComponent, "tidb-priority", &gkeHeterogeneousAssignSettings{
		namespace:   buildNamespace("123"),
		matchLabels: testGatekeeperAssignMatchLabels(t, tidbComponent),
	})
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(tidbPod, tikvComputeClass, tidbAssign).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	resp, err := svc.CreateGKEHeterogeneousGatekeeperAssign(context.Background(), &apiv1.CreateGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId:        "123",
		ComponentType:    apicommon.ComponentType_TIDB,
		TidbGroupId:      "default-group",
		TidbGroupKind:    apiv1.TiDBGroupKind_TIDB_GROUP_KIND_DEFAULT,
		ComputeClassName: tikvComputeClass.GetName(),
	})
	require.NoError(t, err)
	require.Equal(t, tikvComputeClass.GetName(), resp.ComputeClassName)

	resp, err = svc.UpdateGKEHeterogeneousGatekeeperAssign(context.Background(), &apiv1.UpdateGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId:        "123",
		Name:             "tidb-assign",
		ComputeClassName: tikvComputeClass.GetName(),
	})
	require.NoError(t, err)
	require.Equal(t, tikvComputeClass.GetName(), resp.ComputeClassName)
}

func TestAgentService_UpdateTiDBGatekeeperAssignRejectsMissingGroupOwnership(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)

	tidbComponent := &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiDB}
	assign := requireBuildGatekeeperAssign(t, "tidb-assign", "123", tidbComponent, "tidb-priority", &gkeHeterogeneousAssignSettings{
		namespace: buildNamespace("123"),
		matchLabels: map[string]string{
			consts.ComponentLabelKey: string(cloudv1.ComponentTiDB),
			consts.InstanceLabelKey:  consts.ClusterPrefix,
		},
	})
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(assign).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	_, err := svc.UpdateGKEHeterogeneousGatekeeperAssign(context.Background(), &apiv1.UpdateGKEHeterogeneousGatekeeperAssignRequest{
		ClusterId:        "123",
		Name:             "tidb-assign",
		ComputeClassName: "tidb-priority",
	})
	require.Error(t, err)
	require.Equal(t, codes.FailedPrecondition, status.Code(err))
	require.Contains(t, err.Error(), "missing tidb group ownership")
}

func TestAgentService_DeleteGKEHeterogeneousComputeClassRejectsReferencedAssign(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)

	computeClass := testBuildGKEComputeClass([]string{"np-a"})
	computeClassName := computeClass.GetName()
	assign := requireBuildGatekeeperAssign(t, "tikv-assign", "123", &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiKV}, computeClassName, &gkeHeterogeneousAssignSettings{
		namespace:   buildNamespace("123"),
		matchLabels: testGatekeeperAssignMatchLabels(t, &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiKV}),
	})
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(computeClass, assign).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	_, err := svc.DeleteGKEHeterogeneousComputeClass(context.Background(), &apiv1.DeleteGKEHeterogeneousComputeClassRequest{ClusterId: "123", Name: computeClassName})
	require.Error(t, err)
	require.Contains(t, err.Error(), "still referenced")
}

func TestAgentService_DeleteGKEHeterogeneousComputeClassRejectsMalformedReferencedAssign(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)

	computeClass := testBuildGKEComputeClass([]string{"np-a"})
	computeClassName := computeClass.GetName()
	assign := requireBuildGatekeeperAssign(t, "foreign-assign", "456", &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiKV}, computeClassName, &gkeHeterogeneousAssignSettings{
		namespace:   buildNamespace("456"),
		matchLabels: testGatekeeperAssignMatchLabels(t, &gkeHeterogeneousDataplaneComponent{componentType: cloudv1.ComponentTiKV}),
	})
	labels := assign.GetLabels()
	delete(labels, consts.ClusterLabelKey)
	assign.SetLabels(labels)
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(computeClass, assign).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	_, err := svc.DeleteGKEHeterogeneousComputeClass(context.Background(), &apiv1.DeleteGKEHeterogeneousComputeClassRequest{ClusterId: "123", Name: computeClassName})
	require.Error(t, err)
	require.Contains(t, err.Error(), "still referenced")
	require.Contains(t, err.Error(), "foreign-assign")
}

func TestAgentService_CreateGKEHeterogeneousComputeClassRejectsNodePoolReferencedBySameCluster(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)

	existingComputeClass := testBuildGKEComputeClass([]string{"np-a", "np-b"}, []string{"n2-standard-16", "n2-standard-64"})
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(existingComputeClass).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	_, err := svc.CreateGKEHeterogeneousComputeClass(context.Background(), &apiv1.CreateGKEHeterogeneousComputeClassRequest{
		ClusterId:     "123",
		ComponentType: apicommon.ComponentType_TIKV,
		NodePoolNames: []string{"np-a", "np-c"},
		InstanceTypes: []string{"n2-standard-96", "n2-standard-128"},
	})
	require.Error(t, err)
	require.Equal(t, codes.AlreadyExists, status.Code(err))
}

func TestAgentService_CreateGKEHeterogeneousComputeClassRejectsNodePoolReferencedByOtherCluster(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)

	foreignComputeClass := testBuildGKEComputeClass([]string{"np-a", "np-b"}, []string{"n2-standard-16", "n2-standard-64"})
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(foreignComputeClass).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	_, err := svc.CreateGKEHeterogeneousComputeClass(context.Background(), &apiv1.CreateGKEHeterogeneousComputeClassRequest{
		ClusterId:     "123",
		ComponentType: apicommon.ComponentType_TIKV,
		NodePoolNames: []string{"np-c", "np-a"},
		InstanceTypes: []string{"n2-standard-96", "n2-standard-128"},
	})
	require.Error(t, err)
	require.Equal(t, codes.AlreadyExists, status.Code(err))
}

func TestAgentService_UpdateGKEHeterogeneousComputeClassRejectsBindingChanges(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)

	currentComputeClass := testBuildGKEComputeClass([]string{"np-a", "np-b"}, []string{"n2-standard-16", "n2-standard-64"})
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(currentComputeClass).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	_, err := svc.UpdateGKEHeterogeneousComputeClass(context.Background(), &apiv1.UpdateGKEHeterogeneousComputeClassRequest{
		ClusterId:     "123",
		Name:          currentComputeClass.GetName(),
		NodePoolNames: []string{"np-a", "np-c"},
	})
	require.Error(t, err)
	require.Equal(t, codes.FailedPrecondition, status.Code(err))
}

func TestAgentService_UpdateGKEHeterogeneousComputeClassIgnoresCurrentNodePoolRefs(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)

	currentComputeClass := testBuildGKEComputeClass([]string{"np-a", "np-b"})
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(currentComputeClass).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	_, err := svc.UpdateGKEHeterogeneousComputeClass(context.Background(), &apiv1.UpdateGKEHeterogeneousComputeClassRequest{
		ClusterId:     "123",
		Name:          currentComputeClass.GetName(),
		NodePoolNames: []string{"np-b", "np-a"},
	})
	require.Error(t, err)
	require.Equal(t, codes.FailedPrecondition, status.Code(err))
}

func TestAgentService_UpdateGKEHeterogeneousComputeClassRejectsIncompleteIdentityLabels(t *testing.T) {
	tests := []struct {
		name   string
		mutate func(map[string]string)
	}{
		{
			name: "missing-scope",
			mutate: func(labels map[string]string) {
				delete(labels, gkeHeterogeneousComputeClassScopeLabelKey)
			},
		},
		{
			name: "missing-bindings-hash",
			mutate: func(labels map[string]string) {
				delete(labels, gkeHeterogeneousBindingsHashLabelKey)
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			scheme := newGKEHeterogeneousTestScheme(t)
			computeClass := testBuildGKEComputeClass([]string{"np-a"})
			labels := computeClass.GetLabels()
			tt.mutate(labels)
			computeClass.SetLabels(labels)
			cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(computeClass).Build()
			svc := newScopedGKEHeterogeneousTestService(cli)

			_, err := svc.UpdateGKEHeterogeneousComputeClass(context.Background(), &apiv1.UpdateGKEHeterogeneousComputeClassRequest{
				ClusterId:     "123",
				Name:          computeClass.GetName(),
				NodePoolNames: []string{"np-a", "np-a-extra"},
			})
			require.Error(t, err)
			require.Equal(t, codes.PermissionDenied, status.Code(err))
			require.Contains(t, err.Error(), "not managed by binding-scoped gke heterogeneous deploy")
		})
	}
}

func TestAgentService_CreateGKEHeterogeneousComputeClassAllowsSameBindingWithoutClusterOwner(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)

	unknownOwnerComputeClass := testBuildGKEComputeClass([]string{"np-a", "np-b"}, []string{"n2-standard-16", "n2-standard-64"})
	labels := unknownOwnerComputeClass.GetLabels()
	delete(labels, consts.ClusterLabelKey)
	unknownOwnerComputeClass.SetLabels(labels)
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(unknownOwnerComputeClass).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	resp, err := svc.CreateGKEHeterogeneousComputeClass(context.Background(), &apiv1.CreateGKEHeterogeneousComputeClassRequest{
		ClusterId:     "123",
		ComponentType: apicommon.ComponentType_TIKV,
		NodePoolNames: []string{"np-a", "np-b"},
		InstanceTypes: []string{"n2-standard-16", "n2-standard-64"},
	})
	require.NoError(t, err)
	require.Equal(t, []string{"np-a", "np-b"}, resp.NodePoolNames)
}

func TestAgentService_DeleteGKEHeterogeneousComputeClassIsIdempotentForMissingResource(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)

	cli := fake.NewClientBuilder().WithScheme(scheme).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	_, err := svc.DeleteGKEHeterogeneousComputeClass(context.Background(), &apiv1.DeleteGKEHeterogeneousComputeClassRequest{ClusterId: "123", Name: "tikv-priority"})
	require.NoError(t, err)
}

func TestAgentService_DeleteGKEHeterogeneousComputeClassRejectsUnmanagedResource(t *testing.T) {
	scheme := newGKEHeterogeneousTestScheme(t)

	unmanagedComputeClass := testBuildGKEComputeClass([]string{"np-a"})
	unmanagedComputeClass.SetLabels(map[string]string{consts.ClusterLabelKey: "123"})
	cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(unmanagedComputeClass).Build()
	svc := newScopedGKEHeterogeneousTestService(cli)

	_, err := svc.DeleteGKEHeterogeneousComputeClass(context.Background(), &apiv1.DeleteGKEHeterogeneousComputeClassRequest{ClusterId: "123", Name: unmanagedComputeClass.GetName()})
	require.Error(t, err)
	require.Contains(t, err.Error(), "not managed by binding-scoped gke heterogeneous deploy")
}

func TestAgentService_DeleteGKEHeterogeneousComputeClassRejectsIncompleteIdentityLabels(t *testing.T) {
	tests := []struct {
		name   string
		mutate func(map[string]string)
	}{
		{
			name: "missing-scope",
			mutate: func(labels map[string]string) {
				delete(labels, gkeHeterogeneousComputeClassScopeLabelKey)
			},
		},
		{
			name: "missing-bindings-hash",
			mutate: func(labels map[string]string) {
				delete(labels, gkeHeterogeneousBindingsHashLabelKey)
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			scheme := newGKEHeterogeneousTestScheme(t)
			computeClass := testBuildGKEComputeClass([]string{"np-a"})
			labels := computeClass.GetLabels()
			tt.mutate(labels)
			computeClass.SetLabels(labels)
			cli := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(computeClass).Build()
			svc := newScopedGKEHeterogeneousTestService(cli)

			_, err := svc.DeleteGKEHeterogeneousComputeClass(context.Background(), &apiv1.DeleteGKEHeterogeneousComputeClassRequest{ClusterId: "123", Name: computeClass.GetName()})
			require.Error(t, err)
			require.Equal(t, codes.PermissionDenied, status.Code(err))
			require.Contains(t, err.Error(), "not managed by binding-scoped gke heterogeneous deploy")
		})
	}
}

func TestGKEHeterogeneousComputeClassResponse(t *testing.T) {
	name, bindings, err := gkeComputeClassNameAndBindingsFromInput([]string{"np-a", "np-b"}, []string{"n2-standard-16", "n2-standard-64"})
	require.NoError(t, err)
	obj := &unstructured.Unstructured{}
	obj.Object = map[string]interface{}{
		"spec": map[string]interface{}{
			"priorities": []interface{}{
				map[string]interface{}{"nodepools": []interface{}{"np-a"}},
				map[string]interface{}{"nodepools": []interface{}{"np-b"}},
			},
			"whenUnsatisfiable": gkeWhenUnsatisfiableDoNotScaleUp,
		},
	}
	obj.SetName(name)
	labels, err := gkeSharedHeterogeneousComputeClassLabels(bindings)
	require.NoError(t, err)
	obj.SetLabels(labels)
	annotations, err := gkeSharedHeterogeneousComputeClassAnnotations(bindings)
	require.NoError(t, err)
	obj.SetAnnotations(annotations)

	resp, err := gkeHeterogeneousComputeClassResponse("123", nil, obj)
	require.NoError(t, err)
	require.Equal(t, "123", resp.ClusterId)
	require.Equal(t, name, resp.Name)
	require.Equal(t, apicommon.ComponentType_COMPONENT_TYPE_UNSPECIFIED, resp.ComponentType)
	require.Equal(t, []string{"np-a", "np-b"}, resp.NodePoolNames)
	require.Equal(t, []string{"n2-standard-16", "n2-standard-64"}, resp.InstanceTypes)
	require.Equal(t, gkeWhenUnsatisfiableDoNotScaleUp, resp.WhenUnsatisfiable)
}
