package resources

import (
	"encoding/json"
	"testing"

	"github.com/crossplane-contrib/provider-gcp/apis/container/v1beta1"
	commonv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/prometheus/client_golang/prometheus"
	dto "github.com/prometheus/client_model/go"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	shared "github.com/tidbcloud/infra-provider/pkg/controllers/common"
	"github.com/tidbcloud/infra-provider/pkg/namer"
)

const (
	testLabelFalse       = "false"
	testLabelTrue        = "true"
	testComputeClassName = "shared-priority"
)

func TestGKEHeterogeneousDeployMetricMarksStaleBaseNodePoolInvalid(t *testing.T) {
	clusterCR := cloudv1.Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "cluster-a",
			Namespace: "namespace-a",
			Labels: map[string]string{
				cloudv1.ProviderTypeLabel: cloudv1.ProviderTypeGKE,
				consts.TenantLabelKey:     "tenant-a",
				consts.ProjectLabelKey:    "project-a",
			},
			Annotations: map[string]string{
				consts.AnnoKeyControlPlaneVersion: "1.29.0",
				cloudv1.AnnoKeyGKEComputeClassForTiKVBindings: `{
					"computeClassName":"tikv-priority",
					"nodePools":[
						{"role":"base","instanceType":"n2-standard-8","nodePoolName":"stale-base"},
						{"role":"extra","instanceType":"n2d-standard-8","nodePoolName":"extra-pool"}
					]
				}`,
			},
		},
		Spec: cloudv1.ClusterSpec{
			NetworkName: "network-a",
			Kubernetes:  cloudv1.Kubernetes{Name: "dataplane-a"},
			Cluster: cloudv1.ClusterTopology{
				TiKV: cloudv1.TiKV{InstanceType: "n2-standard-8"},
			},
		},
	}

	metrics := cluster{}.ExtractMore(toUnstructured(t, &clusterCR))
	labels := findMetricLabels(t, metrics, "annotation_key", cloudv1.AnnoKeyGKEComputeClassForTiKVBindings)

	if labels["valid"] != testLabelFalse {
		t.Fatalf("valid = %q, want false; labels=%v", labels["valid"], labels)
	}
	if labels["compute_class_name"] != "tikv-priority" {
		t.Fatalf("compute_class_name = %q, want tikv-priority; labels=%v", labels["compute_class_name"], labels)
	}
	if labels["role"] != "" || labels["instance_type"] != "" || labels["nodepool"] != "" {
		t.Fatalf("invalid metric should not expose binding labels; labels=%v", labels)
	}
}

func TestGKEHeterogeneousDeployMetricIncludesValidBindings(t *testing.T) {
	const (
		baseInstanceType  = "n2-standard-8"
		extraInstanceType = "n2d-standard-8"
	)

	clusterCR := cloudv1.Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "cluster-a",
			Namespace: "namespace-a",
			Labels: map[string]string{
				cloudv1.ProviderTypeLabel: cloudv1.ProviderTypeGKE,
				consts.TenantLabelKey:     "tenant-a",
				consts.ProjectLabelKey:    "project-a",
			},
			Annotations: map[string]string{
				consts.AnnoKeyControlPlaneVersion: "1.29.0",
			},
		},
		Spec: cloudv1.ClusterSpec{
			NetworkName: "network-a",
			Kubernetes:  cloudv1.Kubernetes{Name: "dataplane-a"},
			Cluster: cloudv1.ClusterTopology{
				TiKV: cloudv1.TiKV{InstanceType: baseInstanceType},
			},
		},
	}
	baseNodePoolName := namer.GKENodePoolNameForCluster(&clusterCR, baseInstanceType)
	nodePools := []cloudv1.GKEComputeClassNodePoolBinding{
		{
			Role:         cloudv1.GKEComputeClassNodePoolRoleBase,
			InstanceType: baseInstanceType,
			NodePoolName: baseNodePoolName,
		},
		{
			Role:         cloudv1.GKEComputeClassNodePoolRoleExtra,
			InstanceType: extraInstanceType,
			NodePoolName: "tikv-extra",
		},
	}
	computeClassName, err := cloudv1.GKEComputeClassNameForBindings(nodePools)
	if err != nil {
		t.Fatalf("compute class name for bindings: %v", err)
	}
	raw, err := cloudv1.EncodeGKEComputeClassBindingAnnotation(
		cloudv1.GKEComputeClassBindingAnnotation{
			ComputeClassName: computeClassName,
			NodePools:        nodePools,
		},
		cloudv1.AnnoKeyGKEComputeClassForTiKVBindings,
		baseInstanceType,
		baseNodePoolName,
	)
	if err != nil {
		t.Fatalf("encode binding annotation: %v", err)
	}
	clusterCR.Annotations[cloudv1.AnnoKeyGKEComputeClassForTiKVBindings] = raw

	metrics := cluster{}.ExtractMore(toUnstructured(t, &clusterCR))
	baseLabels := findMetricLabels(t, metrics, "role", cloudv1.GKEComputeClassNodePoolRoleBase)
	extraLabels := findMetricLabels(t, metrics, "role", cloudv1.GKEComputeClassNodePoolRoleExtra)

	if baseLabels["valid"] != testLabelTrue {
		t.Fatalf("base valid = %q, want true; labels=%v", baseLabels["valid"], baseLabels)
	}
	if baseLabels["compute_class_name"] != computeClassName {
		t.Fatalf("base compute_class_name = %q, want %q; labels=%v", baseLabels["compute_class_name"], computeClassName, baseLabels)
	}
	if baseLabels["nodepool"] != baseNodePoolName {
		t.Fatalf("base nodepool = %q, want %q; labels=%v", baseLabels["nodepool"], baseNodePoolName, baseLabels)
	}
	if baseLabels["base_instance_type"] != baseInstanceType || baseLabels["instance_type"] != baseInstanceType {
		t.Fatalf("base instance labels are wrong; labels=%v", baseLabels)
	}
	if extraLabels["valid"] != testLabelTrue {
		t.Fatalf("extra valid = %q, want true; labels=%v", extraLabels["valid"], extraLabels)
	}
	if extraLabels["instance_type"] != extraInstanceType || extraLabels["nodepool"] != "tikv-extra" {
		t.Fatalf("extra binding labels are wrong; labels=%v", extraLabels)
	}
}

