package nodepool

import (
	"context"
	"testing"

	gkev1beta1 "github.com/crossplane-contrib/provider-gcp/apis/container/v1beta1"
	"go.uber.org/mock/gomock"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/cloud/gcp"
	"github.com/tidbcloud/infra-provider/pkg/consts"
)

func getTestNetwork(name, namespace string) *cloudv1.Network {
	return &cloudv1.Network{
		ObjectMeta: metav1.ObjectMeta{
			Name:      name,
			Namespace: namespace,
		},
	}
}

func TestTiDBNodePoolName(t *testing.T) {
	tests := []struct {
		name        string
		network     *cloudv1.Network
		version     string
		machineType string
		want        string
	}{
		// Without version, copied from the real environment,
		// verify if it will break existing node pool names.
		// Do NOT modify these cases.
		{
			name:        "dev without version",
			network:     getTestNetwork("gke-us-west2", "nightly-ms-43562"),
			machineType: "e2-medium",
			want:        "e2-medium-nightly-ms-43562-gke--1e97c83e",
		},
		{
			name:        "staging without version",
			network:     getTestNetwork("gke-us-west1", "staging-1369847559693197287"),
			machineType: "n2-highmem-16",
			want:        "n2-highmem-16-staging-136984755-d3d9a74b",
		},
		{
			name:        "prod without version",
			network:     getTestNetwork("gke-us-west1", "prod-1372813089374451283"),
			machineType: "n2-custom-16-32768",
			want:        "n2-custom-16-32768-prod-1372813-8e641608",
		},
		// With version
		{
			name:        "staging with version",
			network:     getTestNetwork("gke-us-west1", "staging-1369847559691427518"),
			version:     "1.27",
			machineType: "n2-standard-16",
			want:        "n2-standard-16-127-staging-1369-45a11168",
		},
		{
			name:        "prod with version",
			network:     getTestNetwork("gke-us-west1", "prod-1372813089374841286"),
			version:     "1.27",
			machineType: "n2-standard-16",
			want:        "n2-standard-16-127-prod-1372813-f12219c0",
		},
		{
			name:        "prod with version, long name and machine type",
			network:     getTestNetwork("asia-northeast1", "prod-1372813089374451283"),
			version:     "1.27",
			machineType: "n2-custom-16-32768",
			want:        "n2-custom-16-32768-127-prod-137-0867868d",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := LegacyTiDBNodePoolName(tt.network, tt.version, tt.machineType); got != tt.want {
				t.Errorf("TiDBNodePoolName() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestGetManagedInstanceCountUsesManagedInstanceGroups(t *testing.T) {
	ctrl := gomock.NewController(t)
	t.Cleanup(ctrl.Finish)

	gcpCli := gcp.NewMockClient(ctrl)
	gcpCli.EXPECT().
		GetInstanceGroupManagerManagedInstanceCount(gomock.Any(), "test-project", "us-central1-a", "ig-a").
		Return(int64(2), nil)
	gcpCli.EXPECT().
		GetInstanceGroupManagerManagedInstanceCount(gomock.Any(), "test-project", "us-central1-b", "ig-b").
		Return(int64(3), nil)

	got, err := GetManagedInstanceCount(context.Background(), gcpCli, []string{
		"https://www.googleapis.com/compute/v1/projects/test-project/zones/us-central1-a/instanceGroupManagers/ig-a",
		"https://www.googleapis.com/compute/v1/projects/test-project/zones/us-central1-b/instanceGroupManagers/ig-b",
	})
	if err != nil {
		t.Fatalf("GetManagedInstanceCount() unexpected error: %v", err)
	}
	if got != 5 {
		t.Fatalf("GetManagedInstanceCount() = %d, want 5", got)
	}
}

func TestGKEClustersShareKubernetesClusterScope(t *testing.T) {
	target := &cloudv1.Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "target",
			Namespace: "ns",
			Labels: map[string]string{
				cloudv1.ProviderTypeLabel: cloudv1.ProviderTypeGKE,
				cloudv1.RegionLabel:       "us-west1",
				cloudv1.ProjectTagKey:     "project-1",
				cloudv1.TenantTagKey:      "tenant-1",
			},
		},
		Spec: cloudv1.ClusterSpec{
			Kubernetes: cloudv1.Kubernetes{Name: "shared-gke"},
		},
	}
	target.EnableFeature(consts.ClusterFeatureGCP3LayerNodePool)
	peerWithNetwork := &cloudv1.Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "peer",
			Namespace: "ns",
			Labels: map[string]string{
				cloudv1.ProviderTypeLabel: cloudv1.ProviderTypeGKE,
				cloudv1.RegionLabel:       "us-west1",
				cloudv1.ProjectTagKey:     "project-1",
				cloudv1.TenantTagKey:      "tenant-1",
			},
		},
		Spec: cloudv1.ClusterSpec{
			NetworkName: "peer-network",
			Kubernetes:  cloudv1.Kubernetes{Name: "shared-gke"},
		},
	}
	peerSame3LayerScope := &cloudv1.Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "peer-3layer",
			Namespace: "other-ns",
			Labels: map[string]string{
				cloudv1.ProviderTypeLabel: cloudv1.ProviderTypeGKE,
				cloudv1.RegionLabel:       "us-west1",
				cloudv1.ProjectTagKey:     "project-1",
				cloudv1.TenantTagKey:      "tenant-1",
			},
		},
		Spec: cloudv1.ClusterSpec{
			Kubernetes: cloudv1.Kubernetes{Name: "shared-gke"},
		},
	}
	peerSame3LayerScope.EnableFeature(consts.ClusterFeatureGCP3LayerNodePool)
	peerDifferentRegion := peerSame3LayerScope.DeepCopy()
	peerDifferentRegion.Name = "peer-different-region"
	peerDifferentRegion.Labels[cloudv1.RegionLabel] = "us-east1"
	peerDifferentProject := peerSame3LayerScope.DeepCopy()
	peerDifferentProject.Name = "peer-different-project"
	peerDifferentProject.Labels[cloudv1.ProjectTagKey] = "project-2"
	peerMissingScopeLabels := peerSame3LayerScope.DeepCopy()
	peerMissingScopeLabels.Name = "peer-missing-scope-labels"
	delete(peerMissingScopeLabels.Labels, cloudv1.TenantTagKey)

	if !GKEClustersShareKubernetesClusterScope(target, peerWithNetwork) {
		t.Fatalf("clusters with the same tenant/project labels should share scope even when NetworkName differs")
	}
	if !GKEClustersShareKubernetesClusterScope(target, peerSame3LayerScope) {
		t.Fatalf("clusters with the same tenant/project labels should share scope")
	}
	if !GKEClustersShareKubernetesClusterScope(target, peerDifferentRegion) {
		t.Fatalf("clusters with the same tenant/project labels should share scope even when region differs")
	}
	if GKEClustersShareKubernetesClusterScope(target, peerDifferentProject) {
		t.Fatalf("clusters with different projects should not share scope")
	}
	if GKEClustersShareKubernetesClusterScope(target, peerMissingScopeLabels) {
		t.Fatalf("clusters with missing scope labels should not share scope")
	}
}

