package v1

import (
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func updateTestGKEComputeClassBinding(t *testing.T, cluster *Cluster, key, baseInstanceType, baseNodePoolName string, extras ...GKEComputeClassNodePoolBinding) string {
	t.Helper()
	annotation := GKEComputeClassBindingAnnotation{
		NodePools: []GKEComputeClassNodePoolBinding{
			{
				Role:         GKEComputeClassNodePoolRoleBase,
				InstanceType: baseInstanceType,
				NodePoolName: baseNodePoolName,
			},
		},
	}
	for _, extra := range extras {
		extra.Role = GKEComputeClassNodePoolRoleExtra
		annotation.NodePools = append(annotation.NodePools, extra)
	}
	computeClassName, err := GKEComputeClassNameForBindings(annotation.NodePools)
	require.NoError(t, err)
	annotation.ComputeClassName = computeClassName
	value, err := EncodeGKEComputeClassBindingAnnotation(annotation, key, baseInstanceType, baseNodePoolName)
	require.NoError(t, err)
	cluster.UpdateAnnotation(key, value)
	return computeClassName
}

func TestDefaultGKEComputeClassNameUsesClusterID(t *testing.T) {
	tikvA := DefaultGKEComputeClassNameForTiKV("cluster-a")
	tikvB := DefaultGKEComputeClassNameForTiKV("cluster-b")
	tidbA := DefaultGKEComputeClassNameForTiDB("cluster-a")
	tidbGroupA := DefaultGKEComputeClassNameForTiDBGroup("cluster-a", "group1")

	assert.NotEqual(t, tikvA, tikvB)
	assert.NotEqual(t, tikvA, tidbA)
	assert.NotEqual(t, tidbA, tidbGroupA)
	assert.LessOrEqual(t, len(tikvA), 63)
	assert.LessOrEqual(t, len(tikvB), 63)
	assert.LessOrEqual(t, len(tidbA), 63)
	assert.LessOrEqual(t, len(tidbGroupA), 63)
	assert.Equal(t, DefaultGKEComputeClassForTiKV, DefaultGKEComputeClassNameForTiKV(""))
	assert.Equal(t, DefaultGKEComputeClassForTiDB, DefaultGKEComputeClassNameForTiDB(""))
	assert.Equal(t, DefaultGKEComputeClassForTiDB, DefaultGKEComputeClassNameForTiDBGroup("", ""))
}

func TestDefaultGKEComputeClassNameNormalizesIdentity(t *testing.T) {
	name := DefaultGKEComputeClassNameForTiDBGroup("Cluster_A", "Group_1")

	_, err := NormalizeGKEComputeClassName(name)
	assert.NoError(t, err)
	assert.NotContains(t, name, "_")
	assert.NotContains(t, name, "C")
	assert.Contains(t, name, "cluster-a-group-1")
}

func TestGKEComputeClassNameForBindings(t *testing.T) {
	bindings := []GKEComputeClassNodePoolBinding{
		{Role: GKEComputeClassNodePoolRoleBase, InstanceType: "n2-standard-16", NodePoolName: "np-a"},
		{Role: GKEComputeClassNodePoolRoleExtra, InstanceType: "n2-standard-64", NodePoolName: "np-b"},
	}
	tikvName, err := GKEComputeClassNameForBindings(bindings)
	assert.NoError(t, err)
	tidbName, err := GKEComputeClassNameForBindings(bindings)
	assert.NoError(t, err)
	reboundName, err := GKEComputeClassNameForBindings([]GKEComputeClassNodePoolBinding{
		{Role: GKEComputeClassNodePoolRoleBase, InstanceType: "n2-standard-16", NodePoolName: "np-a"},
		{Role: GKEComputeClassNodePoolRoleExtra, InstanceType: "n2-standard-64", NodePoolName: "np-c"},
	})
	assert.NoError(t, err)

	assert.Equal(t, tikvName, tidbName)
	assert.NotEqual(t, tikvName, reboundName)
	assert.Contains(t, tikvName, "compute-class-")
	assert.LessOrEqual(t, len(tikvName), 63)
	_, err = NormalizeGKEComputeClassName(tikvName)
	assert.NoError(t, err)
}

func TestNormalizeGKEComputeClassName(t *testing.T) {
	got, err := NormalizeGKEComputeClassName(" priority-a ")
	assert.NoError(t, err)
	assert.Equal(t, "priority-a", got)

	_, err = NormalizeGKEComputeClassName("")
	assert.EqualError(t, err, "compute_class_name is required")

	_, err = NormalizeGKEComputeClassName("Bad_Name")
	assert.EqualError(t, err, `invalid compute_class_name "Bad_Name"`)

	_, err = NormalizeGKEComputeClassName(strings.Repeat("a", gkeResourceNameLimit+1))
	assert.EqualError(t, err, `invalid compute_class_name "`+strings.Repeat("a", gkeResourceNameLimit+1)+`"`)
}

func TestGKEComputeClassNameNoBindingDoesNotEnableTiKVIntent(t *testing.T) {
	cluster := &Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Name: "cluster-a",
			Labels: map[string]string{
				ProviderTypeLabel: ProviderTypeGKE,
			},
		},
	}

	assert.Empty(t, cluster.GKEComputeClassNameForTiKV())
	assert.Empty(t, cluster.GKEComputeClassNameForTiDB())
	assert.False(t, cluster.TiKVGKEComputeClassIntentConfigured())
	assert.False(t, cluster.GKEComputeClassForTiKVEnabled())
	assert.False(t, cluster.GKEComputeClassForTiDBDefaultGroupEnabled())
	assert.False(t, cluster.GKEComputeClassForTiDBGroupEnabled("default-group"))
}