func TestGKEHeterogeneousDeployMetricSkipsOrphanTiDBGroupAnnotation(t *testing.T) {
	const (
		baseInstanceType  = "n2-standard-64"
		extraInstanceType = "n2d-standard-8"
		orphanGroupID     = "orphan"
	)

	clusterCR := cloudv1.Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "cluster-a",
			Namespace: "namespace-a",
			Labels: map[string]string{
				cloudv1.ProviderTypeLabel: cloudv1.ProviderTypeGKE,
				consts.TenantLabelKey:     "tenant-a",
				consts.ProjectLabelKey:    "project-a",
			},
			Annotations: map[string]string{
				cloudv1.DefaultLoadBalancerIDAnnotationKey: "default-group",
				consts.AnnoKeyControlPlaneVersion:          "1.29.0",
			},
		},
		Spec: cloudv1.ClusterSpec{
			NetworkName: "network-a",
			Kubernetes:  cloudv1.Kubernetes{Name: "dataplane-a"},
			Cluster: cloudv1.ClusterTopology{
				TiDB: cloudv1.TiDB{InstanceType: "n2-standard-8"},
			},
		},
	}
	annotationKey := clusterCR.TiDBGKEComputeClassBindingsAnnoKey(orphanGroupID)
	nodePools := []cloudv1.GKEComputeClassNodePoolBinding{
		{
			Role:         cloudv1.GKEComputeClassNodePoolRoleBase,
			InstanceType: baseInstanceType,
			NodePoolName: "tidb-orphan-base",
		},
		{
			Role:         cloudv1.GKEComputeClassNodePoolRoleExtra,
			InstanceType: extraInstanceType,
			NodePoolName: "tidb-orphan-extra",
		},
	}
	computeClassName, err := cloudv1.GKEComputeClassNameForBindings(nodePools)
	if err != nil {
		t.Fatalf("compute class name for bindings: %v", err)
	}
	raw, err := cloudv1.EncodeGKEComputeClassBindingAnnotation(
		cloudv1.GKEComputeClassBindingAnnotation{
			ComputeClassName: computeClassName,
			NodePools:        nodePools,
		},
		annotationKey,
		baseInstanceType,
		"tidb-orphan-base",
	)
	if err != nil {
		t.Fatalf("encode binding annotation: %v", err)
	}
	clusterCR.Annotations[annotationKey] = raw

	metrics := cluster{}.ExtractMore(toUnstructured(t, &clusterCR))
	requireMetricAbsent(t, metrics, "annotation_key", annotationKey)
}