func TestGKEClustersShareHeterogeneousNodePoolScope(t *testing.T) {
	baseLabels := map[string]string{
		cloudv1.ProviderTypeLabel: cloudv1.ProviderTypeGKE,
		cloudv1.RegionLabel:       "us-west1",
		cloudv1.ProjectTagKey:     "project-1",
		cloudv1.TenantTagKey:      "tenant-1",
	}
	newCluster := func(name, namespace, networkName, gkeName string) *cloudv1.Cluster {
		labels := make(map[string]string, len(baseLabels))
		for key, value := range baseLabels {
			labels[key] = value
		}
		return &cloudv1.Cluster{
			ObjectMeta: metav1.ObjectMeta{
				Name:      name,
				Namespace: namespace,
				Labels:    labels,
			},
			Spec: cloudv1.ClusterSpec{
				NetworkName: networkName,
				Kubernetes:  cloudv1.Kubernetes{Name: gkeName},
			},
		}
	}

	target := newCluster("target", "ns-a", "shared-network", "ignored-gke")
	peerSameNetworkScope := newCluster("peer-same-network", "ns-a", "shared-network", "different-gke")
	peerDifferentNetwork := newCluster("peer-different-network", "ns-a", "other-network", "ignored-gke")
	peerSameRawCrossNamespaceNetworkDifferentNamespace := newCluster("peer-other-ns", "ns-b", "network-ns/shared-network", "ignored-gke")
	targetCrossNamespace := newCluster("target-cross-ns", "ns-a", "network-ns/shared-network", "ignored-gke")
	peerDifferentProject := newCluster("peer-different-project", "ns-a", "shared-network", "ignored-gke")
	peerDifferentProject.Labels[cloudv1.ProjectTagKey] = "project-2"
	peer3LayerSameGKE := newCluster("peer-3layer", "ns-b", "", "shared-gke")
	peer3LayerSameGKE.EnableFeature(consts.ClusterFeatureGCP3LayerNodePool)
	target3Layer := newCluster("target-3layer", "ns-a", "", "shared-gke")
	target3Layer.EnableFeature(consts.ClusterFeatureGCP3LayerNodePool)
	target3LayerPaddedGKEName := newCluster("target-3layer-padded", "ns-a", "", " shared-gke ")
	target3LayerPaddedGKEName.EnableFeature(consts.ClusterFeatureGCP3LayerNodePool)
	target3LayerMissingGKEName := newCluster("target-3layer-missing", "ns-a", "", "")
	target3LayerMissingGKEName.EnableFeature(consts.ClusterFeatureGCP3LayerNodePool)
	peer3LayerBlankGKEName := newCluster("peer-3layer-blank", "ns-b", "", " \t ")
	peer3LayerBlankGKEName.EnableFeature(consts.ClusterFeatureGCP3LayerNodePool)

	if !GKEClustersShareHeterogeneousNodePoolScope(target, peerSameNetworkScope) {
		t.Fatalf("clusters with same tenant/project and same ordinary network naming scope should share heterogeneous nodepool scope")
	}
	if GKEClustersShareHeterogeneousNodePoolScope(target, peerDifferentNetwork) {
		t.Fatalf("clusters with same tenant/project but different NetworkName should not share heterogeneous nodepool scope")
	}
	if GKEClustersShareHeterogeneousNodePoolScope(targetCrossNamespace, peerSameRawCrossNamespaceNetworkDifferentNamespace) {
		t.Fatalf("clusters with same raw cross-namespace NetworkName but different cluster namespace should not share heterogeneous nodepool scope")
	}
	if GKEClustersShareHeterogeneousNodePoolScope(target, peerDifferentProject) {
		t.Fatalf("clusters with different tenant/project labels should not share heterogeneous nodepool scope")
	}
	if !GKEClustersShareHeterogeneousNodePoolScope(target3Layer, peer3LayerSameGKE) {
		t.Fatalf("3-layer clusters with the same GKE cluster name should share heterogeneous nodepool scope")
	}
	if GKEClustersShareHeterogeneousNodePoolScope(target3LayerPaddedGKEName, peer3LayerSameGKE) {
		t.Fatalf("3-layer heterogeneous nodepool scope should preserve raw GKE cluster name to match nodepool naming")
	}
	if GKEClustersShareHeterogeneousNodePoolScope(target3LayerMissingGKEName, peer3LayerBlankGKEName) {
		t.Fatalf("3-layer clusters with missing GKE cluster names should not share heterogeneous nodepool scope")
	}
}