func TestClusterActiveTiDBGKEComputeClassGroupIDsUsesGroupBindingIntent(t *testing.T) {
	newCluster := func() *Cluster {
		cluster := &Cluster{
			ObjectMeta: metav1.ObjectMeta{
				Name: "cluster-a",
				Labels: map[string]string{
					ProviderTypeLabel: ProviderTypeGKE,
				},
				Annotations: map[string]string{
					DefaultLoadBalancerIDAnnotationKey: "default-group",
				},
			},
			Spec: ClusterSpec{
				Cluster: ClusterTopology{
					TiDB: TiDB{InstanceType: "n2-standard-8"},
				},
				ExtraGroups: []ExtraGroupSpec{
					{Name: "group1", TiDB: TiDB{Replicas: 2, InstanceType: "n2-standard-32"}},
				},
			},
		}
		return cluster
	}

	t.Run("no group binding", func(t *testing.T) {
		cluster := newCluster()

		assert.False(t, cluster.GKEComputeClassForTiDBDefaultGroupEnabled())
		assert.False(t, cluster.GKEComputeClassForTiDBExtraGroupEnabled("group1"))
		assert.False(t, cluster.GKEComputeClassForTiDBExtraGroupEnabled(""))
		assert.False(t, cluster.GKEComputeClassForTiDBExtraGroupEnabled("default-group"))
		assert.Empty(t, cluster.ActiveTiDBGKEComputeClassGroupIDs())
	})

	t.Run("default group binding only", func(t *testing.T) {
		cluster := newCluster()
		updateTestGKEComputeClassBinding(t, cluster, AnnoKeyGKEComputeClassForTiDBBindings, "n2-standard-8", "tidb-default-base", GKEComputeClassNodePoolBinding{InstanceType: "n2-standard-64", NodePoolName: "tidb-default-extra"})

		assert.True(t, cluster.GKEComputeClassForTiDBDefaultGroupEnabled())
		assert.False(t, cluster.GKEComputeClassForTiDBExtraGroupEnabled("group1"))
		assert.Equal(t, []string{"default-group"}, cluster.ActiveTiDBGKEComputeClassGroupIDs())
	})

	t.Run("extra group binding only", func(t *testing.T) {
		cluster := newCluster()
		updateTestGKEComputeClassBinding(t, cluster, cluster.TiDBGKEComputeClassBindingsAnnoKey("group1"), "n2-standard-32", "tidb-group1-base", GKEComputeClassNodePoolBinding{InstanceType: "n2-standard-64", NodePoolName: "tidb-group1-extra"})

		assert.False(t, cluster.GKEComputeClassForTiDBDefaultGroupEnabled())
		assert.True(t, cluster.GKEComputeClassForTiDBExtraGroupEnabled("group1"))
		assert.Equal(t, []string{"group1"}, cluster.ActiveTiDBGKEComputeClassGroupIDs())
	})

	t.Run("default and extra group bindings", func(t *testing.T) {
		cluster := newCluster()
		updateTestGKEComputeClassBinding(t, cluster, AnnoKeyGKEComputeClassForTiDBBindings, "n2-standard-8", "tidb-default-base", GKEComputeClassNodePoolBinding{InstanceType: "n2-standard-64", NodePoolName: "tidb-default-extra"})
		updateTestGKEComputeClassBinding(t, cluster, cluster.TiDBGKEComputeClassBindingsAnnoKey("group1"), "n2-standard-32", "tidb-group1-base", GKEComputeClassNodePoolBinding{InstanceType: "n2-standard-80", NodePoolName: "tidb-group1-extra"})

		assert.True(t, cluster.GKEComputeClassForTiDBDefaultGroupEnabled())
		assert.True(t, cluster.GKEComputeClassForTiDBExtraGroupEnabled("group1"))
		assert.Equal(t, []string{"default-group", "group1"}, cluster.ActiveTiDBGKEComputeClassGroupIDs())
	})

	t.Run("orphan extra group binding", func(t *testing.T) {
		cluster := newCluster()
		updateTestGKEComputeClassBinding(t, cluster, cluster.TiDBGKEComputeClassBindingsAnnoKey("orphan"), "n2-standard-64", "tidb-orphan-base", GKEComputeClassNodePoolBinding{InstanceType: "n2-standard-80", NodePoolName: "tidb-orphan-extra"})

		assert.False(t, cluster.GKEComputeClassForTiDBExtraGroupEnabled("orphan"))
		assert.Empty(t, cluster.ActiveTiDBGKEComputeClassGroupIDs())
	})

	t.Run("malformed binding fails closed", func(t *testing.T) {
		cluster := newCluster()
		cluster.UpdateAnnotation(AnnoKeyGKEComputeClassForTiDBBindings, `{"computeClassName":"tidb-default-priority","nodePools":[{"role":"extra","instanceType":"n2-standard-64","nodePoolName":"tidb-default-extra"}]}`)

		assert.False(t, cluster.GKEComputeClassForTiDBDefaultGroupEnabled())
		assert.Empty(t, cluster.ActiveTiDBGKEComputeClassGroupIDs())
	})
}