func TestGKEHeterogeneousDeployMetricMarksActiveMalformedTiDBGroupAnnotationInvalid(t *testing.T) {
	const groupID = "group1"

	clusterCR := cloudv1.Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "cluster-a",
			Namespace: "namespace-a",
			Labels: map[string]string{
				cloudv1.ProviderTypeLabel: cloudv1.ProviderTypeGKE,
				consts.TenantLabelKey:     "tenant-a",
				consts.ProjectLabelKey:    "project-a",
			},
			Annotations: map[string]string{
				cloudv1.DefaultLoadBalancerIDAnnotationKey: "default-group",
				consts.AnnoKeyControlPlaneVersion:          "1.29.0",
			},
		},
		Spec: cloudv1.ClusterSpec{
			NetworkName: "network-a",
			Kubernetes:  cloudv1.Kubernetes{Name: "dataplane-a"},
			Cluster: cloudv1.ClusterTopology{
				TiDB: cloudv1.TiDB{InstanceType: "n2-standard-8"},
			},
			ExtraGroups: []cloudv1.ExtraGroupSpec{
				{Name: groupID, TiDB: cloudv1.TiDB{Replicas: 2, InstanceType: "n2-standard-32"}},
			},
		},
	}
	annotationKey := clusterCR.TiDBGKEComputeClassBindingsAnnoKey(groupID)
	clusterCR.Annotations[annotationKey] = `{
		"computeClassName":"tidb-group1-priority",
		"nodePools":[
			{"role":"extra","instanceType":"n2-standard-80","nodePoolName":"tidb-group1-extra"}
		]
	}`

	metrics := cluster{}.ExtractMore(toUnstructured(t, &clusterCR))
	labels := findMetricLabels(t, metrics, "annotation_key", annotationKey)

	if labels["tidb_group_id"] != groupID {
		t.Fatalf("tidb_group_id = %q, want %q; labels=%v", labels["tidb_group_id"], groupID, labels)
	}
	if labels["valid"] != testLabelFalse {
		t.Fatalf("valid = %q, want false; labels=%v", labels["valid"], labels)
	}
	if labels["compute_class_name"] != "tidb-group1-priority" {
		t.Fatalf("compute_class_name = %q, want tidb-group1-priority; labels=%v", labels["compute_class_name"], labels)
	}
	if labels["base_instance_type"] != "n2-standard-32" {
		t.Fatalf("base_instance_type = %q, want n2-standard-32; labels=%v", labels["base_instance_type"], labels)
	}
	if labels["role"] != "" || labels["instance_type"] != "" || labels["nodepool"] != "" {
		t.Fatalf("invalid metric should not expose binding labels; labels=%v", labels)
	}
}

func TestGKEHeterogeneousNodePoolMetricIncludesAppliedOnlyComputeClass(t *testing.T) {
	nodePool := v1beta1.NodePool{
		ObjectMeta: metav1.ObjectMeta{
			Name: "pool-a",
			Labels: map[string]string{
				"tidbcloud-network":                              "network-a",
				"tidbcloud-namespace":                            "network-ns",
				namer.ClusterSelectorKey:                         shared.ClustersShared,
				namer.ComponentSelectorKey:                       shared.ComponentShared,
				cloudv1.ProjectTagKey:                            "project-a",
				consts.GKEHeterogeneousNodePoolManagedByLabelKey: consts.GKEHeterogeneousNodePoolManagedByLabelValue,
			},
			Annotations: map[string]string{
				consts.AnnoKeyGKENodePoolComputeClassApplied: "cleanup-priority",
			},
		},
		Spec: v1beta1.NodePoolSpec{
			ForProvider: v1beta1.NodePoolParameters{
				Autoscaling: &v1beta1.NodePoolAutoscaling{
					MaxNodeCount: int64Ptr(3),
					MinNodeCount: int64Ptr(0),
				},
				Config: &v1beta1.NodeConfig{
					MachineType: stringPtr("n2-standard-8"),
				},
			},
		},
	}

	metrics := nodepool{}.ExtractMore(toUnstructured(t, &nodePool))
	if len(metrics) == 0 {
		t.Fatal("expected heterogeneous nodepool metrics")
	}
	labels := metricLabels(t, metrics[0])

	if labels["cluster"] != "shared" {
		t.Fatalf("cluster = %q, want shared; labels=%v", labels["cluster"], labels)
	}
	if labels["project"] != "project-a" {
		t.Fatalf("project = %q, want project-a; labels=%v", labels["project"], labels)
	}
	if labels["compute_class_name"] != "" {
		t.Fatalf("compute_class_name = %q, want empty; labels=%v", labels["compute_class_name"], labels)
	}
	if labels["applied_compute_class_name"] != "cleanup-priority" {
		t.Fatalf("applied_compute_class_name = %q, want cleanup-priority; labels=%v", labels["applied_compute_class_name"], labels)
	}
	if labels["desired_compute_class_name"] != "" || labels["spec_compute_class_name"] != "" {
		t.Fatalf("desired/spec compute-class labels should be empty; labels=%v", labels)
	}
	if labels["compute_class_metadata_consistent"] != "false" {
		t.Fatalf("compute_class_metadata_consistent = %q, want false; labels=%v", labels["compute_class_metadata_consistent"], labels)
	}
}