func TestGKEHeterogeneousNodePoolScopeLabelsRequire3LayerGKEName(t *testing.T) {
	tests := []struct {
		name    string
		gkeName string
	}{
		{
			name:    "empty GKE name",
			gkeName: "",
		},
		{
			name:    "blank GKE name",
			gkeName: " \t ",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cluster := &cloudv1.Cluster{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "c-missing-gke-name",
					Namespace: "ns",
					Labels: map[string]string{
						cloudv1.ProviderTypeLabel: cloudv1.ProviderTypeGKE,
						cloudv1.ProjectTagKey:     "project-1",
						cloudv1.TenantTagKey:      "tenant-1",
					},
				},
				Spec: cloudv1.ClusterSpec{
					Kubernetes: cloudv1.Kubernetes{Name: tt.gkeName},
				},
			}
			cluster.EnableFeature(consts.ClusterFeatureGCP3LayerNodePool)

			labels, missing := GKEHeterogeneousNodePoolScopeLabels(cluster)
			if len(labels) != 0 {
				t.Fatalf("scope labels with missing GKE name should fail closed, got %#v", labels)
			}
			if len(missing) != 1 || missing[0] != consts.GKEHeterogeneousNodePoolScopeLabelKey {
				t.Fatalf("unexpected missing heterogeneous nodepool scope labels: got %#v", missing)
			}
		})
	}
}