func TestClusterConfiguredTiDBComputeClassAnnotationRefs(t *testing.T) {
	cluster := &Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Name: "cluster-a",
			Labels: map[string]string{
				ProviderTypeLabel: ProviderTypeGKE,
			},
			Annotations: map[string]string{
				DefaultLoadBalancerIDAnnotationKey: "default-group",
			},
		},
		Spec: ClusterSpec{
			Cluster: ClusterTopology{
				TiDB: TiDB{InstanceType: "n2-standard-8"},
			},
			ExtraGroups: []ExtraGroupSpec{
				{Name: "group1", TiDB: TiDB{Replicas: 2, InstanceType: "n2-standard-32"}},
			},
		},
	}
	updateTestGKEComputeClassBinding(t, cluster, AnnoKeyGKEComputeClassForTiDBBindings, "n2-standard-8", "tidb-default-base", GKEComputeClassNodePoolBinding{InstanceType: "n2-standard-64", NodePoolName: "tidb-default-extra"})
	updateTestGKEComputeClassBinding(t, cluster, cluster.TiDBGKEComputeClassBindingsAnnoKey("orphan"), "n2-standard-64", "tidb-orphan-base", GKEComputeClassNodePoolBinding{InstanceType: "n2-standard-80", NodePoolName: "tidb-orphan-extra"})

	beforeExtraEndpoints := append([]ExtraGroupStatus(nil), cluster.Status.ExtraEndpoints...)
	assert.Equal(t, []TiDBComputeClassAnnotationRef{
		{
			TiDBGroupID:   "default-group",
			AnnotationKey: AnnoKeyGKEComputeClassForTiDBBindings,
		},
		{
			TiDBGroupID:   "orphan",
			AnnotationKey: AnnoKeyGKEComputeClassForTiDBBindings + "-orphan",
		},
	}, cluster.ConfiguredTiDBComputeClassAnnotationRefs())
	assert.Equal(t, beforeExtraEndpoints, cluster.Status.ExtraEndpoints)
}