func TestGKEHeterogeneousNodePoolMetricSuppressesDeletingNodePool(t *testing.T) {
	deleteTime := metav1.Now()
	nodePool := v1beta1.NodePool{
		ObjectMeta: metav1.ObjectMeta{
			Name:              "pool-a",
			DeletionTimestamp: &deleteTime,
			Labels: map[string]string{
				"tidbcloud-network":                              "network-a",
				namer.ClusterSelectorKey:                         shared.ClustersShared,
				namer.ComponentSelectorKey:                       shared.ComponentShared,
				cloudv1.ProjectTagKey:                            "project-a",
				consts.GKEHeterogeneousNodePoolManagedByLabelKey: consts.GKEHeterogeneousNodePoolManagedByLabelValue,
			},
			Annotations: map[string]string{
				consts.AnnoKeyGKENodePoolComputeClass:        testComputeClassName,
				consts.AnnoKeyGKENodePoolComputeClassApplied: testComputeClassName,
			},
		},
		Spec: v1beta1.NodePoolSpec{
			ForProvider: v1beta1.NodePoolParameters{
				Config: &v1beta1.NodeConfig{
					MachineType: stringPtr("n2-standard-8"),
					Labels: map[string]string{
						consts.GKEComputeClassLabelKey: testComputeClassName,
					},
				},
			},
		},
	}
	obj := toUnstructured(t, &nodePool)

	baseLabels := metricLabels(t, nodepool{}.Extract(obj))
	if baseLabels["delete_timestamp"] == "" {
		t.Fatalf("base nodepool delete_timestamp should be set; labels=%v", baseLabels)
	}
	if metrics := (nodepool{}).ExtractMore(obj); len(metrics) != 0 {
		t.Fatalf("deleting nodepool should not emit heterogeneous metrics, got %d", len(metrics))
	}
}

func TestNodePoolMetricsHandleMissingConfig(t *testing.T) {
	nodePool := v1beta1.NodePool{
		ObjectMeta: metav1.ObjectMeta{
			Name: "pool-a",
			Labels: map[string]string{
				"tidbcloud-network":                              "network-a",
				namer.ClusterSelectorKey:                         shared.ClustersShared,
				namer.ComponentSelectorKey:                       shared.ComponentShared,
				cloudv1.ProjectTagKey:                            "project-a",
				consts.GKEHeterogeneousNodePoolManagedByLabelKey: consts.GKEHeterogeneousNodePoolManagedByLabelValue,
			},
		},
		Spec: v1beta1.NodePoolSpec{
			ForProvider: v1beta1.NodePoolParameters{
				Autoscaling: &v1beta1.NodePoolAutoscaling{
					MaxNodeCount: int64Ptr(3),
					MinNodeCount: int64Ptr(0),
				},
				Locations: []string{"us-west1-a", "us-west1-b"},
				Version:   stringPtr("1.29.1"),
			},
		},
	}
	obj := toUnstructured(t, &nodePool)

	baseLabels := metricLabels(t, nodepool{}.Extract(obj))
	if baseLabels["machine_type"] != "" {
		t.Fatalf("base machine_type = %q, want empty; labels=%v", baseLabels["machine_type"], baseLabels)
	}
	if baseLabels["locations"] != "us-west1-a,us-west1-b" {
		t.Fatalf("base locations = %q, want us-west1-a,us-west1-b; labels=%v", baseLabels["locations"], baseLabels)
	}
	if baseLabels["desired_version"] != "1.29.1" {
		t.Fatalf("base desired_version = %q, want 1.29.1; labels=%v", baseLabels["desired_version"], baseLabels)
	}

	metrics := nodepool{}.ExtractMore(obj)
	if len(metrics) == 0 {
		t.Fatal("expected heterogeneous nodepool metrics")
	}
	heterogeneousLabels := metricLabels(t, metrics[0])
	if heterogeneousLabels["machine_type"] != "" {
		t.Fatalf("heterogeneous machine_type = %q, want empty; labels=%v", heterogeneousLabels["machine_type"], heterogeneousLabels)
	}
	if heterogeneousLabels["locations"] != "us-west1-a,us-west1-b" {
		t.Fatalf("heterogeneous locations = %q, want us-west1-a,us-west1-b; labels=%v", heterogeneousLabels["locations"], heterogeneousLabels)
	}
	if heterogeneousLabels["desired_version"] != "1.29.1" {
		t.Fatalf("heterogeneous desired_version = %q, want 1.29.1; labels=%v", heterogeneousLabels["desired_version"], heterogeneousLabels)
	}
}