func TestGetGKENodePoolNamePreservesMainNetworkNameRule(t *testing.T) {
	const (
		machineType = "n2-standard-8"
		version     = "1.30"
	)

	t.Run("cross namespace network keeps raw cluster network name identity", func(t *testing.T) {
		cluster := &cloudv1.Cluster{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "c-cross-ns",
				Namespace: "cluster-ns",
			},
			Spec: cloudv1.ClusterSpec{
				NetworkName: "network-ns/shared-network",
			},
		}

		got := GetGKENodePoolName(cluster, machineType, version)
		want := LegacyTiDBNodePoolName(getTestNetwork("network-ns/shared-network", "cluster-ns"), version, machineType)
		if got != want {
			t.Fatalf("unexpected nodepool name: got %q, want %q", got, want)
		}
	})

	t.Run("clusters in different namespaces keep main namespace-scoped naming", func(t *testing.T) {
		clusterA := &cloudv1.Cluster{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "cluster-a",
				Namespace: "tenant-a",
			},
			Spec: cloudv1.ClusterSpec{
				NetworkName: "network-ns/shared-network",
			},
		}
		clusterB := &cloudv1.Cluster{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "cluster-b",
				Namespace: "tenant-b",
			},
			Spec: cloudv1.ClusterSpec{
				NetworkName: "network-ns/shared-network",
			},
		}

		nameA := GetGKENodePoolName(clusterA, machineType, version)
		nameB := GetGKENodePoolName(clusterB, machineType, version)
		if nameA == nameB {
			t.Fatalf("clusters in different namespaces should keep main namespace-scoped nodepool names, got %q and %q", nameA, nameB)
		}
	})
}