func TestClusterGKEComputeClassNameForTiDBGroup(t *testing.T) {
	cluster := &Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Name: "cluster-a",
			Labels: map[string]string{
				ProviderTypeLabel: ProviderTypeGKE,
			},
			Annotations: map[string]string{
				DefaultLoadBalancerIDAnnotationKey: "default-group",
			},
		},
		Spec: ClusterSpec{
			Cluster: ClusterTopology{
				TiDB: TiDB{InstanceType: "n2-standard-8"},
			},
			ExtraGroups: []ExtraGroupSpec{
				{Name: "group1", TiDB: TiDB{Replicas: 1, InstanceType: "n2-standard-8"}},
				{Name: "zero-replica-group", TiDB: TiDB{Replicas: 0, InstanceType: "n2-standard-8"}},
			},
		},
	}
	group1ComputeClassName := updateTestGKEComputeClassBinding(t, cluster, cluster.TiDBGKEComputeClassBindingsAnnoKey("group1"), "n2-standard-8", "group1-np-a", GKEComputeClassNodePoolBinding{InstanceType: "n2-standard-16", NodePoolName: "group1-np-b"})
	updateTestGKEComputeClassBinding(t, cluster, cluster.TiDBGKEComputeClassBindingsAnnoKey("zero-replica-group"), "n2-standard-8", "zero-replica-np-a", GKEComputeClassNodePoolBinding{InstanceType: "n2-standard-16", NodePoolName: "zero-replica-np-b"})
	updateTestGKEComputeClassBinding(t, cluster, cluster.TiDBGKEComputeClassBindingsAnnoKey("orphan"), "n2-standard-8", "orphan-np-a", GKEComputeClassNodePoolBinding{InstanceType: "n2-standard-16", NodePoolName: "orphan-np-b"})

	assert.False(t, cluster.GKEComputeClassForTiDBGroupEnabled(""))
	assert.False(t, cluster.GKEComputeClassForTiDBGroupEnabled("default-group"))
	assert.True(t, cluster.GKEComputeClassForTiDBGroupEnabled("group1"))
	assert.True(t, cluster.GKEComputeClassForTiDBGroupEnabled("zero-replica-group"))
	assert.False(t, cluster.GKEComputeClassForTiDBGroupEnabled("orphan"))
	assert.Equal(t, group1ComputeClassName, cluster.GKEComputeClassNameForTiDBGroup("group1"))
	assert.Equal(t, []string{"group1", "zero-replica-group"}, cluster.ActiveTiDBGKEComputeClassGroupIDs())
}

func TestClusterGKEComputeClassForTiDBGroupEnabledRequiresExplicitNodePoolNames(t *testing.T) {
	cluster := &Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Name: "cluster-a",
			Labels: map[string]string{
				ProviderTypeLabel: ProviderTypeGKE,
			},
			Annotations: map[string]string{
				DefaultLoadBalancerIDAnnotationKey: "default-group",
			},
		},
		Spec: ClusterSpec{
			Cluster: ClusterTopology{
				TiDB: TiDB{InstanceType: "n2-standard-8"},
			},
		},
	}

	assert.False(t, cluster.GKEComputeClassForTiDBGroupEnabled(""))
	assert.False(t, cluster.GKEComputeClassForTiDBGroupEnabled("default-group"))
	assert.Empty(t, cluster.ActiveTiDBGKEComputeClassGroupIDs())

	updateTestGKEComputeClassBinding(t, cluster, AnnoKeyGKEComputeClassForTiDBBindings, "n2-standard-8", "tidb-base", GKEComputeClassNodePoolBinding{InstanceType: "n2-standard-16", NodePoolName: "tidb-extra"})
	assert.True(t, cluster.GKEComputeClassForTiDBGroupEnabled(""))
	assert.True(t, cluster.GKEComputeClassForTiDBGroupEnabled("default-group"))
	assert.Equal(t, []string{"default-group"}, cluster.ActiveTiDBGKEComputeClassGroupIDs())
}