func TestGKEHeterogeneousNodePoolMetricMarksComputeClassMetadataConsistent(t *testing.T) {
	nodePool := testReadyGKEHeterogeneousNodePool()

	metrics := nodepool{}.ExtractMore(toUnstructured(t, &nodePool))
	if len(metrics) == 0 {
		t.Fatal("expected heterogeneous nodepool metrics")
	}
	labels := metricLabels(t, metrics[0])

	if labels["compute_class_name"] != testComputeClassName {
		t.Fatalf("compute_class_name = %q, want shared-priority; labels=%v", labels["compute_class_name"], labels)
	}
	if labels["desired_compute_class_name"] != testComputeClassName {
		t.Fatalf("desired_compute_class_name = %q, want shared-priority; labels=%v", labels["desired_compute_class_name"], labels)
	}
	if labels["applied_compute_class_name"] != testComputeClassName {
		t.Fatalf("applied_compute_class_name = %q, want shared-priority; labels=%v", labels["applied_compute_class_name"], labels)
	}
	if labels["spec_compute_class_name"] != testComputeClassName {
		t.Fatalf("spec_compute_class_name = %q, want shared-priority; labels=%v", labels["spec_compute_class_name"], labels)
	}
	if labels["compute_class_metadata_consistent"] != testLabelTrue {
		t.Fatalf("compute_class_metadata_consistent = %q, want true; labels=%v", labels["compute_class_metadata_consistent"], labels)
	}
}

func TestGKEHeterogeneousNodePoolMetricMarksComputeClassMetadataPendingWithoutApplied(t *testing.T) {
	nodePool := v1beta1.NodePool{
		ObjectMeta: metav1.ObjectMeta{
			Name: "pool-a",
			Labels: map[string]string{
				"tidbcloud-network":                              "network-a",
				namer.ClusterSelectorKey:                         shared.ClustersShared,
				namer.ComponentSelectorKey:                       shared.ComponentShared,
				cloudv1.ProjectTagKey:                            "project-a",
				consts.GKEHeterogeneousNodePoolManagedByLabelKey: consts.GKEHeterogeneousNodePoolManagedByLabelValue,
			},
			Annotations: map[string]string{
				consts.AnnoKeyGKENodePoolComputeClass: testComputeClassName,
			},
		},
		Spec: v1beta1.NodePoolSpec{
			ForProvider: v1beta1.NodePoolParameters{
				Config: &v1beta1.NodeConfig{
					MachineType: stringPtr("n2-standard-8"),
					Labels: map[string]string{
						consts.GKEComputeClassLabelKey: testComputeClassName,
					},
				},
			},
		},
	}

	metrics := nodepool{}.ExtractMore(toUnstructured(t, &nodePool))
	if len(metrics) == 0 {
		t.Fatal("expected heterogeneous nodepool metrics")
	}
	labels := metricLabels(t, metrics[0])

	if labels["desired_compute_class_name"] != testComputeClassName {
		t.Fatalf("desired_compute_class_name = %q, want shared-priority; labels=%v", labels["desired_compute_class_name"], labels)
	}
	if labels["applied_compute_class_name"] != "" {
		t.Fatalf("applied_compute_class_name = %q, want empty; labels=%v", labels["applied_compute_class_name"], labels)
	}
	if labels["spec_compute_class_name"] != testComputeClassName {
		t.Fatalf("spec_compute_class_name = %q, want shared-priority; labels=%v", labels["spec_compute_class_name"], labels)
	}
	if labels["compute_class_metadata_consistent"] != "false" {
		t.Fatalf("compute_class_metadata_consistent = %q, want false; labels=%v", labels["compute_class_metadata_consistent"], labels)
	}
}