func TestGetNameIn3LayerArch(t *testing.T) {
	tests := []struct {
		name           string
		cluster        *cloudv1.Cluster
		machineType    string
		desiredVersion string
		want           string
	}{
		{
			name: "migrated cluster",
			cluster: &cloudv1.Cluster{
				ObjectMeta: metav1.ObjectMeta{
					Namespace: "prod-1372813089454549457",
				},
				Spec: cloudv1.ClusterSpec{
					NetworkName: "gke-us-central1",
				},
			},
			machineType:    "n2-standard-8",
			desiredVersion: "1.30",
			want:           "n2-standard-8-130-prod-13728130-88b8812a",
		},
		{
			name: "original cluster",
			cluster: &cloudv1.Cluster{
				ObjectMeta: metav1.ObjectMeta{
					Annotations: map[string]string{
						"tidbcloud.com/use-3-layer-node-pool": "true",
					},
				},
				Spec: cloudv1.ClusterSpec{
					Kubernetes: cloudv1.Kubernetes{
						Name: "dedicated-dev-us-west1-23fba3a9-8c97494d",
					},
				},
			},
			machineType:    "e2-medium",
			desiredVersion: "1.30",
			want:           "e2-medium-dedicated-dev-us--d8b62980-130",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := GetGKENodePoolName(tt.cluster, tt.machineType, tt.desiredVersion); got != tt.want {
				t.Errorf("GetGKENodePoolName() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestGKEKubernetesClusterScopeLabels(t *testing.T) {
	t.Run("scope uses raw tenant and project labels", func(t *testing.T) {
		cluster := &cloudv1.Cluster{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "c2",
				Namespace: "ns2",
				Labels: map[string]string{
					cloudv1.ProviderTypeLabel: cloudv1.ProviderTypeGKE,
					cloudv1.RegionLabel:       "us-west1",
					cloudv1.ProjectTagKey:     "project-1",
					cloudv1.TenantTagKey:      "tenant-1",
				},
			},
			Spec: cloudv1.ClusterSpec{
				NetworkName: "network-ns/shared-network",
				Kubernetes:  cloudv1.Kubernetes{Name: "shared-gke"},
			},
		}
		cluster.EnableFeature(consts.ClusterFeatureGCP3LayerNodePool)

		labels, missing := GKEKubernetesClusterScopeLabels(cluster)
		if len(missing) != 0 {
			t.Fatalf("complete scope should not report missing labels: got %#v", missing)
		}
		if _, ok := labels[cloudv1.RegionLabel]; ok {
			t.Fatalf("scope should not include region label: got %#v", labels)
		}
		if got, want := labels[cloudv1.ProjectTagKey], "project-1"; got != want {
			t.Fatalf("unexpected project label: got %q, want %q", got, want)
		}
		if got, want := labels[cloudv1.TenantTagKey], "tenant-1"; got != want {
			t.Fatalf("unexpected tenant label: got %q, want %q", got, want)
		}
	})

	t.Run("scope does not require region label", func(t *testing.T) {
		cluster := &cloudv1.Cluster{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "c-no-region",
				Namespace: "ns2",
				Labels: map[string]string{
					cloudv1.ProviderTypeLabel: cloudv1.ProviderTypeGKE,
					cloudv1.ProjectTagKey:     "project-1",
					cloudv1.TenantTagKey:      "tenant-1",
				},
			},
		}
		cluster.EnableFeature(consts.ClusterFeatureGCP3LayerNodePool)

		labels, missing := GKEKubernetesClusterScopeLabels(cluster)
		if len(missing) != 0 {
			t.Fatalf("scope without region should not report missing labels: got %#v", missing)
		}
		if got, want := labels[cloudv1.ProjectTagKey], "project-1"; got != want {
			t.Fatalf("unexpected project label: got %q, want %q", got, want)
		}
		if got, want := labels[cloudv1.TenantTagKey], "tenant-1"; got != want {
			t.Fatalf("unexpected tenant label: got %q, want %q", got, want)
		}
	})

	t.Run("scope does not use fallback project or tenant labels", func(t *testing.T) {
		cluster := &cloudv1.Cluster{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "c-missing-scope-labels",
				Namespace: "ns2",
				Labels: map[string]string{
					cloudv1.ProviderTypeLabel: cloudv1.ProviderTypeGKE,
					cloudv1.RegionLabel:       "us-west1",
				},
			},
			Spec: cloudv1.ClusterSpec{
				Kubernetes: cloudv1.Kubernetes{Name: "shared-gke"},
			},
		}
		cluster.EnableFeature(consts.ClusterFeatureGCP3LayerNodePool)

		labels, missing := GKEKubernetesClusterScopeLabels(cluster)
		if len(labels) != 0 {
			t.Fatalf("scope with missing labels should not use fallback labels: got %#v", labels)
		}
		if len(missing) != 2 || missing[0] != cloudv1.TenantTagKey || missing[1] != cloudv1.ProjectTagKey {
			t.Fatalf("unexpected missing scope labels: got %#v", missing)
		}
	})
}

func TestGKEComputeClassNameForNodePool(t *testing.T) {
	np := &gkev1beta1.NodePool{
		ObjectMeta: metav1.ObjectMeta{
			Annotations: map[string]string{
				consts.AnnoKeyGKENodePoolComputeClass: "annotated-priority",
			},
		},
	}
	if got := GKEComputeClassNameForNodePool(np); got != "annotated-priority" {
		t.Fatalf("GKEComputeClassNameForNodePool() = %q, want %q", got, "annotated-priority")
	}

	np = &gkev1beta1.NodePool{
		Spec: gkev1beta1.NodePoolSpec{
			ForProvider: gkev1beta1.NodePoolParameters{
				Config: &gkev1beta1.NodeConfig{
					Labels: map[string]string{
						consts.GKEComputeClassLabelKey: "labeled-priority",
					},
				},
			},
		},
	}
	if got := GKEComputeClassNameForNodePool(np); got != "labeled-priority" {
		t.Fatalf("GKEComputeClassNameForNodePool() = %q, want %q", got, "labeled-priority")
	}
}

func TestApplyGKEComputeClassMetadataForNodePool(t *testing.T) {
	tests := []struct {
		name        string
		desired     string
		applied     string
		wantDesired string
		wantApplied string
		wantLabel   string
		wantTaint   string
	}{
		{
			name:        "pending desired",
			desired:     "shared-priority",
			wantDesired: "shared-priority",
		},
		{
			name:        "confirmed applied",
			desired:     "shared-priority",
			applied:     "shared-priority",
			wantDesired: "shared-priority",
			wantApplied: "shared-priority",
			wantLabel:   "shared-priority",
			wantTaint:   "shared-priority",
		},
		{
			name:        "cleanup pending keeps applied guard",
			applied:     "shared-priority",
			wantApplied: "shared-priority",
			wantLabel:   "shared-priority",
			wantTaint:   "shared-priority",
		},
		{
			name: "fully cleared",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			np := &gkev1beta1.NodePool{
				ObjectMeta: metav1.ObjectMeta{
					Annotations: map[string]string{
						"keep": "annotation",
					},
				},
				Spec: gkev1beta1.NodePoolSpec{
					ForProvider: gkev1beta1.NodePoolParameters{
						Config: &gkev1beta1.NodeConfig{
							Labels: map[string]string{
								"managed": "keep",
							},
						},
					},
				},
			}

			ApplyGKEComputeClassMetadataForNodePool(np, tt.desired, tt.applied)

			if !GKEComputeClassMetadataMatchesNodePool(np, tt.desired, tt.applied) {
				t.Fatalf("expected metadata to match desired=%q applied=%q", tt.desired, tt.applied)
			}
			if got := GKEDesiredComputeClassNameForNodePool(np); got != tt.wantDesired {
				t.Fatalf("unexpected desired compute-class: got %q, want %q", got, tt.wantDesired)
			}
			if got := GKEAppliedComputeClassNameForNodePool(np); got != tt.wantApplied {
				t.Fatalf("unexpected applied compute-class: got %q, want %q", got, tt.wantApplied)
			}
			if got := GKEComputeClassSpecLabelForNodePool(np); got != tt.wantLabel {
				t.Fatalf("unexpected spec label compute-class: got %q, want %q", got, tt.wantLabel)
			}
			if got := GKEComputeClassSpecTaintForNodePool(np); got != tt.wantTaint {
				t.Fatalf("unexpected spec taint compute-class: got %q, want %q", got, tt.wantTaint)
			}
			if got := np.Annotations["keep"]; got != "annotation" {
				t.Fatalf("unexpected preserved annotation: %q", got)
			}
			if got := np.Spec.ForProvider.Config.Labels["managed"]; got != "keep" {
				t.Fatalf("unexpected preserved label: %q", got)
			}
		})
	}
}