func TestTiDBMemberInstanceTypeAllowedForGroupRequiresActiveGroupIntent(t *testing.T) {
	cluster := &Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Name: "cluster-a",
			Labels: map[string]string{
				ProviderTypeLabel: ProviderTypeGKE,
			},
			Annotations: map[string]string{
				DefaultLoadBalancerIDAnnotationKey: "default-group",
			},
		},
		Spec: ClusterSpec{
			ExtraGroups: []ExtraGroupSpec{
				{Name: "group1", TiDB: TiDB{InstanceType: "n2-standard-8"}},
			},
		},
	}
	updateTestGKEComputeClassBinding(t, cluster, cluster.TiDBGKEComputeClassBindingsAnnoKey("group1"), "n2-standard-8", "group1-base", GKEComputeClassNodePoolBinding{InstanceType: "n2-standard-64", NodePoolName: "group1-extra"})

	assert.True(t, cluster.TiDBMemberInstanceTypeAllowedForGroup("n2-standard-64", "group1", "n2-standard-8"))

	// group2 annotation is stale because group2 is no longer part of the cluster spec,
	// so it must not be treated as an active heterogeneous target.
	updateTestGKEComputeClassBinding(t, cluster, cluster.TiDBGKEComputeClassBindingsAnnoKey("group2"), "n2-standard-8", "group2-base", GKEComputeClassNodePoolBinding{InstanceType: "n2-standard-64", NodePoolName: "group2-extra"})
	assert.False(t, cluster.GKEComputeClassForTiDBGroupEnabled("group2"))
	assert.False(t, cluster.TiDBMemberInstanceTypeAllowedForGroup("n2-standard-64", "group2", "n2-standard-8"))
}

func TestGKEComputeClassMalformedBindingFailsClosed(t *testing.T) {
	cluster := &Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Name: "cluster-a",
			Labels: map[string]string{
				ProviderTypeLabel: ProviderTypeGKE,
			},
			Annotations: map[string]string{
				DefaultLoadBalancerIDAnnotationKey: "default-group",
			},
		},
		Spec: ClusterSpec{
			Cluster: ClusterTopology{
				TiKV: TiKV{InstanceType: "n2-standard-16"},
				TiDB: TiDB{InstanceType: "n2-standard-8"},
			},
		},
	}

	cluster.UpdateAnnotation(AnnoKeyGKEComputeClassForTiKVBindings, `{"computeClassName":"tikv-priority","nodePools":[{"role":"extra","instanceType":"n2-standard-64","nodePoolName":"tikv-extra"}]}`)
	assert.False(t, cluster.GKEComputeClassForTiKVEnabled())
	assert.False(t, cluster.TiKVMemberInstanceTypeAllowed("n2-standard-64"))
	assert.Empty(t, cluster.GKEComputeClassNameForTiKV())
	assert.Empty(t, cluster.TiKVGKEComputeClassInstanceTypes())

	cluster.UpdateAnnotation(AnnoKeyGKEComputeClassForTiDBBindings, `{"nodePools":[{"role":"base","instanceType":"n2-standard-8","nodePoolName":"tidb-base"},{"role":"extra","instanceType":"n2-standard-64","nodePoolName":"tidb-extra"}]}`)
	assert.False(t, cluster.GKEComputeClassForTiDBGroupEnabled(""))
	assert.False(t, cluster.TiDBMemberInstanceTypeAllowedForGroup("n2-standard-64", "", "n2-standard-8"))
	assert.Empty(t, cluster.GKEComputeClassNameForTiDB())
	assert.Empty(t, cluster.TiDBGKEComputeClassInstanceTypes("", "n2-standard-8"))

	cluster.UpdateAnnotation(AnnoKeyGKEComputeClassForTiDBBindings, `{"computeClassName":"tidb-priority","nodePools":[{"role":"base","instanceType":"n2-standard-16","nodePoolName":"tidb-base"},{"role":"extra","instanceType":"n2-standard-64","nodePoolName":"tidb-extra"}]}`)
	assert.False(t, cluster.GKEComputeClassForTiDBGroupEnabled(""))
	assert.False(t, cluster.TiDBMemberInstanceTypeAllowedForGroup("n2-standard-64", "", "n2-standard-8"))
}