func TestGKEHeterogeneousNodePoolMetricMarksComputeClassMetadataDesiredOnlyPending(t *testing.T) {
	nodePool := v1beta1.NodePool{
		ObjectMeta: metav1.ObjectMeta{
			Name: "pool-a",
			Labels: map[string]string{
				"tidbcloud-network":                              "network-a",
				namer.ClusterSelectorKey:                         shared.ClustersShared,
				namer.ComponentSelectorKey:                       shared.ComponentShared,
				cloudv1.ProjectTagKey:                            "project-a",
				consts.GKEHeterogeneousNodePoolManagedByLabelKey: consts.GKEHeterogeneousNodePoolManagedByLabelValue,
			},
			Annotations: map[string]string{
				consts.AnnoKeyGKENodePoolComputeClass: testComputeClassName,
			},
		},
		Spec: v1beta1.NodePoolSpec{
			ForProvider: v1beta1.NodePoolParameters{
				Config: &v1beta1.NodeConfig{
					MachineType: stringPtr("n2-standard-8"),
				},
			},
		},
	}

	metrics := nodepool{}.ExtractMore(toUnstructured(t, &nodePool))
	if len(metrics) == 0 {
		t.Fatal("expected heterogeneous nodepool metrics")
	}
	labels := metricLabels(t, metrics[0])

	if labels["desired_compute_class_name"] != testComputeClassName {
		t.Fatalf("desired_compute_class_name = %q, want shared-priority; labels=%v", labels["desired_compute_class_name"], labels)
	}
	if labels["applied_compute_class_name"] != "" || labels["spec_compute_class_name"] != "" {
		t.Fatalf("applied/spec compute-class labels should be empty; labels=%v", labels)
	}
	if labels["compute_class_metadata_consistent"] != "false" {
		t.Fatalf("compute_class_metadata_consistent = %q, want false; labels=%v", labels["compute_class_metadata_consistent"], labels)
	}
}

func TestGKEHeterogeneousNodePoolMetricMarksComputeClassMetadataMismatch(t *testing.T) {
	nodePool := v1beta1.NodePool{
		ObjectMeta: metav1.ObjectMeta{
			Name: "pool-a",
			Labels: map[string]string{
				"tidbcloud-network":                              "network-a",
				namer.ClusterSelectorKey:                         shared.ClustersShared,
				namer.ComponentSelectorKey:                       shared.ComponentShared,
				cloudv1.ProjectTagKey:                            "project-a",
				consts.GKEHeterogeneousNodePoolManagedByLabelKey: consts.GKEHeterogeneousNodePoolManagedByLabelValue,
			},
			Annotations: map[string]string{
				consts.AnnoKeyGKENodePoolComputeClass: "desired-priority",
			},
		},
		Spec: v1beta1.NodePoolSpec{
			ForProvider: v1beta1.NodePoolParameters{
				Config: &v1beta1.NodeConfig{
					MachineType: stringPtr("n2-standard-8"),
					Labels: map[string]string{
						consts.GKEComputeClassLabelKey: "stale-priority",
					},
				},
			},
		},
	}

	metrics := nodepool{}.ExtractMore(toUnstructured(t, &nodePool))
	if len(metrics) == 0 {
		t.Fatal("expected heterogeneous nodepool metrics")
	}
	labels := metricLabels(t, metrics[0])

	if labels["compute_class_name"] != "desired-priority" {
		t.Fatalf("compute_class_name = %q, want desired-priority; labels=%v", labels["compute_class_name"], labels)
	}
	if labels["desired_compute_class_name"] != "desired-priority" {
		t.Fatalf("desired_compute_class_name = %q, want desired-priority; labels=%v", labels["desired_compute_class_name"], labels)
	}
	if labels["spec_compute_class_name"] != "stale-priority" {
		t.Fatalf("spec_compute_class_name = %q, want stale-priority; labels=%v", labels["spec_compute_class_name"], labels)
	}
	if labels["compute_class_metadata_consistent"] != "false" {
		t.Fatalf("compute_class_metadata_consistent = %q, want false; labels=%v", labels["compute_class_metadata_consistent"], labels)
	}
}