func TestApplyGKEComputeClassMetadataForNodePoolKeepsSpecLabelUntilConfirmed(t *testing.T) {
	np := &gkev1beta1.NodePool{
		ObjectMeta: metav1.ObjectMeta{
			Annotations: map[string]string{
				consts.AnnoKeyGKENodePoolComputeClass:        "old-priority",
				consts.AnnoKeyGKENodePoolComputeClassApplied: "old-priority",
			},
		},
		Spec: gkev1beta1.NodePoolSpec{
			ForProvider: gkev1beta1.NodePoolParameters{
				Config: &gkev1beta1.NodeConfig{
					Labels: map[string]string{
						consts.GKEComputeClassLabelKey: "old-priority",
					},
					Taints: []*gkev1beta1.NodeTaint{
						{
							Key:    consts.GKEComputeClassLabelKey,
							Value:  "old-priority",
							Effect: "NO_SCHEDULE",
						},
					},
				},
			},
		},
	}

	ApplyGKEComputeClassMetadataForNodePool(np, "new-priority", "")

	if got := GKEDesiredComputeClassNameForNodePool(np); got != "new-priority" {
		t.Fatalf("unexpected desired compute-class: got %q, want %q", got, "new-priority")
	}
	if got := GKEAppliedComputeClassNameForNodePool(np); got != "" {
		t.Fatalf("applied compute-class should be cleared while pending, got %q", got)
	}
	if got := GKEComputeClassSpecLabelForNodePool(np); got != "old-priority" {
		t.Fatalf("spec label compute-class should stay on confirmed actual value, got %q", got)
	}
	if got := GKEComputeClassSpecTaintForNodePool(np); got != "old-priority" {
		t.Fatalf("spec taint compute-class should stay on confirmed actual value, got %q", got)
	}
	if !GKEComputeClassMetadataMatchesNodePool(np, "new-priority", "") {
		t.Fatalf("expected pending metadata to match while spec label is still old")
	}
}

func TestApplyGKEComputeClassMetadataForNodePoolKeepsCleanupGuard(t *testing.T) {
	np := &gkev1beta1.NodePool{
		ObjectMeta: metav1.ObjectMeta{
			Annotations: map[string]string{
				"keep":                                "annotation",
				consts.AnnoKeyGKENodePoolComputeClass: "stale-priority",
				consts.AnnoKeyGKENodePoolComputeClassApplied: "old-applied-priority",
			},
		},
		Spec: gkev1beta1.NodePoolSpec{
			ForProvider: gkev1beta1.NodePoolParameters{
				Config: &gkev1beta1.NodeConfig{
					Labels: map[string]string{
						"managed":                      "keep",
						consts.GKEComputeClassLabelKey: "stale-priority",
					},
				},
			},
		},
	}

	ApplyGKEComputeClassMetadataForNodePool(np, "", "cleanup-guard-priority")

	if got := GKEDesiredComputeClassNameForNodePool(np); got != "" {
		t.Fatalf("desired compute-class should be cleared, got %q", got)
	}
	if got := GKEComputeClassSpecLabelForNodePool(np); got != "cleanup-guard-priority" {
		t.Fatalf("spec label compute-class should be restored to cleanup guard, got %q", got)
	}
	if got := GKEComputeClassSpecTaintForNodePool(np); got != "cleanup-guard-priority" {
		t.Fatalf("spec taint compute-class should be restored to cleanup guard, got %q", got)
	}
	if got := GKEAppliedComputeClassNameForNodePool(np); got != "cleanup-guard-priority" {
		t.Fatalf("unexpected applied cleanup guard: got %q, want %q", got, "cleanup-guard-priority")
	}
	if got := np.Annotations["keep"]; got != "annotation" {
		t.Fatalf("unexpected preserved annotation: %q", got)
	}
	if got := np.Spec.ForProvider.Config.Labels["managed"]; got != "keep" {
		t.Fatalf("unexpected preserved label: %q", got)
	}
}