func TestTiKVMemberInstanceTypeAllowedUsesGKEComputeClassPolicy(t *testing.T) {
	var nilCluster *Cluster
	assert.False(t, nilCluster.TiKVMemberInstanceTypeAllowed(""))

	cluster := &Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Name: "cluster-a",
			Labels: map[string]string{
				ProviderTypeLabel: ProviderTypeGKE,
			},
		},
		Spec: ClusterSpec{
			Cluster: ClusterTopology{
				TiKV: TiKV{InstanceType: "n2-standard-16"},
			},
		},
	}

	assert.True(t, cluster.TiKVMemberInstanceTypeAllowed(""))
	assert.True(t, cluster.TiKVMemberInstanceTypeAllowed("n2-standard-16"))
	assert.False(t, cluster.TiKVMemberInstanceTypeAllowed("n2-standard-64"))
	updateTestGKEComputeClassBinding(t, cluster, AnnoKeyGKEComputeClassForTiKVBindings, "n2-standard-16", "tikv-base", GKEComputeClassNodePoolBinding{InstanceType: "n2-standard-64", NodePoolName: "tikv-extra"})

	assert.True(t, cluster.TiKVMemberInstanceTypeAllowed("n2-standard-16"))
	assert.True(t, cluster.TiKVMemberInstanceTypeAllowed("n2-standard-64"))
	assert.False(t, cluster.TiKVMemberInstanceTypeAllowed("n2-standard-80"))
}

func TestTiDBMemberInstanceTypeAllowedForGroupUsesGKEComputeClassPolicy(t *testing.T) {
	var nilCluster *Cluster
	assert.False(t, nilCluster.TiDBMemberInstanceTypeAllowedForGroup("", "", "n2-standard-8"))

	newCluster := func() *Cluster {
		return &Cluster{
			ObjectMeta: metav1.ObjectMeta{
				Name: "cluster-a",
				Labels: map[string]string{
					ProviderTypeLabel: ProviderTypeGKE,
				},
				Annotations: map[string]string{
					DefaultLoadBalancerIDAnnotationKey: "default-group",
				},
			},
			Spec: ClusterSpec{
				Cluster: ClusterTopology{
					TiDB: TiDB{InstanceType: "n2-standard-8"},
				},
				ExtraGroups: []ExtraGroupSpec{
					{Name: "group1", TiDB: TiDB{InstanceType: "n2-standard-32"}},
				},
			},
		}
	}

	t.Run("no group binding", func(t *testing.T) {
		cluster := newCluster()

		assert.True(t, cluster.TiDBMemberInstanceTypeAllowedForGroup("", "", "n2-standard-8"))
		assert.True(t, cluster.TiDBMemberInstanceTypeAllowedForGroup("n2-standard-8", "", "n2-standard-8"))
		assert.False(t, cluster.TiDBMemberInstanceTypeAllowedForGroup("n2-standard-64", "", "n2-standard-8"))
	})

	t.Run("default and extra groups use separate annotations", func(t *testing.T) {
		cluster := newCluster()
		updateTestGKEComputeClassBinding(t, cluster, AnnoKeyGKEComputeClassForTiDBBindings, "n2-standard-8", "tidb-default-base", GKEComputeClassNodePoolBinding{InstanceType: "n2-standard-64", NodePoolName: "tidb-default-extra"})
		updateTestGKEComputeClassBinding(t, cluster, cluster.TiDBGKEComputeClassBindingsAnnoKey("group1"), "n2-standard-32", "tidb-group1-base", GKEComputeClassNodePoolBinding{InstanceType: "n2-standard-80", NodePoolName: "tidb-group1-extra"})

		assert.True(t, cluster.TiDBMemberInstanceTypeAllowedForGroup("n2-standard-8", "", "n2-standard-8"))
		assert.True(t, cluster.TiDBMemberInstanceTypeAllowedForGroup("n2-standard-64", "", "n2-standard-8"))
		assert.False(t, cluster.TiDBMemberInstanceTypeAllowedForGroup("n2-standard-80", "", "n2-standard-8"))

		assert.True(t, cluster.TiDBMemberInstanceTypeAllowedForGroup("n2-standard-32", "group1", "n2-standard-32"))
		assert.True(t, cluster.TiDBMemberInstanceTypeAllowedForGroup("n2-standard-80", "group1", "n2-standard-32"))
		assert.False(t, cluster.TiDBMemberInstanceTypeAllowedForGroup("n2-standard-64", "group1", "n2-standard-32"))
	})

	t.Run("stale extra group annotation fails closed", func(t *testing.T) {
		cluster := newCluster()
		cluster.Spec.ExtraGroups = nil
		updateTestGKEComputeClassBinding(t, cluster, cluster.TiDBGKEComputeClassBindingsAnnoKey("group1"), "n2-standard-32", "tidb-group1-base", GKEComputeClassNodePoolBinding{InstanceType: "n2-standard-80", NodePoolName: "tidb-group1-extra"})

		assert.False(t, cluster.TiDBMemberInstanceTypeAllowedForGroup("n2-standard-80", "group1", "n2-standard-32"))
	})

	t.Run("empty desired instance type fails closed for observed instance type", func(t *testing.T) {
		cluster := newCluster()
		updateTestGKEComputeClassBinding(t, cluster, cluster.TiDBGKEComputeClassBindingsAnnoKey("group1"), "n2-standard-32", "tidb-group1-base", GKEComputeClassNodePoolBinding{InstanceType: "n2-standard-80", NodePoolName: "tidb-group1-extra"})

		assert.False(t, cluster.TiDBMemberInstanceTypeAllowedForGroup("n2-standard-80", "group1", ""))
	})

	t.Run("malformed binding or base mismatch fails closed", func(t *testing.T) {
		cluster := newCluster()
		cluster.UpdateAnnotation(AnnoKeyGKEComputeClassForTiDBBindings, `{"computeClassName":"tidb-default-priority","nodePools":[{"role":"extra","instanceType":"n2-standard-64","nodePoolName":"tidb-default-extra"}]}`)
		assert.False(t, cluster.TiDBMemberInstanceTypeAllowedForGroup("n2-standard-64", "", "n2-standard-8"))

		cluster.UpdateAnnotation(cluster.TiDBGKEComputeClassBindingsAnnoKey("group1"), `{"computeClassName":"tidb-group1-priority","nodePools":[{"role":"base","instanceType":"n2-standard-8","nodePoolName":"tidb-group1-base"},{"role":"extra","instanceType":"n2-standard-80","nodePoolName":"tidb-group1-extra"}]}`)
		assert.False(t, cluster.TiDBMemberInstanceTypeAllowedForGroup("n2-standard-80", "group1", "n2-standard-32"))
	})
}