func TestGKEHeterogeneousNodePoolComputeClassMetadataReadyMetricSkipsPreparedNodePool(t *testing.T) {
	nodePool := testAPIManagedGKEHeterogeneousNodePool()

	metrics := nodepool{}.ExtractMore(toUnstructured(t, &nodePool))
	if len(metrics) == 0 {
		t.Fatal("expected base heterogeneous nodepool metrics")
	}
	assertMetricDescNotContains(t, metrics, "gke_heterogeneous_nodepool_compute_class_metadata_ready")
}

func TestGKEHeterogeneousNodePoolComputeClassMetadataReadyMetricIncludesEverBoundCleanupNodePool(t *testing.T) {
	nodePool := testAPIManagedGKEHeterogeneousNodePool()
	nodePool.Annotations = map[string]string{
		consts.AnnoKeyGKEHeterogeneousNodePoolEverBound: "true",
	}

	metrics := nodepool{}.ExtractMore(toUnstructured(t, &nodePool))
	metric := findMetricByDescContains(t, metrics, "gke_heterogeneous_nodepool_compute_class_metadata_ready")
	labels := metricLabels(t, metric)

	if metricGaugeValue(t, metric) != 0 {
		t.Fatalf("metadata_ready = %v, want 0; labels=%v", metricGaugeValue(t, metric), labels)
	}
	if labels["has_compute_class_metadata"] != testLabelFalse {
		t.Fatalf("has_compute_class_metadata = %q, want false; labels=%v", labels["has_compute_class_metadata"], labels)
	}
	if labels["ever_bound"] != testLabelTrue {
		t.Fatalf("ever_bound = %q, want true; labels=%v", labels["ever_bound"], labels)
	}
}

func TestGKEHeterogeneousNodePoolComputeClassMetadataReadyMetricMarksReady(t *testing.T) {
	nodePool := testReadyGKEHeterogeneousNodePool()

	metrics := nodepool{}.ExtractMore(toUnstructured(t, &nodePool))
	metric := findMetricByDescContains(t, metrics, "gke_heterogeneous_nodepool_compute_class_metadata_ready")
	labels := metricLabels(t, metric)

	if metricGaugeValue(t, metric) != 1 {
		t.Fatalf("metadata_ready = %v, want 1; labels=%v", metricGaugeValue(t, metric), labels)
	}
	if labels["compute_class_name"] != testComputeClassName {
		t.Fatalf("compute_class_name = %q, want %q; labels=%v", labels["compute_class_name"], testComputeClassName, labels)
	}
	if labels["desired_compute_class_name"] != testComputeClassName ||
		labels["applied_compute_class_name"] != testComputeClassName ||
		labels["spec_label_compute_class_name"] != testComputeClassName ||
		labels["spec_taint_compute_class_name"] != testComputeClassName {
		t.Fatalf("compute-class metadata labels should all equal %q; labels=%v", testComputeClassName, labels)
	}
	if labels["has_compute_class_metadata"] != testLabelTrue ||
		labels["has_isolation_taint"] != testLabelFalse ||
		labels["api_managed"] != testLabelTrue {
		t.Fatalf("unexpected readiness flags; labels=%v", labels)
	}
}

func TestGKEHeterogeneousNodePoolComputeClassMetadataReadyMetricMarksPending(t *testing.T) {
	tests := []struct {
		name   string
		mutate func(*v1beta1.NodePool)
	}{
		{
			name: "missing spec label",
			mutate: func(nodePool *v1beta1.NodePool) {
				delete(nodePool.Spec.ForProvider.Config.Labels, consts.GKEComputeClassLabelKey)
			},
		},
		{
			name: "missing spec taint",
			mutate: func(nodePool *v1beta1.NodePool) {
				nodePool.Spec.ForProvider.Config.Taints = nil
			},
		},
		{
			name: "isolation taint remains",
			mutate: func(nodePool *v1beta1.NodePool) {
				nodePool.Spec.ForProvider.Config.Taints = append(nodePool.Spec.ForProvider.Config.Taints, &v1beta1.NodeTaint{
					Key:    consts.GKEHeterogeneousNodePoolIsolationTaintKey,
					Value:  consts.GKEHeterogeneousNodePoolIsolationTaintValue,
					Effect: "NO_SCHEDULE",
				})
			},
		},
		{
			name: "crossplane not synced",
			mutate: func(nodePool *v1beta1.NodePool) {
				for i := range nodePool.Status.Conditions {
					if nodePool.Status.Conditions[i].Type == commonv1.TypeSynced {
						nodePool.Status.Conditions[i].Status = corev1.ConditionFalse
					}
				}
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			nodePool := testReadyGKEHeterogeneousNodePool()
			tt.mutate(&nodePool)

			metrics := nodepool{}.ExtractMore(toUnstructured(t, &nodePool))
			metric := findMetricByDescContains(t, metrics, "gke_heterogeneous_nodepool_compute_class_metadata_ready")
			labels := metricLabels(t, metric)

			if metricGaugeValue(t, metric) != 0 {
				t.Fatalf("metadata_ready = %v, want 0; labels=%v", metricGaugeValue(t, metric), labels)
			}
			infoLabels := metricLabels(t, metrics[0])
			if infoLabels["compute_class_metadata_consistent"] != "false" {
				t.Fatalf("compute_class_metadata_consistent = %q, want false; labels=%v", infoLabels["compute_class_metadata_consistent"], infoLabels)
			}
		})
	}
}