func TestGKEComputeClassMetadataMatchesNodePoolRequiresSpecLabelForCleanupGuard(t *testing.T) {
	np := &gkev1beta1.NodePool{
		ObjectMeta: metav1.ObjectMeta{
			Annotations: map[string]string{
				consts.AnnoKeyGKENodePoolComputeClassApplied: "stale-priority",
			},
		},
		Spec: gkev1beta1.NodePoolSpec{
			ForProvider: gkev1beta1.NodePoolParameters{
				Config: &gkev1beta1.NodeConfig{
					Labels: map[string]string{
						"managed": "keep",
					},
				},
			},
		},
	}

	if GKEComputeClassMetadataMatchesNodePool(np, "", "stale-priority") {
		t.Fatalf("cleanup guard should not match while spec label is missing")
	}

	ApplyGKEComputeClassMetadataForNodePool(np, "", "stale-priority")

	if !GKEComputeClassMetadataMatchesNodePool(np, "", "stale-priority") {
		t.Fatalf("cleanup guard should match after spec label is restored")
	}
	if got := GKEComputeClassSpecLabelForNodePool(np); got != "stale-priority" {
		t.Fatalf("unexpected spec label compute-class: got %q, want %q", got, "stale-priority")
	}
	if got := GKEComputeClassSpecTaintForNodePool(np); got != "stale-priority" {
		t.Fatalf("unexpected spec taint compute-class: got %q, want %q", got, "stale-priority")
	}
	if got := np.Spec.ForProvider.Config.Labels["managed"]; got != "keep" {
		t.Fatalf("unexpected preserved label: %q", got)
	}
}

func TestSetGKEDesiredComputeClassForNodePoolDoesNotWriteSpecLabel(t *testing.T) {
	np := &gkev1beta1.NodePool{}

	SetGKEDesiredComputeClassForNodePool(np, "shared-priority")

	if got := GKEDesiredComputeClassNameForNodePool(np); got != "shared-priority" {
		t.Fatalf("unexpected desired compute-class: got %q, want %q", got, "shared-priority")
	}
	if got := GKEComputeClassSpecLabelForNodePool(np); got != "" {
		t.Fatalf("desired helper should not write spec label, got %q", got)
	}

	SetGKEComputeClassSpecLabelForNodePool(np, "shared-priority")
	if got := GKEComputeClassSpecLabelForNodePool(np); got != "shared-priority" {
		t.Fatalf("unexpected spec label compute-class: got %q, want %q", got, "shared-priority")
	}
}