func TestNormalizeGKENodePoolNames(t *testing.T) {
	got, err := NormalizeGKENodePoolNames([]string{" np-a ", "np-b"})
	assert.NoError(t, err)
	assert.Equal(t, []string{"np-a", "np-b"}, got)

	got, err = NormalizeGKENodePoolNames([]string{strings.Repeat("a", gkeNodePoolNameLimit)})
	assert.NoError(t, err)
	assert.Equal(t, []string{strings.Repeat("a", gkeNodePoolNameLimit)}, got)

	_, err = NormalizeGKENodePoolNames([]string{"np-a", "np-a"})
	assert.EqualError(t, err, "node_pool_names contains duplicate item np-a")

	_, err = NormalizeGKENodePoolNames([]string{"Bad_Name"})
	assert.EqualError(t, err, `node_pool_names contains invalid GKE node pool name "Bad_Name"`)

	_, err = NormalizeGKENodePoolNames([]string{strings.Repeat("a", gkeNodePoolNameLimit+1)})
	assert.EqualError(t, err, `node_pool_names contains invalid GKE node pool name "`+strings.Repeat("a", gkeNodePoolNameLimit+1)+`"`)
}

func TestMergeGKEComputeClassInstanceTypes(t *testing.T) {
	assert.Equal(t, []string{"n2-standard-8", "n2-standard-64", "n2-standard-16"}, MergeGKEComputeClassInstanceTypes(" n2-standard-8 ", []string{"n2-standard-64", "n2-standard-8", "n2-standard-16", ""}))
}