func testAPIManagedGKEHeterogeneousNodePool() v1beta1.NodePool {
	return v1beta1.NodePool{
		ObjectMeta: metav1.ObjectMeta{
			Name: "pool-a",
			Labels: map[string]string{
				"tidbcloud-network":                              "network-a",
				namer.ClusterSelectorKey:                         shared.ClustersShared,
				namer.ComponentSelectorKey:                       shared.ComponentShared,
				cloudv1.ProjectTagKey:                            "project-a",
				consts.GKEHeterogeneousNodePoolManagedByLabelKey: consts.GKEHeterogeneousNodePoolManagedByLabelValue,
			},
		},
		Spec: v1beta1.NodePoolSpec{
			ForProvider: v1beta1.NodePoolParameters{
				Config: &v1beta1.NodeConfig{
					MachineType: stringPtr("n2-standard-8"),
				},
			},
		},
	}
}

func testReadyGKEHeterogeneousNodePool() v1beta1.NodePool {
	nodePool := testAPIManagedGKEHeterogeneousNodePool()
	nodePool.Annotations = map[string]string{
		consts.AnnoKeyGKENodePoolComputeClass:        testComputeClassName,
		consts.AnnoKeyGKENodePoolComputeClassApplied: testComputeClassName,
	}
	nodePool.Spec.ForProvider.Config.Labels = map[string]string{
		consts.GKEComputeClassLabelKey: testComputeClassName,
	}
	nodePool.Spec.ForProvider.Config.Taints = []*v1beta1.NodeTaint{
		{
			Key:    consts.GKEComputeClassLabelKey,
			Value:  testComputeClassName,
			Effect: "NO_SCHEDULE",
		},
	}
	nodePool.Status.Conditions = []commonv1.Condition{
		{
			Type:   commonv1.TypeReady,
			Status: corev1.ConditionTrue,
		},
		{
			Type:   commonv1.TypeSynced,
			Status: corev1.ConditionTrue,
		},
	}
	return nodePool
}

func toUnstructured(t *testing.T, obj interface{}) *unstructured.Unstructured {
	t.Helper()

	data, err := json.Marshal(obj)
	if err != nil {
		t.Fatalf("marshal object: %v", err)
	}
	out := &unstructured.Unstructured{}
	if err := json.Unmarshal(data, &out.Object); err != nil {
		t.Fatalf("unmarshal object into unstructured: %v", err)
	}
	return out
}

func int64Ptr(v int64) *int64 {
	return &v
}

func stringPtr(v string) *string {
	return &v
}

func findMetricLabels(t *testing.T, metrics []prometheus.Metric, labelName, labelValue string) map[string]string {
	t.Helper()

	for _, metric := range metrics {
		labels := metricLabels(t, metric)
		if labels[labelName] == labelValue {
			return labels
		}
	}
	t.Fatalf("metric with %s=%q not found", labelName, labelValue)
	return nil
}

func requireMetricAbsent(t *testing.T, metrics []prometheus.Metric, labelName, labelValue string) {
	t.Helper()

	for _, metric := range metrics {
		labels := metricLabels(t, metric)
		if labels[labelName] == labelValue {
			t.Fatalf("metric with %s=%q should be absent, labels=%v", labelName, labelValue, labels)
		}
	}
}

func metricLabels(t *testing.T, metric prometheus.Metric) map[string]string {
	t.Helper()

	var pb dto.Metric
	if err := metric.Write(&pb); err != nil {
		t.Fatalf("write metric: %v", err)
	}
	out := make(map[string]string, len(pb.Label))
	for _, label := range pb.Label {
		out[label.GetName()] = label.GetValue()
	}
	return out
}