func TestHasGKEComputeClassMetadataForNodePool(t *testing.T) {
	tests := []struct {
		name string
		np   *gkev1beta1.NodePool
		want bool
	}{
		{
			name: "empty nodepool",
			np:   &gkev1beta1.NodePool{},
		},
		{
			name: "desired annotation",
			np: &gkev1beta1.NodePool{
				ObjectMeta: metav1.ObjectMeta{
					Annotations: map[string]string{
						consts.AnnoKeyGKENodePoolComputeClass: "shared-priority",
					},
				},
			},
			want: true,
		},
		{
			name: "applied annotation",
			np: &gkev1beta1.NodePool{
				ObjectMeta: metav1.ObjectMeta{
					Annotations: map[string]string{
						consts.AnnoKeyGKENodePoolComputeClassApplied: "shared-priority",
					},
				},
			},
			want: true,
		},
		{
			name: "spec label",
			np: &gkev1beta1.NodePool{
				Spec: gkev1beta1.NodePoolSpec{
					ForProvider: gkev1beta1.NodePoolParameters{
						Config: &gkev1beta1.NodeConfig{
							Labels: map[string]string{
								consts.GKEComputeClassLabelKey: "shared-priority",
							},
						},
					},
				},
			},
			want: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := HasGKEComputeClassMetadataForNodePool(tt.np); got != tt.want {
				t.Fatalf("HasGKEComputeClassMetadataForNodePool() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestSetAndClearGKEComputeClassForNodePool(t *testing.T) {
	np := &gkev1beta1.NodePool{
		ObjectMeta: metav1.ObjectMeta{
			Annotations: map[string]string{
				"keep": "annotation",
			},
		},
		Spec: gkev1beta1.NodePoolSpec{
			ForProvider: gkev1beta1.NodePoolParameters{
				Config: &gkev1beta1.NodeConfig{
					Labels: map[string]string{
						"managed": "keep",
					},
					Taints: []*gkev1beta1.NodeTaint{
						{Key: "dedicated", Value: "tidb", Effect: "NO_SCHEDULE"},
					},
				},
			},
		},
	}

	SetGKEComputeClassForNodePool(np, "shared-priority")
	if !gkeNodePoolComputeClassMetadataMatches(np, "shared-priority") {
		t.Fatalf("expected synced compute-class metadata")
	}
	if got := np.Annotations[consts.AnnoKeyGKENodePoolComputeClass]; got != "shared-priority" {
		t.Fatalf("unexpected compute-class annotation: %q", got)
	}
	if got := np.Spec.ForProvider.Config.Labels[consts.GKEComputeClassLabelKey]; got != "shared-priority" {
		t.Fatalf("unexpected compute-class spec label: %q", got)
	}
	if got := np.Spec.ForProvider.Config.Labels["managed"]; got != "keep" {
		t.Fatalf("unexpected preserved label: %q", got)
	}
	if got := GKEComputeClassSpecTaintForNodePool(np); got != "shared-priority" {
		t.Fatalf("unexpected compute-class spec taint after sync: got %q, taints %#v", got, np.Spec.ForProvider.Config.Taints)
	}

	SetGKEAppliedComputeClassForNodePool(np, "shared-priority")
	if got := GKEAppliedComputeClassNameForNodePool(np); got != "shared-priority" {
		t.Fatalf("unexpected applied compute-class annotation: %q", got)
	}
	SetGKEComputeClassForNodePool(np, "other-priority")
	if got := GKEAppliedComputeClassNameForNodePool(np); got != "" {
		t.Fatalf("applied compute-class should be cleared when desired changes, got %q", got)
	}
	if got := GKEComputeClassSpecLabelForNodePool(np); got != "other-priority" {
		t.Fatalf("unexpected compute-class spec label after desired change: %q", got)
	}
	if got := GKEComputeClassSpecTaintForNodePool(np); got != "other-priority" {
		t.Fatalf("unexpected compute-class spec taint after desired change: got %q, taints %#v", got, np.Spec.ForProvider.Config.Taints)
	}

	ClearGKEComputeClassForNodePool(np)
	if got := GKEComputeClassNameForNodePool(np); got != "" {
		t.Fatalf("GKEComputeClassNameForNodePool() after clear = %q, want empty", got)
	}
	if got := GKEAppliedComputeClassNameForNodePool(np); got != "" {
		t.Fatalf("GKEAppliedComputeClassNameForNodePool() after clear = %q, want empty", got)
	}
	if got := np.Annotations["keep"]; got != "annotation" {
		t.Fatalf("unexpected preserved annotation: %q", got)
	}
	if got := np.Spec.ForProvider.Config.Labels["managed"]; got != "keep" {
		t.Fatalf("unexpected preserved label after clear: %q", got)
	}
	if len(np.Spec.ForProvider.Config.Taints) != 1 || np.Spec.ForProvider.Config.Taints[0].Key != "dedicated" {
		t.Fatalf("expected existing taint after clear, got %#v", np.Spec.ForProvider.Config.Taints)
	}
}

func gkeNodePoolComputeClassMetadataMatches(np *gkev1beta1.NodePool, computeClass string) bool {
	if np == nil || computeClass == "" {
		return false
	}
	if np.GetAnnotations()[consts.AnnoKeyGKENodePoolComputeClass] != computeClass {
		return false
	}
	return GKEComputeClassSpecLabelForNodePool(np) == computeClass
}