func TestClusterGKEComputeClassBindingAnnotation(t *testing.T) {
	cluster := &Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Annotations: map[string]string{},
		},
	}
	computeClassName := updateTestGKEComputeClassBinding(t, cluster, "bindings", "n2-standard-16", "pool-a", GKEComputeClassNodePoolBinding{InstanceType: "n2-standard-32", NodePoolName: "pool-b"})

	annotation, explicit, err := ClusterGKEComputeClassBindingAnnotation(cluster, "bindings", "n2-standard-16", "pool-a")
	assert.NoError(t, err)
	assert.True(t, explicit)
	assert.Equal(t, computeClassName, annotation.ComputeClassName)
	assert.Equal(t, []GKEComputeClassNodePoolBinding{
		{Role: GKEComputeClassNodePoolRoleBase, InstanceType: "n2-standard-16", NodePoolName: "pool-a"},
		{Role: GKEComputeClassNodePoolRoleExtra, InstanceType: "n2-standard-32", NodePoolName: "pool-b"},
	}, annotation.NodePools)

	delete(cluster.Annotations, "bindings")
	_, explicit, err = ClusterGKEComputeClassBindingAnnotation(cluster, "bindings", "n2-standard-16", "pool-a")
	assert.NoError(t, err)
	assert.False(t, explicit)

	cluster.UpdateAnnotation("bindings", `{"computeClassName":"`+computeClassName+`","nodePools":[{"role":"extra","instanceType":"n2-standard-32","nodePoolName":"pool-b"}]}`)
	_, _, err = ClusterGKEComputeClassBindingAnnotation(cluster, "bindings", "n2-standard-16", "pool-a")
	assert.EqualError(t, err, "gke compute class binding annotation bindings is invalid: gke compute class bindings must contain exactly 2 nodePools, got 1")

	cluster.UpdateAnnotation("bindings", `{"computeClassName":"`+computeClassName+`","nodePools":[{"role":"base","instanceType":"n2-standard-8","nodePoolName":"pool-a"},{"role":"extra","instanceType":"n2-standard-32","nodePoolName":"pool-b"}]}`)
	_, _, err = ClusterGKEComputeClassBindingAnnotation(cluster, "bindings", "n2-standard-16", "pool-a")
	assert.EqualError(t, err, "gke compute class binding annotation bindings base instanceType must be n2-standard-16, got n2-standard-8")

	historicalBindings := []GKEComputeClassNodePoolBinding{
		{Role: GKEComputeClassNodePoolRoleBase, InstanceType: "n2-standard-16", NodePoolName: "pool-b"},
		{Role: GKEComputeClassNodePoolRoleExtra, InstanceType: "n2-standard-32", NodePoolName: "pool-c"},
	}
	historicalComputeClassName, err := GKEComputeClassNameForBindings(historicalBindings)
	require.NoError(t, err)
	cluster.UpdateAnnotation("bindings", `{"computeClassName":"`+historicalComputeClassName+`","nodePools":[{"role":"base","instanceType":"n2-standard-16","nodePoolName":"pool-b"},{"role":"extra","instanceType":"n2-standard-32","nodePoolName":"pool-c"}]}`)
	annotation, explicit, err = ClusterGKEComputeClassBindingAnnotation(cluster, "bindings", "n2-standard-16", "pool-a")
	assert.NoError(t, err)
	assert.True(t, explicit)
	assert.Equal(t, historicalBindings, annotation.NodePools)

	_, err = EncodeGKEComputeClassBindingAnnotation(GKEComputeClassBindingAnnotation{
		ComputeClassName: historicalComputeClassName,
		NodePools:        historicalBindings,
	}, "bindings", "n2-standard-16", "pool-a")
	assert.EqualError(t, err, "gke compute class binding annotation bindings base nodePoolName must be pool-a, got pool-b")

	cluster.UpdateAnnotation("bindings", `{"computeClassName":"`+computeClassName+`","nodePools":[{"role":"base","instanceType":"n2-standard-16","nodePoolName":"pool-a"},{"role":"extra","instanceType":"n2-standard-16","nodePoolName":"pool-b"}]}`)
	_, _, err = ClusterGKEComputeClassBindingAnnotation(cluster, "bindings", "n2-standard-16", "pool-a")
	assert.EqualError(t, err, "gke compute class binding annotation bindings is invalid: gke compute class bindings contains duplicate instanceType n2-standard-16")
}
